#!/bin/bash # assemblrr Bazarr configuration — sourced by config.sh # Provides: configure_bazarr # Requires: lib/core.sh, lib/api.sh, INSTALL_DIR, API_HOST, RADARR_API_KEY, SONARR_API_KEY set -euo pipefail BAZARR_PORT=6767 # --- helpers --- # Resolve Bazarr config file (v1.x uses config.yaml; older used config.ini) _bazarr_config_file() { local candidates=( "$INSTALL_DIR/config/bazarr/config/config.yaml" "$INSTALL_DIR/config/bazarr/config/config.ini" "$INSTALL_DIR/config/bazarr/config.yaml" "$INSTALL_DIR/config/bazarr/config.ini" ) local f for f in "${candidates[@]}"; do if [ -f "$f" ]; then echo "$f" return 0 fi done return 1 } # Extract auth.apikey from yaml or ini _bazarr_extract_apikey() { local cfg="$1" local key="" case "$cfg" in *.yaml|*.yml) # Prefer yq if present; else awk under the auth: section if command -v yq >/dev/null 2>&1; then key=$(yq -r '.auth.apikey // empty' "$cfg" 2>/dev/null || true) fi if [ -z "$key" ]; then key=$(awk ' /^auth:[[:space:]]*$/ { in_auth=1; next } in_auth && /^[^[:space:]]/ { in_auth=0 } in_auth && /^[[:space:]]+apikey:[[:space:]]*/ { sub(/^[[:space:]]+apikey:[[:space:]]*/, "") gsub(/["\047]/, "") print exit } ' "$cfg" 2>/dev/null | head -1 | tr -d '\r') fi ;; *.ini) key=$(sed -n 's/^[[:space:]]*apikey[[:space:]]*=[[:space:]]*//p' "$cfg" 2>/dev/null | head -1 | tr -d '\r') ;; esac echo "$key" } read_bazarr_api_key() { local max_wait=180 local wait_time=0 local cfg key while [ $wait_time -lt $max_wait ]; do if cfg=$(_bazarr_config_file 2>/dev/null); then key=$(_bazarr_extract_apikey "$cfg") if [ -n "$key" ] && [ "$key" != "null" ]; then [ "$wait_time" -gt 0 ] && echo >&2 echo "$key" return 0 fi fi wait_inline "Waiting for Bazarr to initialize" "$wait_time" sleep 3 wait_time=$((wait_time + 3)) done echo >&2 log_step_fail "Bazarr: config.yaml/ini ApiKey missing after ${max_wait}s" return 1 } wait_for_bazarr() { local apikey="$1" local max_wait="${API_READY_TIMEOUT_SECONDS:-120}" local wait_time=0 local code while [ $wait_time -lt $max_wait ]; do code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 3 \ "http://${API_HOST}:${BAZARR_PORT}/api/system/status?apikey=${apikey}" 2>/dev/null || echo "000") if [ "$code" = "200" ]; then [ "$wait_time" -gt 0 ] && echo >&2 return 0 fi wait_inline "Waiting for Bazarr API" "$wait_time" sleep 3 wait_time=$((wait_time + 3)) done echo >&2 log_step_fail "Bazarr: API not ready after ${max_wait}s" return 1 } # POST multipart form settings to Bazarr (args are curl -F fragments after apikey) _bazarr_post_form() { local apikey="$1" shift # Remaining args: -F "key=value" ... curl -s -o /tmp/bazarr-settings-body.txt -w "%{http_code}" --connect-timeout 15 \ -X POST \ "http://${API_HOST}:${BAZARR_PORT}/api/system/settings?apikey=${apikey}" \ "$@" 2>/dev/null || echo "000" } # Map setup / ISO 639-1 or 639-2 codes → Bazarr language filter code2 (ISO 639-1) _bazarr_lang_code2() { local code="${1:-en}" code=${code%%$'\t'*} code=${code// /} case "$code" in ""|"none"|"None") echo "en"; return ;; # Already 2-letter [a-z][a-z]) echo "$code"; return ;; # Common ISO 639-2/B (and some T) → 639-1 (setup has stored "eng" etc.) eng) echo "en" ;; gre|ell) echo "el" ;; deu|ger) echo "de" ;; fra|fre) echo "fr" ;; spa) echo "es" ;; ita) echo "it" ;; por) echo "pt" ;; nld|dut) echo "nl" ;; rus) echo "ru" ;; jpn) echo "ja" ;; zho|chi) echo "zh" ;; kor) echo "ko" ;; ara) echo "ar" ;; tur) echo "tr" ;; pol) echo "pl" ;; swe) echo "sv" ;; nor) echo "no" ;; dan) echo "da" ;; fin) echo "fi" ;; heb) echo "he" ;; hin) echo "hi" ;; *) # Unknown 3-letter: keep as-is (Bazarr may reject; better than inventing) echo "$code" ;; esac } # Unique ISO 639-1 codes, preferred first. Args are stored picker codes (eng, ell, en, …). _bazarr_code2_list() { local -a out=() local c mapped x seen for c in "$@"; do mapped=$(_bazarr_lang_code2 "$c") [ -z "$mapped" ] && continue seen=0 for x in "${out[@]+"${out[@]}"}"; do [ "$x" = "$mapped" ] && seen=1 && break done [ "$seen" -eq 0 ] && out+=("$mapped") done if [ ${#out[@]} -eq 0 ]; then out=(en) fi printf '%s\n' "${out[@]}" } # TRaSH 90/80 assumes hash-rich English catalogs. Non-English OpenSubtitles.com # hits typically miss hash and source (WEB-DL vs community BluRay) and score ~55-74, # so a 90/80 floor skips them. Subsync still retimes anything below 96/86. # Prints: " " _bazarr_min_scores() { local series=90 movie=80 c mapped for c in "$@"; do mapped=$(_bazarr_lang_code2 "$c") if [ "$mapped" != "en" ]; then series=70 movie=50 break fi done echo "$series $movie" } # Append a provider if it is not already in the providers array. _bazarr_providers_add() { local name="$1" p for p in "${providers[@]+"${providers[@]}"}"; do [ "$p" = "$name" ] && return 0 done providers+=("$name") } # Providers that actually carry a language. Only when the picker did not # confirm a set (same rule as auto-adding OpenSubtitles.com). _bazarr_add_language_providers() { local code [ "${FZF_SELECT_STATUS:-}" = "confirmed" ] && return 0 for code in "$@"; do case "$code" in el) _bazarr_providers_add greeksubs ;; esac done } # Default profile JSON: every language in the list, no cutoff (download all of them). _bazarr_languages_profile_json() { local langs_json langs_json=$(_bazarr_code2_list "$@" | jq -R . | jq -sc .) jq -nc --argjson langs "$langs_json" '[ { profileId: 1, name: "Default", cutoff: null, items: ($langs | to_entries | map({ id: (.key + 1), language: .value, audio_exclude: "False", audio_only_include: "False", hi: "False", forced: "False" })), mustContain: [], mustNotContain: [], originalFormat: false, tag: null } ]' } # Fetch Jellyfin movie/TV library names + ids for Bazarr refresh (optional) _bazarr_jellyfin_libraries() { local jf_key="$1" local folders folders=$(curl -s --connect-timeout 5 \ -H "X-Emby-Token: ${jf_key}" \ "http://${API_HOST}:8096/Library/VirtualFolders" 2>/dev/null || echo "[]") # Export via globals used by caller BAZARR_JF_MOVIE_NAMES=() BAZARR_JF_MOVIE_IDS=() BAZARR_JF_SERIES_NAMES=() BAZARR_JF_SERIES_IDS=() local line name id ctype while IFS=$'\t' read -r name id ctype; do [ -z "$name" ] && continue case "$ctype" in movies) BAZARR_JF_MOVIE_NAMES+=("$name") BAZARR_JF_MOVIE_IDS+=("$id") ;; tvshows) BAZARR_JF_SERIES_NAMES+=("$name") BAZARR_JF_SERIES_IDS+=("$id") ;; esac done < <(echo "$folders" | jq -r '.[] | select(.CollectionType=="movies" or .CollectionType=="tvshows") | "\(.Name)\t\(.ItemId // .Guid // "")\t\(.CollectionType)"' 2>/dev/null || true) } configure_bazarr() { local bazarr_key="" local os_user="" os_pass="" local form_args=() local http_code local providers=() local profiles_json local has_sonarr=0 has_radarr=0 local jf_key="" local jellyfin_refresh=0 if ! bazarr_key=$(read_bazarr_api_key); then return 1 fi if ! wait_for_bazarr "$bazarr_key"; then return 1 fi local -a stored_langs=() lang_code2s=() if type _subtitle_stored_lang_codes >/dev/null 2>&1; then while IFS= read -r lang_code2; do [ -n "$lang_code2" ] && stored_langs+=("$lang_code2") done < <(_subtitle_stored_lang_codes) fi if [ ${#stored_langs[@]} -eq 0 ]; then stored_langs=("${SUBTITLE_LANGUAGE:-en}") fi while IFS= read -r lang_code2; do [ -n "$lang_code2" ] && lang_code2s+=("$lang_code2") done < <(_bazarr_code2_list "${stored_langs[@]}") if [ -n "${SONARR_API_KEY:-}" ]; then has_sonarr=1 fi if [ -n "${RADARR_API_KEY:-}" ]; then has_radarr=1 fi if [ "$has_sonarr" -eq 0 ] && [ "$has_radarr" -eq 0 ]; then log_step_fail "Bazarr: neither Sonarr nor Radarr API keys available — skipping" return 1 fi # Optional OpenSubtitles.com credentials from secrets/ if [ -s "$INSTALL_DIR/secrets/opensubtitles_username.txt" ] && [ -s "$INSTALL_DIR/secrets/opensubtitles_password.txt" ]; then os_user=$(cat "$INSTALL_DIR/secrets/opensubtitles_username.txt" 2>/dev/null || echo "") os_pass=$(cat "$INSTALL_DIR/secrets/opensubtitles_password.txt" 2>/dev/null || echo "") fi # Providers: picker confirm = desired set (may be empty). Esc / no picker # leaves Bazarr's current enabled list alone. providers=() if [ "${FZF_SELECT_STATUS:-}" = "confirmed" ]; then if [ "${#SELECTED_SUBTITLE_PROVIDERS[@]}" -gt 0 ]; then providers=("${SELECTED_SUBTITLE_PROVIDERS[@]}") fi else if [ -n "${SELECTED_SUBTITLE_PROVIDERS+x}" ] && [ "${#SELECTED_SUBTITLE_PROVIDERS[@]}" -gt 0 ]; then providers=("${SELECTED_SUBTITLE_PROVIDERS[@]}") fi if [ -n "$os_user" ] && [ -n "$os_pass" ]; then local has_os=0 p for p in "${providers[@]+"${providers[@]}"}"; do [ "$p" = "opensubtitlescom" ] && has_os=1 && break done [ "$has_os" -eq 0 ] && providers+=(opensubtitlescom) fi if [ "${#providers[@]}" -eq 0 ]; then local current_p current_p=$(curl -s --connect-timeout 8 \ "http://${API_HOST}:${BAZARR_PORT}/api/system/settings?apikey=${bazarr_key}" 2>/dev/null \ | jq -r '.general.enabled_providers[]? // empty' 2>/dev/null || true) if [ -n "$current_p" ]; then while IFS= read -r p; do [ -n "$p" ] && providers+=("$p") done <<< "$current_p" log_step "Bazarr: keeping existing providers ($(IFS=,; echo "${providers[*]}"))" else log_step "Bazarr: no subtitle providers selected (configure later in Bazarr UI)" fi fi fi _bazarr_add_language_providers "${lang_code2s[@]}" local min_series=90 min_movie=80 read -r min_series min_movie < <(_bazarr_min_scores "${stored_langs[@]}") # --- Core integrations + TRaSH-style scoring --- form_args=( -F "settings-general-use_sonarr=$([ "$has_sonarr" -eq 1 ] && echo true || echo false)" -F "settings-general-use_radarr=$([ "$has_radarr" -eq 1 ] && echo true || echo false)" -F "settings-general-minimum_score=${min_series}" -F "settings-general-minimum_score_movie=${min_movie}" -F "settings-general-subfolder=current" -F "settings-general-upgrade_subs=true" -F "settings-general-days_to_upgrade_subs=7" -F "settings-general-use_embedded_subs=true" -F "settings-general-serie_default_enabled=true" -F "settings-general-serie_default_profile=1" -F "settings-general-movie_default_enabled=true" -F "settings-general-movie_default_profile=1" -F "settings-subsync-use_subsync=true" -F "settings-subsync-use_subsync_threshold=true" -F "settings-subsync-subsync_threshold=96" -F "settings-subsync-use_subsync_movie_threshold=true" -F "settings-subsync-subsync_movie_threshold=86" ) local p for p in "${providers[@]+"${providers[@]}"}"; do form_args+=(-F "settings-general-enabled_providers=${p}") done if [ "$has_sonarr" -eq 1 ]; then form_args+=( -F "settings-sonarr-ip=sonarr" -F "settings-sonarr-port=8989" -F "settings-sonarr-base_url=/" -F "settings-sonarr-ssl=false" -F "settings-sonarr-apikey=${SONARR_API_KEY}" -F "settings-sonarr-only_monitored=true" ) fi if [ "$has_radarr" -eq 1 ]; then form_args+=( -F "settings-radarr-ip=radarr" -F "settings-radarr-port=7878" -F "settings-radarr-base_url=/" -F "settings-radarr-ssl=false" -F "settings-radarr-apikey=${RADARR_API_KEY}" -F "settings-radarr-only_monitored=true" ) fi if [ -n "$os_user" ] && [ -n "$os_pass" ]; then form_args+=( -F "settings-opensubtitlescom-username=${os_user}" -F "settings-opensubtitlescom-password=${os_pass}" -F "settings-opensubtitlescom-use_hash=true" -F "settings-opensubtitlescom-include_ai_translated=false" -F "settings-opensubtitlescom-include_machine_translated=false" ) fi # First wire only: Bazarr hashes the password on save, so later wires # must not resend it (that would MD5 the hash). Auth edit uses bazarr_set_auth. local auth_type="" auth_type=$(curl -s --connect-timeout 8 \ "http://${API_HOST}:${BAZARR_PORT}/api/system/settings?apikey=${bazarr_key}" 2>/dev/null \ | jq -r '.auth.type // empty' 2>/dev/null || true) if { [ -z "$auth_type" ] || [ "$auth_type" = "null" ] || [ "$auth_type" = "None" ]; } \ && [ -n "${AUTH_USERNAME:-}" ] && [ -n "${AUTH_PASSWORD:-}" ]; then form_args+=( -F "settings-auth-type=form" -F "settings-auth-username=${AUTH_USERNAME}" -F "settings-auth-password=${AUTH_PASSWORD}" ) fi # Jellyfin refresh after subtitle downloads if [ "${MEDIA_SERVICE:-}" = "jellyfin" ]; then jf_key=$(cat "$INSTALL_DIR/secrets/jellyfin_api_key.txt" 2>/dev/null || echo "") if [ -n "$jf_key" ]; then _bazarr_jellyfin_libraries "$jf_key" form_args+=( -F "settings-general-use_jellyfin=true" -F "settings-jellyfin-url=http://jellyfin:8096" -F "settings-jellyfin-apikey=${jf_key}" -F "settings-jellyfin-update_movie_library=true" -F "settings-jellyfin-update_series_library=true" -F "settings-jellyfin-refresh_method=immediate" ) local n if [ "${#BAZARR_JF_MOVIE_NAMES[@]}" -gt 0 ]; then for n in "${BAZARR_JF_MOVIE_NAMES[@]}"; do [ -n "$n" ] && form_args+=(-F "settings-jellyfin-movie_library=${n}") done fi if [ "${#BAZARR_JF_MOVIE_IDS[@]}" -gt 0 ]; then for n in "${BAZARR_JF_MOVIE_IDS[@]}"; do [ -n "$n" ] && form_args+=(-F "settings-jellyfin-movie_library_ids=${n}") done fi if [ "${#BAZARR_JF_SERIES_NAMES[@]}" -gt 0 ]; then for n in "${BAZARR_JF_SERIES_NAMES[@]}"; do [ -n "$n" ] && form_args+=(-F "settings-jellyfin-series_library=${n}") done fi if [ "${#BAZARR_JF_SERIES_IDS[@]}" -gt 0 ]; then for n in "${BAZARR_JF_SERIES_IDS[@]}"; do [ -n "$n" ] && form_args+=(-F "settings-jellyfin-series_library_ids=${n}") done fi jellyfin_refresh=1 else _cfg_log_info "Bazarr: Jellyfin API key not found — skipping JF refresh integration" fi fi # Language filter + profile (id 1 = Default). First language is preferred # (listed first); cutoff stays null so Bazarr still downloads the extras. profiles_json=$(_bazarr_languages_profile_json "${stored_langs[@]}") local enabled for enabled in "${lang_code2s[@]}"; do form_args+=(-F "languages-enabled=${enabled}") done if [ "${#lang_code2s[@]}" -gt 1 ]; then form_args+=(-F "settings-general-single_language=false") fi form_args+=(-F "languages-profiles=${profiles_json}") http_code=$(_bazarr_post_form "$bazarr_key" "${form_args[@]}") if [ "$http_code" = "204" ] || [ "$http_code" = "200" ]; then local provider_list provider_list=$(IFS=,; echo "${providers[*]}") log_step "Bazarr: configured (lang=$(IFS=,; echo "${lang_code2s[*]}"), score S/M=${min_series}/${min_movie}, sync=on, providers=${provider_list})" if [ -n "$os_user" ]; then log_step "Bazarr: OpenSubtitles.com credentials applied" fi if [ "$jellyfin_refresh" -eq 1 ]; then log_step "Bazarr: Jellyfin refresh enabled" fi else local body="" body=$(cat /tmp/bazarr-settings-body.txt 2>/dev/null | head -c 200 || true) log_step_fail "Bazarr: settings POST failed (HTTP ${http_code}${body:+: $body})" return 1 fi # Persist key for operators / future tooling echo -n "$bazarr_key" > "$INSTALL_DIR/secrets/bazarr_api_key.txt" chmod 600 "$INSTALL_DIR/secrets/bazarr_api_key.txt" 2>/dev/null || true # Kick library sync, then search wanted now (don't wait for the 6h interval). if [ "$has_sonarr" -eq 1 ]; then curl -s -o /dev/null --connect-timeout 5 -X POST \ -F "taskid=update_series" \ "http://${API_HOST}:${BAZARR_PORT}/api/system/tasks?apikey=${bazarr_key}" 2>/dev/null || true curl -s -o /dev/null --connect-timeout 5 -X POST \ -F "taskid=wanted_search_missing_subtitles_series" \ "http://${API_HOST}:${BAZARR_PORT}/api/system/tasks?apikey=${bazarr_key}" 2>/dev/null || true fi if [ "$has_radarr" -eq 1 ]; then curl -s -o /dev/null --connect-timeout 5 -X POST \ -F "taskid=update_movies" \ "http://${API_HOST}:${BAZARR_PORT}/api/system/tasks?apikey=${bazarr_key}" 2>/dev/null || true curl -s -o /dev/null --connect-timeout 5 -X POST \ -F "taskid=wanted_search_missing_subtitles_movies" \ "http://${API_HOST}:${BAZARR_PORT}/api/system/tasks?apikey=${bazarr_key}" 2>/dev/null || true fi return 0 } # Set Bazarr form login. Always sends a fresh password (Bazarr MD5s it). # Do not call this from the regular wire loop — only from auth edit. bazarr_set_auth() { local bazarr_key="" if [ -z "${AUTH_USERNAME:-}" ] || [ -z "${AUTH_PASSWORD:-}" ]; then log_step_fail "Bazarr: new credentials missing" return 1 fi if ! bazarr_key=$(read_bazarr_api_key); then return 1 fi if ! wait_for_bazarr "$bazarr_key"; then return 1 fi local http_code http_code=$(_bazarr_post_form "$bazarr_key" \ -F "settings-auth-type=form" \ -F "settings-auth-username=${AUTH_USERNAME}" \ -F "settings-auth-password=${AUTH_PASSWORD}") if [ "$http_code" = "204" ] || [ "$http_code" = "200" ]; then log_step "Bazarr: set login (${AUTH_USERNAME})" return 0 fi local body="" body=$(cat /tmp/bazarr-settings-body.txt 2>/dev/null | head -c 200 || true) log_step_fail "Bazarr: failed to set login (HTTP ${http_code}${body:+: $body})" return 1 }