2026-05-19 10:46:30 +01:00
|
|
|
# services/bazarr-sync.nix — Subtitle sync via bazarr-sync.
|
|
|
|
|
#
|
|
|
|
|
# Two systemd timers:
|
|
|
|
|
# bazarr-sync — hourly, targets only movies/shows added in the last
|
|
|
|
|
# 2 hours (queries Radarr/Sonarr APIs for recent items)
|
|
|
|
|
# bazarr-sync-full — weekly full-library sync (every Sunday 04:00)
|
|
|
|
|
#
|
|
|
|
|
# Both use the same bazarr-sync container image under podman with --network host
|
|
|
|
|
# so it can reach Bazarr on localhost:6767.
|
|
|
|
|
{ config, lib, pkgs, ... }:
|
|
|
|
|
let
|
2026-05-19 10:51:53 +01:00
|
|
|
deps = lib.makeBinPath [ pkgs.curl pkgs.jq pkgs.gnused pkgs.gnugrep pkgs.coreutils pkgs.podman pkgs.yq-go ];
|
2026-05-19 10:46:30 +01:00
|
|
|
|
|
|
|
|
# Shared preamble: extract API keys and write config.yaml
|
|
|
|
|
preamble = ''
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
PATH="${deps}:$PATH"
|
|
|
|
|
|
|
|
|
|
BASE="http://127.0.0.1"
|
|
|
|
|
CONFIG_DIR="/var/lib/bazarr-sync"
|
|
|
|
|
CONFIG_FILE="$CONFIG_DIR/config.yaml"
|
|
|
|
|
|
|
|
|
|
# --- Extract API keys (same pattern as arr-interconnect) ---
|
|
|
|
|
extract_arr_key() {
|
|
|
|
|
if [ -f "$1" ]; then
|
|
|
|
|
sed -n 's/.*<ApiKey>\(.*\)<\/ApiKey>.*/\1/p' "$1"
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SONARR_KEY=$(extract_arr_key "/var/lib/sonarr/config.xml")
|
|
|
|
|
RADARR_KEY=$(extract_arr_key "/var/lib/radarr/config.xml")
|
|
|
|
|
|
|
|
|
|
BAZARR_KEY=""
|
2026-05-19 10:51:53 +01:00
|
|
|
if [ -f "/var/lib/bazarr/config/config.yaml" ]; then
|
|
|
|
|
BAZARR_KEY=$(yq '.auth.apikey' /var/lib/bazarr/config/config.yaml || true)
|
2026-05-19 10:46:30 +01:00
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -z "$BAZARR_KEY" ]; then
|
|
|
|
|
echo "ERROR: Could not extract Bazarr API key"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- Write config.yaml for bazarr-sync ---
|
|
|
|
|
cat > "$CONFIG_FILE" <<EOF
|
|
|
|
|
address: "127.0.0.1"
|
|
|
|
|
port: 6767
|
|
|
|
|
protocol: "http"
|
|
|
|
|
apitoken: "$BAZARR_KEY"
|
|
|
|
|
EOF
|
|
|
|
|
|
|
|
|
|
IMAGE="ghcr.io/ajmandourah/bazarr-sync:latest"
|
|
|
|
|
run_sync() {
|
|
|
|
|
podman run --rm --network host \
|
|
|
|
|
-v "$CONFIG_FILE":/usr/src/app/config.yaml:ro \
|
|
|
|
|
"$IMAGE" bazarr-sync sync "$@"
|
|
|
|
|
}
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
# Hourly: only sync recently added content
|
|
|
|
|
recentScript = pkgs.writeShellScript "bazarr-sync-recent" ''
|
|
|
|
|
${preamble}
|
|
|
|
|
|
|
|
|
|
LOOKBACK_HOURS=2
|
|
|
|
|
CUTOFF=$(date -u -d "-$LOOKBACK_HOURS hours" +%Y-%m-%dT%H:%M:%SZ)
|
|
|
|
|
|
|
|
|
|
# --- Query Radarr for recently added movies ---
|
|
|
|
|
RADARR_IDS=""
|
|
|
|
|
if [ -n "$RADARR_KEY" ]; then
|
|
|
|
|
echo "Querying Radarr for movies added since $CUTOFF..."
|
|
|
|
|
RADARR_IDS=$(curl -sf -H "X-Api-Key: $RADARR_KEY" \
|
|
|
|
|
"$BASE:7878/api/v3/movie" | \
|
|
|
|
|
jq -r --arg cutoff "$CUTOFF" \
|
|
|
|
|
'[.[] | select(.added >= $cutoff) | .id] | map(tostring) | join(",")') || true
|
|
|
|
|
if [ -n "$RADARR_IDS" ]; then
|
|
|
|
|
echo " Found recently added movies: $RADARR_IDS"
|
|
|
|
|
else
|
|
|
|
|
echo " No recently added movies"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- Query Sonarr for series with recently imported episodes ---
|
|
|
|
|
SONARR_IDS=""
|
|
|
|
|
if [ -n "$SONARR_KEY" ]; then
|
|
|
|
|
echo "Querying Sonarr for recently imported episodes since $CUTOFF..."
|
|
|
|
|
SONARR_IDS=$(curl -sf -H "X-Api-Key: $SONARR_KEY" \
|
|
|
|
|
"$BASE:8989/api/v3/history?pageSize=200&sortKey=date&sortDirection=descending" | \
|
|
|
|
|
jq -r --arg cutoff "$CUTOFF" \
|
|
|
|
|
'[.records[] | select(.date >= $cutoff and .eventType == "downloadFolderImported") | .seriesId] | unique | map(tostring) | join(",")') || true
|
|
|
|
|
if [ -n "$SONARR_IDS" ]; then
|
|
|
|
|
echo " Found series with recent imports: $SONARR_IDS"
|
|
|
|
|
else
|
|
|
|
|
echo " No recently imported episodes"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- Exit early if nothing to do ---
|
|
|
|
|
if [ -z "$RADARR_IDS" ] && [ -z "$SONARR_IDS" ]; then
|
|
|
|
|
echo "Nothing new to sync. Exiting."
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- Run bazarr-sync for recent movies ---
|
|
|
|
|
if [ -n "$RADARR_IDS" ]; then
|
|
|
|
|
echo "Syncing subtitles for movies: $RADARR_IDS"
|
|
|
|
|
run_sync movies --radarr-id "$RADARR_IDS" || echo "WARNING: Movie sync failed"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- Run bazarr-sync for recent shows ---
|
|
|
|
|
if [ -n "$SONARR_IDS" ]; then
|
|
|
|
|
echo "Syncing subtitles for shows: $SONARR_IDS"
|
|
|
|
|
run_sync shows --sonarr-id "$SONARR_IDS" || echo "WARNING: Show sync failed"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "Sync complete."
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
# Weekly: full library sync
|
|
|
|
|
fullScript = pkgs.writeShellScript "bazarr-sync-full" ''
|
|
|
|
|
${preamble}
|
|
|
|
|
|
|
|
|
|
echo "Starting full library subtitle sync..."
|
|
|
|
|
|
|
|
|
|
echo "Syncing all movies..."
|
|
|
|
|
run_sync movies || echo "WARNING: Full movie sync failed"
|
|
|
|
|
|
|
|
|
|
echo "Syncing all shows..."
|
|
|
|
|
run_sync shows || echo "WARNING: Full show sync failed"
|
|
|
|
|
|
|
|
|
|
echo "Full sync complete."
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
serviceAfter = [ "bazarr.service" "sonarr.service" "radarr.service" "network-online.target" ];
|
|
|
|
|
in
|
|
|
|
|
{
|
|
|
|
|
config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") {
|
|
|
|
|
|
2026-05-19 10:54:44 +01:00
|
|
|
# Ensure podman is available with a valid container policy
|
|
|
|
|
virtualisation.podman.enable = true;
|
|
|
|
|
|
2026-05-19 10:46:30 +01:00
|
|
|
# Persistent directory for the generated config.yaml
|
|
|
|
|
systemd.tmpfiles.rules = [
|
|
|
|
|
"d /var/lib/bazarr-sync 0700 root root -"
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
# --- Hourly: recent additions only ---
|
|
|
|
|
systemd.services.bazarr-sync = {
|
|
|
|
|
description = "Sync subtitles for recently added media via bazarr-sync";
|
|
|
|
|
after = serviceAfter;
|
|
|
|
|
wants = [ "network-online.target" ];
|
|
|
|
|
serviceConfig = {
|
|
|
|
|
Type = "oneshot";
|
|
|
|
|
ExecStart = recentScript;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
systemd.timers.bazarr-sync = {
|
|
|
|
|
description = "Run bazarr-sync hourly for recent additions";
|
|
|
|
|
wantedBy = [ "timers.target" ];
|
|
|
|
|
timerConfig = {
|
|
|
|
|
OnCalendar = "hourly";
|
|
|
|
|
RandomizedDelaySec = "5m";
|
|
|
|
|
Persistent = true;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
# --- Weekly: full library sync ---
|
|
|
|
|
systemd.services.bazarr-sync-full = {
|
|
|
|
|
description = "Full library subtitle sync via bazarr-sync";
|
|
|
|
|
after = serviceAfter;
|
|
|
|
|
wants = [ "network-online.target" ];
|
|
|
|
|
serviceConfig = {
|
|
|
|
|
Type = "oneshot";
|
|
|
|
|
ExecStart = fullScript;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
systemd.timers.bazarr-sync-full = {
|
|
|
|
|
description = "Run full bazarr-sync weekly (Sunday 04:00)";
|
|
|
|
|
wantedBy = [ "timers.target" ];
|
|
|
|
|
timerConfig = {
|
|
|
|
|
OnCalendar = "Sun *-*-* 04:00:00";
|
|
|
|
|
RandomizedDelaySec = "15m";
|
|
|
|
|
Persistent = true;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
}
|