assemblrr/lib/bazarr.sh
soulis-1256 54f3aaa7cd
Close the wiring blank gap and label uninstall removals.
Skip wait newlines when an API key is already present, and say
config/media were removed instead of listing them like a live status.
2026-08-19 00:05:43 +03:00

462 lines
16 KiB
Bash

#!/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
}
# 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 lang_code2
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
lang_code2=$(_bazarr_lang_code2 "${SUBTITLE_LANGUAGE:-en}")
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
# --- 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=90"
-F "settings-general-minimum_score_movie=80"
-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 + single-language profile (id 1 = Default)
# items: normal (non-HI) preferred language; always include
profiles_json=$(jq -nc \
--arg lang "$lang_code2" \
'[{
profileId: 1,
name: "Default",
cutoff: null,
items: [{
id: 1,
language: $lang,
audio_exclude: "False",
audio_only_include: "False",
hi: "False",
forced: "False"
}],
mustContain: [],
mustNotContain: [],
originalFormat: false,
tag: null
}]')
form_args+=(
-F "languages-enabled=${lang_code2}"
-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=${lang_code2}, score S/M=90/80, 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 tasks (best-effort; scheduler also runs on 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
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
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
}