Close the subtitle gap in the reconcile loop #196
Notifications
Due Date
No due date set.
Blocks
Depends on
#202 Add the missing-subtitles queue
yolo/arr
#188 Fetch subtitles from Podnapisi
yolo/arr
#194 Sync subtitles with alass
yolo/arr
Reference: yolo/arr#196
Reference in New Issue
Block a user
Teach the reconcile loop to close subtitle gaps. This is where the milestone's
pieces become behaviour; everything it calls already exists by the time this
lands.
The loop's rule (§8) applies unchanged: the rows are the work list. For each
media file, the desired state is the wanted set from settings — Portuguese and
English, per DESIGN.md §15 — and the actual state is the subtitle rows from
#186. The gap is a language with no satisfying subtitle.
Per gap, in order:
winner, sync it (#194), write it (#195).
window. The source is an existing subtitle for the file — a fetched one, or
one extracted from a text-format embedded track. Sync and write the result
with the
.mtmarker.leave it for the missing-subtitles queue (#202).
Also:
and episode searches already do.
today's viewing before the back catalogue.
is no background re-search and no replacement — §5.4's rule, applied here.
notification to the operator (§9.5). No new notification classes, and a
subtitle fetch never notifies anyone.
Depends on: #185, #187, #188, #189, #190, #194, #195, #212, #213
Integration note from the milestone driver — carried forward from #187, which is now merged.
Provider candidate ids are not durable across a restart. The OpenSubtitles provider records a candidate's language and format at search time so
downloadcan fillFetchedfrom an opaque id. Podnapisi encodes the language into the id itself ({pid}|{language}), so the two providers differ in how much a bare id carries.Practical consequence: if a candidate id is held anywhere that outlives the search — a queue row, a UI selection the operator comes back to, anything surviving a daemon restart — it must be re-searched before download, or OpenSubtitles answers
Error::NotFound. Do not persist a candidate id and treat it as a durable handle.Also worth knowing:
Candidate.ratingandCandidate.download_countare alwaysNonefor Podnapisi (no documented field), so ranking's tiebreakers do nothing for that provider. That is expected, not a bug to chase.Implemented #196 on branch
subtitles/196-reconcile-subs(one commit, based offblitz/subtitles). A newSubtitleActionon the reconcile tick seeds the wanted set from settings over every probed media file, then per gap: records embedded tracks from the probe data (the half #189 explicitly left here) and satisfies from them for free; searches the enabled providers, ranks per §15 (pt-PT bucket before pt-BR, moviehash computed per file), downloads the winner, syncs with alass, writes the sidecar and records it; with a definitively empty answer it translates immediately — extracting a text-format embedded track when that is the only source — writing the.mtsidecar; with nothing to translate from it recordsunavailablefor the queue (#202). Failures back off on the same §6.2 curve as movie searches; provider errors block the translate fallback (a candidate may exist where nothing could be asked); rate limits land as thecappedqueue state; unreachable providers/translators fold edge-triggered into the existing §9.5 broken notification. Closes run as detached tasks because alass (180 s) and translation outlive the 25 s reconcile action budget — every outcome is written to domain rows, so a crash converges next tick. 11 new tests, including a real-ffmpeg extraction round trip against the arr-probe fixture.just ci: passed (gate exit 0; 664 tests green). Not merged, no PR opened.Findings
arr-daemonnever builds the #191–#193 backends (itsarr-subsdependency doesn't even enable thetranslate-*features, and OpenAI needs a model-name config knob that doesn't exist), andAppState.with_translation_backendshas no production caller either. Both the manual translate endpoint and this loop's translate step therefore report "engine not compiled" in a real deployment. Needs its own wiring issue: feature forwarding inarr-daemon/Cargo.toml, backend construction from config, and passing the list to bothAppStateandSubtitleAction.cappedattempt row.pending()excludescapped, and the daily-allowance reset belongs to #197, so until #197 lands a provider rate limit parks that (file, language) permanently rather than for a day.arr-api::subtitles::translate) never runs alass on its output, though §15 says sync runs on every translated subtitle; the loop's translate path does sync. Worth a small fix on the API side.main.rs's doc comment onsubtitle_providersstill says "Translation backends are not wired: none is implemented yet (#191-#193)" — stale since #193 merged; I left it for the wiring issue to rewrite.