assemblrr/lib/seerr.sh
soulis-1256 784977aeee
Use one quality choice for Radarr, Sonarr, and Seerr.
Express asks the same question as manual. The pick is stored as
QUALITY_TIER and applied to movies and TV, including Seerr. Compose
up hides sidecar Dockerfile output behind a spinner.
2026-08-18 23:07:29 +03:00

475 lines
17 KiB
Bash

#!/bin/bash
# assemblrr Seerr & Recyclarr configuration — sourced by config.sh
# Provides: configure_seerr, configure_recyclarr
# Requires: lib/core.sh (logging), lib/api.sh (api_get/post helpers),
# lib/quality.sh (tier + names), lib/arr.sh (lookups),
# config.sh (_cfg_log_info, log_step, log_step_fail, etc.)
set -euo pipefail
_seerr_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
[ -f "$_seerr_dir/quality.sh" ] && source "$_seerr_dir/quality.sh"
# --- Shared helper: connect or update a service (Radarr/Sonarr) in Seerr ---
# Usage: seerr_connect_service <service_type> <port> <api_key> <profile_func> <active_dir> <is_4k> <cookie_jar> [existing_id]
# With existing_id: PUT update; without: POST create.
# Example: seerr_connect_service "radarr" 7878 "$RADARR_API_KEY" "lookup_radarr_profile" "/data/media/movies" "true" "$cookie_jar"
seerr_connect_service() {
local service_type="$1"
local service_port="$2"
local api_key="$3"
local profile_func="$4"
local active_dir="$5"
local is_4k="$6"
local cookie_jar="$7"
local existing_id="${8:-}"
if [ -z "$api_key" ]; then
log_step_fail "Seerr: ${service_type} API key not provided"
return 1
fi
if [ -z "$cookie_jar" ] || [ ! -f "$cookie_jar" ]; then
log_step_fail "Seerr: cookie jar not found for ${service_type} connection"
return 1
fi
local profile
profile=$($profile_func "$api_key")
local profile_id="${profile%%:*}"
local profile_name="${profile##*:}"
# id is read-only on PUT — omit it from the body
local payload
payload=$(cat <<EOF
{
"name": "${service_type^}",
"hostname": "${service_type}",
"port": ${service_port},
"apiKey": "${api_key}",
"useSsl": false,
"baseUrl": "",
"isDefault": true,
"is4k": ${is_4k},
"minimumAvailability": "released",
"syncEnabled": true,
"activeProfileId": ${profile_id},
"activeProfileName": "${profile_name}",
"activeDirectory": "${active_dir}",
"tags": []
}
EOF
)
# Special handling for Sonarr (needs enableSeasonFolders)
if [ "$service_type" = "sonarr" ]; then
payload=$(echo "$payload" | jq '.enableSeasonFolders = true' 2>/dev/null || echo "$payload")
fi
local http_code
if [ -n "$existing_id" ]; then
http_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 -X PUT -b "$cookie_jar" \
-H "Content-Type: application/json" \
-d "$payload" \
"http://${API_HOST}:5055/api/v1/settings/${service_type}/${existing_id}" 2>/dev/null || echo "000")
if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
log_step "Seerr: updated ${service_type^} profile → ${profile_name}"
return 0
else
log_step_fail "Seerr: failed to update ${service_type} (HTTP $http_code)"
return 1
fi
fi
# POST new connection
http_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 -X POST -b "$cookie_jar" \
-H "Content-Type: application/json" \
-d "$payload" \
"http://${API_HOST}:5055/api/v1/settings/${service_type}" 2>/dev/null || echo "000")
if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
log_step "Seerr: connected ${service_type^} (profile: ${profile_name})"
return 0
else
log_step_fail "Seerr: failed to connect ${service_type} (HTTP $http_code)"
return 1
fi
}
# --- Shared helper: check if a service is already connected to Seerr ---
# Usage: seerr_service_connected <service_type> <cookie_jar>
# Returns 0 and prints count if connected, or 1 on failure
seerr_service_connected() {
local service_type="$1"
local cookie_jar="$2"
local svc_list
svc_list=$(curl -sf --connect-timeout 5 -b "$cookie_jar" \
"http://${API_HOST}:5055/api/v1/service/${service_type}" 2>/dev/null || echo "")
if [ -z "$svc_list" ]; then
return 1
fi
local count
count=$(echo "$svc_list" | jq 'length' 2>/dev/null || echo "0")
echo "$count"
return 0
}
# --- Shared helper: first existing Seerr server id for a service (or empty) ---
# Usage: seerr_service_id <service_type> <cookie_jar>
seerr_service_id() {
local service_type="$1"
local cookie_jar="$2"
local settings
settings=$(curl -sf --connect-timeout 5 -b "$cookie_jar" \
"http://${API_HOST}:5055/api/v1/settings/${service_type}" 2>/dev/null || echo "")
if [ -z "$settings" ] || [ "$settings" = "[]" ]; then
echo ""
return 1
fi
echo "$settings" | jq -r '.[0].id // empty' 2>/dev/null || echo ""
}
# --- Shared helper: get Seerr session cookie ---
# Usage: seerr_get_cookie <cookie_jar> <include_hostname>
# Returns 0 on success, cookie file will be populated
seerr_get_cookie() {
local cookie_jar="$1"
local include_hostname="$2" # "true" or "false"
local payload
if [ "$include_hostname" = "true" ]; then
# Full auth with hostname (for initial setup)
local media_host="jellyfin"
case "${MEDIA_SERVICE:-jellyfin}" in
jellyfin) media_host="jellyfin"; media_port=8096; media_type=2 ;;
emby) media_host="emby"; media_port=8096; media_type=3 ;;
plex) media_host="host.docker.internal"; media_port=32400; media_type=1 ;;
esac
payload=$(jq -nc \
--arg user "$AUTH_USERNAME" \
--arg pass "$AUTH_PASSWORD" \
--arg host "$media_host" \
--argjson port "$media_port" \
--argjson stype "$media_type" \
'{username:$user,password:$pass,hostname:$host,port:$port,useSsl:false,urlBase:"",email:"",serverType:$stype}')
else
# Login-only (for re-runs when media server is already configured)
payload=$(jq -nc --arg user "$AUTH_USERNAME" --arg pass "$AUTH_PASSWORD" \
'{username:$user,password:$pass}')
fi
local auth_endpoint="jellyfin"
curl -s --connect-timeout 5 -c "$cookie_jar" \
-H "Content-Type: application/json" \
-d "$payload" \
"http://${API_HOST}:5055/api/v1/auth/${auth_endpoint}" 2>/dev/null >/dev/null || return 1
# Verify cookie was created
if [ ! -s "$cookie_jar" ] || ! grep -q "connect.sid" "$cookie_jar" 2>/dev/null; then
return 1
fi
return 0
}
# --- Recyclarr configuration ---
configure_recyclarr() {
local recyclarr_config_dir="$INSTALL_DIR/config/recyclarr"
local template_root="$INSTALL_DIR/templates/recyclarr"
mkdir -p "$recyclarr_config_dir/includes"
if [ ! -w "$recyclarr_config_dir" ]; then
log_step_fail "Recyclarr: config directory not writable (${recyclarr_config_dir})"
return 1
fi
# Only substitute known vars so $schema in YAML comments is left alone
export RADARR_API_KEY SONARR_API_KEY
local default_label
default_label=$(quality_profile_names radarr)
default_label="${default_label%%|*}"
if [ ! -f "$template_root/recyclarr.yml" ]; then
log_step_fail "Recyclarr: template root not found (${template_root}/recyclarr.yml)"
return 1
fi
if ! envsubst '${RADARR_API_KEY} ${SONARR_API_KEY}' \
< "$template_root/recyclarr.yml" \
> "$recyclarr_config_dir/recyclarr.yml"; then
log_step_fail "Recyclarr: failed to write config (${recyclarr_config_dir}/recyclarr.yml)"
return 1
fi
# Include packs (no secrets). Replace the directory contents with the template set.
local include_src="$template_root/includes"
if [ ! -d "$include_src" ]; then
log_step_fail "Recyclarr: includes directory not found (${include_src})"
return 1
fi
rm -f "$recyclarr_config_dir/includes"/*.yml 2>/dev/null || true
if ! cp "$include_src"/*.yml "$recyclarr_config_dir/includes/"; then
log_step_fail "Recyclarr: failed to copy include packs"
return 1
fi
local pack_count
pack_count=$(find "$recyclarr_config_dir/includes" -maxdepth 1 -name '*.yml' | wc -l | tr -d ' ')
log_step "Recyclarr: generated config (${pack_count} include packs; Seerr default: ${default_label})"
local sync_out
if wait_while "Syncing Recyclarr (TRaSH Guides)" docker exec recyclarr recyclarr sync; then
log_step "Recyclarr: synced Custom Formats and Quality Profiles successfully"
else
sync_out="${WAIT_WHILE_OUTPUT:-}"
log_step_fail "Recyclarr: failed to sync to Radarr/Sonarr"
echo "$sync_out" | grep -E '•|Error|error|Invalid|YAML' | head -5 | while IFS= read -r line; do
_cfg_log_info " $line"
done
return 1
fi
}
# --- Seerr configuration ---
configure_seerr() {
local seerr_port=5055
local max_wait=120
local wait_time=0
# Step 1: Wait for Seerr to be responsive
echo >&2
while [ $wait_time -lt $max_wait ]; do
local http_code
http_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 3 \
"http://${API_HOST}:${seerr_port}/api/v1/settings/public" 2>/dev/null || echo "000")
if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
break
fi
wait_inline "Waiting for Seerr to start" "$wait_time"
sleep 3
wait_time=$((wait_time + 3))
done
echo >&2
if [ $wait_time -ge $max_wait ]; then
log_step_fail "Seerr: not responsive after ${max_wait}s"
return 1
fi
# Step 2: Check if setup is already complete
local public_settings
public_settings=$(curl -s --connect-timeout 5 \
"http://${API_HOST}:${seerr_port}/api/v1/settings/public" 2>/dev/null || echo "")
if [ -n "$public_settings" ]; then
local initialized
initialized=$(echo "$public_settings" | jq -r '.initialized // ""' 2>/dev/null || echo "")
if [ "$initialized" = "True" ] || [ "$initialized" = "true" ]; then
log_step "Seerr: setup already completed — configuring services"
local seerr_cookie_jar="/tmp/seerr_cookie_jar_$$_${seerr_port}"
# Get session cookie (no hostname needed — already configured)
if ! seerr_get_cookie "$seerr_cookie_jar" "false"; then
log_step_fail "Seerr: failed to get session cookie for reconfiguration"
return 1
fi
echo "Connecting Seerr services" >&2
local connect_errors=0
local is_4k
is_4k=$(quality_seerr_is_4k)
if [ -n "$RADARR_API_KEY" ]; then
local radarr_id
radarr_id=$(seerr_service_id "radarr" "$seerr_cookie_jar" || true)
if ! seerr_connect_service "radarr" 7878 "$RADARR_API_KEY" "lookup_radarr_profile" \
"/data/media/movies" "$is_4k" "$seerr_cookie_jar" "$radarr_id"; then
log_step_fail "Seerr: failed to configure Radarr (re-run to retry)"
connect_errors=$((connect_errors + 1))
fi
fi
if [ -n "$SONARR_API_KEY" ]; then
local sonarr_id
sonarr_id=$(seerr_service_id "sonarr" "$seerr_cookie_jar" || true)
if ! seerr_connect_service "sonarr" 8989 "$SONARR_API_KEY" "lookup_sonarr_profile" \
"/data/media/tv" "$is_4k" "$seerr_cookie_jar" "$sonarr_id"; then
log_step_fail "Seerr: failed to configure Sonarr (re-run to retry)"
connect_errors=$((connect_errors + 1))
fi
fi
echo >&2
rm -f "$seerr_cookie_jar"
[ "$connect_errors" -eq 0 ]
return $?
fi
fi
# Step 3: Determine media server connection details
local media_server_type=""
local media_server_host=""
local media_server_port=0
case "${MEDIA_SERVICE:-jellyfin}" in
jellyfin)
media_server_type="2"
media_server_host="jellyfin"
media_server_port=8096
;;
emby)
media_server_type="3"
media_server_host="emby"
media_server_port=8096
;;
plex)
# Plex uses host networking — Seerr needs the host IP
media_server_type="1"
media_server_host="host.docker.internal"
media_server_port=32400
;;
esac
if [ -z "$media_server_type" ]; then
log_step_fail "Seerr: unsupported media service '${MEDIA_SERVICE:-}'"
return 1
fi
# Step 4: Authenticate against the media server via Seerr's setup endpoint
# This creates the admin user in Seerr and stores the media server connection
local auth_payload
if [ "${MEDIA_SERVICE:-}" = "plex" ]; then
# Plex requires an authToken from Plex.tv OAuth — cannot automate without user interaction
log_step "Seerr: Plex detected — manual setup required via web UI at http://${API_HOST}:${seerr_port}"
_cfg_log_info " Log in with your Plex account to complete Seerr setup"
return 0
fi
# Jellyfin/Emby: use credential-based auth (both use /auth/jellyfin endpoint)
local auth_endpoint="jellyfin"
auth_payload=$(jq -nc \
--arg user "$AUTH_USERNAME" \
--arg pass "$AUTH_PASSWORD" \
--arg host "$media_server_host" \
--argjson port "$media_server_port" \
--argjson stype "$media_server_type" \
'{username:$user,password:$pass,hostname:$host,port:$port,useSsl:false,urlBase:"",email:"",serverType:$stype}')
_cfg_log_info "Seerr: authenticating against ${MEDIA_SERVICE:-jellyfin}..."
# Use -s (not -sf) so we can capture error responses too
local auth_response
auth_response=$(curl -s --connect-timeout 10 -X POST \
-H "Content-Type: application/json" \
-d "$auth_payload" \
"http://${API_HOST}:${seerr_port}/api/v1/auth/${auth_endpoint}" 2>/dev/null || echo "")
if [ -z "$auth_response" ]; then
log_step_fail "Seerr: failed to authenticate (empty response)"
_cfg_log_info " Complete setup manually at http://${API_HOST}:${seerr_port}"
return 1
fi
# "already configured" is success when re-running against an initialized Seerr
local auth_error
auth_error=$(echo "$auth_response" | jq -r '.error // ""' 2>/dev/null || echo "")
if [ -n "$auth_error" ]; then
if echo "$auth_error" | grep -qi "already configured"; then
log_step "Seerr: media server already connected"
else
local auth_message
auth_message=$(echo "$auth_response" | jq -r '.message // .error // "unknown error"' 2>/dev/null || echo "unknown error")
log_step_fail "Seerr: authentication failed — ${auth_message}"
_cfg_log_info " Complete setup manually at http://${API_HOST}:${seerr_port}"
return 1
fi
else
local auth_id
auth_id=$(echo "$auth_response" | jq -r '.id // ""' 2>/dev/null || echo "")
if [ -n "$auth_id" ]; then
log_step "Seerr: authenticated against ${MEDIA_SERVICE:-jellyfin} (admin user created)"
else
log_step_fail "Seerr: authentication returned unexpected response"
_cfg_log_info " Complete setup manually at http://${API_HOST}:${seerr_port}"
return 1
fi
fi
# Step 5: session cookie for subsequent API calls
local seerr_cookie_jar="/tmp/seerr_cookie_jar_$$_${seerr_port}"
if ! seerr_get_cookie "$seerr_cookie_jar" "true"; then
if ! seerr_get_cookie "$seerr_cookie_jar" "false"; then
log_step_fail "Seerr: failed to get session cookie"
rm -f "$seerr_cookie_jar"
return 1
fi
fi
# Step 6: mark setup complete
local init_code
init_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 -X POST -b "$seerr_cookie_jar" \
-H "Content-Type: application/json" \
"http://${API_HOST}:${seerr_port}/api/v1/settings/initialize" 2>/dev/null || echo "000")
if [ "$init_code" -ge 200 ] && [ "$init_code" -lt 300 ]; then
log_step "Seerr: setup completed successfully"
else
log_step_fail "Seerr: failed to finalize setup (HTTP $init_code)"
_cfg_log_info " Complete setup manually at http://${API_HOST}:${seerr_port}"
rm -f "$seerr_cookie_jar"
return 1
fi
echo "Connecting Seerr services" >&2
# Connect Radarr and Sonarr using shared helper
local connect_errors=0
local is_4k
is_4k=$(quality_seerr_is_4k)
if [ -n "$RADARR_API_KEY" ]; then
if ! seerr_connect_service "radarr" 7878 "$RADARR_API_KEY" "lookup_radarr_profile" \
"/data/media/movies" "$is_4k" "$seerr_cookie_jar"; then
connect_errors=$((connect_errors + 1))
fi
fi
if [ -n "$SONARR_API_KEY" ]; then
if ! seerr_connect_service "sonarr" 8989 "$SONARR_API_KEY" "lookup_sonarr_profile" \
"/data/media/tv" "$is_4k" "$seerr_cookie_jar"; then
connect_errors=$((connect_errors + 1))
fi
fi
echo >&2
rm -f "$seerr_cookie_jar"
if [ $connect_errors -gt 0 ]; then
return 1
fi
}
# Seerr has no separate password — it authenticates against Jellyfin/Emby.
seerr_verify_login() {
local seerr_cookie_jar="/tmp/seerr_cookie_jar_verify_$$"
if seerr_get_cookie "$seerr_cookie_jar" "false" || seerr_get_cookie "$seerr_cookie_jar" "true"; then
rm -f "$seerr_cookie_jar"
log_step "Seerr: session works with the new Jellyfin/Emby password"
return 0
fi
rm -f "$seerr_cookie_jar"
log_step_fail "Seerr: could not open a session (it uses the Jellyfin/Emby login)"
return 1
}