Fix quality profile setup and split Recyclarr into include packs

Always sync HD/UHD (and Sonarr WEB) as include packs, name them with
an assemblrr prefix, soften blocking CF scores, zero quality minSize,
relax minimumSeeders, and update Seerr on reconfigure. Document TRaSH
guide drift and tighten comments and uninstall messaging.
This commit is contained in:
soulis-1256 2026-08-05 16:12:47 +03:00
parent a381de1f8f
commit bd126b704e
No known key found for this signature in database
19 changed files with 439 additions and 410 deletions

View file

@ -48,14 +48,13 @@ set_arr_auth() {
# --- qBittorrent helpers ---
# Set qBittorrent credentials and save path using the temp password from container logs
# Called once before configuring any *arr service.
# On re-runs, tries the final credentials first to avoid triggering brute-force bans.
# Set qBittorrent WebUI credentials and default save paths.
# Prefer final credentials when already configured (avoids brute-force lockouts).
qbit_set_credentials() {
local qbit_port=8081
local qbit_cookie_jar="/tmp/qb_cookie_jar_init_$$_${qbit_port}"
# Try logging in with the final credentials first (already set from a previous run)
# Idempotent: succeed if final credentials already work
if [ -n "$AUTH_USERNAME" ] && [ -n "$AUTH_PASSWORD" ]; then
curl -s --connect-timeout 5 -c "$qbit_cookie_jar" \
-H "Referer: http://${API_HOST}:${qbit_port}" \
@ -64,7 +63,6 @@ qbit_set_credentials() {
local qbit_sid
qbit_sid=$(qbit_cookie_sid "$qbit_cookie_jar")
if [ -n "$qbit_sid" ]; then
# Credentials already set — ensure save path is configured
curl -s --connect-timeout 5 \
"http://${API_HOST}:${qbit_port}/api/v2/app/setPreferences" \
-b "$qbit_cookie_jar" \
@ -77,7 +75,7 @@ qbit_set_credentials() {
fi
fi
# Credentials not set yet — use temp password from container logs
# First-time setup: temporary password from container logs
local wait_time=0
local max_wait=60
@ -116,7 +114,7 @@ qbit_set_credentials() {
return 1
}
# Create a qBittorrent category with a save path
# Create or update a qBittorrent category with a save path
qbit_create_category() {
local service_name="$1"
local category="$2"
@ -131,13 +129,20 @@ qbit_create_category() {
local qbit_sid
qbit_sid=$(qbit_cookie_sid "$qbit_cookie_jar")
if [ -n "$qbit_sid" ]; then
# createCategory + editCategory so savePath is set whether the category is new or existing
curl -s --connect-timeout 10 \
"http://${API_HOST}:${qbit_port}/api/v2/torrents/createCategory" \
-b "$qbit_cookie_jar" \
-H "Referer: http://${API_HOST}:${qbit_port}" \
-d "category=${category}&savePath=${save_path}" \
2>/dev/null >/dev/null || true
log_step "${service_name}: created qBittorrent '${category}' category (${save_path})"
curl -s --connect-timeout 10 \
"http://${API_HOST}:${qbit_port}/api/v2/torrents/editCategory" \
-b "$qbit_cookie_jar" \
-H "Referer: http://${API_HOST}:${qbit_port}" \
-d "category=${category}&savePath=${save_path}" \
2>/dev/null >/dev/null || true
log_step "${service_name}: qBittorrent '${category}' category → ${save_path}"
else
log_step_fail "${service_name}: could not login to qBittorrent to create categories"
fi
@ -184,16 +189,16 @@ configure_arr_service() {
log_step "${service_name}: root folder ${root_path} already exists"
fi
# 1b. Remove stale auto-detected root folders
# Keep only the configured root folder
local all_root_folders
all_root_folders=$(api_get "$port" "/api/v3/rootfolder" "$apikey")
local stale_ids
stale_ids=$(echo "$all_root_folders" | jq -r --arg rp "$root_path" '.[] | select(.path != $rp) | .id' 2>/dev/null || echo "")
local extra_ids
extra_ids=$(echo "$all_root_folders" | jq -r --arg rp "$root_path" '.[] | select(.path != $rp) | .id' 2>/dev/null || echo "")
for stale_id in $stale_ids; do
if [ -n "$stale_id" ]; then
api_delete "$port" "/api/v3/rootfolder/${stale_id}" "$apikey" >/dev/null || true
log_step "${service_name}: removed stale root folder (id: ${stale_id})"
for extra_id in $extra_ids; do
if [ -n "$extra_id" ]; then
api_delete "$port" "/api/v3/rootfolder/${extra_id}" "$apikey" >/dev/null || true
log_step "${service_name}: removed extra root folder (id: ${extra_id})"
fi
done
@ -296,6 +301,81 @@ configure_arr_service() {
set_arr_auth "$service_name" "$port" "$apikey"
}
# --- Quality sizes & default profiles ---
# Set minSize=0 on every quality definition (no minimum file-size filter).
# Values live in the *arr DB; they only change when written via this API.
relax_quality_sizes() {
local service_name="$1"
local port="$2"
local apikey="$3"
local defs
defs=$(api_get "$port" "/api/v3/qualitydefinition" "$apikey")
if [ -z "$defs" ] || [ "$defs" = "[]" ]; then
log_step_fail "${service_name}: no quality definitions to update"
return 1
fi
# Only minSize is cleared; leave max/preferred alone (bulk update is picky about nulls).
local payload
payload=$(echo "$defs" | jq '[.[] | .minSize = 0]' 2>/dev/null || echo "")
if [ -z "$payload" ]; then
log_step_fail "${service_name}: failed to build quality definition payload"
return 1
fi
local result
result=$(api_put "$port" "/api/v3/qualitydefinition/update" "$apikey" "$payload")
if echo "$result" | jq -e 'type == "array"' >/dev/null 2>&1; then
log_step "${service_name}: quality minSize set to 0"
return 0
fi
# Fallback: one PUT per definition
local ok=0 total=0
while IFS= read -r row; do
[ -z "$row" ] && continue
total=$((total + 1))
local id
id=$(echo "$row" | jq -r '.id // empty')
[ -z "$id" ] && continue
if api_put "$port" "/api/v3/qualitydefinition/${id}" "$apikey" "$row" >/dev/null 2>&1; then
ok=$((ok + 1))
fi
done < <(echo "$payload" | jq -c '.[]' 2>/dev/null || true)
if [ "$ok" -gt 0 ]; then
log_step "${service_name}: quality minSize set to 0 (${ok}/${total})"
return 0
fi
log_step_fail "${service_name}: failed to update quality definitions"
return 1
}
# Record the preferred quality profile id/name for this install (setup choice).
set_default_quality_profile() {
local service_name="$1"
local port="$2"
local apikey="$3"
local profile_func="$4"
local profile profile_id
profile=$($profile_func "$apikey")
profile_id="${profile%%:*}"
local profile_name="${profile##*:}"
if [ -z "$profile_id" ] || [ "$profile_id" = "1" ] && [ "$profile_name" = "Any" ]; then
log_step "${service_name}: preferred quality profile → Any"
return 0
fi
if [ -n "${INSTALL_DIR:-}" ]; then
mkdir -p "$INSTALL_DIR/config"
echo "${profile_id}:${profile_name}" > "$INSTALL_DIR/config/.${service_name,,}-default-quality-profile" 2>/dev/null || true
fi
log_step "${service_name}: preferred quality profile → ${profile_name} (id ${profile_id})"
}
# --- Quality profile lookup ---
lookup_radarr_profile() {
@ -308,27 +388,42 @@ lookup_radarr_profile() {
return
fi
# Recyclarr creates profiles with names like "Ultra-HD" or "HD Bluray + WEB"
# Match by keyword since the exact name depends on TRaSH Guide version
# Default Radarr profiles to skip: Any, SD, HD-720p, HD-1080p, HD - 720p/1080p
local target_keyword
# Prefer assemblrr-named Recyclarr profiles; fall back to stock *arr names.
local target_name=""
local stock_fallback=""
if [ "${SEERR_IS_4K:-false}" = "true" ]; then
target_keyword="Ultra"
target_name="assemblrr UHD Bluray + WEB"
stock_fallback="Ultra-HD"
elif [ "${SEERR_DEFAULT_PROFILE:-1}" != "1" ]; then
target_keyword="Bluray + WEB"
target_name="assemblrr HD Bluray + WEB"
stock_fallback="HD-1080p"
else
echo "1:Any"
return
fi
local matched
matched=$(echo "$profiles_json" | jq -r --arg kw "$target_keyword" '.[] | select(.name | contains($kw)) | select(.name as $n | ["Any","SD","HD-720p","HD-1080p","HD - 720p/1080p"] | index($n) | not) | "\(.id):\(.name)"' 2>/dev/null | head -1 || echo "")
matched=$(echo "$profiles_json" | jq -r --arg name "$target_name" '
.[] | select(.name == $name) | "\(.id):\(.name)"
' 2>/dev/null | head -1 || echo "")
if [ -n "$matched" ]; then
echo "$matched"
else
echo "1:Any"
return
fi
if [ -n "$stock_fallback" ]; then
matched=$(echo "$profiles_json" | jq -r --arg name "$stock_fallback" '
.[] | select(.name == $name) | "\(.id):\(.name)"
' 2>/dev/null | head -1 || echo "")
if [ -n "$matched" ]; then
echo "$matched"
return
fi
fi
echo "1:Any"
}
lookup_sonarr_profile() {
@ -337,28 +432,43 @@ lookup_sonarr_profile() {
profiles_json=$(api_get "8989" "/api/v3/qualityprofile" "$apikey")
if [ -z "$profiles_json" ]; then
echo "1:Default"
echo "1:Any"
return
fi
# Recyclarr creates profiles with names like "Ultra-HD" or "WEB-1080p"
# Match by keyword since the exact name depends on TRaSH Guide version
# Default Sonarr profiles to skip: Any, SD, HD-720p, HD-1080p, HD - 720p/1080p
local target_keyword
if [ "${SEERR_IS_4K:-false}" = "true" ]; then
target_keyword="Ultra"
else
target_keyword="WEB"
fi
# Default TV profile is assemblrr WEB-1080p (movies 4K choice does not change this).
local target_name="assemblrr WEB-1080p"
local stock_fallback="HD-1080p"
local matched
matched=$(echo "$profiles_json" | jq -r --arg kw "$target_keyword" '.[] | select(.name | contains($kw)) | select(.name as $n | ["Any","SD","HD-720p","HD-1080p","HD - 720p/1080p"] | index($n) | not) | "\(.id):\(.name)"' 2>/dev/null | head -1 || echo "")
matched=$(echo "$profiles_json" | jq -r --arg name "$target_name" '
.[] | select(.name == $name) | "\(.id):\(.name)"
' 2>/dev/null | head -1 || echo "")
if [ -n "$matched" ]; then
echo "$matched"
else
echo "1:Default"
return
fi
matched=$(echo "$profiles_json" | jq -r '
.[] | select(.name | test("^assemblrr.*WEB.*1080"; "i")) | "\(.id):\(.name)"
' 2>/dev/null | head -1 || echo "")
if [ -n "$matched" ]; then
echo "$matched"
return
fi
if [ -n "$stock_fallback" ]; then
matched=$(echo "$profiles_json" | jq -r --arg name "$stock_fallback" '
.[] | select(.name == $name) | "\(.id):\(.name)"
' 2>/dev/null | head -1 || echo "")
if [ -n "$matched" ]; then
echo "$matched"
return
fi
fi
echo "1:Any"
}
# --- Radarr configuration ---
@ -374,6 +484,58 @@ configure_radarr() {
# --- Prowlarr configuration ---
# Set minimumSeeders=0 on Prowlarr app profiles and *arr indexers.
# Peer counts from indexers are often incomplete; requiring seeders>=1 blocks valid grabs.
relax_minimum_seeders() {
local prowlarr_key="$1"
local radarr_key="${2:-}"
local sonarr_key="${3:-}"
local profiles
profiles=$(api_get "9696" "/api/v1/appprofile" "$prowlarr_key")
if [ -n "$profiles" ] && [ "$profiles" != "[]" ]; then
local profile_id profile_payload
while IFS= read -r row; do
[ -z "$row" ] && continue
profile_id=$(echo "$row" | jq -r '.id // empty')
[ -z "$profile_id" ] && continue
profile_payload=$(echo "$row" | jq '.minimumSeeders = 0' 2>/dev/null || echo "")
[ -z "$profile_payload" ] && continue
if api_put "9696" "/api/v1/appprofile/${profile_id}" "$prowlarr_key" "$profile_payload" >/dev/null 2>&1; then
log_step "Prowlarr: app profile '${profile_id}' minimumSeeders → 0"
fi
done < <(echo "$profiles" | jq -c '.[]' 2>/dev/null || true)
fi
local port key name
for port_key_name in "7878:${radarr_key}:Radarr" "8989:${sonarr_key}:Sonarr"; do
port="${port_key_name%%:*}"
rest="${port_key_name#*:}"
key="${rest%%:*}"
name="${rest#*:}"
[ -z "$key" ] && continue
local indexers
indexers=$(api_get "$port" "/api/v3/indexer" "$key")
[ -z "$indexers" ] || [ "$indexers" = "[]" ] && continue
while IFS= read -r idx; do
[ -z "$idx" ] && continue
local idx_id idx_payload
idx_id=$(echo "$idx" | jq -r '.id // empty')
[ -z "$idx_id" ] && continue
idx_payload=$(echo "$idx" | jq '
.fields = [.fields[] |
if .name == "minimumSeeders" then .value = 0 else . end
]
' 2>/dev/null || echo "")
[ -z "$idx_payload" ] && continue
api_put "$port" "/api/v3/indexer/${idx_id}" "$key" "$idx_payload" >/dev/null 2>&1 || true
done < <(echo "$indexers" | jq -c '.[]' 2>/dev/null || true)
log_step "${name}: indexer minimumSeeders → 0"
done
}
configure_prowlarr() {
local apikey="$1"
local radarr_apikey="$2"
@ -436,8 +598,8 @@ configure_prowlarr() {
fi
fi
# 2. Add public indexers
echo "Adding ${#PUBLIC_INDEXERS[@]} indexer(s) to Prowlarr" >&2
# 2. Add selected indexers
echo "Adding ${#SELECTED_INDEXERS[@]} indexer(s) to Prowlarr" >&2
local indexer_schemas
indexer_schemas=$(api_get "9696" "/api/v1/indexer/schema" "$apikey")
@ -452,7 +614,7 @@ configure_prowlarr() {
local existing_names
existing_names=$(jq_json_list_values "$existing_indexers" "name")
for indexer_name in ${PUBLIC_INDEXERS[@]+"${PUBLIC_INDEXERS[@]}"}; do
for indexer_name in ${SELECTED_INDEXERS[@]+"${SELECTED_INDEXERS[@]}"}; do
# Skip if already added
if echo "$existing_names" | grep -q -F -x "$indexer_name"; then
log_step "Prowlarr: ${indexer_name} indexer already exists"
@ -502,7 +664,9 @@ configure_prowlarr() {
echo >&2
log_success "Indexers added to Prowlarr!" >&2
# 3. Set authentication
relax_minimum_seeders "$apikey" "$radarr_apikey" "${sonarr_apikey:-}"
# 4. Set authentication
set_arr_auth "Prowlarr" "9696" "$apikey" "v1"
}

View file

@ -245,7 +245,7 @@ prepare_install_dirs() {
"scripts"
)
# Reclaim install root if a previous run left it root-owned
# Ensure install root is owned by the target user
if [ -d "$install_dir" ] && [ ! -w "$install_dir" ]; then
ensure_owned "$install_dir" "$(id -u)" "$(id -g)"
fi

View file

@ -1,12 +1,12 @@
#!/bin/bash
# fzf-based TUI for selections (Prowlarr indexers, Jellyfin languages)
# Provides reusable fzf selection patterns for assemblrr.
# Sets PUBLIC_INDEXERS array for indexers.
# Sets SELECTED_INDEXERS array for Prowlarr.
# Usage: source lib/fzf-tui.sh; configure_indexers "$api_key"
set -euo pipefail
PUBLIC_INDEXERS=()
SELECTED_INDEXERS=()
# --- TTY detection (reused across functions) ---
_fzf_has_tty() {
@ -73,8 +73,8 @@ fzf_single_select() {
}
# --- Generic multi-select fzf (for indexers) ---
# Args: $1 = data_source (jq expression or curl), $2 = awk filter
# Sets PUBLIC_INDEXERS array
# Args: $1 = tab-separated list lines
# Sets SELECTED_INDEXERS array
fzf_multi_select() {
local data="$1"
local has_tty=false
@ -104,20 +104,20 @@ fzf_multi_select() {
if [ -n "$selected" ]; then
while IFS= read -r idx; do
[ -n "$idx" ] && PUBLIC_INDEXERS+=("$idx")
[ -n "$idx" ] && SELECTED_INDEXERS+=("$idx")
done <<< "$selected"
echo "Selected ${#PUBLIC_INDEXERS[@]} indexer(s): $(IFS=,; echo "${PUBLIC_INDEXERS[*]}")" >&2
echo "Selected ${#SELECTED_INDEXERS[@]} indexer(s): $(IFS=,; echo "${SELECTED_INDEXERS[*]}")" >&2
else
echo "No indexers selected." >&2
fi
else
# No TTY or no fzf — show top public indexers and prompt
echo "No interactive terminal available — listing popular public indexers." >&2
echo "$data" | jq -r 'sort_by(.name | ascii_downcase) | .[0:20][] | " \(.name)"' 2>/dev/null >&2
# No TTY or no fzf — list options and prompt
echo "No interactive terminal available — listing available indexers." >&2
echo "$data" | cut -f1 | head -20 | sed 's/^/ /' >&2
echo "Enter indexer names separated by spaces (or blank to skip):" >&2
read -r input_line || true
for idx in $input_line; do
PUBLIC_INDEXERS+=("$idx")
SELECTED_INDEXERS+=("$idx")
done
fi
}

View file

@ -7,8 +7,9 @@
set -euo pipefail
# --- Shared helper: connect a service (Radarr/Sonarr) to Seerr ---
# Usage: seerr_connect_service <service_type> <port> <api_key> <profile_func> <active_dir> <is_4k> <cookie_jar>
# --- 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"
@ -18,6 +19,7 @@ seerr_connect_service() {
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"
@ -29,13 +31,12 @@ seerr_connect_service() {
return 1
fi
# Look up quality profile from the service (created by Recyclarr)
local profile
profile=$($profile_func "$api_key")
local profile_id="${profile%%:*}"
local profile_name="${profile##*:}"
# Build the service payload
# id is read-only on PUT — omit it from the body
local payload
payload=$(cat <<EOF
{
@ -62,15 +63,29 @@ EOF
payload=$(echo "$payload" | jq '.enableSeasonFolders = true' 2>/dev/null || echo "$payload")
fi
# POST to Seerr
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^}"
log_step "Seerr: connected ${service_type^} (profile: ${profile_name})"
return 0
else
log_step_fail "Seerr: failed to connect ${service_type} (HTTP $http_code)"
@ -99,6 +114,24 @@ seerr_service_connected() {
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
@ -158,42 +191,60 @@ EOF
configure_recyclarr() {
local recyclarr_config_dir="$INSTALL_DIR/config/recyclarr"
mkdir -p "$recyclarr_config_dir"
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
# Export variables for envsubst
# Only substitute known vars so $schema in YAML comments is left alone
export RADARR_API_KEY SONARR_API_KEY
# Pick the right template based on user's 4K choice
local template_file="recyclarr-full_hd.yml" # default: 1080p
local profile_label="HD Bluray + WEB"
local default_label="assemblrr HD Bluray + WEB"
if [ "${SEERR_IS_4K}" = "true" ]; then
template_file="recyclarr-ultra_hd.yml" # 4K
profile_label="UHD Bluray + WEB"
default_label="assemblrr UHD Bluray + WEB"
fi
if [ -f "$INSTALL_DIR/templates/${template_file}" ]; then
if envsubst < "$INSTALL_DIR/templates/${template_file}" > "$recyclarr_config_dir/recyclarr.yml"; then
log_step "Recyclarr: generated config (${profile_label} profile)"
else
log_step_fail "Recyclarr: failed to write config (${recyclarr_config_dir}/recyclarr.yml)"
return 1
fi
else
log_step_fail "Recyclarr: template ${template_file} not found"
if [ ! -f "$template_root/recyclarr.yml" ]; then
log_step_fail "Recyclarr: template root not found (${template_root}/recyclarr.yml)"
return 1
fi
# Trigger a sync
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})"
_cfg_log_info "Recyclarr: syncing TRaSH Guides to Radarr and Sonarr... this might take a moment"
if docker exec recyclarr recyclarr sync > /dev/null 2>&1; then
local sync_out
if sync_out=$(docker exec recyclarr recyclarr sync 2>&1); then
log_step "Recyclarr: synced Custom Formats and Quality Profiles successfully"
else
log_step_fail "Recyclarr: failed to sync to Radarr/Sonarr (check docker logs recyclarr)"
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
}
@ -245,31 +296,21 @@ configure_seerr() {
echo "Connecting Seerr services" >&2
# Connect Radarr if not already connected
if [ -n "$RADARR_API_KEY" ]; then
local radarr_count
radarr_count=$(seerr_service_connected "radarr" "$seerr_cookie_jar") || radarr_count=0
if [ "$radarr_count" -eq 0 ]; then
if ! seerr_connect_service "radarr" 7878 "$RADARR_API_KEY" "lookup_radarr_profile" \
"/data/media/movies" "${SEERR_IS_4K:-false}" "$seerr_cookie_jar"; then
log_step_fail "Seerr: failed to connect Radarr (re-run to retry)"
fi
else
log_step "Seerr: Radarr already connected"
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" "${SEERR_IS_4K:-false}" "$seerr_cookie_jar" "$radarr_id"; then
log_step_fail "Seerr: failed to configure Radarr (re-run to retry)"
fi
fi
# Connect Sonarr if not already connected
if [ -n "$SONARR_API_KEY" ]; then
local sonarr_count
sonarr_count=$(seerr_service_connected "sonarr" "$seerr_cookie_jar") || sonarr_count=0
if [ "$sonarr_count" -eq 0 ]; then
if ! seerr_connect_service "sonarr" 8989 "$SONARR_API_KEY" "lookup_sonarr_profile" \
"/data/media/tv" "false" "$seerr_cookie_jar"; then
log_step_fail "Seerr: failed to connect Sonarr (re-run to retry)"
fi
else
log_step "Seerr: Sonarr already connected"
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" "false" "$seerr_cookie_jar" "$sonarr_id"; then
log_step_fail "Seerr: failed to configure Sonarr (re-run to retry)"
fi
fi
@ -350,8 +391,7 @@ EOF
return 1
fi
# Check for error in response — "hostname already configured" means media server
# was connected by a previous run, which is fine (admin user already exists)
# "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
@ -365,7 +405,6 @@ EOF
return 1
fi
else
# Successful auth — verify admin user was created
local auth_id
auth_id=$(echo "$auth_response" | jq -r '.id // ""' 2>/dev/null || echo "")
if [ -n "$auth_id" ]; then
@ -377,12 +416,10 @@ EOF
fi
fi
# Step 5: Get a session cookie for subsequent API calls
# Step 5: session cookie for subsequent API calls
local seerr_cookie_jar="/tmp/seerr_cookie_jar_$$_${seerr_port}"
# Get session cookie (with hostname for initial setup)
if ! seerr_get_cookie "$seerr_cookie_jar" "true"; then
# Retry with login-only if first attempt fails (hostname already configured)
if ! seerr_get_cookie "$seerr_cookie_jar" "false"; then
log_step_fail "Seerr: failed to get session cookie"
rm -f "$seerr_cookie_jar"
@ -390,10 +427,7 @@ EOF
fi
fi
# Note: Library sync is skipped here — Jellyfin hasn't indexed anything yet on first setup.
# Seerr will auto-sync libraries periodically (every 24h) once content exists.
# Step 6: Initialize Seerr (mark setup as complete)
# 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" \