Commit Graph

302 Commits

Author SHA1 Message Date
Miguel Palhas fe00220c5b Merge #205: cover the subtitle path in arr-e2e
Closes #205
2026-08-25 05:26:19 +01:00
Miguel Palhas bc43084f74 Merge #217: sync translated subtitles with alass
Closes #217
2026-08-25 05:26:19 +01:00
Miguel Palhas 6ee79519ca test(arr-e2e): cover the subtitle path end to end
Cross-process against the real arr binary, real ffmpeg and real alass:
a provider fetch synced and named under DESIGN.md §15, never
re-searched once satisfied; an embedded English track extracted and
translated into a .mt sidecar; an implausible sync kept unsynced and
surfaced in the missing-subtitles queue. Podnapisi and the translation
backend are stubbed at the HTTP/process boundary, never a live tracker
or a live translation API.
2026-08-25 05:25:33 +01:00
Miguel Palhas e47d2c920b feat(arr-e2e): extend the harness for subtitle scenarios
Daemon::spawn_with_env for extra child environment, database_path()
and media_root() accessors so a test can seed a media file directly,
the daemon built with translate-command, and the plausible/farfetched
Podnapisi zip fixtures the subtitle tests download from.
2026-08-25 05:25:28 +01:00
Miguel Palhas 5fc6854705 feat(arr): add an env-only Podnapisi URL seam
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.
2026-08-25 05:25:25 +01:00
Miguel Palhas da1a932459 fix(arr): correct alass reference/subtitle argument order
Syncer::run passed [subtitle, video] where alass expects
<reference-file> <incorrect-sub-file>; every real invocation failed
before comparing timings, silently degrading to SyncState::NotRun.
2026-08-25 05:25:19 +01:00
Miguel Palhas 9f0a7de37a fix(arr): sync translated subtitles too 2026-08-25 04:56:35 +01:00
Miguel Palhas e0e7ddf6de docs(arr): correct the subtitle lane's comment
Said translation backends are passed empty; #216 wired them and #219 made
both callers share one set.
2026-08-25 04:55:06 +01:00
Miguel Palhas 78aabe99e5 Merge #202: add the missing-subtitles queue
Closes #202
2026-08-25 04:54:12 +01:00
Miguel Palhas b3cca4f692 fix(api): drop subtitle queue gaps for unwanted languages
An attempt row survives after a language leaves wanted_languages —
missing_for (#201) already bounds by the current wanted set, so the
queue reads the same way instead of showing a stale gap forever.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 03:16:54 +01:00
Miguel Palhas e338a76c66 fix(arr): wire the command timeout to the settings row 2026-08-25 03:15:33 +01:00
Miguel Palhas 47c7ef6682 fix(arr): re-read the command timeout every batch 2026-08-25 03:15:33 +01:00
Miguel Palhas fb40b35156 feat(api): add the missing-subtitles queue endpoint
GET /api/queues/subtitles, grouped by title with why each language
is a gap (#186's attempt states, plus a sync alass rejected). Series
episodes collapse into one season row when the gap is uniform, the
same restraint §9.5 gives the TV attention queues.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 03:03:17 +01:00
Miguel Palhas 4003c3a5a0 fix(arr): gate the shared LLM prompt helpers
#193 moved them out of openai.rs so the command backend could share them,
but left them ungated: with neither LLM backend compiled in they are dead
code. just ci missed it because it lints the workspace with --all-features;
building arr-api alone does not enable arr-subs' features.
2026-08-25 02:50:55 +01:00
Miguel Palhas 9ca3c3ce43 Merge #203: manual subtitle search and translation
Closes #203
2026-08-25 02:49:45 +01:00
Miguel Palhas 100a869c89 Merge #218: delete subtitle sidecars with their file
Closes #218
2026-08-25 02:49:45 +01:00
Miguel Palhas 22faad66ca fix(arr): allow the candidate DTO its five bools 2026-08-25 02:49:02 +01:00
Miguel Palhas 3a7d80c295 feat(arr): spend the subtitle budget in the reconcile loop
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.
2026-08-25 02:40:09 +01:00
Miguel Palhas 4d84625332 feat(arr): token bucket table for subtitle budgets
One row per provider/translator per day; try_spend is a single
atomic upsert so concurrent reconcile closes can't both slip a spend
past the daily cap. No allowance configured reads as unlimited.
2026-08-25 02:40:06 +01:00
Miguel Palhas f49507b7d1 refactor(arr): compute translator billing per call
Backend::characters_billed() polled a cumulative counter that races
under concurrent closes and can't attribute cost to one call. Drop it
in favour of the caller counting source characters before it sends
anything, which #197's budget needs anyway.
2026-08-25 02:40:02 +01:00
Miguel Palhas 9a21649afa feat(arr): report release-name match per candidate
The manual subtitle view shows the facts that decided a row (§9.3), and
release-name match is §15's second ranking tier. The file's own release
name is not otherwise on the wire, so the UI cannot derive it.
2026-08-25 02:29:50 +01:00
Miguel Palhas 2fa74137f9 fix(arr): delete subtitle sidecars with their file
remove_library_files only resolved video paths from media_files, so a
season/episode-scoped delete dropped subtitle_files rows via cascade
but left the .srt sidecars on disk (#218).
2026-08-25 02:29:13 +01:00
Miguel Palhas 7b4cff1874 fix(arr): give the reconcile loop its translators
#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.
2026-08-25 02:21:21 +01:00
Miguel Palhas 4110555183 Merge #196: close the subtitle gap in the reconcile loop
Closes #196
2026-08-25 02:20:11 +01:00
Miguel Palhas 987bfa2864 Merge #216: wire translation backends into the daemon
Closes #216
2026-08-25 02:20:07 +01:00
Miguel Palhas e5092034d3 feat(arr): serve subtitle status for title detail
Exposes per-media-file subtitles and missing wanted languages, with the
attempt reason, so #201's UI has one call per title (movies, episodes)
and one bulk call per series instead of one per episode.
2026-08-25 02:16:20 +01:00
Miguel Palhas c2ca25895f feat(arr): reconcile subtitle gaps in the daemon
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>
2026-08-25 02:10:49 +01:00
Miguel Palhas e49bc2736d feat(arr): wire translation backends into daemon
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.
2026-08-25 02:01:55 +01:00
Miguel Palhas f1a58c0810 refactor(arr): split api_state out of run
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.
2026-08-25 01:50:17 +01:00
Miguel Palhas 999cb8b08c Merge #214: sync manually grabbed subtitles with alass
Closes #214

# Conflicts:
#	crates/arr-api/src/state.rs
#	crates/arr-daemon/src/main.rs
2026-08-25 01:49:20 +01:00
Miguel Palhas 0d91beff14 Merge #195: write subtitle sidecars, refresh Jellyfin
Closes #195
2026-08-25 01:48:30 +01:00
Miguel Palhas 07921c9e1e test(arr): drive the command backend via stub scripts 2026-08-25 01:44:52 +01:00
Miguel Palhas 8b92a68edd feat(arr): remote-command translation backend 2026-08-25 01:44:52 +01:00
Miguel Palhas 4af6e0a083 refactor(arr): share the LLM prompt helpers across backends 2026-08-25 01:44:52 +01:00
Miguel Palhas 8baef11c0e feat(arr): refresh jellyfin after a subtitle write
DESIGN.md §7.5's watcher gap applies to a sidecar dropped next to a
file Jellyfin already knows about, same as an imported file. A grab
or translation now makes the same refresh call import does; a
refresh failure logs and never fails the write that already landed.
2026-08-25 01:31:59 +01:00
Miguel Palhas 910d28f639 refactor(arr): give arr-api its own jellyfin client
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).
2026-08-25 01:31:54 +01:00
Miguel Palhas f06e0e94bc feat(arr): sync manually grabbed subtitles with alass
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.
2026-08-25 01:24:09 +01:00
Miguel Palhas 635a651bee feat(arr): add Syncer::settle as the alass seam
Folds an implausible result and an unusable alass binary into one
SyncState both the grab handler (#199) and reconcile loop (#196)
can record without re-deriving the same match arms.
2026-08-25 01:24:02 +01:00
Miguel Palhas c6cfdff2c9 test(arr): read available_engines from the build
Second test that assumed the empty default feature set. It asserted
available_engines was literally [], which --all-features makes false.
2026-08-25 01:11:08 +01:00
Miguel Palhas f773489fd7 test(arr): make the engine gate test ask the build
Turning on --all-features compiled every translation backend, so the test
asserting that a known-but-uncompiled engine is refused had nothing left to
refuse and failed. It hardcoded "deepl" and a comment that no feature was
on, which stopped being true in the same commit that made CI see it.

It now picks whichever engine this build did not compile, and when all of
them are compiled asserts the complementary truth instead: a compiled
engine is accepted. Meaningful under either feature set.
2026-08-25 01:08:15 +01:00
Miguel Palhas db9ae271cb fix(arr): make CI compile the translation backends
Every translation backend sits behind a default-off cargo feature, and the
gate ran with the default set, so clippy and the test run never saw a line
of arr-subs' openai, deepl or google modules. #191 and #192 each reported
it after verifying their own work by hand. lint and test now pass
--all-features.

Also drops a duplicated wiremock suppression the #188 merge left behind and
puts the module list back in order.
2026-08-25 01:05:50 +01:00
Miguel Palhas 0f0d56ef0e Merge #192: DeepL and Google Translate backends
Closes #192
2026-08-25 01:04:51 +01:00
Miguel Palhas dd1e5d02a3 Merge #191: OpenAI-compatible translation backend
Closes #191
2026-08-25 01:04:47 +01:00
Miguel Palhas f84b04536a Merge #194: sync subtitles with alass
Closes #194
2026-08-25 01:04:43 +01:00
Miguel Palhas 3e5fdc6908 feat(arr): translate through Google Translate 2026-08-25 01:01:17 +01:00
Miguel Palhas 0c07fbaed5 feat(arr): translate through the DeepL API 2026-08-25 01:01:01 +01:00
Miguel Palhas ef0fac4933 feat(arr): let backends report billed characters 2026-08-25 01:00:41 +01:00
Miguel Palhas 82620e940c feat(arr): wrap alass for subtitle sync 2026-08-25 00:50:25 +01:00
Miguel Palhas 8efb9cbf23 feat(arr): OpenAI-compatible translation backend
Behind the existing translate-openai feature. One JSON-in/JSON-out
chat/completions request per batch; refusal, content-filter and
truncated replies fail loudly instead of validating as a short batch.
2026-08-25 00:50:06 +01:00
Miguel Palhas 26bd25e2f6 feat(arr): convert non-SRT grabs instead of refusing
#199 shipped the manual grab before #213 existed, so srt_text answered 422
for any provider not serving SRT. Fetched::to_srt already decodes and
converts, so this is that call. A format with no parser still fails rather
than reaching the disk.

Leaves the alass half of #214 open; that waits on #194.
2026-08-25 00:46:33 +01:00