diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a3982b..97b8c8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +October 27, 2024 : +RoonCommandLine version 2.1.4 release 1 + + This release primarily provides support for unattended installation + * Check and repair the RoonCommandLine Python virtual environment + June 11, 2024 : RoonCommandLine version 2.1.3 release 2 diff --git a/Install b/Install index bc9fe75..846208e 100755 --- a/Install +++ b/Install @@ -21,33 +21,44 @@ PKG_SELECTED= # User should not be root. Prompt to proceed if root user iamroot= +SUDO="sudo -E" if [ "${EUID}" ] then - [ ${EUID} -eq 0 ] && iamroot=1 + [ ${EUID} -eq 0 ] && { + iamroot=1 + SUDO= + } else uid=`id -u` - [ ${uid} -eq 0 ] && iamroot=1 + [ ${uid} -eq 0 ] && { + iamroot=1 + SUDO= + } fi +[ "$1" == "unattended" ] && export ROON_UNATTENDED="unattended" + [ "${iamroot}" ] && { - printf "\nThe ${BOLD}Install${NORM} command should be run as a normal user." - printf "\nIt appears it has been invoked with 'root' user privileges.\n\n" - while true - do - read -p "Do you intend to use RoonCommandLine as the 'root' user ? (y/n) " yn - case $yn in - [Yy]* ) + [ "${ROON_UNATTENDED}" ] || { + printf "\nThe ${BOLD}Install${NORM} command should be run as a normal user." + printf "\nIt appears it has been invoked with 'root' user privileges.\n\n" + while true + do + read -p "Do you intend to use RoonCommandLine as the 'root' user ? (y/n) " yn + case $yn in + [Yy]* ) break ;; - [Nn]* ) + [Nn]* ) printf "\nRe-run this command as a normal user." printf "\nExiting.\n\n" exit 0 ;; - * ) echo "Please answer yes or no." + * ) echo "Please answer yes or no." ;; - esac - done + esac + done + } } SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) @@ -79,10 +90,13 @@ get_available_packages() { install_selected() { for pkg in ${PKG_AVAILABLE} do - while true - do - read -p "Install ${pkg} ? ('Y'/'N'): " yn - case $yn in + if [ "${ROON_UNATTENDED}" ]; then + PKG_SELECTED="${PKG_SELECTED} $pkg" + else + while true + do + read -p "Install ${pkg} ? ('Y'/'N'): " yn + case $yn in [Yy]*) PKG_SELECTED="${PKG_SELECTED} $pkg" break @@ -93,8 +107,9 @@ install_selected() { * ) echo "Please answer yes or no." ;; - esac - done + esac + done + fi done PKG_SELECTED=`echo $PKG_SELECTED | sed -e "s/^ //"` } @@ -102,7 +117,7 @@ install_selected() { plat=`uname -s` if [ "$plat" == "Darwin" ] then - ./macInstall + ./macInstall ${ROON_UNATTENDED} else debian= have_apt=`type -p apt` @@ -189,11 +204,11 @@ else then if [ "${have_apt}" ] then - sudo apt install "${PKG}" + ${SUDO} apt install "${PKG}" else if [ "${have_dpkg}" ] then - sudo dpkg -i "${PKG}" + ${SUDO} dpkg -i "${PKG}" else echo "Cannot locate either apt or dpkg to install. Skipping." fi @@ -201,11 +216,11 @@ else else if [ "${have_yum}" ] then - sudo yum localinstall "${PKG}" + ${SUDO} yum localinstall "${PKG}" else if [ "${have_rpm}" ] then - sudo rpm -i "${PKG}" + ${SUDO} rpm -i "${PKG}" else echo "Cannot locate either yum or rpm to install. Skipping." fi @@ -213,25 +228,29 @@ else fi done else - while true - do - echo "" - echo "No packages for version ${PKG_VER} are currently available." - echo "Would you like to perform a scripted install on this platform?" - echo "" - read -p "Install ${pkg} ? ('Y'/'N'): " yn - case $yn in - [Yy]*) - ./linInstall - break - ;; - [Nn]*) - break - ;; - *) - echo "Please answer yes or no." - ;; - esac - done + if [ "${ROON_UNATTENDED}" ]; then + ./linInstall unattended + else + while true + do + echo "" + echo "No packages for version ${PKG_VER} are currently available." + echo "Would you like to perform a scripted install on this platform?" + echo "" + read -p "Install ${pkg} ? ('Y'/'N'): " yn + case $yn in + [Yy]*) + ./linInstall + break + ;; + [Nn]*) + break + ;; + *) + echo "Please answer yes or no." + ;; + esac + done + fi fi fi diff --git a/VERSION b/VERSION index 214fb07..fee9955 100644 --- a/VERSION +++ b/VERSION @@ -1,2 +1,2 @@ -VERSION=2.1.3 -RELEASE=2 +VERSION=2.1.4 +RELEASE=1 diff --git a/bin/roon b/bin/roon index c11ca77..130af91 100755 --- a/bin/roon +++ b/bin/roon @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # roon - frontend script to issue commands to Roon via the Python Roon API # @@ -59,7 +59,7 @@ usage() { rich " [cyan]-F[/] [yellow]\[from zone][/] [cyan]-f[/] [yellow]\[on|onlog|off|status][/] [cyan]-g[/] [yellow]genre[/] [cyan]-G[/] [yellow]zone_group[/] [cyan]-i[/]" --print rich " [cyan]-I -l[/] [yellow]\[albums|artists|artalbums|albtracks|arttracks|composers|comalbums|[/]" --print rich " [yellow]genres|genalbums|genartists|playlists|playtracks|tags|zones][/]" --print - rich " [cyan]-c[/] [yellow]\[group|ungroup|play|play_all|pause|pause_all|stop|stop_all|[/]" --print + rich " [cyan]-c[/] [yellow]\[discover|group|ungroup|play|play_all|pause|pause_all|stop|stop_all|[/]" --print rich " [yellow]next|previous|shuffle|unshuffle|repeat|unrepeat|mute|mute_all][/]" --print rich " [cyan]-s[/] [yellow]search[/] [cyan]-p[/] [yellow]playlist[/] [cyan]-T[/] [yellow]track[/] [cyan]-t[/] [yellow]tag[/] [cyan]-z[/] [yellow]zone[/] [cyan]-L[/] [cyan]-S[/] [cyan]-r[/] [yellow]radio[/]" --print rich " [cyan]-X[/] [yellow]ex_album[/] [cyan]-x[/] [yellow]ex_artist[/] [cyan]\[-EuU][/]" --print @@ -97,7 +97,7 @@ usage() { rich " [cyan]-T[/] [yellow]track[/] specifies a track to play" --print rich " [cyan]-t[/] [yellow]tag[/] selects an tag to play" --print rich " [cyan]-z[/] [yellow]zone[/] selects the Roon Zone in which to play" --print - rich " [cyan]-c[/] [yellow]\[group|ungroup|play|play_all|pause|pause_all|playpause|stop|stop_all|[/]" --print + rich " [cyan]-c[/] [yellow]\[discover|group|ungroup|play|play_all|pause|pause_all|playpause|stop|stop_all|[/]" --print rich " [yellow]next|previous|shuffle|unshuffle|repeat|unrepeat|mute|mute_all][/]" --print rich " issues the command in the selected zone" --print rich " '[italic green]roon -c mute[/]' toggles the zone's muted or unmuted state" --print @@ -131,7 +131,7 @@ usage() { printf "\n\t-F [from zone] -f [on|onlog|off|status] -g genre -G zone_group -i" printf "\n\t-I -l [albums|artists|artalbums|albtracks|arttracks|composers|comalbums|" printf "\n\t genres|genalbums|genartists|playlists|playtracks|tags|zones]" - printf "\n\t-c [group|ungroup|play|play_all|pause|pause_all|stop|stop_all|" + printf "\n\t-c [discover|group|ungroup|play|play_all|pause|pause_all|stop|stop_all|" printf "\n\t next|previous|shuffle|unshuffle|repeat|unrepeat|mute|mute_all]" printf "\n\t-s search -p playlist -T track -t tag -z zone -L -S -r radio" printf "\n\t-X ex_album -x ex_artist [-EuU]" @@ -168,7 +168,7 @@ usage() { printf "\n\t-T track specifies a track to play" printf "\n\t-t tag selects an tag to play" printf "\n\t-z zone selects the Roon Zone in which to play" - printf "\n\t-c [group|ungroup|play|play_all|pause|pause_all|playpause|stop|stop_all|" + printf "\n\t-c [discover|group|ungroup|play|play_all|pause|pause_all|playpause|stop|stop_all|" printf "\n\t next|previous|shuffle|unshuffle|repeat|unrepeat|mute|mute_all]" printf "\n\t issues the command in the selected zone" printf "\n\t 'mute' toggles the zone's muted or unmuted state" @@ -800,6 +800,13 @@ LOCAL=false BOLD=$(tput bold 2>/dev/null) NORMAL=$(tput sgr0 2>/dev/null) +# Check Python virtual environment +[ -e ${ROON}/venv/bin/python3 ] || { + printf "\nRepairing RoonCommandLine Python virtual environment ..." + [ -x ${ROONETC}/upgrade-venv ] && ${ROONETC}/upgrade-venv + printf " done\n" +} + [ -f ${ROON}/venv/bin/activate ] && source ${ROON}/venv/bin/activate [ -x ${ROON}/venv/bin/python ] && export PYTHON=${ROON}/venv/bin/python @@ -2168,6 +2175,16 @@ while getopts A:a:bBc:C:dD:f:F:g:G:hHiImnNOp:T:t:z:l:s:LSr:v:x:X:EuU flag; do esac done +[ "${comm}" == "discover" ] && { + if [ -x ${ROONETC}/discover ]; then + ${ROONETC}/discover + exit 0 + else + echo "ERROR: ${ROONETC}/discover not found or not executable. Reinstall." + exit 1 + fi +} + [ "${inst_roon_gui}" ] && { [ -x ${ROON}/etc/install-roon-gui ] && ${ROON}/etc/install-roon-gui if [ -x ${ROON}/bin/roongui ]; then diff --git a/bin/roon_fade b/bin/roon_fade index e48536d..13ae3f5 100755 --- a/bin/roon_fade +++ b/bin/roon_fade @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # Written by Ron Record December, 2022 # diff --git a/bin/roonanim b/bin/roonanim index 3e1b36b..7ed03aa 100755 --- a/bin/roonanim +++ b/bin/roonanim @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # roonanim - display, animate, zoom RoonCommandLine splash screen using Kitty # diff --git a/bin/roontui b/bin/roontui index 380f8a2..0c3dd66 100755 --- a/bin/roontui +++ b/bin/roontui @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # roontui - Frontend for roon-tui terminal user interface for Roon # Default to ~/.config/roon-tui/ for roon-tui configuration and log files diff --git a/etc/discover b/etc/discover new file mode 100755 index 0000000..9ef54f5 --- /dev/null +++ b/etc/discover @@ -0,0 +1,177 @@ +#!/bin/bash +# +# Discover script for rooncommandline +# +# Author: Ronald Joe Record + +ROON=/usr/local/Roon +ROONETC=${ROON}/etc +ROON_INI=${ROONETC}/roon_api.ini +ROONCONF=${ROONETC}/pyroonconf +SUDO="$1" +SARG="$2" +SUSR="$3" + +export PATH=$PATH:/usr/local/bin +if [ -x /home/linuxbrew/.linuxbrew/bin/brew ]; then + eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" +else + if [ -x /opt/homebrew/bin/brew ]; then + eval "$(/opt/homebrew/bin/brew shellenv)" + else + [ -x /usr/local/bin/brew ] && eval "$(/usr/local/bin/brew shellenv)" + fi +fi + +echo "In order to configure the Python Roon API we must set the IP address" +echo "of the Roon Core. Discovery will be used to determine the Roon Core IP." +echo "When prompted for authorization, go to a Roon Remote window and click" +echo " Settings -> Extensions -> Enable" +echo "to authorize discovery" +echo "" + +# Get and set the Roon Core IP address +${SUDO} ${SARG} ${SUSR} ${ROON}/bin/get_core_ip 2>&1 | tee /tmp/discover$$ + +echo "Approval granted, retrieving zones and zone info ..." +sleep 5 +CORE_IP=$(cat /tmp/discover$$ | grep RoonCoreIP | awk -F '=' ' { print $2 } ') +CORE_IP="$(echo -e "${CORE_IP}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" +CORE_PORT=$(cat /tmp/discover$$ | grep RoonCorePort | awk -F '=' ' { print $2 } ') +CORE_PORT="$(echo -e "${CORE_PORT}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" +cat ${ROON_INI} | sed -e "s/${EX_CORE_IP}/${CORE_IP}/" -e "s/${EX_CORE_PORT}/${CORE_PORT}/" >/tmp/core$$ +cp /tmp/core$$ ${ROON_INI} +rm -f /tmp/core$$ /tmp/discover$$ /tmp/roonapi.log + +# Get and set the default zone and initial zone groupings +# Attempting to avoid grouping incompatible zones is somewhat convoluted +defaultZone= +groupOne= +groupTwo= +groupRee= +groupFor= +zones=$(${SUDO} ${SARG} ${SUSR} ${ROON}/bin/get_zones) +numZones=$(echo "${zones}" | awk -F ',' ' { print NF } ') +numWith=0 +[ ${numZones} -gt 0 ] && { + defaultZone=$(echo "${zones}" | awk -F ',' ' { print $1 } ') + withDefaultZone=$(${SUDO} ${SARG} ${SUSR} ${ROON}/bin/get_zone_info -l -z "${defaultZone}") + numWith=$(echo "${withDefaultZone}" | awk -F ',' ' { print NF } ') +} +[ ${numZones} -gt 2 ] && { + [ ${numWith} -gt 0 ] && { + groupOne=$(echo "${defaultZone},${withDefaultZone}" | sed -e "s/,/, /g" -e "s/:/,/") + # Find a zone not in groupOne and use it for the second zone group + zoneTwo= + numWithTwo=0 + zonearray=() + arrayone=() + unset saved_IFS + [ -n "${IFS+set}" ] && saved_IFS=$IFS + IFS="," + for i in ${zones}; do + thiszone="$(echo -e "$i" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + zonearray+=("${thiszone}") + done + for i in ${groupOne}; do + thisgroup="$(echo -e "$i" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + arrayone+=("${thisgroup}") + done + + Array3=() + for zone in ${zonearray[@]}; do + for one in ${arrayone[@]}; do + [ "${zone}" == "${one}" ] && continue 2 + done + Array3+=("${zone}") + done + + zoneTwo=${Array3[0]} + [ "${zoneTwo}" ] || zoneTwo=${arrayone[1]} + withZoneTwo=$(${SUDO} ${SARG} ${SUSR} ${ROON}/bin/get_zone_info -l -z "${zoneTwo}") + numWithTwo=$(echo "${withZoneTwo}" | awk -F ',' ' { print NF } ') + [ "${numWithTwo}" ] && { + [ -z "${numWithTwo//[0-9]}" ] && { + [ ${numWithTwo} -gt 0 ] && { + groupTwo=$(echo "${zoneTwo},${withZoneTwo}" | sed -e "s/${defaultZone},//" -e "s/,/, /g" -e "s/:/,/") + } + } + } + unset IFS + [ -n "${saved_IFS+set}" ] && { + IFS=$saved_IFS + unset saved_IFS + } + } +} +[ ${numZones} -gt 3 ] && { + secondZone=$(echo "${zones}" | awk -F ',' ' { print $2 } ' | sed -e "s/^ //") + withSecondZone=$(${SUDO} ${SARG} ${SUSR} ${ROON}/bin/get_zone_info -l -z "${secondZone}") + withSecondZone=$(echo ${withSecondZone} | sed -e "s/${defaultZone},//") + numWith=$(echo "${withSecondZone}" | awk -F ',' ' { print NF } ') + [ -z "${numWith//[0-9]}" ] && { + if [ ${numWith} -gt 1 ]; then + withDefOne=$(echo "${withSecondZone}" | awk -F ',' ' { print $1 ", " $2 } ') + groupRee="${secondZone}, ${withDefOne}" + else + [ ${numWith} -gt 0 ] && { + withDefOne=$(echo "${withSecondZone}" | awk -F ',' ' { print $1 } ') + groupRee="${secondZone}, ${withDefOne}" + } + fi + } +} +[ ${numZones} -gt 4 ] && { + lastZone=$(echo "${zones}" | awk -F ',' ' { print $(NF) } ' | sed -e "s/^ //") + withLastZone=$(${SUDO} ${SARG} ${SUSR} ${ROON}/bin/get_zone_info -l -z "${lastZone}") + withLastZone=$(echo ${withLastZone} | sed -e "s/${defaultZone},//") + withLastZone=$(echo ${withLastZone} | sed -e "s/${secondZone},//") + numWith=$(echo "${withLastZone}" | awk -F ',' ' { print NF } ') + if [ ${numWith} -gt 2 ]; then + withDefOne=$(echo "${withLastZone}" | awk -F ',' ' { print $(NF-2) ", " $(NF-1) ", " $(NF) } ') + groupFor="${lastZone}, ${withDefOne}" + else + if [ ${numWith} -gt 1 ]; then + withDefOne=$(echo "${withLastZone}" | awk -F ',' ' { print $(NF-1) ", " $(NF) } ') + groupFor="${lastZone}, ${withDefOne}" + else + [ ${numWith} -gt 0 ] && { + withDefOne=$(echo "${withLastZone}" | awk -F ',' ' { print $(NF) } ') + groupFor="${lastZone}, ${withDefOne}" + } + fi + fi +} +cat ${ROON_INI} \ + | sed -e "s/__ALL_ZONES__/${zones}/" \ + -e "s/__GROUP_ONE__/${groupOne}/" \ + -e "s/__GROUP_TWO__/${groupTwo}/" \ + -e "s/__GROUP_REE__/${groupRee}/" \ + -e "s/__GROUP_FOR__/${groupFor}/" \ + -e "s/__VERSION__/${version}/" \ + -e "s/__RELEASE__/${release}/" \ + -e "s/__DEF_ZONE__/${defaultZone}/" >/tmp/zones$$ +cp /tmp/zones$$ ${ROON_INI} +rm -f /tmp/zones$$ + +DEFZONE=$(grep ^DefaultZone ${ROON_INI} | awk -F '=' ' { print $2 } ') +# Remove leading and trailing spaces in DEFZONE +DEFZONE="$(echo -e "${DEFZONE}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" +# Set ROON_ZONE in pyroonconf if not already set +if [ -s ${ROONCONF} ]; then + grep ROON_ZONE ${ROONCONF} >/dev/null || { + echo "ROON_ZONE=\"$DEFZONE\"" >>${ROONCONF} + } +else + echo "ROON_ZONE=\"$DEFZONE\"" >${ROONCONF} +fi + +echo "" +echo "Verify the 'server' and 'user' settings in the roon script are correct." +echo "If you wish to deploy RoonCommandLine on other systems, install the package" +echo "there or copy the 'roon' frontend shell script to a location in your execution" +echo "PATH on those systems from which you wish to control Roon via SSH." +echo "" +echo "Edit the Roon Command Line configuration settings at:" +echo "${ROON_INI}" +echo "and verify the settings in the configuration file ${ROONCONF}" diff --git a/etc/postinstall b/etc/postinstall index 664ba5c..47b4d4f 100755 --- a/etc/postinstall +++ b/etc/postinstall @@ -91,8 +91,8 @@ if [ -f "${ROON_DOC}/VERSION" ]; then release=${RELEASE} else # Should not happen but if it did we fake it - version="2.1.3" - release="2" + version="2.1.4" + release="1" fi # Copy in distributed roon_api.ini template if no previous one exists @@ -146,7 +146,7 @@ rm -f /tmp/r$$ IP=$(hostname -I | awk ' { print $1 } ') -USER="root" +ROON_SSH_USER="root" numusers=0 users=() for homedir in /home/*; do @@ -155,12 +155,22 @@ for homedir in /home/*; do username=$(basename "${homedir}") exists=$(id -g -n "${username}" 2>/dev/null) [ "${exists}" ] && { - USER="${username}" + ROON_SSH_USER="${username}" users+=("${username}") numusers=$((numusers + 1)) } } done +[ $numusers -gt 1 ] && { + # Check for ROON_USER environment variable, use if set + if [ "${ROON_USER}" ] && [ -d /home/"${ROON_USER}" ]; then + ROON_SSH_USER="${ROON_USER}" + numusers=1 + else + # Unattended installs use the last user found + [ "${ROON_UNATTENDED}" ] && numusers=1 + fi +} [ $numusers -gt 1 ] && { # Create a selection dialog to allow user to select USER PS3="Please enter SSH user (numeric or text): " @@ -172,7 +182,7 @@ done ;; *) [ -d /home/"${opt}" ] && { - USER="${opt}" + ROON_SSH_USER="${opt}" break } printf "\nInvalid entry. Please try again" @@ -203,7 +213,7 @@ cp /tmp/roon$$ ${ROONCONF} rm -f /tmp/roon$$ echo "Setting the Python Roon API server IP address to $IP" -cat ${ROON}/bin/roon | sed -e "s/XX.X.X.XXX/$IP/" -e "s/SSH_USERNAME/$USER/" >/tmp/roon$$ +cat ${ROON}/bin/roon | sed -e "s/XX.X.X.XXX/$IP/" -e "s/SSH_USERNAME/$ROON_SSH_USER/" >/tmp/roon$$ cp ${ROON}/bin/roon ${ROON}/bin/roon.orig cp /tmp/roon$$ ${ROON}/bin/roon rm -f /tmp/roon$$ @@ -280,17 +290,17 @@ else echo "List commands will not function properly." fi -if [ "${USER}" ]; then - GROUP=$(id -g -n "${USER}") - chown -R "${USER}":"${GROUP}" ${ROONETC} - if [ "${USER}" == "root" ]; then +if [ "${ROON_SSH_USER}" ]; then + GROUP=$(id -g -n "${ROON_SSH_USER}") + chown -R "${ROON_SSH_USER}":"${GROUP}" ${ROONETC} + if [ "${ROON_SSH_USER}" == "root" ]; then SUDO= SARG= SUSR= else SUDO="sudo" SARG="-u" - SUSR="${USER}" + SUSR="${ROON_SSH_USER}" fi else SUDO= @@ -304,160 +314,16 @@ fi [ -x ${ROON}/etc/install-gum ] && ${SUDO} ${SARG} ${SUSR} ${ROON}/etc/install-gum echo "" -echo "In order to configure the Python Roon API we must set the IP address" -echo "of the Roon Core. Discovery will be used to determine the Roon Core IP." -echo "When prompted for authorization, go to a Roon Remote window and click" -echo " Settings -> Extensions -> Enable" -echo "to authorize discovery" -echo "" - -# Get and set the Roon Core IP address -${SUDO} ${SARG} ${SUSR} ${ROON}/bin/get_core_ip 2>&1 | tee /tmp/discover$$ - -echo "Approval granted, retrieving zones and zone info ..." -sleep 5 -CORE_IP=$(cat /tmp/discover$$ | grep RoonCoreIP | awk -F '=' ' { print $2 } ') -CORE_IP="$(echo -e "${CORE_IP}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" -CORE_PORT=$(cat /tmp/discover$$ | grep RoonCorePort | awk -F '=' ' { print $2 } ') -CORE_PORT="$(echo -e "${CORE_PORT}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" -cat ${ROON_INI} | sed -e "s/${EX_CORE_IP}/${CORE_IP}/" -e "s/${EX_CORE_PORT}/${CORE_PORT}/" >/tmp/core$$ -cp /tmp/core$$ ${ROON_INI} -rm -f /tmp/core$$ /tmp/discover$$ /tmp/roonapi.log - -# Get and set the default zone and initial zone groupings -# Attempting to avoid grouping incompatible zones is somewhat convoluted -defaultZone= -groupOne= -groupTwo= -groupRee= -groupFor= -zones=$(${SUDO} ${SARG} ${SUSR} ${ROON}/bin/get_zones) -numZones=$(echo "${zones}" | awk -F ',' ' { print NF } ') -numWith=0 -[ ${numZones} -gt 0 ] && { - defaultZone=$(echo "${zones}" | awk -F ',' ' { print $1 } ') - withDefaultZone=$(${SUDO} ${SARG} ${SUSR} ${ROON}/bin/get_zone_info -l -z "${defaultZone}") - numWith=$(echo "${withDefaultZone}" | awk -F ',' ' { print NF } ') -} -[ ${numZones} -gt 2 ] && { - [ ${numWith} -gt 0 ] && { - groupOne=$(echo "${defaultZone},${withDefaultZone}" | sed -e "s/,/, /g" -e "s/:/,/") - # Find a zone not in groupOne and use it for the second zone group - zoneTwo= - numWithTwo=0 - zonearray=() - arrayone=() - unset saved_IFS - [ -n "${IFS+set}" ] && saved_IFS=$IFS - IFS="," - for i in ${zones}; do - thiszone="$(echo -e "$i" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - zonearray+=("${thiszone}") - done - for i in ${groupOne}; do - thisgroup="$(echo -e "$i" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - arrayone+=("${thisgroup}") - done - - Array3=() - for zone in ${zonearray[@]}; do - for one in ${arrayone[@]}; do - [ "${zone}" == "${one}" ] && continue 2 - done - Array3+=("${zone}") - done - - zoneTwo=${Array3[0]} - [ "${zoneTwo}" ] || zoneTwo=${arrayone[1]} - withZoneTwo=$(${SUDO} ${SARG} ${SUSR} ${ROON}/bin/get_zone_info -l -z "${zoneTwo}") - numWithTwo=$(echo "${withZoneTwo}" | awk -F ',' ' { print NF } ') - [ "${numWithTwo}" ] && { - [ -z "${numWithTwo//[0-9]}" ] && { - [ ${numWithTwo} -gt 0 ] && { - groupTwo=$(echo "${zoneTwo},${withZoneTwo}" | sed -e "s/${defaultZone},//" -e "s/,/, /g" -e "s/:/,/") - } - } - } - unset IFS - [ -n "${saved_IFS+set}" ] && { - IFS=$saved_IFS - unset saved_IFS - } - } -} -[ ${numZones} -gt 3 ] && { - secondZone=$(echo "${zones}" | awk -F ',' ' { print $2 } ' | sed -e "s/^ //") - withSecondZone=$(${SUDO} ${SARG} ${SUSR} ${ROON}/bin/get_zone_info -l -z "${secondZone}") - withSecondZone=$(echo ${withSecondZone} | sed -e "s/${defaultZone},//") - numWith=$(echo "${withSecondZone}" | awk -F ',' ' { print NF } ') - [ -z "${numWith//[0-9]}" ] && { - if [ ${numWith} -gt 1 ]; then - withDefOne=$(echo "${withSecondZone}" | awk -F ',' ' { print $1 ", " $2 } ') - groupRee="${secondZone}, ${withDefOne}" - else - [ ${numWith} -gt 0 ] && { - withDefOne=$(echo "${withSecondZone}" | awk -F ',' ' { print $1 } ') - groupRee="${secondZone}, ${withDefOne}" - } - fi - } -} -[ ${numZones} -gt 4 ] && { - lastZone=$(echo "${zones}" | awk -F ',' ' { print $(NF) } ' | sed -e "s/^ //") - withLastZone=$(${SUDO} ${SARG} ${SUSR} ${ROON}/bin/get_zone_info -l -z "${lastZone}") - withLastZone=$(echo ${withLastZone} | sed -e "s/${defaultZone},//") - withLastZone=$(echo ${withLastZone} | sed -e "s/${secondZone},//") - numWith=$(echo "${withLastZone}" | awk -F ',' ' { print NF } ') - if [ ${numWith} -gt 2 ]; then - withDefOne=$(echo "${withLastZone}" | awk -F ',' ' { print $(NF-2) ", " $(NF-1) ", " $(NF) } ') - groupFor="${lastZone}, ${withDefOne}" - else - if [ ${numWith} -gt 1 ]; then - withDefOne=$(echo "${withLastZone}" | awk -F ',' ' { print $(NF-1) ", " $(NF) } ') - groupFor="${lastZone}, ${withDefOne}" - else - [ ${numWith} -gt 0 ] && { - withDefOne=$(echo "${withLastZone}" | awk -F ',' ' { print $(NF) } ') - groupFor="${lastZone}, ${withDefOne}" - } - fi - fi -} -cat ${ROON_INI} \ - | sed -e "s/__ALL_ZONES__/${zones}/" \ - -e "s/__GROUP_ONE__/${groupOne}/" \ - -e "s/__GROUP_TWO__/${groupTwo}/" \ - -e "s/__GROUP_REE__/${groupRee}/" \ - -e "s/__GROUP_FOR__/${groupFor}/" \ - -e "s/__VERSION__/${version}/" \ - -e "s/__RELEASE__/${release}/" \ - -e "s/__DEF_ZONE__/${defaultZone}/" >/tmp/zones$$ -cp /tmp/zones$$ ${ROON_INI} -rm -f /tmp/zones$$ - -DEFZONE=$(grep ^DefaultZone ${ROON_INI} | awk -F '=' ' { print $2 } ') -# Remove leading and trailing spaces in DEFZONE -DEFZONE="$(echo -e "${DEFZONE}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" -# Set ROON_ZONE in pyroonconf if not already set -if [ -s ${ROONCONF} ]; then - grep ROON_ZONE ${ROONCONF} >/dev/null || { - echo "ROON_ZONE=\"$DEFZONE\"" >>${ROONCONF} - } +if [ "${ROON_UNATTENDED}" ]; then + echo "Roon Core discovery and authorizing the RoonCommandLine extension" + echo "must be performed post-installation in unattended installations." + echo "After the installation of RoonCommandLine completes, run the command:" + echo " /usr/local/bin/roon -c discover" else - echo "ROON_ZONE=\"$DEFZONE\"" >${ROONCONF} + ${ROON}/etc/discover "${SUDO}" "${SARG}" "${SUSR}" fi - -[ "${USER}" ] && chown -R ${USER}:${GROUP} ${ROONETC} - -echo "" -echo "Verify the 'server' and 'user' settings in the roon script are correct." -echo "If you wish to deploy RoonCommandLine on other systems, install the package" -echo "there or copy the 'roon' frontend shell script to a location in your execution" -echo "PATH on those systems from which you wish to control Roon via SSH." -echo "" -echo "Edit the Roon Command Line configuration settings at:" -echo "${ROON_INI}" -echo "and verify the settings in the configuration file ${ROONCONF}" echo "" +[ "${ROON_SSH_USER}" ] && chown -R ${ROON_SSH_USER}:${GROUP} ${ROONETC} + exit 0 diff --git a/etc/roon_faded b/etc/roon_faded index cd522e0..ada183a 100755 --- a/etc/roon_faded +++ b/etc/roon_faded @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # Written by Ron Record December, 2022 # diff --git a/etc/upgrade-venv b/etc/upgrade-venv new file mode 100755 index 0000000..5356853 --- /dev/null +++ b/etc/upgrade-venv @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +ROON=/usr/local/Roon +PIPARGS="--no-cache-dir --upgrade --force-reinstall" + +if [[ $EUID -eq 0 ]] +then + SUDO= +else + SUDO=sudo +fi + +export PATH=$PATH:/usr/local/bin +if [ -x /home/linuxbrew/.linuxbrew/bin/brew ]; then + eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" +else + if [ -x /opt/homebrew/bin/brew ]; then + eval "$(/opt/homebrew/bin/brew shellenv)" + else + [ -x /usr/local/bin/brew ] && eval "$(/usr/local/bin/brew shellenv)" + fi +fi + +have_python3=$(type -p python3) +if [ "${have_python3}" ]; then + PYTHON=python3 +else + have_python=$(type -p python) + if [ "${have_python}" ]; then + PYTHON=python + else + echo "ERROR: unable to locate Python executable in PATH" + exit 1 + fi +fi + +[ -d ${ROON} ] || { + echo "ERROR: RoonCommandLine not installed." + exit 1 +} + +# Remove previously installed Python virtual environment +${SUDO} rm -rf ${ROON}/venv + +# Create the RoonCommandLine Python virtual environment +${SUDO} ${PYTHON} -m venv ${ROON}/venv >/dev/null 2>&1 +# Use the RoonCommandLine Python virtual environment +[ -f ${ROON}/venv/bin/activate ] && source ${ROON}/venv/bin/activate +[[ ":$PATH:" == *":/usr/local/Roon/venv/bin:"* ]] || { + export PATH=/usr/local/Roon/venv/bin:${PATH} +} +[ -x ${ROON}/venv/bin/python ] && export PYTHON=${ROON}/venv/bin/python +# Make sure we have pip installed +${SUDO} ${PYTHON} -m ensurepip --upgrade >/dev/null 2>&1 +[ -x ${ROON}/venv/bin/pip ] || { + PYOUT="/tmp/get-pip$$.py" + PYURL="https://bootstrap.pypa.io/get-pip.py" + have_curl=$(type -p curl) + if [ "${have_curl}" ]; then + curl -sSL -o ${PYOUT} ${PYURL} + else + have_wget=$(type -p wget) + if [ "${have_wget}" ]; then + wget -q -O ${PYOUT} ${PYURL} + else + echo "WARNING: Unable to locate curl or wget to download pip install script" + fi + fi + if [ -f ${PYOUT} ]; then + ${SUDO} ${PYTHON} ${PYOUT} + else + echo "WARNING: pip install script not found" + fi + rm -f ${PYOUT} +} +[ -x ${ROON}/venv/bin/pip ] || { + echo "WARNING: ${ROON}/venv/bin/pip not found or not executable" +} + +${SUDO} ${PYTHON} -m pip install ${PIPARGS} roonapi >/dev/null 2>&1 +${SUDO} ${PYTHON} -m pip install ${PIPARGS} rich-cli >/dev/null 2>&1 diff --git a/linInstall b/linInstall index 0241238..9cbd3cf 100755 --- a/linInstall +++ b/linInstall @@ -11,7 +11,25 @@ ROONETC=${ROON}/etc ROON_INI=${ROONETC}/roon_api.ini ROONCONF=${ROONETC}/pyroonconf TMP_ROON_API="/tmp/_roon_api_ini_.save" -export SUDO=sudo + +# User should not be root. Prompt to proceed if root user +iamroot= +SUDO="sudo -E" +if [ "${EUID}" ] +then + [ ${EUID} -eq 0 ] && { + iamroot=1 + SUDO= + } +else + uid=`id -u` + [ ${uid} -eq 0 ] && { + iamroot=1 + SUDO= + } +fi + +[ "$1" == "unattended" ] && export ROON_UNATTENDED="unattended" BOLD=$(tput bold 2>/dev/null) NORMAL=$(tput sgr0 2>/dev/null) @@ -52,20 +70,22 @@ SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) version=${VERSION} release=${RELEASE} -while true; do - read -r -p "Install ${PKG_NAME} version ${version}r${release} ? ('Y'/'N'): " yn - case $yn in - [Yy]*) - break - ;; - [Nn]*) - exit 0 - ;; - *) - echo "Please answer yes or no." - ;; - esac -done +[ "${ROON_UNATTENDED}" ] || { + while true; do + read -r -p "Install ${PKG_NAME} version ${version}r${release} ? ('Y'/'N'): " yn + case $yn in + [Yy]*) + break + ;; + [Nn]*) + exit 0 + ;; + *) + echo "Please answer yes or no." + ;; + esac + done +} [ -d /usr/local ] || ${SUDO} mkdir /usr/local [ -d /usr/local/bin ] || ${SUDO} mkdir /usr/local/bin @@ -175,7 +195,7 @@ ${SUDO} chown -R root:root ${ROON} # Try to configure the roon script with the IP and username IP=$(hostname -I | awk ' { print $1 } ') -USER=$(id -u -n) +ROON_SSH_USER=$(id -u -n) cd /usr/local/bin || exit 1 for command in "${ROON}"/bin/*; do @@ -227,6 +247,7 @@ fi ${SUDO} cp /tmp/r$$ ${ROON_INI} ${SUDO} rm -f /tmp/r$$ +ROON_SSH_USER="root" users=() numusers=0 for homedir in /home/*; do @@ -235,12 +256,23 @@ for homedir in /home/*; do username=$(basename "${homedir}") exists=$(id -g -n "${username}") [ "${exists}" ] && { + ROON_SSH_USER="${username}" users+=("${username}") numusers=$((numusers + 1)) } } done +[ $numusers -gt 1 ] && { + # Check for ROON_USER environment variable, use if set + if [ "${ROON_USER}" ] && [ -d /home/"${ROON_USER}" ]; then + ROON_SSH_USER="${ROON_USER}" + numusers=1 + else + # Unattended installs use the last user found + [ "${ROON_UNATTENDED}" ] && numusers=1 + fi +} [ ${numusers} -gt 1 ] && { while true; do read -r -p "Using ${USER} as the Roon user. 'Y' for OK, 'N' to select a different user: " yn @@ -255,7 +287,7 @@ done ;; *) [ -d /home/"${opt}" ] && { - USER="${opt}" + ROON_SSH_USER="${opt}" break } printf "\nInvalid entry. Please try again" @@ -287,7 +319,7 @@ fi ${SUDO} cp /tmp/roon$$ ${ROONCONF} ${SUDO} rm -f /tmp/roon$$ -cat ${ROON}/bin/roon | sed -e "s/XX.X.X.XXX/$IP/" -e "s/SSH_USERNAME/$USER/" >/tmp/roon$$ +cat ${ROON}/bin/roon | sed -e "s/XX.X.X.XXX/$IP/" -e "s/SSH_USERNAME/$ROON_SSH_USER/" >/tmp/roon$$ ${SUDO} cp ${ROON}/bin/roon ${ROON}/bin/roon.orig ${SUDO} cp /tmp/roon$$ ${ROON}/bin/roon ${SUDO} rm -f /tmp/roon$$ @@ -372,9 +404,9 @@ else echo "List commands will not function properly." fi -[ "${USER}" ] && { - GROUP=$(id -g -n "${USER}") - ${SUDO} chown -R "${USER}":"${GROUP}" ${ROONETC} +[ "${ROON_SSH_USER}" ] && { + GROUP=$(id -g -n "${ROON_SSH_USER}") + ${SUDO} chown -R "${ROON_SSH_USER}":"${GROUP}" ${ROONETC} } # Install utilities used by the RoonCommandLine menu system @@ -383,151 +415,14 @@ fi [ -x ${ROON}/etc/install-gum ] && ${ROON}/etc/install-gum echo "" -echo "In order to configure the Python Roon API we must set the IP address" -echo "of the Roon Core. Discovery will be used to determine the Roon Core IP." -echo "When prompted for authorization, go to a Roon Remote window and click" -echo " Settings -> Extensions -> Enable" -echo "to authorize discovery" -echo "" - -# Get and set the Roon Core IP address -${ROON}/bin/get_core_ip 2>&1 | tee /tmp/discover$$ -echo "Approval granted, retrieving zones and zone info ..." -CORE_IP=$(cat /tmp/discover$$ | grep RoonCoreIP | awk -F '=' ' { print $2 } ') -CORE_IP="$(echo -e "${CORE_IP}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" -CORE_PORT=$(cat /tmp/discover$$ | grep RoonCorePort | awk -F '=' ' { print $2 } ') -CORE_PORT="$(echo -e "${CORE_PORT}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - -cat ${ROON_INI} | sed -e "s/${EX_CORE_IP}/${CORE_IP}/" -e "s/${EX_CORE_PORT}/${CORE_PORT}/" >/tmp/core$$ -${SUDO} cp /tmp/core$$ ${ROON_INI} -${SUDO} rm -f /tmp/core$$ /tmp/discover$$ - -# Get and set the default zone and initial zone groupings -# Attempting to avoid grouping incompatible zones is somewhat convoluted -defaultZone= -groupOne= -groupTwo= -groupRee= -groupFor= -zones=$(${ROON}/bin/get_zones) -numZones=$(echo "${zones}" | awk -F ',' ' { print NF } ') -numWith=0 -[ ${numZones} -gt 0 ] && { - defaultZone=$(echo "${zones}" | awk -F ',' ' { print $1 } ') - withDefaultZone=$(${ROON}/bin/get_zone_info -l -z "${defaultZone}") - numWith=$(echo "${withDefaultZone}" | awk -F ',' ' { print NF } ') -} -[ ${numZones} -gt 2 ] && { - [ ${numWith} -gt 0 ] && { - groupOne=$(echo "${defaultZone},${withDefaultZone}" | sed -e "s/,/, /g" -e "s/:/,/") - # Find a zone not in groupOne and use it for the second zone group - zoneTwo= - numWithTwo=0 - zonearray=() - arrayone=() - unset saved_IFS - [ -n "${IFS+set}" ] && saved_IFS=$IFS - IFS="," - for i in ${zones}; do - thiszone="$(echo -e "$i" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - zonearray+=("${thiszone}") - done - for i in ${groupOne}; do - thisgroup="$(echo -e "$i" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - arrayone+=("${thisgroup}") - done - - Array3=() - for zone in ${zonearray[@]}; do - for one in ${arrayone[@]}; do - [ "${zone}" == "${one}" ] && continue 2 - done - Array3+=("${zone}") - done - - zoneTwo=${Array3[0]} - [ "${zoneTwo}" ] || zoneTwo=${arrayone[1]} - withZoneTwo=$(${ROON}/bin/get_zone_info -l -z "${zoneTwo}") - numWithTwo=$(echo "${withZoneTwo}" | awk -F ',' ' { print NF } ') - [ -z "${numWithTwo//[0-9]}" ] && { - [ ${numWithTwo} -gt 0 ] && { - groupTwo=$(echo "${zoneTwo},${withZoneTwo}" | sed -e "s/${defaultZone},//" -e "s/,/, /g" -e "s/:/,/") - } - } - unset IFS - [ -n "${saved_IFS+set}" ] && { - IFS=$saved_IFS - unset saved_IFS - } - } -} -[ ${numZones} -gt 3 ] && { - secondZone=$(echo "${zones}" | awk -F ',' ' { print $2 } ' | sed -e "s/^ //") - withSecondZone=$(${ROON}/bin/get_zone_info -l -z "${secondZone}") - withSecondZone=$(echo ${withSecondZone} | sed -e "s/${defaultZone},//") - numWith=$(echo "${withSecondZone}" | awk -F ',' ' { print NF } ') - if [ ${numWith} -gt 1 ]; then - withDefOne=$(echo "${withSecondZone}" | awk -F ',' ' { print $1 ", " $2 } ') - groupRee="${secondZone}, ${withDefOne}" - else - [ ${numWith} -gt 0 ] && { - withDefOne=$(echo "${withSecondZone}" | awk -F ',' ' { print $1 } ') - groupRee="${secondZone}, ${withDefOne}" - } - fi -} -[ ${numZones} -gt 4 ] && { - lastZone=$(echo "${zones}" | awk -F ',' ' { print $(NF) } ' | sed -e "s/^ //") - withLastZone=$(${ROON}/bin/get_zone_info -l -z "${lastZone}") - withLastZone=$(echo ${withLastZone} | sed -e "s/${defaultZone},//") - withLastZone=$(echo ${withLastZone} | sed -e "s/${secondZone},//") - numWith=$(echo "${withLastZone}" | awk -F ',' ' { print NF } ') - if [ ${numWith} -gt 2 ]; then - withDefOne=$(echo "${withLastZone}" | awk -F ',' ' { print $(NF-2) ", " $(NF-1) ", " $(NF) } ') - groupFor="${lastZone}, ${withDefOne}" - else - if [ ${numWith} -gt 1 ]; then - withDefOne=$(echo "${withLastZone}" | awk -F ',' ' { print $(NF-1) ", " $(NF) } ') - groupFor="${lastZone}, ${withDefOne}" - else - [ ${numWith} -gt 0 ] && { - withDefOne=$(echo "${withLastZone}" | awk -F ',' ' { print $(NF) } ') - groupFor="${lastZone}, ${withDefOne}" - } - fi - fi -} -cat ${ROON_INI} \ - | sed -e "s/__ALL_ZONES__/${zones}/" \ - -e "s/__GROUP_ONE__/${groupOne}/" \ - -e "s/__GROUP_TWO__/${groupTwo}/" \ - -e "s/__GROUP_REE__/${groupRee}/" \ - -e "s/__GROUP_FOR__/${groupFor}/" \ - -e "s/__VERSION__/${version}/" \ - -e "s/__RELEASE__/${release}/" \ - -e "s/__DEF_ZONE__/${defaultZone}/" >/tmp/zones$$ -${SUDO} cp /tmp/zones$$ ${ROON_INI} -rm -f /tmp/zones$$ - -DEFZONE=$(grep ^DefaultZone ${ROON_INI} | awk -F '=' ' { print $2 } ') -# Remove leading and trailing spaces in DEFZONE -DEFZONE="$(echo -e "${DEFZONE}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" -# Set ROON_ZONE in pyroonconf if not already set -if [ -s ${ROONCONF} ]; then - grep ROON_ZONE ${ROONCONF} >/dev/null || { - echo "ROON_ZONE=\"$DEFZONE\"" | ${SUDO} tee -a ${ROONCONF} >/dev/null - } +if [ "${ROON_UNATTENDED}" ]; then + echo "Roon Core discovery and authorizing the RoonCommandLine extension" + echo "must be performed post-installation in unattended installations." + echo "After the installation of RoonCommandLine completes, run the command:" + echo " /usr/local/bin/roon -c discover" else - echo "ROON_ZONE=\"$DEFZONE\"" | ${SUDO} tee ${ROONCONF} >/dev/null + ${ROON}/etc/discover "${SUDO}" "${SARG}" "${SUSR}" fi - -echo "" -echo "Verify the 'server' and 'user' settings in the roon script are correct." -echo "If you wish to deploy RoonCommandLine on other systems, install the package" -echo "there or copy the 'roon' frontend shell script to a location in your execution" -echo "PATH on those systems from which you wish to control Roon via SSH." -echo "" -echo "Edit the Roon Command Line configuration settings at:" -echo "${ROON_INI}" -echo "and verify the settings in the configuration file ${ROONCONF}" echo "" + +[ "${ROON_SSH_USER}" ] && chown -R ${ROON_SSH_USER}:${GROUP} ${ROONETC} diff --git a/macInstall b/macInstall index 5da3464..8139483 100755 --- a/macInstall +++ b/macInstall @@ -9,7 +9,25 @@ ROONETC=${ROON}/etc ROON_INI=${ROONETC}/roon_api.ini ROONCONF=${ROONETC}/pyroonconf TMP_ROON_API="/tmp/_roon_api_ini_.save" -export SUDO=sudo + +# User should not be root. Prompt to proceed if root user +iamroot= +SUDO="sudo -E" +if [ "${EUID}" ] +then + [ ${EUID} -eq 0 ] && { + iamroot=1 + SUDO= + } +else + uid=`id -u` + [ ${uid} -eq 0 ] && { + iamroot=1 + SUDO= + } +fi + +[ "$1" == "unattended" ] && export ROON_UNATTENDED="unattended" BOLD=$(tput bold 2>/dev/null) NORMAL=$(tput sgr0 2>/dev/null) @@ -98,24 +116,25 @@ SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) version=${VERSION} release=${RELEASE} -while true; do - read -r -p "Install ${PKG_NAME} version ${version}r${release} ? ('Y'/'N'): " yn - case $yn in - [Yy]*) - break - ;; - [Nn]*) - exit 0 - ;; - *) - echo "Please answer yes or no." - ;; - esac -done +[ "${ROON_UNATTENDED}" ] || { + while true; do + read -r -p "Install ${PKG_NAME} version ${version}r${release} ? ('Y'/'N'): " yn + case $yn in + [Yy]*) + break + ;; + [Nn]*) + exit 0 + ;; + *) + echo "Please answer yes or no." + ;; + esac + done +} # Install/upgrade the Python Roon API ${SUDO} ${PYTHON} -m pip install --upgrade roonapi >/dev/null 2>&1 - ${SUDO} ${PYTHON} -m pip install rich-cli >/dev/null 2>&1 [ -d /usr/local ] || ${SUDO} mkdir /usr/local @@ -191,7 +210,7 @@ ${SUDO} chown -R root:wheel ${ROON} # Try to configure the roon script with the IP and username IP=$(ifconfig en0 | awk '/inet / {print $2; }') -USER=$(id -u -n) +ROON_SSH_USER=$(id -u -n) cd /usr/local/bin || exit 1 for command in "${ROON}"/bin/*; do @@ -245,6 +264,7 @@ fi ${SUDO} cp /tmp/r$$ ${ROON_INI} ${SUDO} rm -f /tmp/r$$ +ROON_SSH_USER="root" users=() numusers=0 for homedir in /Users/*; do @@ -254,15 +274,26 @@ for homedir in /Users/*; do username=$(basename "${homedir}") exists=$(id -g -n "${username}" 2>/dev/null) [ "${exists}" ] && { + ROON_SSH_USER="${username}" users+=("${username}") numusers=$((numusers + 1)) } } done +[ $numusers -gt 1 ] && { + # Check for ROON_USER environment variable, use if set + if [ "${ROON_USER}" ] && [ -d /home/"${ROON_USER}" ]; then + ROON_SSH_USER="${ROON_USER}" + numusers=1 + else + # Unattended installs use the last user found + [ "${ROON_UNATTENDED}" ] && numusers=1 + fi +} [ ${numusers} -gt 1 ] && { while true; do - read -r -p "Using ${USER} as the Roon user. 'Y' for OK, 'N' to select a different user: " yn + read -r -p "Using ${ROON_SSH_USER} as the Roon user. 'Y' for OK, 'N' to select a different user: " yn case $yn in [Nn]*) PS3="${BOLD}Please enter Roon user (numeric or text): ${NORMAL}" @@ -274,7 +305,7 @@ done ;; *) [ -d /Users/${opt} ] && { - USER="${opt}" + ROON_SSH_USER="${opt}" break } printf "\nInvalid entry. Please try again" @@ -316,7 +347,7 @@ fi ${SUDO} cp /tmp/roon$$ ${ROONCONF} ${SUDO} rm -f /tmp/roon$$ -cat ${ROON}/bin/roon | sed -e "s/XX.X.X.XXX/$IP/" -e "s/SSH_USERNAME/$USER/" >/tmp/roon$$ +cat ${ROON}/bin/roon | sed -e "s/XX.X.X.XXX/$IP/" -e "s/SSH_USERNAME/$ROON_SSH_USER/" >/tmp/roon$$ ${SUDO} cp ${ROON}/bin/roon ${ROON}/bin/roon.orig ${SUDO} cp /tmp/roon$$ ${ROON}/bin/roon ${SUDO} rm -f /tmp/roon$$ @@ -401,9 +432,9 @@ else echo "List commands will not function properly." fi -[ "${USER}" ] && { - GROUP=$(id -g -n "${USER}") - ${SUDO} chown -R "${USER}":"${GROUP}" ${ROONETC} +[ "${ROON_SSH_USER}" ] && { + GROUP=$(id -g -n "${ROON_SSH_USER}") + ${SUDO} chown -R "${ROON_SSH_USER}":"${GROUP}" ${ROONETC} } # Install utilities used by the RoonCommandLine menu system @@ -412,159 +443,14 @@ fi [ -x ${ROON}/etc/install-gum ] && ${ROON}/etc/install-gum echo "" -echo "In order to configure the Python Roon API we must set the IP address" -echo "of the Roon Core. Discovery will be used to determine the Roon Core IP." -echo "When prompted for authorization, go to a Roon Remote window and click" -echo " Settings -> Extensions -> Enable" -echo "to authorize discovery" -echo "" - -# Get and set the Roon Core IP address -if [ "${USER}" ]; then - ${SUDO} -u "${USER}" ${ROON}/bin/get_core_ip 2>&1 | tee /tmp/discover$$ +if [ "${ROON_UNATTENDED}" ]; then + echo "Roon Core discovery and authorizing the RoonCommandLine extension" + echo "must be performed post-installation in unattended installations." + echo "After the installation of RoonCommandLine completes, run the command:" + echo " /usr/local/bin/roon -c discover" else - ${ROON}/bin/get_core_ip 2>&1 | tee /tmp/discover$$ + ${ROON}/etc/discover "${SUDO}" "${SARG}" "${SUSR}" fi -echo "Approval granted, retrieving zones and zone info ..." -CORE_IP=$(cat /tmp/discover$$ | grep RoonCoreIP | awk -F '=' ' { print $2 } ') -CORE_IP="$(echo -e "${CORE_IP}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" -CORE_PORT=$(cat /tmp/discover$$ | grep RoonCorePort | awk -F '=' ' { print $2 } ') -CORE_PORT="$(echo -e "${CORE_PORT}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" -# Could use Python (or better, yq) to modify roon_api.ini something like: -# python -c 'import yaml;f=open("roon_api.ini");y=yaml.safe_load(f);y["DEFAULT"]["RoonCoreIP"] = ${CORE_IP}; print(yaml.dump(y, default_flow_style=False, sort_keys=False))' -# But for now we use sed -cat ${ROON_INI} | sed -e "s/${EX_CORE_IP}/${CORE_IP}/" -e "s/${EX_CORE_PORT}/${CORE_PORT}/" >/tmp/core$$ -${SUDO} cp /tmp/core$$ ${ROON_INI} -${SUDO} rm -f /tmp/core$$ /tmp/discover$$ - -# Get and set the default zone and initial zone groupings -# Attempting to avoid grouping incompatible zones is somewhat convoluted -defaultZone= -groupOne= -groupTwo= -groupRee= -groupFor= -zones=$(${ROON}/bin/get_zones) -numZones=$(echo "${zones}" | awk -F ',' ' { print NF } ') -numWith=0 -[ ${numZones} -gt 0 ] && { - defaultZone=$(echo "${zones}" | awk -F ',' ' { print $1 } ') - withDefaultZone=$(${ROON}/bin/get_zone_info -l -z "${defaultZone}") - numWith=$(echo "${withDefaultZone}" | awk -F ',' ' { print NF } ') -} -[ ${numZones} -gt 2 ] && { - [ ${numWith} -gt 0 ] && { - groupOne=$(echo "${defaultZone},${withDefaultZone}" | sed -e "s/,/, /g" -e "s/:/,/") - # Find a zone not in groupOne and use it for the second zone group - zoneTwo= - numWithTwo=0 - zonearray=() - arrayone=() - unset saved_IFS - [ -n "${IFS+set}" ] && saved_IFS=$IFS - IFS="," - for i in ${zones}; do - thiszone="$(echo -e "$i" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - zonearray+=("${thiszone}") - done - for i in ${groupOne}; do - thisgroup="$(echo -e "$i" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - arrayone+=("${thisgroup}") - done - - Array3=() - for zone in ${zonearray[@]}; do - for one in ${arrayone[@]}; do - [ "${zone}" == "${one}" ] && continue 2 - done - Array3+=("${zone}") - done - - zoneTwo=${Array3[0]} - [ "${zoneTwo}" ] || zoneTwo=${arrayone[1]} - withZoneTwo=$(${ROON}/bin/get_zone_info -l -z "${zoneTwo}") - numWithTwo=$(echo "${withZoneTwo}" | awk -F ',' ' { print NF } ') - [ -z "${numWithTwo//[0-9]}" ] && { - [ ${numWithTwo} -gt 0 ] && { - groupTwo=$(echo "${zoneTwo},${withZoneTwo}" | sed -e "s/${defaultZone},//" -e "s/,/, /g" -e "s/:/,/") - } - } - unset IFS - [ -n "${saved_IFS+set}" ] && { - IFS=$saved_IFS - unset saved_IFS - } - } -} -[ ${numZones} -gt 3 ] && { - secondZone=$(echo "${zones}" | awk -F ',' ' { print $2 } ' | sed -e "s/^ //") - withSecondZone=$(${ROON}/bin/get_zone_info -l -z "${secondZone}") - withSecondZone=$(echo ${withSecondZone} | sed -e "s/${defaultZone},//") - numWith=$(echo "${withSecondZone}" | awk -F ',' ' { print NF } ') - if [ ${numWith} -gt 1 ]; then - withDefOne=$(echo "${withSecondZone}" | awk -F ',' ' { print $1 ", " $2 } ') - groupRee="${secondZone}, ${withDefOne}" - else - [ ${numWith} -gt 0 ] && { - withDefOne=$(echo "${withSecondZone}" | awk -F ',' ' { print $1 } ') - groupRee="${secondZone}, ${withDefOne}" - } - fi -} -[ ${numZones} -gt 4 ] && { - lastZone=$(echo "${zones}" | awk -F ',' ' { print $(NF) } ' | sed -e "s/^ //") - withLastZone=$(${ROON}/bin/get_zone_info -l -z "${lastZone}") - withLastZone=$(echo ${withLastZone} | sed -e "s/${defaultZone},//") - withLastZone=$(echo ${withLastZone} | sed -e "s/${secondZone},//") - numWith=$(echo "${withLastZone}" | awk -F ',' ' { print NF } ') - if [ ${numWith} -gt 2 ]; then - withDefOne=$(echo "${withLastZone}" | awk -F ',' ' { print $(NF-2) ", " $(NF-1) ", " $(NF) } ') - groupFor="${lastZone}, ${withDefOne}" - else - if [ ${numWith} -gt 1 ]; then - withDefOne=$(echo "${withLastZone}" | awk -F ',' ' { print $(NF-1) ", " $(NF) } ') - groupFor="${lastZone}, ${withDefOne}" - else - [ ${numWith} -gt 0 ] && { - withDefOne=$(echo "${withLastZone}" | awk -F ',' ' { print $(NF) } ') - groupFor="${lastZone}, ${withDefOne}" - } - fi - fi -} -cat ${ROON_INI} \ - | sed -e "s/__ALL_ZONES__/${zones}/" \ - -e "s/__GROUP_ONE__/${groupOne}/" \ - -e "s/__GROUP_TWO__/${groupTwo}/" \ - -e "s/__GROUP_REE__/${groupRee}/" \ - -e "s/__GROUP_FOR__/${groupFor}/" \ - -e "s/__VERSION__/${version}/" \ - -e "s/__RELEASE__/${release}/" \ - -e "s/__DEF_ZONE__/${defaultZone}/" >/tmp/zones$$ -${SUDO} cp /tmp/zones$$ ${ROON_INI} -rm -f /tmp/zones$$ - -DEFZONE=$(grep ^DefaultZone ${ROON_INI} | awk -F '=' ' { print $2 } ') -# Remove leading and trailing spaces in DEFZONE -DEFZONE="$(echo -e "${DEFZONE}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" -# Set ROON_ZONE in pyroonconf if not already set -if [ -f ${ROONCONF} ]; then - grep ROON_ZONE ${ROONCONF} >/dev/null || { - echo "ROON_ZONE=\"$DEFZONE\"" | ${SUDO} tee -a ${ROONCONF} >/dev/null - } -else - echo "ROON_ZONE=\"$DEFZONE\"" | ${SUDO} tee ${ROONCONF} >/dev/null -fi - -[ "${USER}" ] && ${SUDO} chown -R ${USER}:${GROUP} ${ROONETC} - -echo "" -echo "Verify the 'server' and 'user' settings in the roon script are correct." -echo "If you wish to deploy RoonCommandLine on other systems, install the package" -echo "there or copy the 'roon' frontend shell script to a location in your execution" -echo "PATH on those systems from which you wish to control Roon via SSH." -echo "" -echo "Edit the Roon Command Line configuration settings at:" -echo "${ROON_INI}" -echo "and verify the settings in the configuration file ${ROONCONF}" echo "" + +[ "${ROON_SSH_USER}" ] && chown -R ${ROON_SSH_USER}:${GROUP} ${ROONETC} diff --git a/pkg/release.md b/pkg/release.md index 27e9cb3..7df41b9 100644 --- a/pkg/release.md +++ b/pkg/release.md @@ -1,10 +1,15 @@ # RoonCommandLine Release Notes -RoonCommandLine version 2.1.3 release 2 integrates the `Roon Community Remote` GUI for Linux +RoonCommandLine version 2.1.4 release 1 provides support for unattended installation. -- Add menu options to install, update, and open the Roon GUI -- Install `figlet`, `gum`, and `fzf` in postinstall -- Add splash screen to menu open +- Set and export the `ROON_UNATTENDED` environment variable to perform an unattended installation + - `export ROON_UNATTENDED="unattended"` + - Use `sudo -E ...` to install Debian or RPM format packages + - Alternately, `./Install unattended` will also perform an unattended installation + - After an unattended installation, execute `/usr/local/bin/roon -c discover`. +- This release checks and repairs the RoonCommandLine Python virtual environment + - Can occur if the system Python is upgraded and the previously installed Python is removed + - Typically this issue is restricted to Homebrew installs of Python ## Installation @@ -15,13 +20,13 @@ Download the latest Debian or RPM package format release from the **Assets** sec Install the package on Debian based systems by executing the command ```bash -sudo apt install ./RoonCommandLine_2.1.3-2.deb +sudo apt install ./RoonCommandLine_2.1.4-1.deb ``` Install the package on RPM based systems by executing the command ```bash -sudo yum localinstall ./RoonCommandLine-2.1.3-2.rpm +sudo yum localinstall ./RoonCommandLine-2.1.4-1.rpm ``` Removal of the package on Debian based systems can be accomplished by issuing the command: @@ -46,6 +51,12 @@ cd RoonCommandLine ## Release history +RoonCommandLine version 2.1.3 release 2 integrates the `Roon Community Remote` GUI for Linux + +- Add menu options to install, update, and open the Roon GUI +- Install `figlet`, `gum`, and `fzf` in postinstall +- Add splash screen to menu open + RoonCommandLine version 2.1.3 release 1 provides support for specifying the default zone and last zone used - Add usage to `get_zone_info`