docs(arr): add DESIGN.md §15 subtitles contract

Records the subtitle decisions settled during Subtitles milestone
planning: global wanted set (pt + en), embedded-track extraction,
providers, ranking, translation backends, sidecar naming, alass sync,
budgets, and stated non-goals. Points §13 item 9 at §15 and adds
arr-subs to the crate lists.

Closes-Issue: #183

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Miguel Palhas
2026-08-24 21:37:37 +01:00
parent 405358bf24
commit 1401e9e00f
2 changed files with 88 additions and 1 deletions
+1
View File
@@ -18,6 +18,7 @@ crates/
├── arr-indexer/ Torznab via Prowlarr ├── arr-indexer/ Torznab via Prowlarr
├── arr-dl/ Transmission RPC ├── arr-dl/ Transmission RPC
├── arr-probe/ ffprobe wrapper ├── arr-probe/ ffprobe wrapper
├── arr-subs/ subtitle providers, translation, sync
├── arr-db/ sqlx + migrations ├── arr-db/ sqlx + migrations
├── arr-api/ axum + OpenAPI ├── arr-api/ axum + OpenAPI
├── arr-compat/ Radarr/Sonarr v3 shim for Jellyseerr ├── arr-compat/ Radarr/Sonarr v3 shim for Jellyseerr
+87 -1
View File
@@ -666,6 +666,7 @@ arr-meta TMDB client
arr-indexer Torznab via Prowlarr arr-indexer Torznab via Prowlarr
arr-dl Transmission RPC arr-dl Transmission RPC
arr-probe ffprobe wrapper arr-probe ffprobe wrapper
arr-subs subtitle providers, translation, sync
arr-db sqlx + migrations arr-db sqlx + migrations
arr-api axum + OpenAPI arr-api axum + OpenAPI
arr-compat Radarr/Sonarr v3 shim for Jellyseerr arr-compat Radarr/Sonarr v3 shim for Jellyseerr
@@ -748,7 +749,7 @@ previous one.
grabbing, derived status. grabbing, derived status.
7. **Owners and notifications** — tags, per-person ntfy topics, filtered views. 7. **Owners and notifications** — tags, per-person ntfy topics, filtered views.
8. **Jellyseerr compat**`arr-compat`. 8. **Jellyseerr compat**`arr-compat`.
9. **Subtitles** — replaces Bazarr. Out of scope for this document. 9. **Subtitles** — replaces Bazarr. See §15.
Movies before TV because TV adds season packs, air-date calendars and Movies before TV because TV adds season packs, air-date calendars and
per-episode state on top of an otherwise identical pipeline. Doing it second per-episode state on top of an otherwise identical pipeline. Doing it second
@@ -768,3 +769,88 @@ means that pipeline is already proven.
RSS lane obeys the same guard — it skips packs for any season with episodes RSS lane obeys the same guard — it skips packs for any season with episodes
on disk. Whether a re-grab is ever wanted remains unresolved and deliberately on disk. Whether a re-grab is ever wanted remains unresolved and deliberately
deferred. deferred.
## 15. Subtitles
Replaces Bazarr. Phase 9 in §13; the `arr-subs` crate in §11.
**Wanted set.** Global, not per root. Two languages are separately wanted for
every media file: Portuguese — pt-PT preferred, pt-BR accepted — and English.
A file is satisfied for a language when a subtitle in it exists, embedded or as
a sidecar. This is deliberately unlike §5.2's audio rules, which attach to a
root: subtitles carry no blacklist there and none here. pt-BR subtitles are
always fine.
**Embedded tracks.** An embedded subtitle track satisfies its language.
Text-format tracks (`subrip`, `ass`, `mov_text`) are additionally extracted to
a sidecar SRT, because an extracted track is a legal translation source.
Image-format tracks (PGS on BluRay, VobSub on DVD) carry bitmaps, not text:
they satisfy viewing but can never feed a translator, and arr does not OCR
them. `arr-probe` already reports subtitle tracks with resolved languages; the
format is the new fact it must carry.
**Providers.** OpenSubtitles.com and Podnapisi, behind one trait.
**Ranking.** A `moviehash` match wins outright. Then an exact release-name
match, then same release group or same source, then uploader rating and
download count as tiebreakers. Every rejected candidate names the rule that
killed it, so the manual view described in §9.3 works unchanged for subtitles.
**Forced and SDH.** A forced track covers only foreign-language lines and
on-screen signs; it never satisfies a want and arr never goes looking for one.
An SDH track is complete and satisfies, ranked below a plain subtitle.
**Translation.** When no provider has a wanted language, arr translates
immediately — there is no waiting window. The source is an existing subtitle:
a downloaded one, or one extracted from a text-format embedded track. Being
able to translate from an embedded track is a deliberate improvement on
Bazarr, which cannot.
**Translation backends.** Pluggable, each behind its own cargo feature: an
OpenAI-compatible HTTP endpoint, DeepL, Google Translate, and a generic remote
command driven by a configured template (`ssh box claude -p` is one instance
of that template, not a backend of its own). The engine in use is a database
setting, so switching does not need a rebuild when the feature is compiled in.
Subtitles are sent in batches of cues; a reply whose cue count or numbering
does not match the batch is rejected. Timing data never leaves arr.
**No upgrade loop.** Once a language is satisfied — by a machine translation
too — arr stops working on it. A real subtitle appearing later does not
replace anything. Replacement is a manual action from the UI. This is §5.4's
rule applied to subtitles.
**On disk.** Sidecars live next to the video inside the §7.4 title folder,
named `<video basename>.<lang>.srt`, e.g.
`… - [2160p][WEB-DL][HDR10].pt-PT.srt`. A machine translation carries an extra
`.mt` segment: `… [HDR10].pt-PT.mt.srt`. That keeps §7.4's audit-by-`ls`
property — with the app stopped, the filename says which subtitles are
machine-made. Folder-level delete stays atomic because sidecars are inside the
folder.
**Sync.** `alass` runs on every fetched and every translated subtitle. It is a
single small binary invoked like `ffprobe`, so it costs nothing at rest. It
reports no confidence value, so its output is accepted unless it is
implausible — a shift beyond 60 seconds, or cues lost — in which case the
unsynced original is kept and the file is flagged.
**Configuration.** Provider credentials and translator API keys are bootstrap
config or environment, per §10. Wanted languages, chosen engine, per-provider
enable and the daily budgets are database rows edited from `/settings` without
a restart.
**Budgets.** A token bucket per provider and per translator, with a configured
daily allowance. The reconcile loop spends it newest-import-first, so enabling
this on an existing library drains the backlog over days instead of hitting
every rate limit at once. Being at the cap is a visible queue state, not an
error.
**Notifications.** No new event classes. §9.5 stands: subtitle fetches never
notify, and a provider or translator being unreachable folds into the existing
"Broken" message to the operator alone.
**Non-goals**, stated here so they do not creep back: OCR of image-based
tracks, transcribing audio when no subtitle exists anywhere, adopting subtitle
files already on disk that arr did not write (§2 already says the service
knows only what it put there — which does mean arr may fetch a second copy
alongside one Bazarr left), and any background loop that upgrades a subtitle
in place.