1004 lines
39 KiB
Bash
1004 lines
39 KiB
Bash
#!/bin/bash
|
|
|
|
# ===================================================================================
|
|
# Shairport-Sync AirPlay 2 ROBUST Installer - ENHANCED VERSION 3.0
|
|
#
|
|
# Tailored for: Raspberry Pi (Zero 2/3/4/5) with USB DAC
|
|
# Version: 3.0 - Production Ready
|
|
# Features:
|
|
# - Comprehensive error handling with rollback capability
|
|
# - Dependency validation before installation
|
|
# - Build failure recovery
|
|
# - Audio device validation
|
|
# - Service health checks
|
|
# - Firewall configuration
|
|
# - Latest package versions
|
|
# ===================================================================================
|
|
|
|
# curl -X PUT "http://192.168.23.218:3689/api/player/stop"
|
|
# curl -X POST "http://192.168.23.218:3689/api/queue/items/add?uris=http://icecast.radiofrance.fr/fip-hifi.aac&clear=true&playback=start"
|
|
|
|
|
|
set -eo pipefail # Exit on error and pipe failures
|
|
IFS=$'\n\t' # Safer word splitting
|
|
|
|
# --- Global Variables ---
|
|
SCRIPT_VERSION="3.0"
|
|
LOG_FILE="/tmp/airplay_install_$(date +%Y%m%d_%H%M%S).log"
|
|
BACKUP_DIR="/tmp/airplay_backup_$(date +%Y%m%d_%H%M%S)"
|
|
INSTALLATION_FAILED=0
|
|
|
|
# Audio configuration variables
|
|
audio_device=""
|
|
audio_device_plug=""
|
|
card_number=""
|
|
device_number=""
|
|
mixer_control=""
|
|
selected_device=""
|
|
airplay_name=""
|
|
disable_wifi_pm=false
|
|
|
|
# --- Cleanup Handler ---
|
|
cleanup() {
|
|
local exit_code=$?
|
|
if [ $exit_code -ne 0 ] && [ $INSTALLATION_FAILED -eq 1 ]; then
|
|
cecho "red" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
cecho "red" " Installation Failed - Cleaning Up"
|
|
cecho "red" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
|
|
# Stop services if they were started
|
|
sudo systemctl stop shairport-sync 2>/dev/null || true
|
|
sudo systemctl stop nqptp 2>/dev/null || true
|
|
|
|
# Restore backups if they exist
|
|
if [ -d "$BACKUP_DIR" ]; then
|
|
cecho "yellow" "Restoring original configuration..."
|
|
[ -f "$BACKUP_DIR/shairport-sync.conf" ] && \
|
|
sudo cp "$BACKUP_DIR/shairport-sync.conf" /etc/shairport-sync.conf 2>/dev/null || true
|
|
fi
|
|
|
|
cecho "yellow" "Installation log saved to: $LOG_FILE"
|
|
cecho "yellow" "Please check the log for details."
|
|
fi
|
|
|
|
# Cleanup temp build directories
|
|
rm -rf /tmp/nqptp /tmp/shairport-sync 2>/dev/null || true
|
|
}
|
|
|
|
trap cleanup EXIT ERR INT TERM
|
|
|
|
# --- Logging Function ---
|
|
log() {
|
|
# Create log file if it doesn't exist
|
|
touch "$LOG_FILE" 2>/dev/null || true
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
|
|
}
|
|
|
|
# --- Helper Functions ---
|
|
cecho() {
|
|
local code="\033["
|
|
case "$1" in
|
|
"red") color="${code}1;31m" ;;
|
|
"green") color="${code}1;32m" ;;
|
|
"yellow") color="${code}1;33m" ;;
|
|
"blue") color="${code}1;34m" ;;
|
|
"magenta") color="${code}1;35m" ;;
|
|
*) color="${code}0m" ;;
|
|
esac
|
|
echo -e "${color}$2\033[0m"
|
|
}
|
|
|
|
# Show spinner during long operations
|
|
show_spinner() {
|
|
local pid=$1
|
|
local delay=0.1
|
|
local spinstr='|/-\'
|
|
while ps -p $pid > /dev/null 2>&1; do
|
|
local temp=${spinstr#?}
|
|
printf " [%c] " "$spinstr"
|
|
local spinstr=$temp${spinstr%"$temp"}
|
|
sleep $delay
|
|
printf "\b\b\b\b\b\b"
|
|
done
|
|
printf " \b\b\b\b"
|
|
}
|
|
|
|
# Check if a command exists
|
|
command_exists() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
# Check if a service is running properly
|
|
check_service() {
|
|
local service_name=$1
|
|
local max_retries=${2:-3}
|
|
local retry=0
|
|
|
|
while [ $retry -lt $max_retries ]; do
|
|
if systemctl is-active --quiet "$service_name"; then
|
|
cecho "green" "✓ $service_name is running"
|
|
return 0
|
|
fi
|
|
retry=$((retry + 1))
|
|
[ $retry -lt $max_retries ] && sleep 2
|
|
done
|
|
|
|
cecho "red" "✗ $service_name failed to start after $max_retries attempts"
|
|
cecho "yellow" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
cecho "yellow" " Diagnostic Information:"
|
|
cecho "yellow" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo
|
|
echo "Service Status:" | tee -a "$LOG_FILE"
|
|
sudo systemctl status "$service_name" --no-pager -l 2>&1 | tee -a "$LOG_FILE"
|
|
echo
|
|
echo "Recent Logs:" | tee -a "$LOG_FILE"
|
|
sudo journalctl -u "$service_name" -n 30 --no-pager 2>&1 | tee -a "$LOG_FILE"
|
|
echo
|
|
cecho "yellow" "Check the log file for more details: $LOG_FILE"
|
|
return 1
|
|
}
|
|
|
|
# Validate package installation
|
|
validate_package() {
|
|
local package=$1
|
|
if dpkg -l "$package" 2>/dev/null | grep -q "^ii"; then
|
|
return 0
|
|
else
|
|
cecho "red" "✗ Package $package failed to install"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Safe directory change
|
|
safe_cd() {
|
|
cd "$1" || {
|
|
cecho "red" "Failed to change directory to: $1"
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# --- Pre-flight Checks ---
|
|
pre_flight_checks() {
|
|
cecho "blue" "═══════════════════════════════════════"
|
|
cecho "blue" " Running Pre-Flight Checks..."
|
|
cecho "blue" "═══════════════════════════════════════"
|
|
echo
|
|
|
|
# Check for root user
|
|
if [ "$EUID" -eq 0 ]; then
|
|
cecho "red" "❌ Error: Don't run this script with sudo or as root."
|
|
cecho "yellow" " Just run: bash install_airplay_v3.sh"
|
|
exit 1
|
|
fi
|
|
|
|
# Check if user can sudo
|
|
if ! sudo -n true 2>/dev/null; then
|
|
cecho "yellow" "Checking sudo access..."
|
|
if ! sudo true; then
|
|
cecho "red" "❌ This script requires sudo access."
|
|
exit 1
|
|
fi
|
|
fi
|
|
cecho "green" "✓ Sudo access confirmed"
|
|
|
|
# Check for internet connection
|
|
cecho "yellow" "Checking internet connection..."
|
|
local test_hosts=("8.8.8.8" "1.1.1.1" "github.com")
|
|
local connection_ok=0
|
|
|
|
for host in "${test_hosts[@]}"; do
|
|
# Use timeout command to prevent hanging
|
|
cecho "blue" " Testing $host..."
|
|
timeout 8 ping -c 1 -W 5 "$host" >/dev/null 2>&1 || true
|
|
local result=$?
|
|
if [ $result -eq 0 ]; then
|
|
connection_ok=1
|
|
log "Internet check: Successfully pinged $host"
|
|
cecho "green" " ✓ Connected"
|
|
break
|
|
elif [ $result -eq 124 ]; then
|
|
log "Internet check: Timeout pinging $host"
|
|
cecho "yellow" " ✗ Timeout"
|
|
else
|
|
log "Internet check: Failed to ping $host (exit $result)"
|
|
cecho "yellow" " ✗ Failed"
|
|
fi
|
|
done
|
|
|
|
if [ $connection_ok -eq 0 ]; then
|
|
cecho "red" "❌ No internet connection detected."
|
|
cecho "yellow" " Troubleshooting:"
|
|
cecho "yellow" " 1. Check Wi-Fi is connected: iwconfig"
|
|
cecho "yellow" " 2. Test manually: ping -c 3 8.8.8.8"
|
|
cecho "yellow" " 3. Check DNS: ping -c 3 github.com"
|
|
echo
|
|
cecho "blue" " Your network status:"
|
|
ip addr show wlan0 2>/dev/null | grep "inet " || echo " No wlan0 IP address"
|
|
echo
|
|
read -p "Skip internet check and continue anyway? (y/N): " skip_check || true
|
|
if [[ "$skip_check" =~ ^[Yy]$ ]]; then
|
|
cecho "yellow" "⚠ Continuing without internet check (may fail later)"
|
|
log "User skipped internet check"
|
|
else
|
|
exit 1
|
|
fi
|
|
else
|
|
cecho "green" "✓ Internet connection OK"
|
|
fi
|
|
|
|
# Check if running on Raspberry Pi
|
|
if [ ! -f /proc/device-tree/model ]; then
|
|
cecho "yellow" "⚠ Warning: This doesn't appear to be a Raspberry Pi."
|
|
read -p "Continue anyway? (y/N): " continue_choice || true
|
|
[[ ! "$continue_choice" =~ ^[Yy]$ ]] && exit 1
|
|
else
|
|
local pi_model
|
|
pi_model=$(tr -d '\0' < /proc/device-tree/model)
|
|
cecho "green" "✓ Detected: $pi_model"
|
|
|
|
# Warn on older Pi models
|
|
if echo "$pi_model" | grep -qE "Pi Zero W|Pi 1"; then
|
|
cecho "yellow" "⚠ Warning: $pi_model may not have enough power for AirPlay 2"
|
|
cecho "yellow" " Recommended: Pi Zero 2 or newer"
|
|
read -p "Continue anyway? (y/N): " continue_choice || true
|
|
[[ ! "$continue_choice" =~ ^[Yy]$ ]] && exit 1
|
|
fi
|
|
fi
|
|
|
|
# Check available disk space (need at least 1GB)
|
|
local available_space
|
|
available_space=$(df / | tail -1 | awk '{print $4}')
|
|
if [ "$available_space" -lt 1000000 ]; then
|
|
cecho "red" "❌ Not enough disk space. Need at least 1GB free."
|
|
cecho "yellow" " Current available: $((available_space / 1024)) MB"
|
|
exit 1
|
|
fi
|
|
cecho "green" "✓ Sufficient disk space available: $((available_space / 1024)) MB"
|
|
|
|
# Check available memory
|
|
local available_mem
|
|
available_mem=$(free -m | awk '/^Mem:/{print $7}')
|
|
if [ "$available_mem" -lt 100 ]; then
|
|
cecho "yellow" "⚠ Warning: Low available memory ($available_mem MB)"
|
|
cecho "yellow" " Consider closing other applications."
|
|
else
|
|
cecho "green" "✓ Available memory: $available_mem MB"
|
|
fi
|
|
|
|
# Check for required base tools
|
|
cecho "yellow" "Checking required tools..."
|
|
local required_tools=("git" "gcc" "make" "aplay" "amixer")
|
|
local missing_tools=()
|
|
|
|
for tool in "${required_tools[@]}"; do
|
|
if ! command_exists "$tool"; then
|
|
missing_tools+=("$tool")
|
|
fi
|
|
done
|
|
|
|
if [ ${#missing_tools[@]} -gt 0 ]; then
|
|
cecho "yellow" "⚠ Missing tools: ${missing_tools[*]}"
|
|
cecho "yellow" " These will be installed with dependencies."
|
|
else
|
|
cecho "green" "✓ All base tools present"
|
|
fi
|
|
|
|
echo
|
|
}
|
|
|
|
# --- Detect and Select Audio Device (FIXED) ---
|
|
select_audio_device() {
|
|
echo
|
|
cecho "yellow" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
cecho "yellow" " Step 1: Audio Device Selection"
|
|
cecho "yellow" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo
|
|
cecho "cyan" "⏸ PLEASE RESPOND TO THIS PROMPT ⏸"
|
|
echo
|
|
|
|
# Get list of all audio cards into an array
|
|
cecho "blue" "Scanning for audio devices..."
|
|
mapfile -t all_cards < <(aplay -l 2>/dev/null | grep '^card')
|
|
|
|
if [ ${#all_cards[@]} -eq 0 ]; then
|
|
cecho "red" "❌ No audio devices detected at all!"
|
|
cecho "yellow" " Make sure your USB DAC is connected."
|
|
cecho "yellow" " Try running 'lsusb' to check hardware connection."
|
|
exit 1
|
|
fi
|
|
|
|
# Always show the menu and force manual selection
|
|
cecho "green" "Available audio devices:"
|
|
for i in "${!all_cards[@]}"; do
|
|
# Display 1-based index for user friendliness
|
|
echo " [$((i+1))] ${all_cards[$i]}"
|
|
done
|
|
echo
|
|
|
|
local device_choice
|
|
local array_index
|
|
|
|
while true; do
|
|
read -p "Enter the number of your device [1-${#all_cards[@]}]: " device_choice || true
|
|
|
|
# Check if input is a number
|
|
if [[ "$device_choice" =~ ^[0-9]+$ ]]; then
|
|
# Convert 1-based input to 0-based array index
|
|
array_index=$((device_choice - 1))
|
|
|
|
# Check if index exists in array
|
|
if [ "$array_index" -ge 0 ] && [ "$array_index" -lt "${#all_cards[@]}" ]; then
|
|
selected_device="${all_cards[$array_index]}"
|
|
break
|
|
fi
|
|
fi
|
|
cecho "red" "Invalid selection. Please enter a number between 1 and ${#all_cards[@]}."
|
|
done
|
|
|
|
cecho "green" "✓ You selected: $selected_device"
|
|
|
|
# Extract card and device numbers
|
|
# Regex looks for "card X" and "device Y"
|
|
card_number=$(echo "$selected_device" | grep -oP 'card \K\d+' || echo "")
|
|
device_number=$(echo "$selected_device" | grep -oP 'device \K\d+' || echo "0")
|
|
|
|
if [ -z "$card_number" ]; then
|
|
cecho "red" "❌ Failed to extract card number from selection."
|
|
exit 1
|
|
fi
|
|
|
|
audio_device="hw:$card_number,$device_number"
|
|
audio_device_plug="plughw:$card_number,$device_number"
|
|
|
|
cecho "green" "✓ Audio device set to: $audio_device_plug"
|
|
|
|
# Validate the audio device actually works
|
|
cecho "blue" "Validating audio device..."
|
|
if aplay -D "$audio_device_plug" -l >/dev/null 2>&1; then
|
|
cecho "green" "✓ Audio device validation passed"
|
|
else
|
|
cecho "red" "❌ Audio device validation failed"
|
|
cecho "yellow" " The device may not support playback."
|
|
read -p "Continue anyway? (y/N): " continue_choice || true
|
|
[[ ! "$continue_choice" =~ ^[Yy]$ ]] && exit 1
|
|
fi
|
|
|
|
# Detect available mixer controls for this card
|
|
cecho "blue" "Detecting volume controls..."
|
|
mapfile -t mixers < <(amixer -c "$card_number" scontrols 2>/dev/null | grep -oP "Simple mixer control '\K[^']+" || true)
|
|
|
|
if [ ${#mixers[@]} -eq 0 ]; then
|
|
cecho "yellow" "⚠ No hardware mixer controls found."
|
|
cecho "yellow" " Software volume will be used."
|
|
mixer_control=""
|
|
else
|
|
cecho "green" "Available mixer controls:"
|
|
for mixer in "${mixers[@]}"; do
|
|
echo " - $mixer"
|
|
done
|
|
|
|
# Try to find the best mixer control
|
|
mixer_control=""
|
|
for preferred in "PCM" "Master" "Speaker" "Headphone" "Digital"; do
|
|
for mixer in "${mixers[@]}"; do
|
|
if [[ "$mixer" == "$preferred" ]]; then
|
|
mixer_control="$mixer"
|
|
break 2
|
|
fi
|
|
done
|
|
done
|
|
|
|
# If no preferred mixer found, use the first one
|
|
if [ -z "$mixer_control" ]; then
|
|
mixer_control="${mixers[0]}"
|
|
fi
|
|
|
|
cecho "green" "✓ Selected volume control: $mixer_control"
|
|
fi
|
|
echo
|
|
}
|
|
|
|
# --- Get AirPlay Name ---
|
|
get_airplay_name() {
|
|
echo
|
|
cecho "yellow" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
cecho "yellow" " Step 2: Name Your AirPlay Device"
|
|
cecho "yellow" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo
|
|
cecho "cyan" "⏸ PLEASE RESPOND TO THIS PROMPT ⏸"
|
|
echo
|
|
|
|
local hostname
|
|
hostname=$(hostname)
|
|
cecho "blue" "This is the name that will appear on your iPhone/iPad."
|
|
cecho "blue" "Examples: Living Room, Bedroom Speaker, Kitchen Audio"
|
|
echo
|
|
cecho "green" ">>> "
|
|
read -p "Enter a name (or press Enter for '$hostname AirPlay'): " airplay_name || true
|
|
|
|
if [ -z "$airplay_name" ]; then
|
|
airplay_name="$hostname AirPlay"
|
|
fi
|
|
|
|
# Sanitize the name (remove special characters that might cause issues)
|
|
airplay_name=$(echo "$airplay_name" | sed 's/[^a-zA-Z0-9 _-]//g')
|
|
|
|
cecho "green" "✓ AirPlay name: '$airplay_name'"
|
|
echo
|
|
}
|
|
|
|
# --- Wi-Fi Power Management ---
|
|
configure_wifi() {
|
|
echo
|
|
cecho "yellow" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
cecho "yellow" " Step 3: Wi-Fi Optimization"
|
|
cecho "yellow" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo
|
|
cecho "cyan" "⏸ PLEASE RESPOND TO THIS PROMPT ⏸"
|
|
echo
|
|
|
|
# Check if Wi-Fi is actually being used
|
|
if ! ip link show wlan0 &>/dev/null; then
|
|
cecho "yellow" "⚠ No Wi-Fi interface detected (wlan0 not found)"
|
|
cecho "yellow" " Skipping Wi-Fi optimization."
|
|
disable_wifi_pm=false
|
|
echo
|
|
return
|
|
fi
|
|
|
|
cecho "blue" "Wi-Fi power saving can cause audio stuttering and dropouts."
|
|
cecho "blue" "Disabling it ensures smooth, uninterrupted playback."
|
|
echo
|
|
cecho "green" ">>> "
|
|
read -p "Disable Wi-Fi power saving? (Y/n): " wifi_choice || true
|
|
|
|
if [[ -z "$wifi_choice" || "$wifi_choice" =~ ^[Yy]$ ]]; then
|
|
disable_wifi_pm=true
|
|
cecho "green" "✓ Wi-Fi power management will be disabled"
|
|
else
|
|
disable_wifi_pm=false
|
|
cecho "yellow" "⚠ Keeping default Wi-Fi settings (may cause dropouts)"
|
|
fi
|
|
echo
|
|
}
|
|
|
|
# --- Main Installation ---
|
|
main() {
|
|
# Initialize log file immediately
|
|
touch "$LOG_FILE" 2>/dev/null || LOG_FILE="/tmp/airplay_install_fallback.log"
|
|
|
|
clear
|
|
cecho "green" "╔═════════════════════════════════════════════════════╗"
|
|
cecho "green" "║ ║"
|
|
cecho "green" "║ AirPlay 2 Installer for Raspberry Pi + DAC ║"
|
|
cecho "green" "║ Version $SCRIPT_VERSION ║"
|
|
cecho "green" "║ ║"
|
|
cecho "green" "╚═════════════════════════════════════════════════════╝"
|
|
echo
|
|
cecho "blue" "This installer will turn your Raspberry Pi into a"
|
|
cecho "blue" "high-quality AirPlay 2 receiver. Just follow the prompts!"
|
|
echo
|
|
cecho "yellow" "Installation log: $LOG_FILE"
|
|
echo
|
|
|
|
log "=== AirPlay 2 Installation Started ==="
|
|
log "Script Version: $SCRIPT_VERSION"
|
|
log "Date: $(date)"
|
|
log "User: $(whoami)"
|
|
log "System: $(uname -a)"
|
|
echo
|
|
|
|
# Check if running interactively
|
|
if [ -t 0 ]; then
|
|
read -p "Press Enter to begin..." || true
|
|
else
|
|
cecho "yellow" "⚠ Non-interactive mode detected - using defaults"
|
|
sleep 2
|
|
fi
|
|
echo
|
|
|
|
# Run all setup steps
|
|
pre_flight_checks
|
|
select_audio_device
|
|
get_airplay_name
|
|
configure_wifi
|
|
|
|
# --- Confirmation ---
|
|
echo
|
|
echo
|
|
cecho "magenta" "╔═════════════════════════════════════════════════════╗"
|
|
cecho "magenta" "║ INSTALLATION CONFIGURATION ║"
|
|
cecho "magenta" "╚═════════════════════════════════════════════════════╝"
|
|
echo
|
|
cecho "yellow" " 📱 AirPlay Name: $airplay_name"
|
|
cecho "yellow" " 🔊 Audio Output: $audio_device_plug"
|
|
cecho "yellow" " 🎚️ Volume Control: ${mixer_control:-None (fixed volume)}"
|
|
cecho "yellow" " 📡 Disable Wi-Fi PM: $disable_wifi_pm"
|
|
echo
|
|
cecho "blue" "Installation will take 10-30 minutes depending on your Pi model."
|
|
cecho "blue" "(Pi Zero 2 will be slower, Pi 4/5 will be faster)"
|
|
echo
|
|
echo
|
|
cecho "cyan" "⏸ FINAL CONFIRMATION - PRESS ENTER TO CONTINUE ⏸"
|
|
echo
|
|
if [ -t 0 ]; then
|
|
read -p "Press Enter to start installation, or Ctrl+C to cancel..." || true
|
|
else
|
|
cecho "yellow" "Auto-starting in 5 seconds (non-interactive mode)..."
|
|
sleep 5
|
|
fi
|
|
echo
|
|
|
|
INSTALLATION_FAILED=1 # Mark that installation has started
|
|
|
|
# Create backup directory
|
|
mkdir -p "$BACKUP_DIR"
|
|
[ -f /etc/shairport-sync.conf ] && cp /etc/shairport-sync.conf "$BACKUP_DIR/" 2>/dev/null || true
|
|
|
|
# --- System Update ---
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
cecho "blue" " Updating System Packages..."
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
log "Updating package lists..."
|
|
|
|
if ! sudo apt-get update -qq 2>&1 | tee -a "$LOG_FILE"; then
|
|
cecho "red" "❌ Failed to update package lists"
|
|
exit 1
|
|
fi
|
|
|
|
cecho "yellow" "Upgrading existing packages (this may take a few minutes)..."
|
|
if ! sudo apt-get upgrade -y 2>&1 | tee -a "$LOG_FILE"; then
|
|
cecho "yellow" "⚠ Package upgrade had issues, but continuing..."
|
|
fi
|
|
|
|
cecho "green" "✓ System updated"
|
|
echo
|
|
|
|
# --- Install Dependencies ---
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
cecho "blue" " Installing Dependencies..."
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
log "Installing build dependencies..."
|
|
|
|
local dependencies=(
|
|
build-essential git autoconf automake libtool pkg-config
|
|
libpopt-dev libconfig-dev libasound2-dev
|
|
avahi-daemon libavahi-client-dev libssl-dev
|
|
libsoxr-dev libplist-dev libsodium-dev
|
|
libavutil-dev libavcodec-dev libavformat-dev
|
|
uuid-dev libgcrypt20-dev xxd alsa-utils
|
|
)
|
|
|
|
if ! sudo apt-get install -y "${dependencies[@]}" 2>&1 | tee -a "$LOG_FILE"; then
|
|
cecho "red" "❌ Failed to install dependencies"
|
|
exit 1
|
|
fi
|
|
|
|
# Validate critical packages
|
|
local critical_packages=("build-essential" "git" "libasound2-dev" "avahi-daemon")
|
|
for package in "${critical_packages[@]}"; do
|
|
if ! validate_package "$package"; then
|
|
cecho "red" "❌ Critical package $package is not installed"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
cecho "green" "✓ Dependencies installed"
|
|
echo
|
|
|
|
# Make sure avahi-daemon is running
|
|
if ! systemctl is-active --quiet avahi-daemon; then
|
|
cecho "yellow" "Starting avahi-daemon..."
|
|
sudo systemctl enable avahi-daemon
|
|
sudo systemctl start avahi-daemon
|
|
fi
|
|
|
|
# --- Install NQPTP ---
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
cecho "blue" " Installing NQPTP (Timing System)..."
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
log "Cloning NQPTP repository..."
|
|
|
|
# Verify /tmp is writable
|
|
if [ ! -w /tmp ]; then
|
|
cecho "red" "❌ /tmp directory is not writable"
|
|
exit 1
|
|
fi
|
|
|
|
safe_cd /tmp
|
|
rm -rf nqptp 2>/dev/null || true
|
|
|
|
if ! git clone https://github.com/mikebrady/nqptp.git 2>&1 | tee -a "$LOG_FILE"; then
|
|
cecho "red" "❌ Failed to clone NQPTP repository"
|
|
cecho "yellow" " Possible causes:"
|
|
cecho "yellow" " - No internet connection"
|
|
cecho "yellow" " - GitHub is down"
|
|
cecho "yellow" " - Firewall blocking access"
|
|
exit 1
|
|
fi
|
|
|
|
safe_cd nqptp
|
|
log "Building NQPTP..."
|
|
|
|
if ! autoreconf -fi 2>&1 | tee -a "$LOG_FILE"; then
|
|
cecho "red" "❌ NQPTP autoreconf failed"
|
|
exit 1
|
|
fi
|
|
|
|
if ! ./configure --with-systemd-startup 2>&1 | tee -a "$LOG_FILE"; then
|
|
cecho "red" "❌ NQPTP configure failed"
|
|
exit 1
|
|
fi
|
|
|
|
if ! make -j"$(nproc)" 2>&1 | tee -a "$LOG_FILE"; then
|
|
cecho "red" "❌ NQPTP compilation failed"
|
|
exit 1
|
|
fi
|
|
|
|
if ! sudo make install 2>&1 | tee -a "$LOG_FILE"; then
|
|
cecho "red" "❌ NQPTP installation failed"
|
|
exit 1
|
|
fi
|
|
|
|
# Verify binary was installed
|
|
if ! command_exists nqptp; then
|
|
cecho "red" "❌ NQPTP binary not found after installation"
|
|
cecho "yellow" " Expected location: /usr/local/bin/nqptp"
|
|
exit 1
|
|
fi
|
|
|
|
# Enable and start NQPTP
|
|
sudo systemctl enable nqptp 2>&1 | tee -a "$LOG_FILE"
|
|
sudo systemctl restart nqptp 2>&1 | tee -a "$LOG_FILE"
|
|
sleep 3
|
|
|
|
if ! check_service "nqptp"; then
|
|
cecho "red" "❌ NQPTP service failed to start"
|
|
exit 1
|
|
fi
|
|
echo
|
|
|
|
# --- Install Shairport-Sync ---
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
cecho "blue" " Installing Shairport-Sync..."
|
|
cecho "blue" " (This takes 10-20 mins on slower Pis)"
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
log "Cloning Shairport-Sync repository..."
|
|
|
|
safe_cd /tmp
|
|
rm -rf shairport-sync 2>/dev/null || true
|
|
|
|
if ! git clone https://github.com/mikebrady/shairport-sync.git 2>&1 | tee -a "$LOG_FILE"; then
|
|
cecho "red" "❌ Failed to clone Shairport-Sync repository"
|
|
cecho "yellow" " Possible causes:"
|
|
cecho "yellow" " - No internet connection"
|
|
cecho "yellow" " - GitHub is down"
|
|
cecho "yellow" " - Firewall blocking access"
|
|
exit 1
|
|
fi
|
|
|
|
safe_cd shairport-sync
|
|
log "Building Shairport-Sync..."
|
|
|
|
if ! autoreconf -fi 2>&1 | tee -a "$LOG_FILE"; then
|
|
cecho "red" "❌ Shairport-Sync autoreconf failed"
|
|
exit 1
|
|
fi
|
|
|
|
cecho "yellow" "Configuring build..."
|
|
if ! ./configure --sysconfdir=/etc --with-alsa --with-avahi \
|
|
--with-ssl=openssl --with-soxr --with-systemd \
|
|
--with-airplay-2 2>&1 | tee -a "$LOG_FILE"; then
|
|
cecho "red" "❌ Shairport-Sync configure failed"
|
|
exit 1
|
|
fi
|
|
|
|
cecho "yellow" "Compiling (be patient, this takes time)..."
|
|
log "Starting compilation with $(nproc) cores..."
|
|
|
|
# Run make in background so we can show spinner
|
|
local make_log="${LOG_FILE}.make"
|
|
make -j"$(nproc)" > "$make_log" 2>&1 &
|
|
local make_pid=$!
|
|
show_spinner $make_pid
|
|
|
|
# Wait for make to complete and check status
|
|
if ! wait $make_pid; then
|
|
cecho "red" "❌ Shairport-Sync compilation failed"
|
|
cecho "yellow" "Last 20 lines of build log:"
|
|
tail -20 "$make_log" 2>/dev/null || echo " (log file not available)"
|
|
exit 1
|
|
fi
|
|
cat "$make_log" >> "$LOG_FILE" 2>/dev/null || true
|
|
|
|
cecho "yellow" "Installing..."
|
|
# Note: make install may fail on systemd service install, but that's OK
|
|
# We'll create the service file manually later
|
|
sudo make install 2>&1 | tee -a "$LOG_FILE" || true
|
|
|
|
# What matters is that the binary was installed
|
|
if ! command_exists shairport-sync; then
|
|
cecho "red" "❌ Shairport-Sync binary not found after installation"
|
|
cecho "yellow" " Expected location: /usr/local/bin/shairport-sync"
|
|
cecho "yellow" " The 'make install' may have failed - check the log"
|
|
exit 1
|
|
fi
|
|
|
|
cecho "green" "✓ Shairport-Sync compiled and installed"
|
|
log "Note: make install may have shown errors about systemd service - this is normal"
|
|
echo
|
|
|
|
# --- Configure Shairport-Sync ---
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
cecho "blue" " Configuring Shairport-Sync..."
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
log "Creating configuration file from sample..."
|
|
|
|
# Copy sample config as base (installed by make install)
|
|
if [ -f /etc/shairport-sync.conf.sample ]; then
|
|
sudo cp /etc/shairport-sync.conf.sample /etc/shairport-sync.conf
|
|
log "Copied sample config to /etc/shairport-sync.conf"
|
|
else
|
|
cecho "yellow" "⚠ Sample config not found, creating minimal config"
|
|
# Fallback to minimal config if sample doesn't exist
|
|
sudo tee /etc/shairport-sync.conf > /dev/null <<'FALLBACK_EOF'
|
|
// Minimal configuration - sample file was not available
|
|
general = {};
|
|
alsa = {};
|
|
FALLBACK_EOF
|
|
fi
|
|
|
|
# Now edit the config file to set our values using simple, reliable sed commands
|
|
log "Configuring AirPlay name: $airplay_name"
|
|
|
|
# Set the AirPlay device name - uncomment and set it
|
|
sudo sed -i "s|^//[[:space:]]*name = .*| name = \"$airplay_name\";|" /etc/shairport-sync.conf
|
|
|
|
# Set output device - uncomment and set it
|
|
log "Configuring audio output: $audio_device_plug"
|
|
sudo sed -i "s|^[[:space:]]*output_device = .*| output_device = \"$audio_device_plug\";|" /etc/shairport-sync.conf
|
|
sudo sed -i "s|^//[[:space:]]*output_device = .*| output_device = \"$audio_device_plug\";|" /etc/shairport-sync.conf
|
|
|
|
# Set mixer control if available
|
|
if [ -n "$mixer_control" ]; then
|
|
log "Configuring mixer control: $mixer_control on hw:$card_number"
|
|
sudo sed -i "s|^//[[:space:]]*mixer_control_name = .*| mixer_control_name = \"$mixer_control\";|" /etc/shairport-sync.conf
|
|
sudo sed -i "s|^[[:space:]]*mixer_control_name = .*| mixer_control_name = \"$mixer_control\";|" /etc/shairport-sync.conf
|
|
# Also set mixer_device if needed (usually commented out by default)
|
|
sudo sed -i "s|^//[[:space:]]*mixer_device = .*| mixer_device = \"hw:$card_number\";|" /etc/shairport-sync.conf
|
|
sudo sed -i "s|^[[:space:]]*mixer_device = .*| mixer_device = \"hw:$card_number\";|" /etc/shairport-sync.conf
|
|
fi
|
|
|
|
# Set output format
|
|
sudo sed -i "s|^//[[:space:]]*output_rate = .*| output_rate = \"auto\";|" /etc/shairport-sync.conf
|
|
sudo sed -i "s|^[[:space:]]*output_rate = .*| output_rate = \"auto\";|" /etc/shairport-sync.conf
|
|
sudo sed -i "s|^//[[:space:]]*output_format = .*| output_format = \"S16\";|" /etc/shairport-sync.conf
|
|
sudo sed -i "s|^[[:space:]]*output_format = .*| output_format = \"S16\";|" /etc/shairport-sync.conf
|
|
|
|
# Set volume settings
|
|
sudo sed -i "s|^//[[:space:]]*volume_max_db = .*| volume_max_db = 4.0;|" /etc/shairport-sync.conf
|
|
sudo sed -i "s|^//[[:space:]]*default_airplay_volume = .*| default_airplay_volume = -6.0;|" /etc/shairport-sync.conf
|
|
sudo sed -i "s|^//[[:space:]]*high_volume_idle_timeout_in_minutes = .*| high_volume_idle_timeout_in_minutes = 1;|" /etc/shairport-sync.conf
|
|
|
|
# Verify config file was created
|
|
if [ ! -f /etc/shairport-sync.conf ]; then
|
|
cecho "red" "❌ Configuration file was not created"
|
|
exit 1
|
|
fi
|
|
|
|
cecho "green" "✓ Configuration file created and customized"
|
|
|
|
# Set mixer volume to maximum if available
|
|
if [ -n "$mixer_control" ]; then
|
|
cecho "blue" "Setting mixer volume to 100%..."
|
|
if amixer -c "$card_number" set "$mixer_control" 100% unmute > /dev/null 2>&1; then
|
|
sudo alsactl store > /dev/null 2>&1 || true
|
|
cecho "green" "✓ Mixer volume set to maximum"
|
|
else
|
|
cecho "yellow" "⚠ Could not set mixer volume (may not be supported)"
|
|
fi
|
|
fi
|
|
echo
|
|
|
|
# --- Create/Update Systemd Service ---
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
cecho "blue" " Setting Up Auto-Start Service..."
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
log "Creating shairport-sync user and group..."
|
|
|
|
# Create user and group for shairport-sync service
|
|
if ! getent group shairport-sync >/dev/null 2>&1; then
|
|
sudo groupadd -r shairport-sync
|
|
log "Created shairport-sync group"
|
|
fi
|
|
|
|
if ! getent passwd shairport-sync >/dev/null 2>&1; then
|
|
sudo useradd -r -M -g shairport-sync -s /usr/sbin/nologin -G audio shairport-sync
|
|
log "Created shairport-sync user"
|
|
fi
|
|
|
|
log "Creating systemd service manually (make install sometimes fails at this step)..."
|
|
|
|
# Create systemd service file manually - this is more reliable than 'make install'
|
|
# which often fails on the systemd service installation step on Raspberry Pi
|
|
sudo tee /lib/systemd/system/shairport-sync.service > /dev/null <<EOF
|
|
[Unit]
|
|
Description=Shairport Sync - AirPlay Audio Receiver
|
|
After=sound.target network-online.target
|
|
|
|
[Service]
|
|
ExecStart=/usr/local/bin/shairport-sync
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable shairport-sync 2>&1 | tee -a "$LOG_FILE"
|
|
sudo systemctl restart shairport-sync 2>&1 | tee -a "$LOG_FILE"
|
|
sleep 5
|
|
|
|
if ! check_service "shairport-sync" 5; then
|
|
cecho "red" "❌ Shairport-Sync service failed to start"
|
|
cecho "yellow" "Checking service status..."
|
|
sudo systemctl status shairport-sync --no-pager -l | tail -20
|
|
exit 1
|
|
fi
|
|
|
|
# Verify avahi-daemon is running (required for AirPlay discovery)
|
|
cecho "blue" "Checking Avahi daemon (required for device discovery)..."
|
|
if ! systemctl is-active --quiet avahi-daemon; then
|
|
cecho "yellow" "⚠ Avahi daemon is not running, attempting to start..."
|
|
sudo systemctl start avahi-daemon
|
|
sleep 2
|
|
if systemctl is-active --quiet avahi-daemon; then
|
|
cecho "green" "✓ Avahi daemon started"
|
|
else
|
|
cecho "red" "❌ Avahi daemon failed to start - AirPlay device may not be discoverable"
|
|
fi
|
|
else
|
|
cecho "green" "✓ Avahi daemon is running"
|
|
fi
|
|
echo
|
|
|
|
# --- Wi-Fi Power Management Instructions ---
|
|
if [ "$disable_wifi_pm" = true ]; then
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
cecho "blue" " Wi-Fi Power Management"
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
log "User requested Wi-Fi power management disable"
|
|
|
|
cecho "yellow" "📝 Manual Wi-Fi Power Management Configuration Needed:"
|
|
echo
|
|
cecho "blue" "After installation completes, disable Wi-Fi power saving to prevent"
|
|
cecho "blue" "audio dropouts. You have two options:"
|
|
echo
|
|
cecho "green" "Option 1: Using raspi-config (Recommended)"
|
|
cecho "blue" " 1. Run: sudo raspi-config"
|
|
cecho "blue" " 2. Go to: Performance Options → Wireless LAN → Power Management"
|
|
cecho "blue" " 3. Select: Disable"
|
|
echo
|
|
cecho "green" "Option 2: Manual command"
|
|
cecho "blue" " Run: sudo iw dev wlan0 set power_save off"
|
|
cecho "blue" " (Note: This is temporary, resets on reboot)"
|
|
echo
|
|
cecho "yellow" "⚠ We're not doing this automatically to avoid disconnecting your SSH session"
|
|
log "Wi-Fi power management instructions provided to user"
|
|
echo
|
|
fi
|
|
|
|
# --- Configure Firewall (if active) ---
|
|
if command_exists ufw && sudo ufw status | grep -q "Status: active"; then
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
cecho "blue" " Configuring Firewall..."
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
|
|
# Allow mDNS for AirPlay discovery
|
|
sudo ufw allow 5353/udp comment 'mDNS for AirPlay' 2>&1 | tee -a "$LOG_FILE"
|
|
# Allow NQPTP ports
|
|
sudo ufw allow 319/udp comment 'NQPTP PTP' 2>&1 | tee -a "$LOG_FILE"
|
|
sudo ufw allow 320/udp comment 'NQPTP PTP' 2>&1 | tee -a "$LOG_FILE"
|
|
# AirPlay ports
|
|
sudo ufw allow 7000/tcp comment 'AirPlay' 2>&1 | tee -a "$LOG_FILE"
|
|
|
|
cecho "green" "✓ Firewall rules added"
|
|
echo
|
|
fi
|
|
|
|
# --- Audio Test ---
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
cecho "blue" " Testing Audio Output..."
|
|
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo
|
|
|
|
read -p "Do you want to test audio output? (Y/n): " test_audio || true
|
|
if [[ -z "$test_audio" || "$test_audio" =~ ^[Yy]$ ]]; then
|
|
cecho "yellow" "Playing test sound in 2 seconds..."
|
|
cecho "yellow" "(You should hear a voice saying 'Front Left', 'Front Right')"
|
|
sleep 2
|
|
|
|
if timeout 10 speaker-test -D "$audio_device_plug" -c 2 -t wav -l 1 > /dev/null 2>&1; then
|
|
echo
|
|
cecho "green" "✓ Audio test completed!"
|
|
read -p "Did you hear the test sound? (y/N): " heard_sound || true
|
|
if [[ ! "$heard_sound" =~ ^[Yy]$ ]]; then
|
|
cecho "yellow" "⚠ If you didn't hear sound, check:"
|
|
cecho "yellow" " - Speaker/headphone connections"
|
|
cecho "yellow" " - Volume level on your amplifier/speakers"
|
|
cecho "yellow" " - USB DAC power"
|
|
fi
|
|
else
|
|
cecho "yellow" "⚠ Audio test couldn't run, but setup is complete."
|
|
cecho "yellow" " You can test manually with: speaker-test -D $audio_device_plug -c 2 -t wav"
|
|
fi
|
|
fi
|
|
echo
|
|
|
|
# --- Cleanup ---
|
|
cecho "blue" "Cleaning up temporary files..."
|
|
rm -rf /tmp/nqptp /tmp/shairport-sync 2>/dev/null || true
|
|
rm -f "${LOG_FILE}.make" 2>/dev/null || true
|
|
cecho "green" "✓ Cleanup complete"
|
|
echo
|
|
|
|
INSTALLATION_FAILED=0 # Installation succeeded
|
|
|
|
# --- Success Message ---
|
|
cecho "green" "╔═════════════════════════════════════════════════════╗"
|
|
cecho "green" "║ ║"
|
|
cecho "green" "║ ✅ INSTALLATION COMPLETE! ✅ ║"
|
|
cecho "green" "║ ║"
|
|
cecho "green" "╚═════════════════════════════════════════════════════╝"
|
|
echo
|
|
log "=== Installation completed successfully ==="
|
|
|
|
cecho "magenta" "🎵 Your AirPlay 2 device is ready!"
|
|
echo
|
|
cecho "yellow" " 📱 Device Name: $airplay_name"
|
|
cecho "yellow" " 🔊 Audio Output: $audio_device_plug"
|
|
cecho "yellow" " 🎚️ Volume: ${mixer_control:-Fixed (no hardware control)}"
|
|
echo
|
|
cecho "blue" "┌─────────────────────────────────────────────────────┐"
|
|
cecho "blue" "│ How to use: │"
|
|
cecho "blue" "│ 1. Open Music/Spotify/YouTube on your iPhone/iPad │"
|
|
cecho "blue" "│ 2. Tap the AirPlay icon (📡) │"
|
|
cecho "blue" "│ 3. Select '$airplay_name' │"
|
|
cecho "blue" "│ 4. Enjoy high-quality wireless audio! │"
|
|
cecho "blue" "└─────────────────────────────────────────────────────┘"
|
|
echo
|
|
cecho "yellow" "💡 Tips:"
|
|
cecho "yellow" " • Device should appear within 30 seconds after reboot"
|
|
cecho "yellow" " • Make sure iPhone and Pi are on the same Wi-Fi network"
|
|
cecho "yellow" " • For best quality, use lossless audio sources"
|
|
if [ "$disable_wifi_pm" = true ]; then
|
|
echo
|
|
cecho "yellow" "📝 IMPORTANT - After reboot:"
|
|
cecho "yellow" " Don't forget to disable Wi-Fi power management using raspi-config!"
|
|
cecho "yellow" " This prevents audio dropouts and stuttering."
|
|
fi
|
|
echo
|
|
cecho "blue" "📋 Useful commands:"
|
|
cecho "blue" " View live logs: sudo journalctl -u shairport-sync -f"
|
|
cecho "blue" " Restart service: sudo systemctl restart shairport-sync"
|
|
cecho "blue" " Check status: sudo systemctl status shairport-sync"
|
|
cecho "blue" " Edit config: sudo nano /etc/shairport-sync.conf"
|
|
cecho "blue" " Installation log: $LOG_FILE"
|
|
echo
|
|
|
|
read -p "Press Enter to reboot now (recommended), or Ctrl+C to reboot later..." || {
|
|
echo
|
|
cecho "yellow" "Reboot cancelled. Remember to reboot later with: sudo reboot"
|
|
exit 0
|
|
}
|
|
|
|
log "User initiated reboot"
|
|
cecho "yellow" "Rebooting in 3 seconds..."
|
|
sleep 3
|
|
sudo reboot
|
|
}
|
|
|
|
# --- Script Entry Point ---
|
|
main "$@"
|