Serve the subtitle API #199
Notifications
Due Date
No due date set.
Reference: yolo/arr#199
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Serve the subtitle API — the surface every screen in this milestone is a client
of, per §9.1.
(embedded, extracted, provider, machine), provider or engine, forced and SDH
flags, sync state, path.
return ranked candidates with their verdicts from #185, including the rejected
ones with the rule that killed each. Same vocabulary the release deck uses, so
§9.3's treatment applies unchanged.
language and an engine, run the translation (#190) and write the result. Any
existing subtitle is a legal source — including one extracted from an
embedded track, and including another machine translation.
OpenAPI annotations on every handler, like the rest of
arr-api.Manual actions bypass the wanted-set logic entirely — the operator asking for a
Spanish subtitle gets a Spanish subtitle, and the reconcile loop does not then
treat it as a gap or remove it.
Depends on: #186, #187, #188, #190
Implemented on
subtitles/199-subs-api(based onblitz/subtitles, not merged, no PR).Seven handlers in a new
arr-api::subtitlesmodule, all with#[utoipa::path]: list per media file, per movie and per episode; manual search; manual grab; manual translate; delete. Search runs the providers this deployment has credentials for, intersected with theproviders_enabledsetting, filters to the requested language, ranks througharr_core::subs::rank(#185) and returns every candidate with §9.3's vocabulary —verdictofeligible/rejectedplusrejected_rule, so a rejected row names the rule that killed it exactly as the release deck does. A provider that cannot answer is reported inprovider_errorsbeside the candidates rather than failing the search, since §15 configures two at once. Grab downloads the named candidate, writes the sidecar next to the video under §15's<video basename>.<lang>.srt, and records it; translate reads any existing subtitle on the file (including an extracted embedded track and including another machine translation), runs it through the #190 layer and writes<…>.<lang>.mt.srt. Delete removes the sidecar and the row, and puts the language back towantedwhen nothing in it is left, because §15 reads satisfaction off the files.The issue's "manual actions bypass the wanted-set logic" is enforced by every write marking its language satisfied whether or not it is in the wanted set — so a manually requested Spanish subtitle is never read as a gap and never replaced. A forced track is the one exception §15 names: recorded, satisfying nothing.
Unlike the release deck these handlers work inline and answer with the result rather than returning 202. A subtitle search is one or two HTTP calls and nothing persists its candidates, so there is nothing to poll for. The consequence a client has to know: a grab repeats the
forcedandsdhfacts the search reported, because the server does not remember a search.just cipassed through the gate: 581 tests, clippy clean,cargo macheteclean, web checks clean. Offline sqlx query data was regenerated and committed.Findings
alasssync (#194) is not merged intoblitz/subtitles, so a manual grab records its sidecar unsynced. The issue names syncing as part of manual grab. The seam is a single call inarr_api::subtitles::grabonce #194 lands; every fetched row currently readssync: "not_run".arr_core::layout::subtitle_nameand DESIGN.md §15 give one name per (video, language, machine-made) triple, but a file can legitimately carry both a plain and a forced pt-PT subtitle, andsubtitle_files.pathis UNIQUE. Rather than invent a naming scheme the design document does not have, a grab whose target path is already taken returns 409 and the operator deletes first — which matches §15's "replacement is a manual action". A design decision is needed on whether forced/SDH sidecars get their own filename segment.srt_textinarr-api/src/subtitles.rsshould convert instead of refusing.POST …/translatecannot succeed on a real deployment.AppState::with_translation_backendsis the registry they plug into; until then the endpoint answers "engine is not compiled into this binary". Tested against a stub backend.podnapisi_username/podnapisi_passwordinarr-daemon's config are read but unusable.arr_subs::Podnapisihas no credential surface —PodnapisiBuildertakes only a base URL and a timeout. Either the provider grows authentication or the two config fields should go.media_fileshas no release name, so ranking's exact-name tier is best-effort. It is recovered by joininggrabs→releases(falling back to the season's pack grab for an episode). A file imported before that grab record existed, or one imported by hand, ranks without the tier.naps62-yolo referenced this issue2026-08-25 02:51:24 +01:00