#216 added translate_openai_model as an explicit stopgap; the database row
replaces it, along with translate_openai_base_url. Only the API key stays
in the environment. deny_unknown_fields makes a config file still carrying
either one a parse error, so the move is visible rather than ignored.
Podnapisi's search and download are unauthenticated (#188), so the
ARR_PODNAPISI_USERNAME/PASSWORD fields were config nothing read. §10
keeps bootstrap config an honest list.
#200 branched before #205's ARR_PODNAPISI_URL seam was pushed, so its new
broken::SubtitleUpstreams call site was written against the three-argument
subtitle_providers. The merge was textually clean and did not build.
ARR_PODNAPISI_URL, same shape as the existing tmdb_url seam: a test
harness can point the provider at a wiremock fake without touching
DESIGN.md §10's config surface.
Providers are charged one unit per download, claimed atomically
right before the call; a provider at its cap is skipped in favour of
the next-ranked candidate rather than failing the whole gap.
Translators are charged the source character count before
translating. Either cap is a queue state, same as a provider's own
429.
#196 built SubtitleAction with an empty backend list, correctly: no
translation backend existed when it was written. #216 then built
translation_backends() and wired it into AppState, so the API can translate.
Merged, the reconcile loop still got Vec::new() and its translate step
reported "no engine" in every real deployment.
Neither branch was wrong alone; the gap only exists once both are in.
Closes each unsatisfied wanted language per DESIGN.md §8/§15: embedded
tracks satisfy for free (recording them as #189 left to this issue),
then provider search + ranked fetch + alass sync + sidecar write, then
immediate machine translation — extracting a text-format embedded track
when that is the only source — and otherwise the reason lands on the
attempt row for the missing-subtitles queue.
Closes run as detached tasks because alass and translation outlive the
25 s reconcile action budget; every outcome is recorded in domain rows
first, so a crash converges on the next tick. Failures back off on the
same §6.2 curve as movie searches; a rate-limited provider is the
'capped' queue state; unreachable providers and translators fold into
the existing §9.5 broken notification, edge-triggered.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Forward each translate-* feature from arr-daemon to arr-subs, add the
missing OpenAI model bootstrap key, and construct the compiled and
credentialed backends at startup so the translate endpoint stops
answering 503 unconditionally.
Both #195 and #214 added a builder call to the AppState chain, which pushed
run past the too-many-lines limit. The chain grows a line per upstream the
API learns to talk to, so it gets its own function.
arr-daemon depends on arr-api, so a handler in arr-api can never
reach the daemon's private JellyfinClient. Move it into arr-api and
attach an instance to AppState, so a manual subtitle write can ask
for the same refresh import already does (#195).
The grab handler runs alass before recording the row, replacing the
sidecar with the synced text on acceptance and flagging it as
rejected otherwise (§15). Wires a Syncer into AppState, defaulting
to alass on PATH; the daemon binary points it at config.alass_path.
Provider credentials are bootstrap config and never reach the database
(DESIGN.md §10), so which providers exist is settled once at startup;
which of them a search runs is the `providers_enabled` row the API reads
per request. OpenSubtitles.com cannot be called without a registered API
key, so without one it is not offered at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SubtitleTrack now carries the codec, split text formats from bitmap
ones per DESIGN.md §15, plus the forced and SDH dispositions ffprobe
reports. Without the forced flag a file carrying only a forced track
read as satisfied for that language.
An empty season deck was three truths wearing one message, and the one
it chose to blame was wrong: a season on the per-episode lane sat on
"sweeping indexers…" for the full wait and then blamed a backoff for a
pack search that was never going to run.
`GET /api/series/{id}/seasons/{n}/pack-state` says which lane the
season takes and why, from `season_grab_reason` in arr-core, plus the
failed-pack tally and when #181's window reopens. Seasons gain
`last_pack_search_at`, written only by a season-scoped sweep, so a
pack search that ran and found nothing is a settled answer rather than
a pending one.
The deck then says the true thing in each case, and a season held off
the pack lane by a failure offers the retry that waives its window.
Refs #182
A size band describes one episode (DESIGN.md §5.5), so both the target
penalty and the floor now compare a release's size divided by the number
of episodes it covers. The caller supplies the count — arr-core has no
IO. Movies and unknown counts divide by one, so movie scoring is
unchanged and pinned by test.
Closes#180
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A failed season-pack grab held the season off the pack lane forever:
pack_hard_failed was a bare EXISTS over failed grabs, so one bad
torrent disabled pack search for good, against §6.2's "it never gives
up entirely, it goes quiet".
The guard now rides the shared backoff curve (backoff_elapsed, 1h → 6h
→ 1d → 3d, capped 7d), counting failed pack grabs as attempts and
anchoring on the latest one's grabbed_at. Both the targeted and RSS
lanes agree. A manual season search waives the window outright — the
season deck is §6.2's escape hatch.
Closes#181.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The metadata lane runs daily, so a series added a moment ago showed no
seasons for up to 24 hours and a movie had no digital release date —
the field §6.2 gates targeted search on.
AppState now carries a MetadataCommand channel alongside the movie,
episode and season ones. Both create handlers send on it after the row
is committed, and a new daemon lane drains it. Its own task rather than
an arm of manual::run: a refresh against TMDB can take a while and must
not sit in front of an operator's manual search.
The add never waits on TMDB and never fails because of it. A refresh
that fails leaves metadata_refreshed_at NULL, which is what the daily
sweep already treats as due, so the title is retried rather than lost.
A command naming a title deleted in between finds no row and does
nothing. METADATA_INTERVAL is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A series' first metadata refresh reveals its whole back catalogue, and
apply_auto_track flagged every season as new — adding Rick and Morty
tracked S01-S09 and wanted 91 episodes (#160).
Per DESIGN.md 4.1 the rule applies from the second refresh onward.
The caller passes whether metadata_refreshed_at is set; arr-core stays
IO-free and decides. Wired through the daemon's daily refresh and the
API's create-season endpoint.
TMDB reports vote_average: 0 where no rating exists (#156). Normalise
it to None at the arr-meta edge, like non_empty does for "", and let
the Option flow through the daemon refresh and API add paths so the
nullable columns from #145 do their job.
§9.6 keeps rich detail out of the database except for the three fields
pure-SQL views need. Adds poster_path, backdrop_path and vote_average to
movies and series, written by the daily metadata refresh in both lanes
and filled at add time from the TMDB response the create flows already
fetch.
One feed pass now serves films and episodes alike. A
single-episode release grabs its open episode directly; a season
pack only grabs when season_grab_mode allows packs for that
season (§14, #117), and when it wins, the singles stand down.
Blocked series keep matching RSS (§6.3) and nothing here touches
the targeted-search backoff (§6.2).