#!/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 # --verify-ssl Enable SSL certificate verification # --no-verify-ssl Disable SSL verification (default) # -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 } # ============================================================ # Helper: Internet reachability probe (dependency-free) # ============================================================ # Returns 0 if the public internet (PyPI) is reachable, 1 otherwise. # Uses only the Python stdlib so it works on a bare, offline host. has_internet() { python3 - << 'PYEOF' >/dev/null 2>&1 import socket try: socket.setdefaulttimeout(3) socket.create_connection(("pypi.org", 443), timeout=3).close() raise SystemExit(0) except Exception: raise SystemExit(1) PYEOF return $? } # ============================================================ # Helper: Fetch a URL using curl, wget, or the Python stdlib # ============================================================ # Works on minimal/offline images where curl/wget may be absent. fetch_url() { local url="$1" if command -v curl &>/dev/null; then curl -sSf --max-time 20 "$url" elif command -v wget &>/dev/null; then wget -qO- --timeout=20 "$url" else python3 - "$url" << 'PYEOF' import sys, urllib.request url = sys.argv[1] try: with urllib.request.urlopen(url, timeout=20) as r: sys.stdout.write(r.read().decode("utf-8", "replace")) except Exception as e: sys.stderr.write("fetch failed: %s\n" % e) raise SystemExit(1) PYEOF 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="" NO_REBOOT=false 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 " --verify-ssl Enable SSL certificate verification" echo " --no-verify-ssl Disable SSL certificate verification (default)" 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 " --no-reboot Do not reboot automatically at the end" 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 ;; --verify-ssl) VERIFY_SSL="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 ;; --no-reboot) NO_REBOOT=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 # Probe internet reachability once (guards every network step below) HAS_INTERNET=false if has_internet; then HAS_INTERNET=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 [ "$HAS_INTERNET" = true ]; then echo "Internet: reachable" else echo "Internet: NOT available (local-network / isolated install)" 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..." if [ "$HAS_INTERNET" = true ]; then run_sudo apt update 2>/dev/null || echo " WARNING: 'apt update' failed; continuing with existing lists" else echo " Offline / no internet — skipping 'apt update'" run_sudo apt update 2>/dev/null || true fi echo "Installing system packages..." # System dependencies required for Kivy + video playback APT_PACKAGES="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 \ curl wget" # Prefer the offline .deb bundle when present USED_DEB_BUNDLE=false if [ -d "$DEB_DIR" ] && ls "$DEB_DIR"/*.deb >/dev/null 2>&1; then echo "Installing from offline .deb packages in $DEB_DIR..." cd "$DEB_DIR" run_sudo dpkg -i *.deb 2>/dev/null || true # Resolve dependencies from the local bundle only (no-op if already satisfied) run_sudo apt-get install -f -y 2>/dev/null || true cd "$SCRIPT_DIR" USED_DEB_BUNDLE=true elif [ "$OFFLINE_MODE" = true ]; then echo "WARNING: Offline mode but no .deb bundle found in $DEB_DIR" echo " System packages will still be attempted via apt (may fail without internet)" fi # Install/complete system packages. Never abort the installer: # offline, some packages may already be present (fine) or simply unavailable. if run_sudo apt install -y $APT_PACKAGES 2>/dev/null; then echo "System dependencies installed" else echo "WARNING: bulk system package install did not fully succeed." echo " Installing available packages individually..." for pkg in $APT_PACKAGES; do run_sudo apt install -y "$pkg" 2>/dev/null || echo " - unavailable (skipped): $pkg" done if [ "$HAS_INTERNET" != true ] && [ "$USED_DEB_BUNDLE" != true ]; then echo " NOTE: Host is offline and no .deb bundle was found." echo " Add .deb files to $DEB_DIR to satisfy missing system packages." fi fi 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" if [ "$OFFLINE_MODE" = true ] && [ "$HAS_INTERNET" != true ]; then echo "Skipping pip self-upgrade (offline, no internet)" else echo "Upgrading pip..." pip install --upgrade pip --quiet 2>/dev/null || echo " WARNING: pip upgrade failed; continuing" fi 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)" if [ "$HAS_INTERNET" = true ]; then echo " Falling back to online installation (requires internet)..." pip install -r "$REQUIREMENTS_FILE" --find-links="$WHEELS_DIR" 2>&1 || true else echo " No internet available — cannot fall back to online installation." echo " Add the missing/compatible wheels to: $WHEELS_DIR" fi 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 a critical package is missing, fall back to online install only when reachable for pkg in kivy ffpyplayer; do if python3 -c "import $pkg" 2>/dev/null; then echo " $pkg OK" else echo " WARNING: $pkg could not be imported in the virtual environment" if [ "$HAS_INTERNET" = true ]; then echo " Attempting online fallback install for $pkg..." pip install "$pkg" 2>/dev/null || echo " - $pkg online install failed (continuing)" else echo " Offline: ensure a compatible '$pkg' wheel exists in $WHEELS_DIR" fi fi done 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 (secondary to XDG autostart; start.sh enforces single-instance) if command -v crontab &>/dev/null; then CRON_WRAPPER="$SCRIPT_DIR/.start-player-cron.sh" CRON_MARKER_TAG=".start-player-cron.sh" cat > "$CRON_WRAPPER" << EOF #!/bin/bash # Kiwy signage player cron fallback (managed by install.sh) sleep 15 cd $SCRIPT_DIR && bash start.sh EOF chmod +x "$CRON_WRAPPER" # Idempotent: strip ALL previous Kiwy cron lines, then add exactly one. # (The old guard grepped for a string that never appeared in the entry, # so every re-install appended another @reboot line.) CURRENT_CRON="$(sudo -u "$ACTUAL_USER" crontab -l 2>/dev/null || true)" CLEANED_CRON="$(printf '%s\n' "$CURRENT_CRON" \ | grep -v -F "$CRON_MARKER_TAG" \ | grep -v -F "$CRON_WRAPPER" \ | grep -v -F "# Kiwy signage player" || true)" { printf '%s\n' "$CLEANED_CRON" | grep -v '^$' echo "# Kiwy signage player autostart fallback (managed by install.sh)" echo "@reboot sleep 20 && $CRON_WRAPPER > /tmp/kivy-player-cron.log 2>&1" } | sudo -u "$ACTUAL_USER" crontab - 2>/dev/null || true echo "Cron fallback configured (single entry, deduplicated)" 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=$(fetch_url "$PROVISION_URL" 2>/dev/null || 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: # VERIFY_SSL holds the requested verification state ("true"/"false"). # (Previously this was inverted: '--no-verify-ssl' set verify_ssl=True.) 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', True) config.setdefault('use_https', False) config.setdefault('verify_ssl', False) 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": true, "use_https": false, "verify_ssl": false } 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 [ "$NO_REBOOT" = true ]; then echo "==========================================" echo "Reboot Skipped (--no-reboot)" echo "==========================================" echo "" echo "Some changes (HDMI settings, GPU memory) may require a reboot." echo "Reboot manually when convenient: sudo reboot" echo "" elif [ -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