diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..53e022b --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +PUID=1000 +PGID=1000 +TZ=UTC +CONFIG_DIR=/tmp/config +DATA_DIR=/tmp/data +MOUNT_DIR=/tmp/mount +TORBOX_API_KEY=testkey1234567890123456789012345 +RADARR_API_KEY=radarrkey12345678901234567890123 +SONARR_API_KEY=sonarrkey12345678901234567890123 +PROWLARR_API_KEY=prowlarrkey1234567890123456789012 +DECYPHARR_USER=torbox +DECYPHARR_PASS=password +PLEX_CLAIM=claim-xxxxx diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f600cf2..6e4b606 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v4 - name: Install ShellCheck - run: sudo apt-get install -y shellcheck + run: sudo apt-get update && sudo apt-get install -y shellcheck - name: Lint setup.sh run: shellcheck setup.sh @@ -44,10 +44,28 @@ jobs: # so bash -n checks the actual script content, not the heredoc container. sed -n '/^cat > "${INSTALL_DIR}\/manage.sh" << .MANAGE_EOF.$/,/^MANAGE_EOF$/p' setup.sh | sed '1d;$d' > /tmp/manage_extracted.sh sed -n '/^cat >> "${INSTALL_DIR}\/manage.sh" << .MANAGE_INLINE.$/,/^MANAGE_INLINE$/p' setup.sh | sed '1d;$d' >> /tmp/manage_extracted.sh - bash -n /tmp/manage_extracted.sh && echo "manage.sh syntax OK" || echo "manage.sh has syntax errors" + bash -n /tmp/manage_extracted.sh && echo "manage.sh syntax OK" || { echo "manage.sh has syntax errors"; exit 1; } - name: Validate uninstall.sh syntax run: bash -n uninstall.sh && echo "uninstall.sh syntax OK" - name: Validate setup.sh syntax run: bash -n setup.sh && echo "setup.sh syntax OK" + + - name: Validate docker-compose.yml + run: | + # Validate compose template with representative environment variables + export PUID=1000 + export PGID=1000 + export TZ=UTC + export CONFIG_DIR=/tmp/config + export DATA_DIR=/tmp/data + export MOUNT_DIR=/tmp/mount + export TORBOX_API_KEY=testkey1234567890123456789012345 + export RADARR_API_KEY=radarrkey12345678901234567890123 + export SONARR_API_KEY=sonarrkey12345678901234567890123 + export PROWLARR_API_KEY=prowlarrkey1234567890123456789012 + export DECYPHARR_USER=torbox + export DECYPHARR_PASS=password + export PLEX_CLAIM=claim-xxxxx + docker compose config -q diff --git a/README.md b/README.md index 7789f24..5c74360 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ User Request Search & Automation Cloud Download | **Plex** | 32400 | Media server option 1. Streams your library to any device. Requires a free Plex account. | | **Jellyfin** | 8096 | Media server option 2 (open-source, no account needed). Streams your library to any device. | -> **Note:** All service ports except Plex and Jellyfin are bound to `127.0.0.1` (localhost only) by default for security. Plex and Jellyfin bind to all interfaces so other devices on your LAN can stream. See [Accessing From Other Devices](#accessing-from-other-devices) to open access for other services. +> **Note:** All service ports including Plex and Jellyfin are bound to `127.0.0.1` (localhost only) by default for security. To stream from other devices on your LAN, you will need to expose the Plex/Jellyfin ports. See [Accessing From Other Devices](#accessing-from-other-devices). ## Before You Begin @@ -288,12 +288,8 @@ Decypharr is the critical bridge between your media managers and TorBox. **Nothi Prowlarr manages your torrent indexers (the sites where torrents are found). The script already connected Byparr, Radarr, and Sonarr — and pre-added **1337x** as a default indexer. 1. Open **http://localhost:9696** -2. **Create login credentials** (important!): - - Go to **Settings → General** - - Authentication is already set to **Forms (Login Page)** — just enter a **username** and **password** - - Click **Save Changes** at the top - - > ⚠️ Authentication is disabled for local addresses by default to allow the setup script's API calls to work. You should set up credentials now for security. +2. Log in using the **auto-generated admin credentials** displayed at the end of the setup script. + > 💡 You can retrieve these credentials at any time by running `./manage.sh keys --show-secrets` 3. **1337x is already configured** as a default indexer. To add more, go to **Indexers → Add Indexer** (the `+` button) 4. Search for and add additional indexers you want. Some popular public options: @@ -315,13 +311,10 @@ Prowlarr manages your torrent indexers (the sites where torrents are found). The ### Step 3: Verify Radarr (~2 minutes) -Radarr manages your movie library. The script already configured everything — you just need to create login credentials and verify. +Radarr manages your movie library. The script already configured everything. 1. Open **http://localhost:7878** -2. **Create login credentials:** - - Go to **Settings → General** - - Authentication is already set to **Forms (Login Page)** — just enter a **username** and **password** - - Click **Save Changes** +2. Log in using the **auto-generated admin credentials**. 3. Verify the auto-configuration worked: - **Settings → Download Clients** → you should see **Decypharr** listed - **Settings → Media Management** → Root Folders should show `/data/media/movies` @@ -341,10 +334,7 @@ Radarr manages your movie library. The script already configured everything — Sonarr manages your TV show library. Same auto-configuration as Radarr. 1. Open **http://localhost:8989** -2. **Create login credentials:** - - Go to **Settings → General** - - Authentication is already set to **Forms (Login Page)** — just enter a **username** and **password** - - Click **Save Changes** +2. Log in using the **auto-generated admin credentials**. 3. Verify the auto-configuration worked: - **Settings → Download Clients** → you should see **Decypharr** listed - **Settings → Media Management** → Root Folders should show `/data/media/tv` @@ -519,9 +509,9 @@ For remote access outside your home network, use a reverse proxy like [Caddy](ht ## Security Notes -- **Ports are bound to `127.0.0.1`** by default, preventing LAN/WAN exposure of admin UIs -- **Authentication is set to `Forms` with `DisabledForLocalAddresses`** after setup — **you must create login credentials** in each service's Settings → General before exposing them to your LAN (see Steps 2–4 in the walkthrough above) -- **The `.env` file** contains your TorBox API key and *arr API keys — it's `chmod 600` (owner-read only). Don't commit it to version control +- **Ports are bound to `127.0.0.1`** by default, preventing LAN/WAN exposure of admin UIs, including Plex and Jellyfin +- **Authentication is set to `Forms` with `Enabled`** automatically during setup. Secure admin credentials are auto-generated for Radarr, Sonarr, and Prowlarr, ensuring they are protected by default if you choose to expose them to your LAN. +- **The `.env` file** contains your TorBox API key, admin credentials, and *arr API keys — it's `chmod 600` (owner-read only). Don't commit it to version control - **Only Decypharr** gets `SYS_ADMIN` capability and FUSE access — other containers only read files via symlinks - **Decypharr config is mounted read-only** — the config directory is bound as `:ro` to prevent containers from modifying their own configuration @@ -641,20 +631,19 @@ This usually means Radarr or Sonarr hasn't finished starting yet. Wait a minute - **Seerr** instead of Overseerr — Overseerr was archived in 2024; Seerr is the merged successor supporting Plex, Jellyfin, and Emby - **Byparr** instead of FlareSolverr — FlareSolverr is currently non-functional (Cloudflare detects it); Byparr is a drop-in replacement using the same API - **Only Decypharr gets FUSE/SYS_ADMIN** — Plex/Jellyfin/Radarr/Sonarr only read files, they don't need elevated privileges -- **Plex on bridge networking** — Plex runs on the same Docker bridge network as all other services, allowing Seerr to connect via container name (`http://plex:32400`). Port 32400 is exposed on all interfaces for LAN streaming. Host networking was avoided because many Linux firewalls (UFW, firewalld) block traffic from Docker bridge containers to the host, causing Seerr <-> Plex connectivity failures +- **Plex on bridge networking** — Plex runs on the same Docker bridge network as all other services, allowing Seerr to connect via container name (`http://plex:32400`). Host networking was avoided because many Linux firewalls (UFW, firewalld) block traffic from Docker bridge containers to the host, causing Seerr <-> Plex connectivity failures - **Plex notifications on Radarr/Sonarr** — triggers an instant Plex library scan when content is imported, upgraded, or deleted, so new media appears in seconds instead of waiting for Plex's periodic scan interval -- **Ports bound to localhost** — prevents accidental LAN/WAN exposure of admin UIs +- **Ports bound to localhost** — prevents accidental LAN/WAN exposure of admin UIs, including Plex and Jellyfin - **Mount propagation** — uses `rshared` on Decypharr (the mount source) and `rslave` on media servers (consumers); a systemd service (`torbox-media-server`) handles this automatically on boot, and `manage.sh` re-applies it as a safety net - **Hardlinks disabled** — debrid setups use symlinks from Decypharr's WebDAV mount, not local files; hardlinks would fail - **Systemd auto-start** — a `torbox-media-server.service` unit handles mount propagation and container startup on boot, so users never have to manually start services after a reboot -- **`DisabledForLocalAddresses` auth** — allows the setup script's API calls to configure services on first launch without requiring credentials; users should enable full auth afterward +- **Auto-configured Auth** — the setup script uses `DisabledForLocalAddresses` initially to configure services via API, then securely locks them down to `Forms` (`Enabled`) with auto-generated credentials before finishing. - **Pre-seeded API keys** — generated during setup and injected into config.xml before containers start, enabling fully automated API-based configuration - **jq for JSON manipulation** — used to modify *arr config via API; auto-installed as a dependency - **Quality profile upgrades enabled** — without this, Radarr/Sonarr won't replace a 720p version with a 1080p one; most users want automatic upgrades - **Docker images pinned to specific versions** — avoids breakage from upstream changes; re-run `setup.sh` to pick up newer versions intentionally - **Decypharr config mounted read-only** — config.json is bind-mounted as `:ro` to prevent containers from accidentally modifying it - **Decypharr credentials pre-seeded** — generated during setup and injected into config.json, eliminating manual credential creation -- **Plex and Jellyfin on all interfaces** — both media servers expose their ports on all interfaces for LAN streaming consistency; other services remain localhost-only for security ## Updating diff --git a/docker-compose.yml b/docker-compose.yml index 86cff63..55e739a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -210,7 +210,7 @@ services: networks: - media-network ports: - - "32400:32400" + - "127.0.0.1:32400:32400" environment: - PUID=${PUID} - PGID=${PGID} @@ -244,8 +244,8 @@ services: networks: - media-network ports: - - "8096:8096" - - "8920:8920" + - "127.0.0.1:8096:8096" + - "127.0.0.1:8920:8920" environment: - PUID=${PUID} - PGID=${PGID} diff --git a/setup.sh b/setup.sh index 85b6f11..9713194 100755 --- a/setup.sh +++ b/setup.sh @@ -13,6 +13,7 @@ set -euo pipefail VERSION="1.0.0" DRY_RUN=false +SERVICES_STARTED=false trap 'cleanup_on_interrupt' INT TERM @@ -477,6 +478,12 @@ gather_config() { done fi + # Validate media server choice + if [[ "$MEDIA_SERVER" != "plex" && "$MEDIA_SERVER" != "jellyfin" ]]; then + log_warn "Invalid media server '${MEDIA_SERVER}'. Defaulting to plex." + MEDIA_SERVER="plex" + fi + PLEX_CLAIM="${TORBOX_PLEX_CLAIM:-}" if [[ "$MEDIA_SERVER" == "plex" && -z "$PLEX_CLAIM" && "$NON_INTERACTIVE" != "true" ]]; then echo "" @@ -746,10 +753,12 @@ generate_decypharr_config() { fi # Generate credentials for Decypharr web UI - DECYPHARR_USER="torbox" - DECYPHARR_PASS="$(openssl rand -base64 12 2>/dev/null | tr -d '/+=' | head -c 12)" - if [[ -z "$DECYPHARR_PASS" ]]; then - DECYPHARR_PASS="$(head -c 12 /dev/urandom | base64 | tr -d '/+=' | head -c 12)" + DECYPHARR_USER="${DECYPHARR_USER:-torbox}" + if [[ -z "${DECYPHARR_PASS:-}" ]]; then + DECYPHARR_PASS="$(openssl rand -base64 12 2>/dev/null | tr -d '/+=' | head -c 12)" + if [[ -z "$DECYPHARR_PASS" ]]; then + DECYPHARR_PASS="$(head -c 12 /dev/urandom | base64 | tr -d '/+=' | head -c 12)" + fi fi cat > "${CONFIG_DIR}/decypharr/config.json" << DECYPHARR_EOF @@ -1187,22 +1196,44 @@ case "${1:-help}" in show_urls ;; keys) + local show_secrets=false + if [[ "\$2" == "--show-secrets" ]]; then + show_secrets=true + fi + + mask_val() { + local val="\$1" + if [[ "\$show_secrets" == "true" ]]; then + echo "\$val" + elif [[ \${#val} -gt 8 ]]; then + echo "\${val:0:4}......\${val: -4}" + else + echo "******" + fi + } + echo -e "\n${CYAN}━━━━ API Keys ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" - echo -e " ${YELLOW}WARNING: Sensitive credentials below. Do not share this output.${NC}\n" - echo -e " ${BOLD}TorBox${NC} $(env_val TORBOX_API_KEY)" - echo -e " ${BOLD}Radarr${NC} $(env_val RADARR_API_KEY)" - echo -e " ${BOLD}Sonarr${NC} $(env_val SONARR_API_KEY)" - echo -e " ${BOLD}Prowlarr${NC} $(env_val PROWLARR_API_KEY)" + if [[ "\$show_secrets" == "true" ]]; then + echo -e " ${YELLOW}WARNING: Sensitive credentials below. Do not share this output.${NC}\n" + else + echo -e " ${YELLOW}Secrets masked. Use './manage.sh keys --show-secrets' to reveal.${NC}\n" + fi + + echo -e " ${BOLD}TorBox${NC} \$(mask_val \"\$(env_val TORBOX_API_KEY)\")" + echo -e " ${BOLD}Radarr${NC} \$(mask_val \"\$(env_val RADARR_API_KEY)\")" + echo -e " ${BOLD}Sonarr${NC} \$(mask_val \"\$(env_val SONARR_API_KEY)\")" + echo -e " ${BOLD}Prowlarr${NC} \$(mask_val \"\$(env_val PROWLARR_API_KEY)\")" + local _radarr_pass _sonarr_pass _prowlarr_pass - _radarr_pass="$(env_val RADARR_ADMIN_PASS)" - _sonarr_pass="$(env_val SONARR_ADMIN_PASS)" - _prowlarr_pass="$(env_val PROWLARR_ADMIN_PASS)" - if [[ -n "$_radarr_pass" ]]; then + _radarr_pass="\$(env_val RADARR_ADMIN_PASS)" + _sonarr_pass="\$(env_val SONARR_ADMIN_PASS)" + _prowlarr_pass="\$(env_val PROWLARR_ADMIN_PASS)" + if [[ -n "\$_radarr_pass" ]]; then echo "" echo -e " ${BOLD}Admin Credentials:${NC}" - echo -e " ${BOLD}Radarr${NC} user: $(env_val RADARR_ADMIN_USER) pass: ${_radarr_pass}" - echo -e " ${BOLD}Sonarr${NC} user: $(env_val SONARR_ADMIN_USER) pass: ${_sonarr_pass}" - echo -e " ${BOLD}Prowlarr${NC} user: $(env_val PROWLARR_ADMIN_USER) pass: ${_prowlarr_pass}" + echo -e " ${BOLD}Radarr${NC} user: \$(env_val RADARR_ADMIN_USER) pass: \$(mask_val \"\${_radarr_pass}\")" + echo -e " ${BOLD}Sonarr${NC} user: \$(env_val SONARR_ADMIN_USER) pass: \$(mask_val \"\${_sonarr_pass}\")" + echo -e " ${BOLD}Prowlarr${NC} user: \$(env_val PROWLARR_ADMIN_USER) pass: \$(mask_val \"\${_prowlarr_pass}\")" fi echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" ;; @@ -2038,9 +2069,13 @@ configure_arr_auth() { auth_config=$(curl -sf --connect-timeout 5 --max-time 15 -H "X-Api-Key: ${api_key}" "${url}/api/v3/config/host" 2>/dev/null) || true [[ -z "$auth_config" ]] && { log_warn " Could not retrieve ${name} auth config."; return 1; } - # Check if auth is already set to Forms - if echo "$auth_config" | grep -q '"authenticationMethod":"Forms"' 2>/dev/null || \ - echo "$auth_config" | grep -q '"authenticationMethod": "Forms"' 2>/dev/null; then + # Check if auth is already set to Forms and Required + if (echo "$auth_config" | grep -q '"authenticationMethod":"Forms"' 2>/dev/null || \ + echo "$auth_config" | grep -q '"authenticationMethod": "Forms"' 2>/dev/null) && \ + (echo "$auth_config" | grep -q '"authenticationRequired":"Enabled"' 2>/dev/null || \ + echo "$auth_config" | grep -q '"authenticationRequired": "Enabled"' 2>/dev/null) && \ + (echo "$auth_config" | grep -q '"username":' 2>/dev/null && \ + ! echo "$auth_config" | grep -q '"username": ""' 2>/dev/null); then log_info " ${name} already has Forms authentication configured." return 0 fi @@ -2072,28 +2107,28 @@ configure_arr_auth() { '.authenticationMethod = "Forms" | .authenticationRequired = "Enabled" | .username = $user | .password = $pass' 2>/dev/null) || true [[ -z "$updated_auth" ]] && { log_warn " Could not update ${name} auth config."; return 1; } - curl -sf --connect-timeout 5 --max-time 15 -X PUT \ + if curl -sf --connect-timeout 5 --max-time 15 -X PUT \ -H "Content-Type: application/json" \ -H "X-Api-Key: ${api_key}" \ "${url}/api/v3/config/host/${auth_id}" \ - -d "$updated_auth" -o /dev/null 2>/dev/null && { + -d "$updated_auth" -o /dev/null 2>/dev/null; then log_info " ${name} auth set to Forms (Enabled) with auto-generated credentials." - local env_key + local env_key_prefix case "$name" in - Radarr) env_key="RADARR_ADMIN_USER" ;; - Sonarr) env_key="SONARR_ADMIN_USER" ;; - Prowlarr) env_key="PROWLARR_ADMIN_USER" ;; - + Radarr) env_key_prefix="RADARR_ADMIN" ;; + Sonarr) env_key_prefix="SONARR_ADMIN" ;; + Prowlarr) env_key_prefix="PROWLARR_ADMIN" ;; esac - # Remove old entries and append new ones - grep -v "^${env_key}_USER=\|^${env_key}_PASS=" "${ENV_FILE}" > "${ENV_FILE}.tmp" 2>/dev/null || true - echo "${env_key}_USER=\"${admin_user}\"" >> "${ENV_FILE}.tmp" - echo "${env_key}_PASS=\"${admin_pass}\"" >> "${ENV_FILE}.tmp" - mv "${ENV_FILE}.tmp" "${ENV_FILE}" - chmod 600 "${ENV_FILE}" - } - } || log_warn " Failed to configure ${name} auth." + # Remove old entries and append new ones + grep -v "^${env_key_prefix}_USER=\|^${env_key_prefix}_PASS=" "${ENV_FILE}" > "${ENV_FILE}.tmp" 2>/dev/null || true + echo "${env_key_prefix}_USER=\"${admin_user}\"" >> "${ENV_FILE}.tmp" + echo "${env_key_prefix}_PASS=\"${admin_pass}\"" >> "${ENV_FILE}.tmp" + mv "${ENV_FILE}.tmp" "${ENV_FILE}" + chmod 600 "${ENV_FILE}" + else + log_warn " Failed to configure ${name} auth." + fi } # ============================================================================ @@ -2420,6 +2455,10 @@ check_existing_installation() { EXISTING_PROWLARR_ADMIN_USER=$(grep '^PROWLARR_ADMIN_USER=' "${ENV_FILE}" 2>/dev/null | cut -d= -f2- | tr -d '"' | tr -d "'") || true EXISTING_PROWLARR_ADMIN_PASS=$(grep '^PROWLARR_ADMIN_PASS=' "${ENV_FILE}" 2>/dev/null | cut -d= -f2- | tr -d '"' | tr -d "'") || true + # Extract existing Decypharr credentials + DECYPHARR_USER=$(grep "^DECYPHARR_USER=" "${ENV_FILE}" 2>/dev/null | cut -d= -f2- | tr -d "\"" | tr -d "'") || true + DECYPHARR_PASS=$(grep "^DECYPHARR_PASS=" "${ENV_FILE}" 2>/dev/null | cut -d= -f2- | tr -d "\"" | tr -d "'") || true + # Validate extracted API keys are valid 32-char hex; regenerate if corrupted if [[ -n "$EXISTING_RADARR_API_KEY" && ! "$EXISTING_RADARR_API_KEY" =~ ^[0-9a-f]{32}$ ]]; then log_warn "Corrupted API key detected for Radarr. Will regenerate." @@ -2474,6 +2513,7 @@ start_services() { log_info "You can start services later with:" echo " cd ${INSTALL_DIR} && ./manage.sh start" log_info "Once started, re-run this script or configure services manually." + SERVICES_STARTED=false fi } diff --git a/tests/test_utils.sh b/tests/test_utils.sh index e0c2db1..9cb2000 100644 --- a/tests/test_utils.sh +++ b/tests/test_utils.sh @@ -14,38 +14,17 @@ failed=0 pass() { echo -e "${GREEN}[PASS]${NC} $1"; passed=$((passed + 1)); } fail() { echo -e "${RED}[FAIL]${NC} $1"; failed=$((failed + 1)); } -# Mask API key for display (show first/last 4 chars) -mask_key() { - local k="$1" - if [[ ${#k} -gt 4 ]]; then - echo "${k:0:4}...${k: -4}" - else - echo "$k" - fi -} - -# Generate a deterministic-length API key (32-char hex) -generate_api_key() { - local key="" - if key=$(openssl rand -hex 16 2>/dev/null); then - : - elif key=$(xxd -p -l 16 /dev/urandom 2>/dev/null); then - : - elif key=$(od -An -tx1 -N16 /dev/urandom 2>/dev/null | tr -d ' \t\n'); then - : - elif key=$(head -c 16 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \t\n'); then - : - else - echo "" - return 1 - fi - key=$(echo "$key" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-f0-9' | head -c 32) - if [[ ${#key} -ne 32 ]]; then - echo "" - return 1 - fi - echo "$key" -} +# Source functions directly from setup.sh to ensure tests match implementation +SETUP_SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/setup.sh" +if [[ -f "$SETUP_SCRIPT" ]]; then + source <(sed -n '/^generate_api_key() {/,/^}/p' "$SETUP_SCRIPT") + source <(sed -n '/^mask_key() /,/^}/p' "$SETUP_SCRIPT" 2>/dev/null || true) + # Inline mask_key since it's a one-liner in setup.sh: + source <(grep '^mask_key() ' "$SETUP_SCRIPT") +else + echo "Error: setup.sh not found at $SETUP_SCRIPT" + exit 1 +fi print_summary() { echo "" diff --git a/uninstall.sh b/uninstall.sh index a158017..5804ac4 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -39,11 +39,14 @@ env_val() { # Detect the correct docker compose command COMPOSE_CMD=() +DOCKER_CMD=() detect_compose_cmd() { if docker info &>/dev/null; then COMPOSE_CMD=(docker compose) + DOCKER_CMD=(docker) else COMPOSE_CMD=(sudo docker compose) + DOCKER_CMD=(sudo docker) fi } @@ -89,23 +92,27 @@ echo "" # Step 1: Stop and remove containers log_info "Stopping and removing Docker containers..." +if [[ ${#DOCKER_CMD[@]} -eq 0 ]]; then + detect_compose_cmd +fi + if [[ -f "${ENV_FILE}" && -f "${COMPOSE_FILE}" ]]; then compose_cmd down --remove-orphans 2>/dev/null || { log_warn "Docker compose down failed. Attempting manual cleanup..." for svc in decypharr prowlarr byparr radarr sonarr seerr plex jellyfin; do - docker rm -f "$svc" 2>/dev/null || true + "${DOCKER_CMD[@]}" rm -f "$svc" 2>/dev/null || true done } else log_warn "Missing .env or docker-compose.yml. Skipping compose down." for svc in decypharr prowlarr byparr radarr sonarr seerr plex jellyfin; do - docker rm -f "$svc" 2>/dev/null || true + "${DOCKER_CMD[@]}" rm -f "$svc" 2>/dev/null || true done fi # Remove the Docker network (dynamically computed from project directory name) project_name="$(basename "${INSTALL_DIR}")" -docker network rm "${project_name}_media-network" 2>/dev/null || true +"${DOCKER_CMD[@]}" network rm "${project_name}_media-network" 2>/dev/null || true # Step 2: Remove systemd service log_info "Removing systemd service..." @@ -160,10 +167,13 @@ else fi if [[ "${remove_images,,}" == "y" ]]; then log_info "Removing Docker images..." + if [[ ${#DOCKER_CMD[@]} -eq 0 ]]; then + detect_compose_cmd + fi local_removed=0 if [[ ${#_images[@]} -gt 0 ]]; then for img in "${_images[@]}"; do - if docker rmi "$img" 2>/dev/null; then + if "${DOCKER_CMD[@]}" rmi "$img" 2>/dev/null; then log_info " Removed: $img" local_removed=$((local_removed + 1)) fi