assemblrr/lib/bazarr.sh
soulis-1256 692d535418
fix(bazarr): lower min scores for non-English subtitle languages
TRaSH 90/80 skips OpenSubtitles.com results that miss hash and source.
Use 70/50 when any selected language is not English, enable greeksubs
for Greek, and search wanted after wiring.
2026-08-23 23:41:05 +03:00

550 lines
20 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
}
# 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: "<series> <movie>"
_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
}