#\!/bin/bash # Kivy Signage Player Installation Script # Supports both online and offline installation using a Python virtual environment # # Usage: # bash install.sh [OPTIONS] # # Options: # -U, --sudo-user USER Username for sudo (avoids password prompts) # -W, --sudo-password PASS Password for sudo (use with caution) # --offline Force offline installation from local packages # -q, --quiet Silent mode # -h, --help Show help # # Configuration options (for Digiserver): # -s, --server IP Server IP/hostname # -p, --port PORT Server port (default: 8080) # -n, --name NAME Screen/player name # -k, --key KEY Quick-connect / auth key # --https Enable HTTPS # --no-verify-ssl Disable SSL verification # -o, --orientation Landscape or Portrait # --touch Enable touch input # -r, --resolution WxH Max resolution # --edit Enable on-device edit feature # --provision URL One-time provisioning URL # --config FILE Path to pre-made JSON config set -e # ============================================================ # Configuration # ============================================================ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$SCRIPT_DIR/repo" WHEELS_DIR="$REPO_DIR/python-wheels" SYSTEM_DIR="$REPO_DIR/system-packages" DEB_DIR="$SYSTEM_DIR/debs" VENV_DIR="$SCRIPT_DIR/.venv" REQUIREMENTS_FILE="$SCRIPT_DIR/requirements.txt" SUDO_USERNAME="" SUDO_PASSWORD="" OFFLINE_MODE=false PI_BOOT_PATCH_SCRIPT="$SCRIPT_DIR/scripts/pi_boot_config_patch_once.sh" # ============================================================ # Helper: Run sudo with optional password # ============================================================ run_sudo() { if [ -n "$SUDO_PASSWORD" ]; then echo "$SUDO_PASSWORD" | sudo -S "$@" 2>/dev/null else sudo "$@" fi } # ============================================================ # Helpers # ============================================================ find_boot_config() { if [ -f /boot/firmware/config.txt ]; then echo "/boot/firmware/config.txt" elif [ -f /boot/config.txt ]; then echo "/boot/config.txt" else echo "" fi } detect_raspberry_pi() { BOOT_CONFIG=$(find_boot_config) if grep -qi "raspberry" /proc/cpuinfo 2>/dev/null || [ -n "$BOOT_CONFIG" ]; then return 0 else return 1 fi } # ============================================================ # CLI Argument Parsing # ============================================================ SERVER_IP="" SERVER_PORT="" SCREEN_NAME="" QUICKCONNECT_KEY="" USE_HTTPS="" VERIFY_SSL="" ORIENTATION="" TOUCH_ENABLED="" MAX_RESOLUTION="" EDIT_ENABLED="" PROVISION_URL="" CONFIG_FILE_PATH="" usage() { echo "Usage: $0 [OPTIONS]" echo "" echo "Authentication options (avoids sudo password prompts):" echo " -U, --sudo-user USER Username for sudo commands" echo " -W, --sudo-password PASS Password for sudo commands" echo "" echo "Installation modes (choose one):" echo " --provision URL One-time provisioning URL from Digiserver" echo " --config FILE Path to pre-made JSON config file" echo " (individual params) Set each config value manually" echo "" echo "Configuration options:" echo " -s, --server IP Digiserver IP or hostname" echo " -p, --port PORT Server port (default: 8080)" echo " -n, --name NAME Screen/player name" echo " -k, --key KEY Quick-connect / auth key" echo " --https Enable HTTPS for server communication" echo " --no-verify-ssl Disable SSL certificate verification" echo " -o, --orientation Landscape or Portrait (default: Landscape)" echo " --touch Enable touch input" echo " -r, --resolution WxH Max resolution (default: 1920x1080)" echo " --edit Enable on-device edit feature" echo "" echo "Installation options:" echo " --offline Force offline installation from local packages" echo " -q, --quiet Silent mode (minimal output)" echo " -h, --help Show this help" exit 0 } while [ $# -gt 0 ]; do case "$1" in -U|--sudo-user) SUDO_USERNAME="$2" shift 2 ;; -W|--sudo-password) SUDO_PASSWORD="$2" shift 2 ;; --provision) PROVISION_URL="$2" shift 2 ;; --config) CONFIG_FILE_PATH="$2" shift 2 ;; -s|--server) SERVER_IP="$2" shift 2 ;; -p|--port) SERVER_PORT="$2" shift 2 ;; -n|--name) SCREEN_NAME="$2" shift 2 ;; -k|--key) QUICKCONNECT_KEY="$2" shift 2 ;; --https) USE_HTTPS="true" shift ;; --no-verify-ssl) VERIFY_SSL="false" shift ;; -o|--orientation) ORIENTATION="$2" shift 2 ;; --touch) TOUCH_ENABLED="True" shift ;; -r|--resolution) MAX_RESOLUTION="$2" shift 2 ;; --edit) EDIT_ENABLED="true" shift ;; --offline) OFFLINE_MODE=true shift ;; -q|--quiet) exec > >(tee -a /tmp/kivy-install.log) 2>&1 shift ;; -h|--help) usage ;; *) echo "Unknown option: $1" usage ;; esac done # Auto-detect offline mode if wheels exist if [ "$OFFLINE_MODE" = false ] && [ -d "$WHEELS_DIR" ] && [ "$(ls -A "$WHEELS_DIR"/*.whl 2>/dev/null)" ]; then OFFLINE_MODE=true fi # ============================================================ # Mode Banner # ============================================================ echo "==========================================" echo "Kivy Signage Player v1.2.0 - Installer" echo "==========================================" if [ "$OFFLINE_MODE" = true ]; then echo "Mode: Offline Installation" else echo "Mode: Online Installation" fi if [ -n "$SUDO_USERNAME" ]; then echo "Sudo user: $SUDO_USERNAME" fi echo "" # ============================================================ # Step 1: System Dependencies # ============================================================ echo "Step 1/6: Installing system dependencies..." echo "--------------------" echo "Updating package lists..." run_sudo apt update echo "Installing system packages..." if [ "$OFFLINE_MODE" = true ] && [ -d "$DEB_DIR" ] && [ "$(ls -A $DEB_DIR/*.deb 2>/dev/null)" ]; then echo "Installing from offline .deb packages..." cd "$DEB_DIR" run_sudo dpkg -i *.deb 2>/dev/null || true run_sudo apt-get install -f -y || true cd "$SCRIPT_DIR" fi run_sudo apt install -y \ python3-pip python3-setuptools python3-dev \ python3-venv \ libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \ libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev \ zlib1g-dev ffmpeg libavcodec-extra \ gstreamer1.0-plugins-base gstreamer1.0-plugins-good echo "System dependencies installed" echo "" # ============================================================ # Step 2: Python Virtual Environment # ============================================================ echo "Step 2/6: Setting up Python virtual environment..." echo "--------------------" if [ -d "$VENV_DIR" ]; then echo "Removing old virtual environment..." rm -rf "$VENV_DIR" fi echo "Creating virtual environment (with system site-packages)..." python3 -m venv --system-site-packages "$VENV_DIR" echo "Virtual environment created at $VENV_DIR" source "$VENV_DIR/bin/activate" echo "Upgrading pip..." pip install --upgrade pip --quiet echo "" # ============================================================ # Step 3: Install Python Packages # ============================================================ echo "Step 3/6: Installing Python packages..." echo "--------------------" PIP_EXTRA_ARGS="" if [ "$OFFLINE_MODE" = true ]; then echo "Using offline wheel repository: $WHEELS_DIR" PIP_EXTRA_ARGS="--no-index --find-links=$WHEELS_DIR" fi if [ -f "$REQUIREMENTS_FILE" ]; then echo "Installing packages from requirements.txt..." if [ "$OFFLINE_MODE" = true ]; then if pip install $PIP_EXTRA_ARGS -r "$REQUIREMENTS_FILE"; then echo "All packages installed from offline repository" else echo "" echo "Offline installation failed (some wheels may be missing or incompatible)" echo " Falling back to online installation (requires internet)..." echo "" pip install -r "$REQUIREMENTS_FILE" --find-links="$WHEELS_DIR" 2>&1 || true fi else pip install -r "$REQUIREMENTS_FILE" --find-links="$WHEELS_DIR" 2>&1 || true fi # Verify critical packages are importable echo "" echo "Verifying installed packages..." pip list --format=columns 2>/dev/null | grep -iE "kivy|ffpyplayer" || true # If kivy is still missing after pip install, try system-wide fallback python3 -c "import kivy" 2>/dev/null && echo " kivy OK" || { echo "" echo " kivy not found in venv — installing system-wide via pip (break-system-packages)..." pip install kivy --break-system-packages 2>/dev/null || pip install --user kivy 2>/dev/null || true } python3 -c "import ffpyplayer" 2>/dev/null && echo " ffpyplayer OK" || { echo " ffpyplayer not found — installing system-wide..." pip install ffpyplayer --break-system-packages 2>/dev/null || pip install --user ffpyplayer 2>/dev/null || true } else echo "requirements.txt not found - skipping pip install" fi echo "" deactivate # ============================================================ # Step 4: Autostart Workflow # ============================================================ echo "Step 4/6: Setting up autostart..." echo "--------------------" ACTUAL_USER="${SUDO_USERNAME:-${SUDO_USER:-$(whoami)}}" ACTUAL_HOME=$(eval echo ~"$ACTUAL_USER") AUTOSTART_DIR="$ACTUAL_HOME/.config/autostart" echo "Configuring autostart for user: $ACTUAL_USER" mkdir -p "$AUTOSTART_DIR" cat > "$AUTOSTART_DIR/kivy-signage-player.desktop" << EOF [Desktop Entry] Type=Application Name=Kivy Signage Player Comment=Digital Signage Player Exec=/bin/bash -c "cd $SCRIPT_DIR && exec bash start.sh" Icon=media-video-display Categories=Utility; NoDisplay=false Terminal=false StartupNotify=false Hidden=false X-GNOME-Autostart-enabled=true X-GNOME-Autostart-delay=3 X-XFCE-Autostart-Override=true EOF run_sudo chown "$ACTUAL_USER:$ACTUAL_USER" "$AUTOSTART_DIR/kivy-signage-player.desktop" 2>/dev/null || true chmod 644 "$AUTOSTART_DIR/kivy-signage-player.desktop" 2>/dev/null || true echo "XDG autostart entry created" # Cron fallback if command -v crontab &>/dev/null; then CRON_WRAPPER="$SCRIPT_DIR/.start-player-cron.sh" cat > "$CRON_WRAPPER" << EOF #\!/bin/bash sleep 15 cd $SCRIPT_DIR && bash start.sh EOF chmod +x "$CRON_WRAPPER" CRON_ENTRY="@reboot sleep 20 && $CRON_WRAPPER > /tmp/kivy-player-cron.log 2>&1" # Use sudo -u which works when running with sudo (no password prompt) if sudo -u "$ACTUAL_USER" crontab -l 2>/dev/null | grep -q "kivy-signage-player"; then true else (sudo -u "$ACTUAL_USER" crontab -l 2>/dev/null || true; echo "$CRON_ENTRY") | sudo -u "$ACTUAL_USER" crontab - 2>/dev/null || true fi echo "Cron fallback configured" fi echo "" # ============================================================ # Step 5: Raspberry Pi Optimization # ============================================================ if detect_raspberry_pi; then echo "Step 5/6: Optimizing for Raspberry Pi..." echo "--------------------" BOOT_CONFIG=$(find_boot_config) if [ -n "$BOOT_CONFIG" ]; then echo "Configuring HDMI settings..." for setting in "hdmi_blanking=0" "hdmi_ignore_cec_init=1" "hdmi_force_hotplug=1"; do key="${setting%%=*}" if grep -q "^$key" "$BOOT_CONFIG" 2>/dev/null; then run_sudo sed -i "s/^$key=.*/$setting/" "$BOOT_CONFIG" else echo "$setting" | run_sudo tee -a "$BOOT_CONFIG" > /dev/null fi done echo "HDMI settings configured" fi echo "Setting CPU to performance mode..." for cpu in /sys/devices/system/cpu/cpu[0-9]*; do if [ -f "$cpu/cpufreq/scaling_governor" ]; then echo performance | run_sudo tee "$cpu/cpufreq/scaling_governor" > /dev/null 2>&1 || true fi done run_sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target 2>/dev/null || true echo "System sleep disabled" if [ -f "$SCRIPT_DIR/.video-optimization.sh" ]; then bash "$SCRIPT_DIR/.video-optimization.sh" fi echo "" fi # ============================================================ # Step 6: Finalize Player Setup # ============================================================ echo "Step 6/6: Finalizing player setup..." echo "--------------------" # Create required directories echo "Creating player directories..." mkdir -p "$SCRIPT_DIR/media" mkdir -p "$SCRIPT_DIR/playlists" mkdir -p "$SCRIPT_DIR/config/resources" mkdir -p "$SCRIPT_DIR/logs" touch "$SCRIPT_DIR/logs/.gitkeep" 2>/dev/null || true echo "Player directories created" # Write player configuration write_player_config() { local config_dir="$SCRIPT_DIR/config" local config_file="$config_dir/app_config.json" mkdir -p "$config_dir" if [ -n "$PROVISION_URL" ]; then echo "Fetching provisioning data from: $PROVISION_URL" local response response=$(curl -sSf "$PROVISION_URL" 2>&1 || true) if [ -n "$response" ]; then echo "$response" > "$config_file" echo "Provisioning data saved" return 0 fi echo "Failed to fetch provisioning data" fi if [ -n "$CONFIG_FILE_PATH" ] && [ -f "$CONFIG_FILE_PATH" ]; then cp "$CONFIG_FILE_PATH" "$config_file" echo "Configuration copied from $CONFIG_FILE_PATH" return 0 fi if [ -n "$SERVER_IP" ] || [ -n "$SCREEN_NAME" ] || [ -n "$QUICKCONNECT_KEY" ]; then export SERVER_IP SERVER_PORT SCREEN_NAME QUICKCONNECT_KEY export ORIENTATION TOUCH_ENABLED MAX_RESOLUTION export USE_HTTPS VERIFY_SSL EDIT_ENABLED python3 << PYEOF import json, os config_file = "$config_file" config = {} if os.path.exists(config_file): with open(config_file) as f: config = json.load(f) cli_params = { 'server_ip': os.environ.get('SERVER_IP', ''), 'port': os.environ.get('SERVER_PORT', ''), 'screen_name': os.environ.get('SCREEN_NAME', ''), 'quickconnect_key': os.environ.get('QUICKCONNECT_KEY', ''), 'orientation': os.environ.get('ORIENTATION', ''), 'touch': os.environ.get('TOUCH_ENABLED', ''), 'max_resolution': os.environ.get('MAX_RESOLUTION', ''), } for key, val in cli_params.items(): if val: config[key] = val https_val = os.environ.get('USE_HTTPS', '') verify_val = os.environ.get('VERIFY_SSL', '') edit_val = os.environ.get('EDIT_ENABLED', '') if https_val: config['use_https'] = https_val.lower() == 'true' if verify_val: config['verify_ssl'] = verify_val.lower() == 'false' if edit_val: config['edit_feature_enabled'] = edit_val.lower() == 'true' config.setdefault('port', '8080') config.setdefault('orientation', 'Landscape') config.setdefault('touch', 'True') config.setdefault('max_resolution', '1920x1080') config.setdefault('edit_feature_enabled', False) config.setdefault('use_https', False) config.setdefault('verify_ssl', True) with open(config_file, 'w') as f: json.dump(config, f, indent=2) print("Configuration written") PYEOF elif [ \! -f "$config_file" ]; then cat > "$config_file" << 'EOF' { "server_ip": "", "port": "8080", "screen_name": "", "quickconnect_key": "", "orientation": "Landscape", "touch": "True", "max_resolution": "1920x1080", "edit_feature_enabled": false, "use_https": false, "verify_ssl": true } EOF echo "Default config template created" fi } write_player_config # Initialize authentication config_file="$SCRIPT_DIR/config/app_config.json" if [ -f "$config_file" ]; then if python3 -c " import json c = json.load(open('$config_file')) if c.get('server_ip') and c.get('screen_name') and c.get('quickconnect_key'): exit(0) else: exit(1) " 2>/dev/null; then echo "Attempting player registration..." cd "$SCRIPT_DIR/src" source "$VENV_DIR/bin/activate" python3 -c " import json, sys sys.path.insert(0, '.') from get_playlists_v2 import ensure_authenticated with open('$config_file') as f: config = json.load(f) auth = ensure_authenticated(config) if auth: print('Player registered successfully') sys.exit(0) else: print('Registration deferred') sys.exit(1) " 2>&1 || echo "Auth initialization deferred" deactivate 2>/dev/null || true cd "$SCRIPT_DIR" else echo "Incomplete config - auth will happen on first run" fi fi # Create background runner NOHUP_WRAPPER="$SCRIPT_DIR/.run-background.sh" cat > "$NOHUP_WRAPPER" << EOF #\!/bin/bash SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LOG_FILE="$SCRIPT_DIR/logs/player-background.log" mkdir -p "$(dirname "$LOG_FILE")" echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting player in background..." >> "$LOG_FILE" cd "$SCRIPT_DIR" || exit 1 nohup bash start.sh >> "$LOG_FILE" 2>&1 & PLAYER_PID=\$\! echo "[$(date '+%Y-%m-%d %H:%M:%S')] Player started with PID: \$PLAYER_PID" >> "$LOG_FILE" echo "Player started in background (PID: \$PLAYER_PID)" echo "Logs: \$LOG_FILE" EOF chmod +x "$NOHUP_WRAPPER" echo "Background runner created" # ============================================================ # Final Summary # ============================================================ echo "" echo "==========================================" echo "Installation Complete\!" echo "==========================================" echo "" echo "Installation: $SCRIPT_DIR" echo "Virtual Env: $VENV_DIR" echo "" if [ -f "$SCRIPT_DIR/config/app_config.json" ]; then echo "Player Configuration:" echo "------------------------" python3 -c " import json with open('$SCRIPT_DIR/config/app_config.json') as f: c = json.load(f) print(f' Server: {c.get(\"server_ip\",\"not set\")}') print(f' Port: {c.get(\"port\",\"8080\")}') print(f' Screen: {c.get(\"screen_name\",\"not set\")}') print(f' HTTPS: {c.get(\"use_https\",False)}') print(f' Resolution: {c.get(\"max_resolution\",\"auto\")}') " 2>/dev/null || echo " (config present)" echo "" fi echo "Quick Start Commands:" echo "------------------------" echo " bash start.sh Start with watchdog (auto-restart)" echo " bash run_player.sh Run once (no auto-restart)" echo " bash .run-background.sh Run in background" echo " bash check_player_status.sh Check player status" echo " bash stop_player.sh Stop the player" echo "" echo "Autostart enabled for user: $ACTUAL_USER" echo "" echo "Logs: $SCRIPT_DIR/logs/" echo "" if [ -f "$PI_BOOT_PATCH_SCRIPT" ]; then echo "Applying one-time Raspberry Pi boot config patch..." run_sudo chmod +x "$PI_BOOT_PATCH_SCRIPT" 2>/dev/null || true if run_sudo bash "$PI_BOOT_PATCH_SCRIPT"; then echo "One-time boot patch applied (or already present)" else echo "Boot patch skipped or failed; continuing installation" fi echo "" fi if [ -z "$SUDO_PASSWORD" ]; then echo "==========================================" echo "System Reboot Recommended" echo "==========================================" echo "" echo "Some changes (HDMI settings, GPU memory) require a reboot." echo "" echo "Rebooting in 5 seconds... Press Ctrl+C to cancel." sleep 5 echo "Rebooting now..." run_sudo reboot fi