From 4e490e8771c2d47760f68bfe266858715a6c7ce5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Feb 2026 05:17:52 +0000 Subject: [PATCH] Add Proxmox LXC installer script Bash executable script that installs SoulSync and all dependencies (slskd, Python, ffmpeg, etc.) inside a Debian/Ubuntu Proxmox LXC container. Features include: - Interactive setup for Soulseek credentials, Spotify API, Tidal, and media server (Plex/Jellyfin/Navidrome) configuration - Automatic slskd download, configuration, and systemd service - Python venv with all webui requirements - Proxmox community scripts-style UX (spinner, color-coded messages, emoji icons, section headers) - Auto-generated slskd API key wired into SoulSync config - MOTD banner with access URLs and service management commands - Existing installation detection with reinstall option - Full error handling with install log at /var/log/soulsync-install.log https://claude.ai/code/session_013gJJD3cxafQSVmyn9pXcdn --- scripts/proxmox/soulsync-install.sh | 861 ++++++++++++++++++++++++++++ 1 file changed, 861 insertions(+) create mode 100755 scripts/proxmox/soulsync-install.sh diff --git a/scripts/proxmox/soulsync-install.sh b/scripts/proxmox/soulsync-install.sh new file mode 100755 index 00000000..f7552400 --- /dev/null +++ b/scripts/proxmox/soulsync-install.sh @@ -0,0 +1,861 @@ +#!/usr/bin/env bash + +# ============================================================================ +# SoulSync Installer for Proxmox LXC +# ============================================================================ +# Installs SoulSync and all dependencies (slskd, Python, ffmpeg, etc.) +# inside a Debian/Ubuntu-based Proxmox LXC container. +# +# Usage: +# bash soulsync-install.sh +# +# Requirements: +# - Debian 12+ or Ubuntu 22.04+ LXC container +# - Root privileges +# - Network connectivity +# - x86_64 / amd64 architecture +# ============================================================================ + +set -Eeuo pipefail + +# -- Constants ---------------------------------------------------------------- + +APP="SoulSync" +SOULSYNC_REPO="https://github.com/tanujdargan/SoulSync.git" +SOULSYNC_DIR="/opt/soulsync" +SLSKD_DIR="/opt/slskd" +SOULSYNC_USER="soulsync" +SOULSYNC_GROUP="soulsync" +LOGFILE="/var/log/soulsync-install.log" + +SOULSYNC_PORT=8008 +SLSKD_PORT=5030 +SPOTIFY_CB_PORT=8888 +TIDAL_CB_PORT=8889 + +# -- Colors & Formatting (Proxmox community scripts style) -------------------- + +RD='\033[01;31m' +GN='\033[1;92m' +YW='\033[33m' +YWB='\033[93m' +BL='\033[36m' +BGN='\033[4;92m' +DGN='\033[32m' +CL='\033[m' +BOLD='\033[1m' +DIM='\033[2m' +ITALIC='\033[3m' +TAB=' ' +BFR="\\r\\033[K" + +CM="${TAB}βœ”οΈ${TAB}${CL}" +CROSS="${TAB}βœ–οΈ${TAB}${CL}" +INFO="${TAB}πŸ’‘${TAB}${CL}" +NETWORK="${TAB}πŸ“‘${TAB}${CL}" +CREATING="${TAB}πŸš€${TAB}${CL}" +GEAR="${TAB}βš™οΈ${TAB}${CL}" +LOCK="${TAB}πŸ”’${TAB}${CL}" +MUSIC="${TAB}🎡${TAB}${CL}" +HOURGLASS="${TAB}⏳${TAB}${CL}" + +SPINNER_PID="" + +# -- Ensure log file exists --------------------------------------------------- + +touch "$LOGFILE" + +# -- Spinner & Message Functions ----------------------------------------------- + +spinner() { + local chars="⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" + local i=0 + while true; do + printf "\r${TAB}${YWB}%s${CL} %s" "${chars:i++%${#chars}:1}" "$1" >&2 + sleep 0.1 + done +} + +stop_spinner() { + if [[ -n "${SPINNER_PID}" ]]; then + kill "$SPINNER_PID" 2>/dev/null + wait "$SPINNER_PID" 2>/dev/null || true + SPINNER_PID="" + fi +} + +msg_info() { + stop_spinner + spinner "$1" & + SPINNER_PID=$! +} + +msg_ok() { + stop_spinner + printf "${BFR}${CM}${GN}%s${CL}\n" "$1" >&2 +} + +msg_error() { + stop_spinner + printf "${BFR}${CROSS}${RD}%s${CL}\n" "$1" >&2 +} + +msg_warn() { + stop_spinner + printf "${INFO}${YWB}%s${CL}\n" "$1" >&2 +} + +# -- Cleanup on exit ----------------------------------------------------------- + +cleanup() { + stop_spinner +} +trap cleanup EXIT + +# -- Error handler ------------------------------------------------------------- + +error_handler() { + local exit_code=$? + local line_number=${1:-unknown} + stop_spinner + msg_error "Installation failed on line ${line_number} (exit code: ${exit_code})" + echo -e "\n${TAB}${GEAR}${DIM} Check the log for details: ${LOGFILE}${CL}\n" >&2 + exit "$exit_code" +} +trap 'error_handler ${LINENO}' ERR + +# -- Helpers ------------------------------------------------------------------- + +# Escape special characters for JSON string values +json_escape() { + local s="$1" + s="${s//\\/\\\\}" + s="${s//\"/\\\"}" + s="${s//$'\n'/\\n}" + s="${s//$'\r'/\\r}" + s="${s//$'\t'/\\t}" + printf '%s' "$s" +} + +generate_api_key() { + openssl rand -hex 24 2>/dev/null || head -c 48 /dev/urandom | od -An -tx1 | tr -d ' \n' | head -c 48 +} + +get_ip() { + hostname -I 2>/dev/null | awk '{print $1}' || ip -4 addr show scope global | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1 || echo "localhost" +} + +# -- Header -------------------------------------------------------------------- + +header_info() { + clear + echo "" + cat <<"BANNER" + ____ _ ____ + / ___| ___ _ _| |/ ___| _ _ _ __ ___ + \___ \ / _ \| | | | |\___ \| | | | '_ \ / __| + ___) | (_) | |_| | | ___) | |_| | | | | (__ + |____/ \___/ \__,_|_||____/ \__, |_| |_|\___| + |___/ +BANNER + echo "" + echo -e " ${BL}${BOLD}Proxmox LXC Installer${CL}" + echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo "" +} + +# -- Prerequisite Checks ------------------------------------------------------ + +root_check() { + if [[ "$(id -u)" -ne 0 ]]; then + msg_error "This script must be run as root" + echo -e "\n${TAB}Run with: ${GN}sudo bash $(basename "$0")${CL}\n" >&2 + exit 1 + fi +} + +os_check() { + if [[ ! -f /etc/os-release ]]; then + msg_error "Cannot detect operating system" + exit 1 + fi + # shellcheck source=/dev/null + source /etc/os-release + if [[ "$ID" != "debian" && "$ID" != "ubuntu" ]]; then + msg_error "Unsupported OS: ${ID}. This script requires Debian or Ubuntu." + exit 1 + fi + msg_ok "Operating System: ${ID^} ${VERSION_ID}" +} + +arch_check() { + local arch + arch=$(dpkg --print-architecture 2>/dev/null || uname -m) + if [[ "$arch" != "amd64" && "$arch" != "x86_64" ]]; then + msg_error "Unsupported architecture: ${arch}. This script requires amd64/x86_64." + exit 1 + fi +} + +network_check() { + msg_info "Checking network connectivity" + local tries=0 + while ! ping -c 1 -W 3 github.com &>/dev/null; do + ((tries++)) + if [[ $tries -ge 3 ]]; then + msg_error "No network connectivity (cannot reach github.com)" + echo -e "${TAB}${DIM}Ensure your LXC has a working network configuration.${CL}" >&2 + exit 1 + fi + sleep 2 + done + msg_ok "Network connectivity verified" +} + +existing_install_check() { + if [[ -f /etc/systemd/system/soulsync.service ]]; then + echo "" + msg_warn "An existing SoulSync installation was detected!" + echo "" + if prompt_confirm "Do you want to reinstall? This will overwrite the current installation" "n"; then + msg_info "Stopping existing services" + systemctl stop soulsync 2>/dev/null || true + systemctl stop slskd 2>/dev/null || true + msg_ok "Stopped existing services" + else + echo -e "\n${TAB}${DIM}Installation cancelled.${CL}\n" >&2 + exit 0 + fi + fi +} + +# -- Prompt Functions ---------------------------------------------------------- + +prompt_input() { + local prompt="$1" + local default="${2:-}" + local result + + if [[ -n "$default" ]]; then + printf "\n${TAB}${GEAR} ${YW}%s ${DIM}[%s]${CL}: " "$prompt" "$default" >&2 + else + printf "\n${TAB}${GEAR} ${YW}%s${CL}: " "$prompt" >&2 + fi + read -r result + echo "${result:-$default}" +} + +prompt_password() { + local prompt="$1" + local result + + printf "\n${TAB}${LOCK} ${YW}%s${CL}: " "$prompt" >&2 + read -rs result + echo "" >&2 + echo "$result" +} + +prompt_confirm() { + local prompt="$1" + local default="${2:-y}" + local hint result + + if [[ "$default" == "y" ]]; then + hint="Y/n" + else + hint="y/N" + fi + + printf "\n${TAB}${INFO} ${YW}%s ${DIM}[%s]${CL}: " "$prompt" "$hint" >&2 + read -r result + result="${result:-$default}" + [[ "${result,,}" =~ ^(y|yes)$ ]] +} + +prompt_select() { + local prompt="$1" + shift + local options=("$@") + + echo "" >&2 + echo -e "${TAB}${GEAR} ${YW}${prompt}${CL}" >&2 + local i=1 + for opt in "${options[@]}"; do + echo -e "${TAB}${TAB}${GN}${i})${CL} ${opt}" >&2 + ((i++)) + done + + local result + while true; do + printf "${TAB}${TAB}${YW}Select [1-%d]${CL}: " "${#options[@]}" >&2 + read -r result + if [[ "$result" =~ ^[0-9]+$ ]] && ((result >= 1 && result <= ${#options[@]})); then + echo "$result" + return + fi + echo -e "${TAB}${TAB}${RD}Invalid selection, try again${CL}" >&2 + done +} + +# -- Installation Steps ------------------------------------------------------- + +update_os() { + msg_info "Updating system packages" + apt-get update -qq >>"$LOGFILE" 2>&1 + DEBIAN_FRONTEND=noninteractive apt-get -y -qq dist-upgrade >>"$LOGFILE" 2>&1 + msg_ok "System packages updated" +} + +install_dependencies() { + msg_info "Installing system dependencies" + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ + curl \ + wget \ + git \ + unzip \ + gcc \ + libc6-dev \ + libffi-dev \ + libssl-dev \ + openssl \ + ffmpeg \ + libchromaprint-tools \ + python3 \ + python3-pip \ + python3-venv \ + python3-dev \ + >>"$LOGFILE" 2>&1 + msg_ok "Installed system dependencies" +} + +create_user_and_dirs() { + msg_info "Creating SoulSync user and directories" + + if ! id -u "$SOULSYNC_USER" &>/dev/null; then + useradd -r -s /usr/sbin/nologin -d "$SOULSYNC_DIR" "$SOULSYNC_USER" >>"$LOGFILE" 2>&1 + fi + + mkdir -p \ + "$SOULSYNC_DIR"/{config,data,logs,downloads,Transfer,Staging} \ + "$SLSKD_DIR" + + msg_ok "Created user '${SOULSYNC_USER}' and directories" +} + +install_slskd() { + msg_info "Fetching latest slskd release info" + local slskd_version download_url + + slskd_version=$(curl -fsSL "https://api.github.com/repos/slskd/slskd/releases/latest" \ + | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') + + if [[ -z "$slskd_version" ]]; then + msg_error "Failed to determine latest slskd version" + exit 1 + fi + msg_ok "Latest slskd release: ${slskd_version}" + + msg_info "Downloading slskd ${slskd_version}" + download_url="https://github.com/slskd/slskd/releases/download/${slskd_version}/slskd-${slskd_version}-linux-x64.zip" + wget -q "$download_url" -O /tmp/slskd.zip >>"$LOGFILE" 2>&1 + msg_ok "Downloaded slskd ${slskd_version}" + + msg_info "Extracting slskd" + unzip -o -q /tmp/slskd.zip -d "$SLSKD_DIR" >>"$LOGFILE" 2>&1 + chmod +x "$SLSKD_DIR/slskd" + rm -f /tmp/slskd.zip + msg_ok "Installed slskd to ${SLSKD_DIR}" +} + +configure_slskd() { + echo "" >&2 + echo -e "${TAB}${MUSIC} ${BOLD}${BL}Soulseek Configuration${CL}" >&2 + echo -e "${TAB}${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" >&2 + echo -e "${TAB}${DIM}These credentials connect slskd to the Soulseek P2P network.${CL}" >&2 + echo -e "${TAB}${DIM}Use your existing Soulseek account or create a new one.${CL}" >&2 + + local slskd_user slskd_pass + + slskd_user=$(prompt_input "Soulseek username") + while [[ -z "$slskd_user" ]]; do + msg_error "Username cannot be empty" + slskd_user=$(prompt_input "Soulseek username") + done + + slskd_pass=$(prompt_password "Soulseek password") + while [[ -z "$slskd_pass" ]]; do + msg_error "Password cannot be empty" + slskd_pass=$(prompt_password "Soulseek password") + done + + SLSKD_API_KEY=$(generate_api_key) + + echo "" >&2 + msg_info "Writing slskd configuration" + + # Escape values for YAML (double-quoted strings) + local esc_user esc_pass esc_key + esc_user=$(json_escape "$slskd_user") + esc_pass=$(json_escape "$slskd_pass") + esc_key=$(json_escape "$SLSKD_API_KEY") + + cat >"$SLSKD_DIR/slskd.yml" <>"$LOGFILE" 2>&1 + else + git clone -q "$SOULSYNC_REPO" /tmp/soulsync-src >>"$LOGFILE" 2>&1 + # Copy repo contents into install dir (preserving our created subdirs) + cp -a /tmp/soulsync-src/. "$SOULSYNC_DIR/" + rm -rf /tmp/soulsync-src + fi + msg_ok "Cloned SoulSync repository" + + msg_info "Creating Python virtual environment" + python3 -m venv "$SOULSYNC_DIR/venv" >>"$LOGFILE" 2>&1 + msg_ok "Created Python virtual environment" + + msg_info "Installing Python dependencies (this may take a minute)" + "$SOULSYNC_DIR/venv/bin/pip" install --upgrade pip >>"$LOGFILE" 2>&1 + "$SOULSYNC_DIR/venv/bin/pip" install -r "$SOULSYNC_DIR/requirements-webui.txt" >>"$LOGFILE" 2>&1 + msg_ok "Installed Python dependencies" +} + +configure_soulsync() { + local ip + ip=$(get_ip) + + # ── Spotify ────────────────────────────────────────────────── + echo "" >&2 + echo -e "${TAB}${MUSIC} ${BOLD}${BL}Spotify Configuration${CL}" >&2 + echo -e "${TAB}${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" >&2 + echo -e "${TAB}${DIM}Create a Spotify app at:${CL}" >&2 + echo -e "${TAB} ${BGN}https://developer.spotify.com/dashboard${CL}" >&2 + echo -e "${TAB}${DIM}Set the redirect URI in your Spotify app to:${CL}" >&2 + echo -e "${TAB} ${BGN}http://${ip}:${SPOTIFY_CB_PORT}/callback${CL}" >&2 + + local spotify_id="SpotifyClientID" + local spotify_secret="SpotifyClientSecret" + local spotify_redirect="http://${ip}:${SPOTIFY_CB_PORT}/callback" + + if prompt_confirm "Configure Spotify now?"; then + spotify_id=$(prompt_input "Spotify Client ID") + while [[ -z "$spotify_id" ]]; do + msg_error "Client ID cannot be empty" + spotify_id=$(prompt_input "Spotify Client ID") + done + spotify_secret=$(prompt_password "Spotify Client Secret") + while [[ -z "$spotify_secret" ]]; do + msg_error "Client Secret cannot be empty" + spotify_secret=$(prompt_password "Spotify Client Secret") + done + else + msg_warn "Skipped β€” configure Spotify later via the web UI" + fi + + # ── Tidal (optional) ───────────────────────────────────────── + echo "" >&2 + echo -e "${TAB}${MUSIC} ${BOLD}${BL}Tidal Configuration ${DIM}(optional)${CL}" >&2 + echo -e "${TAB}${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" >&2 + + local tidal_id="TidalClientID" + local tidal_secret="TidalClientSecret" + local tidal_redirect="http://${ip}:${TIDAL_CB_PORT}/tidal/callback" + + if prompt_confirm "Configure Tidal now?" "n"; then + tidal_id=$(prompt_input "Tidal Client ID") + tidal_secret=$(prompt_password "Tidal Client Secret") + else + msg_warn "Skipped β€” configure Tidal later via the web UI" + fi + + # ── Media Server ───────────────────────────────────────────── + echo "" >&2 + echo -e "${TAB}${MUSIC} ${BOLD}${BL}Media Server Configuration${CL}" >&2 + echo -e "${TAB}${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" >&2 + + local media_server="plex" + local plex_url="http://localhost:32400" + local plex_token="PLEX_API_TOKEN" + local jellyfin_url="http://localhost:8096" + local jellyfin_key="JELLYFIN_API_KEY" + local navidrome_url="http://localhost:4533" + local navidrome_user="NAVIDROME_USERNAME" + local navidrome_pass="NAVIDROME_PASSWORD" + + local ms_choice + ms_choice=$(prompt_select "Which media server do you use?" "Plex" "Jellyfin" "Navidrome" "Skip (configure later)") + + case "$ms_choice" in + 1) + media_server="plex" + plex_url=$(prompt_input "Plex server URL" "http://localhost:32400") + plex_token=$(prompt_input "Plex API token") + while [[ -z "$plex_token" ]]; do + msg_error "Token cannot be empty" + plex_token=$(prompt_input "Plex API token") + done + ;; + 2) + media_server="jellyfin" + jellyfin_url=$(prompt_input "Jellyfin server URL" "http://localhost:8096") + jellyfin_key=$(prompt_input "Jellyfin API key") + while [[ -z "$jellyfin_key" ]]; do + msg_error "API key cannot be empty" + jellyfin_key=$(prompt_input "Jellyfin API key") + done + ;; + 3) + media_server="navidrome" + navidrome_url=$(prompt_input "Navidrome server URL" "http://localhost:4533") + navidrome_user=$(prompt_input "Navidrome username" "admin") + navidrome_pass=$(prompt_password "Navidrome password") + while [[ -z "$navidrome_pass" ]]; do + msg_error "Password cannot be empty" + navidrome_pass=$(prompt_password "Navidrome password") + done + ;; + 4) + msg_warn "Skipped β€” configure your media server later via the web UI" + ;; + esac + + # ── Timezone ───────────────────────────────────────────────── + echo "" >&2 + echo -e "${TAB}${GEAR} ${BOLD}${BL}General Settings${CL}" >&2 + echo -e "${TAB}${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" >&2 + + local timezone + timezone=$(prompt_input "Timezone" "America/New_York") + + # ── Write config.json ──────────────────────────────────────── + echo "" >&2 + msg_info "Writing SoulSync configuration" + + # Pass all values via environment variables for safety (handles special chars + # in passwords, API keys, etc.) and use a quoted heredoc so bash doesn't + # interfere with the Python code. + _SS_MEDIA_SERVER="$media_server" \ + _SS_SPOTIFY_ID="$spotify_id" \ + _SS_SPOTIFY_SECRET="$spotify_secret" \ + _SS_SPOTIFY_REDIRECT="$spotify_redirect" \ + _SS_TIDAL_ID="$tidal_id" \ + _SS_TIDAL_SECRET="$tidal_secret" \ + _SS_TIDAL_REDIRECT="$tidal_redirect" \ + _SS_PLEX_URL="$plex_url" \ + _SS_PLEX_TOKEN="$plex_token" \ + _SS_JELLYFIN_URL="$jellyfin_url" \ + _SS_JELLYFIN_KEY="$jellyfin_key" \ + _SS_NAVIDROME_URL="$navidrome_url" \ + _SS_NAVIDROME_USER="$navidrome_user" \ + _SS_NAVIDROME_PASS="$navidrome_pass" \ + _SS_SLSKD_PORT="$SLSKD_PORT" \ + _SS_SLSKD_API_KEY="$SLSKD_API_KEY" \ + _SS_INSTALL_DIR="$SOULSYNC_DIR" \ + "$SOULSYNC_DIR/venv/bin/python3" - <<'PYEOF' +import json, os + +e = os.environ +d = e["_SS_INSTALL_DIR"] + +config = { + "active_media_server": e["_SS_MEDIA_SERVER"], + "spotify": { + "client_id": e["_SS_SPOTIFY_ID"], + "client_secret": e["_SS_SPOTIFY_SECRET"], + "redirect_uri": e["_SS_SPOTIFY_REDIRECT"] + }, + "tidal": { + "client_id": e["_SS_TIDAL_ID"], + "client_secret": e["_SS_TIDAL_SECRET"], + "redirect_uri": e["_SS_TIDAL_REDIRECT"] + }, + "plex": { + "base_url": e["_SS_PLEX_URL"], + "token": e["_SS_PLEX_TOKEN"], + "auto_detect": True + }, + "jellyfin": { + "base_url": e["_SS_JELLYFIN_URL"], + "api_key": e["_SS_JELLYFIN_KEY"], + "auto_detect": True + }, + "navidrome": { + "base_url": e["_SS_NAVIDROME_URL"], + "username": e["_SS_NAVIDROME_USER"], + "password": e["_SS_NAVIDROME_PASS"], + "auto_detect": True + }, + "soulseek": { + "slskd_url": f"http://localhost:{e['_SS_SLSKD_PORT']}", + "api_key": e["_SS_SLSKD_API_KEY"], + "download_path": f"{d}/downloads", + "transfer_path": f"{d}/Transfer", + "search_timeout": 60, + "search_timeout_buffer": 15 + }, + "logging": { + "path": f"{d}/logs/app.log", + "level": "INFO" + }, + "database": { + "path": f"{d}/data/music_library.db", + "max_workers": 5 + }, + "metadata_enhancement": { + "enabled": True, + "embed_album_art": True + }, + "file_organization": { + "enabled": True, + "templates": { + "album_path": "$albumartist/$albumartist - $album/$track - $title", + "single_path": "$artist/$artist - $title/$title", + "compilation_path": "Compilations/$album/$track - $artist - $title", + "playlist_path": "$playlist/$artist - $title" + } + }, + "playlist_sync": { + "create_backup": True + }, + "listenbrainz": { + "token": "LISTENBRAINZ_TOKEN" + }, + "download_source": { + "mode": "soulseek", + "hybrid_primary": "soulseek", + "youtube_min_confidence": 0.65 + }, + "acoustid": { + "api_key": "", + "enabled": False + }, + "musicbrainz": { + "embed_tags": True + }, + "settings": { + "audio_quality": "flac" + }, + "import": { + "staging_path": f"{d}/Staging" + } +} + +with open(f"{d}/config/config.json", "w") as f: + json.dump(config, f, indent=2) +PYEOF + + chown -R "$SOULSYNC_USER:$SOULSYNC_GROUP" "$SOULSYNC_DIR" + chmod 600 "$SOULSYNC_DIR/config/config.json" + + # Write timezone to environment + echo "TZ=${timezone}" >>"$SOULSYNC_DIR/.env" + chown "$SOULSYNC_USER:$SOULSYNC_GROUP" "$SOULSYNC_DIR/.env" + + msg_ok "Configured SoulSync" +} + +create_services() { + msg_info "Creating systemd services" + + # ── slskd service ── + cat >/etc/systemd/system/slskd.service </etc/systemd/system/soulsync.service <>"$LOGFILE" 2>&1 + msg_ok "Created systemd services" +} + +start_services() { + msg_info "Starting slskd" + systemctl enable -q slskd >>"$LOGFILE" 2>&1 + systemctl start slskd >>"$LOGFILE" 2>&1 + msg_ok "Started slskd" + + msg_info "Starting SoulSync" + systemctl enable -q soulsync >>"$LOGFILE" 2>&1 + systemctl start soulsync >>"$LOGFILE" 2>&1 + msg_ok "Started SoulSync" +} + +setup_motd() { + local ip + ip=$(get_ip) + + cat >/etc/motd <>"$LOGFILE" 2>&1 + apt-get -y -qq autoclean >>"$LOGFILE" 2>&1 + msg_ok "Cleaned up" +} + +# -- Completion Banner --------------------------------------------------------- + +show_completion() { + local ip + ip=$(get_ip) + + echo "" >&2 + echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" >&2 + echo -e " ${CREATING}${GN}${BOLD}SoulSync has been successfully installed!${CL}" >&2 + echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" >&2 + echo "" >&2 + + echo -e " ${NETWORK}${YW}SoulSync Web UI${CL} ${BGN}http://${ip}:${SOULSYNC_PORT}${CL}" >&2 + echo -e " ${NETWORK}${YW}slskd Web UI${CL} ${BGN}http://${ip}:${SLSKD_PORT}${CL}" >&2 + + echo "" >&2 + echo -e " ${GEAR}${BOLD}Paths${CL}" >&2 + echo -e " ${TAB}${DIM}Config:${CL} ${SOULSYNC_DIR}/config/config.json" >&2 + echo -e " ${TAB}${DIM}slskd:${CL} ${SLSKD_DIR}/slskd.yml" >&2 + echo -e " ${TAB}${DIM}Downloads:${CL} ${SOULSYNC_DIR}/downloads/" >&2 + echo -e " ${TAB}${DIM}Transfer:${CL} ${SOULSYNC_DIR}/Transfer/" >&2 + echo -e " ${TAB}${DIM}Logs:${CL} ${SOULSYNC_DIR}/logs/" >&2 + echo -e " ${TAB}${DIM}Install log:${CL} ${LOGFILE}" >&2 + + echo "" >&2 + echo -e " ${GEAR}${BOLD}Service Management${CL}" >&2 + echo -e " ${TAB}${DIM}systemctl {start|stop|restart|status} soulsync${CL}" >&2 + echo -e " ${TAB}${DIM}systemctl {start|stop|restart|status} slskd${CL}" >&2 + + echo "" >&2 + echo -e " ${INFO}${YW}Next steps:${CL}" >&2 + echo -e " ${TAB}1. Open the SoulSync web UI at ${BGN}http://${ip}:${SOULSYNC_PORT}${CL}" >&2 + echo -e " ${TAB}2. Complete Spotify OAuth by clicking 'Connect' in Settings" >&2 + echo -e " ${TAB}3. Start syncing your music library!" >&2 + echo "" >&2 +} + +# -- Main Execution ------------------------------------------------------------ + +main() { + header_info + root_check + arch_check + os_check + network_check + existing_install_check + + echo "" >&2 + echo -e " ${CREATING}${BOLD}${GN}Starting SoulSync installation${CL}" >&2 + echo "" >&2 + + update_os + install_dependencies + create_user_and_dirs + install_slskd + configure_slskd + install_soulsync + configure_soulsync + create_services + start_services + setup_motd + cleanup_install + show_completion +} + +main "$@"