New menus/complete_uninstall.sh (Core Settings), the last of the "destructive trio". Rather than re-implementing every addon's teardown a second time (the legacy shape), it composes the *_do_uninstall helpers each addon already has - if an addon's removal logic changes, Complete Uninstall picks it up automatically. Every addon menu with an uninstall action (CUPS, VNC, WireGuard, Tailscale, Netbird, LMS, Squeezelite, Asterisk Intercom) plus power_schedule's "remove all schedules" and Emergency Hotspot's disable action were each split into a confirm-and-call wrapper (unchanged from the user's perspective) and a silent do-the-removal helper that both the wrapper and Complete Uninstall call. Bug fix found while composing these: several *_do_uninstall helpers (CUPS's apt autoremove/apt clean, VNC/WireGuard/Tailscale/Netbird's apt remove) had a bare, unguarded apt call as their second-to-last statement. Previously this only risked aborting that one menu action if the package was already gone. Composed together as sequential calls inside Complete Uninstall, the same failure would have silently truncated the entire uninstall sequence partway through. Guarded all of them with `|| true`. Non-addon teardown (kiosk user/files, Node.js, LightDM/Openbox, remaining systemd units/scripts, polkit rules, re-enabling virtual consoles, final package cleanup) stays inline in menus/complete_uninstall.sh, since no single addon owns those paths. Upgrade and Full Reinstall stay in ubuntu-based-kiosk.sh only - both are coupled to its own heredoc self-extraction of main.js/preload.js/ etc, which has no modular equivalent yet. Full command-level stubbed test suite exercising the full 12-step teardown, confirmation-text validation, and reboot prompt. Full 19-suite regression + real end-to-end menu navigation via install.sh all pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VfsFSoRqfbRG7XAg5RoE7e
363 lines
13 KiB
Bash
363 lines
13 KiB
Bash
#!/bin/bash
|
||
################################################################################
|
||
# menus/addon_lms_squeezelite.sh - "LMS Server / Squeezelite Player" addon.
|
||
#
|
||
# Two independent pieces sharing one menu, same as the legacy code: an LMS
|
||
# (Lyrion/Logitech Media Server) server the kiosk can host, and a
|
||
# Squeezelite player the kiosk can run to play music from any LMS server
|
||
# (this one or another one on the LAN). LMS itself is a real apt-managed
|
||
# subsystem with its own fixed paths (repo file, GPG keyring, ufw rules,
|
||
# /etc/squeezeboxserver) - like CUPS, those get full command-level `sudo`/
|
||
# `wget`/`apt` stubbing in tests rather than relocation. Squeezelite's own
|
||
# start script and systemd unit are ours to place, so - like power_schedule
|
||
# and the other addons - they go through $BIN_DIR/$SYSTEMD_DIR (lib/
|
||
# config.sh) instead of hardcoded /usr/local/bin and /etc/systemd/system,
|
||
# so tests can point them at a scratch directory.
|
||
#
|
||
# LMS ships under two package/service names depending on version -
|
||
# "logitechmediaserver" (older) and "lyrionmusicserver" (the project's
|
||
# current name after its rename) - so detection and every service call
|
||
# has to check both.
|
||
#
|
||
# Depends on: lib/menu.sh, lib/config.sh being sourced first.
|
||
################################################################################
|
||
|
||
lms_service_name() {
|
||
if systemctl list-unit-files 2>/dev/null | grep -q "lyrionmusicserver.service"; then
|
||
echo "lyrionmusicserver"
|
||
elif systemctl list-unit-files 2>/dev/null | grep -q "logitechmediaserver.service"; then
|
||
echo "logitechmediaserver"
|
||
fi
|
||
}
|
||
|
||
lms_is_installed() {
|
||
is_service_active logitechmediaserver || is_service_enabled logitechmediaserver || \
|
||
is_service_active lyrionmusicserver || is_service_enabled lyrionmusicserver
|
||
}
|
||
|
||
lms_is_running() {
|
||
is_service_active logitechmediaserver || is_service_active lyrionmusicserver
|
||
}
|
||
|
||
squeezelite_is_installed() {
|
||
is_service_active squeezelite || is_service_enabled squeezelite
|
||
}
|
||
|
||
addon_lms_squeezelite_status() {
|
||
if lms_is_installed; then
|
||
echo "LMS Server: Installed"
|
||
if lms_is_running; then
|
||
echo " Status: Running"
|
||
else
|
||
echo " Status: Stopped"
|
||
fi
|
||
echo " Web: http://$(get_ip_address):9000"
|
||
echo
|
||
fi
|
||
|
||
if squeezelite_is_installed; then
|
||
local player_name="Unknown"
|
||
if [[ -f "$BIN_DIR/squeezelite-start.sh" ]]; then
|
||
player_name=$(grep '^PLAYER_NAME=' "$BIN_DIR/squeezelite-start.sh" 2>/dev/null | cut -d'=' -f2 | tr -d '"' || echo "Unknown")
|
||
fi
|
||
echo "Squeezelite Player: Installed"
|
||
if is_service_active squeezelite; then
|
||
echo " Status: Running"
|
||
else
|
||
echo " Status: Stopped"
|
||
fi
|
||
echo " Name: $player_name"
|
||
echo
|
||
fi
|
||
|
||
echo "ℹ A server is needed to stream music. The kiosk can run the"
|
||
echo " server (if sufficient resources) or connect to another server."
|
||
}
|
||
|
||
addon_lms_squeezelite_menu_builder() {
|
||
MENU_LABELS=("Install/Configure LMS Server" "Install/Configure Squeezelite Player")
|
||
MENU_HANDLERS=(action_install_lms action_install_squeezelite)
|
||
|
||
if lms_is_installed; then
|
||
MENU_LABELS+=("Uninstall LMS Server")
|
||
MENU_HANDLERS+=(action_uninstall_lms)
|
||
fi
|
||
|
||
if squeezelite_is_installed; then
|
||
MENU_LABELS+=("Uninstall Squeezelite Player")
|
||
MENU_HANDLERS+=(action_uninstall_squeezelite)
|
||
fi
|
||
}
|
||
|
||
addon_lms_squeezelite_menu() {
|
||
run_menu "LMS SERVER / SQUEEZELITE PLAYER" addon_lms_squeezelite_menu_builder addon_lms_squeezelite_status
|
||
}
|
||
|
||
################################################################################
|
||
# Actions - LMS Server
|
||
################################################################################
|
||
|
||
action_install_lms() {
|
||
echo
|
||
if lms_is_installed; then
|
||
echo "LMS is already installed."
|
||
if ask_yes_no "Reconfigure port?" "n"; then
|
||
local new_port
|
||
new_port=$(ask_integer "New HTTP port" 9000 1 65535)
|
||
sudo sed -i "s/httpport:.*/httpport: $new_port/" /etc/squeezeboxserver/prefs/server.prefs 2>/dev/null || true
|
||
sudo systemctl restart lyrionmusicserver 2>/dev/null || sudo systemctl restart logitechmediaserver 2>/dev/null || true
|
||
log_success "LMS reconfigured on port $new_port"
|
||
fi
|
||
pause
|
||
return
|
||
fi
|
||
|
||
echo "Installing Lyrion Music Server..."
|
||
|
||
# Try the repository method first.
|
||
if wget -qO - https://debian.slimdevices.com/debian/squeezebox-keyring.gpg | sudo gpg --dearmor -o /usr/share/keyrings/lms-keyring.gpg 2>/dev/null; then
|
||
echo "deb [signed-by=/usr/share/keyrings/lms-keyring.gpg] http://debian.slimdevices.com/debian stable main" | sudo tee /etc/apt/sources.list.d/lms.list
|
||
# `|| true`: a bare, unguarded `apt update` failing here (bad
|
||
# mirror, no network) would otherwise crash the whole session
|
||
# under set -e instead of falling through to the direct-download
|
||
# fallback below, which is exactly the degrade path this is
|
||
# supposed to hit when the repository route doesn't work.
|
||
sudo apt update 2>/dev/null || true
|
||
if sudo apt install -y logitechmediaserver 2>/dev/null; then
|
||
log_success "LMS installed via repository"
|
||
else
|
||
log_warning "Repository install failed, trying direct download..."
|
||
fi
|
||
fi
|
||
|
||
# Fall back to a direct .deb download if the repository didn't produce
|
||
# either possible package.
|
||
if ! command -v logitechmediaserver &>/dev/null && ! command -v lyrionmusicserver &>/dev/null; then
|
||
local lms_deb="/tmp/lms.deb"
|
||
echo "Downloading LMS v9.0.3..."
|
||
if wget -q https://downloads.lms-community.org/LyrionMusicServer_v9.0.3/lyrionmusicserver_9.0.3_amd64.deb -O "$lms_deb"; then
|
||
echo "Installing LMS package..."
|
||
if sudo apt install -y "$lms_deb"; then
|
||
log_success "LMS installed via direct download"
|
||
else
|
||
log_error "Failed to install LMS package"
|
||
rm -f "$lms_deb"
|
||
pause
|
||
return 1
|
||
fi
|
||
rm -f "$lms_deb"
|
||
else
|
||
log_error "Failed to download LMS from lms-community.org"
|
||
pause
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
local service_name
|
||
service_name=$(lms_service_name)
|
||
|
||
if [[ -z "$service_name" ]]; then
|
||
log_warning "Service file not found, checking installed files..."
|
||
service_name=$(dpkg -L lyrionmusicserver logitechmediaserver 2>/dev/null | grep -m1 '\.service$' | xargs -r basename | sed 's/\.service$//' || echo "")
|
||
fi
|
||
|
||
if [[ -z "$service_name" ]]; then
|
||
log_error "Could not detect LMS service name"
|
||
echo "Manual steps:"
|
||
echo " 1. Find service: systemctl list-unit-files | grep -i lms"
|
||
echo " 2. Enable: sudo systemctl enable SERVICE_NAME"
|
||
echo " 3. Start: sudo systemctl start SERVICE_NAME"
|
||
pause
|
||
return 1
|
||
fi
|
||
|
||
log_info "Using service: $service_name"
|
||
# enable_and_start_units, not a bare `sudo systemctl enable ... | tee`
|
||
# pipe: the legacy version's `2>&1 | tee /tmp/lms-enable.log` made the
|
||
# whole statement's exit status depend on `tee` (always 0) rather than
|
||
# `systemctl enable`, so a real enable/start failure was silently
|
||
# swallowed instead of falling through to the warning below.
|
||
if enable_and_start_units "$service_name"; then
|
||
sudo ufw allow 9000/tcp comment 'LMS-HTTP' 2>/dev/null || true
|
||
sudo ufw allow 3483/tcp comment 'LMS-SlimProto' 2>/dev/null || true
|
||
sudo ufw allow 3483/udp comment 'LMS-Discovery' 2>/dev/null || true
|
||
|
||
log_success "LMS installed"
|
||
echo " Web interface: http://$(get_ip_address):9000"
|
||
else
|
||
log_warning "LMS installed, but systemctl enable/start failed - check 'systemctl status $service_name'"
|
||
fi
|
||
|
||
pause
|
||
}
|
||
|
||
action_uninstall_lms() {
|
||
echo
|
||
ask_yes_no "Remove LMS Server?" "n" || { echo "Cancelled"; pause; return; }
|
||
lms_do_uninstall ask
|
||
pause
|
||
}
|
||
|
||
# The actual removal, no confirmation prompt - shared with Complete
|
||
# Uninstall so that operation doesn't need to re-implement LMS teardown a
|
||
# second time. $1: "ask" to prompt about data removal interactively (the
|
||
# normal case), "purge" to remove data without asking (Complete Uninstall).
|
||
lms_do_uninstall() {
|
||
local data_choice="${1:-ask}"
|
||
|
||
local service_name
|
||
service_name=$(lms_service_name)
|
||
|
||
if [[ -n "$service_name" ]]; then
|
||
echo "Stopping $service_name..."
|
||
sudo systemctl stop "$service_name" 2>/dev/null || true
|
||
sudo systemctl disable "$service_name" 2>/dev/null || true
|
||
fi
|
||
|
||
# Try to remove both possible package names - only one will actually
|
||
# be installed, the other is a harmless no-op.
|
||
sudo apt remove -y lyrionmusicserver 2>/dev/null || true
|
||
sudo apt remove -y logitechmediaserver 2>/dev/null || true
|
||
|
||
sudo rm -f /etc/apt/sources.list.d/lms.list
|
||
sudo rm -f /usr/share/keyrings/lms-keyring.gpg
|
||
|
||
local purge_data=false
|
||
if [[ "$data_choice" == "purge" ]]; then
|
||
purge_data=true
|
||
elif [[ "$data_choice" == "ask" ]] && ask_yes_no "Remove LMS data and configuration?" "n"; then
|
||
purge_data=true
|
||
fi
|
||
|
||
if $purge_data; then
|
||
sudo rm -rf /var/lib/squeezeboxserver
|
||
sudo rm -rf /etc/squeezeboxserver
|
||
log_success "LMS and data removed"
|
||
else
|
||
log_success "LMS removed (data preserved)"
|
||
fi
|
||
}
|
||
|
||
################################################################################
|
||
# Actions - Squeezelite Player
|
||
################################################################################
|
||
|
||
action_install_squeezelite() {
|
||
echo
|
||
if squeezelite_is_installed; then
|
||
echo "Squeezelite is already installed."
|
||
ask_yes_no "Reconfigure?" "n" || { pause; return; }
|
||
fi
|
||
|
||
if ! command -v squeezelite &>/dev/null; then
|
||
if ! sudo apt install -y squeezelite; then
|
||
log_error "squeezelite package installation failed"
|
||
pause
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
local player_name
|
||
player_name=$(ask_text "Player name" "Kiosk")
|
||
|
||
echo
|
||
echo "LMS Server Configuration:"
|
||
echo " Enter IP:PORT of your LMS server"
|
||
echo " Leave blank for auto-discovery on LAN"
|
||
echo
|
||
local lms_server
|
||
lms_server=$(ask_text "LMS Server (e.g., 192.168.1.100:3483)" "")
|
||
|
||
sudo tee "$BIN_DIR/squeezelite-start.sh" > /dev/null <<SQSTART
|
||
#!/bin/bash
|
||
|
||
PLAYER_NAME="$player_name"
|
||
LMS_SERVER="$lms_server"
|
||
|
||
for i in {1..20}; do
|
||
pactl info >/dev/null 2>&1 && break
|
||
sleep 1
|
||
done
|
||
|
||
if ! pactl info >/dev/null 2>&1; then
|
||
logger "ERROR: Squeezelite - PipeWire not available"
|
||
exit 1
|
||
fi
|
||
|
||
if [[ -n "\$LMS_SERVER" ]]; then
|
||
exec /usr/bin/squeezelite -n "\$PLAYER_NAME" -s "\$LMS_SERVER" -o pulse -a 80:4:: -b 512:1024 -C 5
|
||
else
|
||
exec /usr/bin/squeezelite -n "\$PLAYER_NAME" -o pulse -a 80:4:: -b 512:1024 -C 5
|
||
fi
|
||
SQSTART
|
||
|
||
sudo chmod +x "$BIN_DIR/squeezelite-start.sh"
|
||
|
||
local kiosk_uid
|
||
kiosk_uid=$(id -u "$KIOSK_USER")
|
||
|
||
sudo tee "$SYSTEMD_DIR/squeezelite.service" > /dev/null <<EOF
|
||
[Unit]
|
||
Description=Squeezelite
|
||
After=sound.target network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=$KIOSK_USER
|
||
Environment="XDG_RUNTIME_DIR=/run/user/$kiosk_uid"
|
||
ExecStartPre=/bin/sleep 10
|
||
ExecStart=${BIN_DIR}/squeezelite-start.sh
|
||
Restart=always
|
||
RestartSec=10
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
sudo systemctl daemon-reload 2>/dev/null || true
|
||
# Enable only, not start: squeezelite needs the kiosk user's real
|
||
# session (PipeWire, XDG_RUNTIME_DIR) up first, which is why a reboot
|
||
# is required below rather than starting it immediately.
|
||
if ! sudo systemctl enable squeezelite 2>/dev/null; then
|
||
log_warning "Squeezelite files written, but 'systemctl enable' failed - check 'systemctl status squeezelite'"
|
||
fi
|
||
|
||
log_success "Squeezelite installed: $player_name"
|
||
if [[ -n "$lms_server" ]]; then
|
||
echo " Server: $lms_server"
|
||
else
|
||
echo " Server: Auto-discovery"
|
||
fi
|
||
echo
|
||
echo "⚠️ IMPORTANT: Squeezelite requires a reboot to work properly"
|
||
echo
|
||
if ask_yes_no "Reboot now?" "n"; then
|
||
echo "Rebooting in 5 seconds..."
|
||
sleep 5
|
||
sudo reboot
|
||
else
|
||
echo "⚠️ Remember to reboot before using Squeezelite"
|
||
echo " Command: sudo reboot"
|
||
fi
|
||
|
||
pause
|
||
}
|
||
|
||
action_uninstall_squeezelite() {
|
||
echo
|
||
ask_yes_no "Remove Squeezelite Player?" "n" || { echo "Cancelled"; pause; return; }
|
||
squeezelite_do_uninstall
|
||
pause
|
||
}
|
||
|
||
# Shared with Complete Uninstall - same reasoning as lms_do_uninstall.
|
||
squeezelite_do_uninstall() {
|
||
sudo systemctl stop squeezelite 2>/dev/null || true
|
||
sudo systemctl disable squeezelite 2>/dev/null || true
|
||
sudo rm -f "$SYSTEMD_DIR/squeezelite.service"
|
||
sudo rm -f "$BIN_DIR/squeezelite-start.sh"
|
||
sudo apt remove -y squeezelite 2>/dev/null || true
|
||
log_success "Squeezelite removed"
|
||
}
|