Manual subtitle search and translation in the UI #203
Closed
opened 2026-08-24 21:28:40 +01:00 by naps62-yolo
·
2 comments
No Branch/Tag Specified
main
ui-mobile-first
codex/fix-action-button-reload
translation
qbit-2
docker
bugs
blitz/subtitles
fix/255-provider-reorder
blitz/feedback-3
subtitles/251-icon-vocab
subtitles/237-relocate-subs
fix/248-title-move-creates-root
blitz/feedback-2
issue/227-abandoned-pack-visible
issue/245-backoff-anchor
issue/240-design-coherence
issue/211-waived-rule
issue/232-plainer-words
issue/246-policy-reclassify
issue/244-root-slash-stored
issue/231-settings-rows
issue/241-reclassify-on-root-change
issue/239-failure-window
issue/230-icon-only
issue/243-root-path-edges
issue/238-attention-liveness
issue/229-back-button
issue/236-root-path-move
issue/226-attention-threshold
issue/228-root-move
blitz/size-bands
size-bands/209-runtime-scale
size-bands/210-size-waiver
size-bands/208-runtime-design
blitz/reliability
reliability/155-sqlx-migrations
reliability/176-refresh-on-add
removal-nav/175-removal-controls
removal-nav/174-season-file-removal
removal-nav/172-search-titles
removal-nav/173-homepage-library
removal-nav/171-untrack-clears
removal-nav/170-chip-align
removal-nav/169-design-amendment
feedback/167-deck-autosearch
feedback/165-chip-colour
feedback/166-header-rail
feedback/163-drop-cast
feedback/164-control-geometry
feedback/162-season-ordering
feedback/161-search-episodes
feedback/160-autotrack-seed
feedback/159-design-amend
blitz/rich-metadata
rich/151-grid
rich/148-search-rows
rich/150-series-meta
rich/149-movie-page
rich/147-search-art
rich/156-rating-null
rich/146-meta-api
rich/145-poster-cols
rich/144-trailer
rich/143-meta-detail
rich/142-design-amend
blitz/153-empty-episode-titles
blitz/152-deck-routes
blitz/141-season-vanished-api
blitz/129-series-detail-view
blitz/140-attention-episode-numbers
blitz/137-season-removals
blitz/133-attention-tv-lanes
blitz/139-movie-id-episode-guard
blitz/134-stale-attention-queue
blitz/124-rss-episode-matching
blitz/136-imdb-series-lookup
blitz/132-manual-commands
blitz/128-series-delete-files
blitz/131-status-seasons-input
blitz/138-plural-season-range
blitz/135-pack-name-parse
blitz/122-upstream-removals
blitz/130-add-series-search
blitz/121-series-refresh
blitz/127-unified-search-tv
blitz/123-episode-matching
blitz/126-tv-attention
blitz/119-tracked-rule
blitz/125-season-deck
blitz/120-tvdb-id
blitz/118-season-zero
blitz/117-design-amend
issue/115-avail-search
issue/112-name-truncate
issue/111-score-colors
No results found.
Labels
Clear labels
area/api
area/ci
area/compat
area/core
area/daemon
area/db
area/dl
area/indexer
area/infra
area/meta
area/parse
area/probe
area/subs
area/web
difficulty/easy
difficulty/hard
difficulty/moderate
difficulty/trivial
phase/1-skeleton
phase/2-logic
phase/3-sourcing
phase/4-movies
phase/5-ui
phase/6-tv
phase/7-people
phase/8-compat
phase/9-subtitles
type/bug
type/chore
type/feature
type/test
arr-api
CI and test harness
arr-compat
arr-core
arr-daemon
arr-db
arr-dl
arr-indexer
workspace and tooling
arr-meta
arr-parse
arr-probe
arr-subs crate: providers, extraction, translation, sync
web/
bounded, obvious approach, few files
subtle correctness or cross-cutting
multiple files, judgement, an interface to design
one file, mechanical, no design decisions
workspace, CI, config, database, empty API and SPA
parsing and policy engine, pure, no network
TMDB and Prowlarr, read-only
movies end to end
search, buckets, library views, queues
seasons, episodes, tracking, derived status
owner tags and notifications
Jellyseerr shim
build order: subtitles, DESIGN.md §15
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Depends on
#199 Serve the subtitle API
yolo/arr
Reference: yolo/arr#203
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Build the manual subtitle surface — the thing you reach for when the automatic
pick is wrong, and the piece the operator specifically asked for.
Manual search, given the §9.3 treatment: candidates as chips — provider,
hash match, release-name match, rating, downloads, SDH — with the release name
secondary and truncated. Eligible first, rejected collapsed to a count and
expandable, each rejected row naming the rule that killed it. One click grabs,
syncs and writes.
Manual translation, on the same surface: pick any subtitle already on the
file as the source — a fetched one, an extracted embedded track, even an
existing machine translation — pick a target language, pick an engine from the
ones the running binary has compiled in, run it. The result lands as a new
sidecar. Being able to translate from an embedded track is what Bazarr cannot
do and is the reason this exists.
Both run against #199. Neither changes the wanted set: a manually fetched
Spanish subtitle stays, and the loop does not treat it as a gap or a duplicate.
area/web: through theimpeccableskill, and it shares its layout languagewith the release deck rather than inventing a second one.
Depends on: #199, #201
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 the manual subtitle surface as an inline panel on each media file's row — on movie detail and on every episode row of a series — opened by a
subtitlescontrol that sits at the end of the existing §201 chip line. It borrows the release deck's layout language rather than inventing a second one:deck-group/deck-head/colhead/ fixed-width.cw-*chips /rel-linewith the release name secondary and truncated, and the sameshow Nbucket toggle. The fetch lane picks a language (wanted languages first, then pt-PT/pt-BR/en) and searches, showing candidates as chips — provider, hash match, release-name match, uploader rating, download count, SDH/forced — with eligible open and rejected collapsed to a count, each rejected row carrying arejected · forcedchip. One click grabs, and the result line says whetheralasssynced or was rejected and the unsynced original kept. The translate lane takes any subtitle already on the file as the source (a fetched one, an extracted embedded track, another machine translation), bars the source's own language as a target, and lists only the engines the running binary reports inavailable_engines. Neither lane touches the wanted set; both go through #199's endpoints unchanged. After a write, the page re-reads its own subtitle status and repaints the chips in place, so an open panel keeps its search.One API addition was needed:
SubtitleCandidatenow carriesrelease_match. §15's second ranking tier is the release-name match and the issue asks for it as a chip, but the file's own release name is not otherwise on the wire, so the UI could not derive it.Branch:
subtitles/203-manual-subs-uijust cipassed through the gate — 670 tests, clippy clean, biome clean,check-tokensclean,tscclean.Visual pass was done with
agent-browseragainst a throwaway stub of the API (populated results, provider failure, missing release name, rejected bucket, grab and translate round trips) at 1440px and 375px. Two defects found and fixed there: truncated column headers, and the panel forcing horizontal scroll at phone width because a flex item will not shrink below its min-content. At ≤40rem the rating and download columns are shed and the column header with them, so the remaining chips name themselves (hash,release) rather than readingmatchunder a header that no longer aligns.Findings
/settingssurface for subtitles.GET/PUT /api/settings/subtitlesexist and carry wanted languages, per-provider enable, engine choice and the daily budgets, but nothing inweb/reads or writes them. §15 says these are database rows edited from/settingswithout a restart. This panel reads that endpoint for its language and engine choices, so it is the only consumer today.NotFoundfor an id from an earlier search. The panel handles it by appending "search again" to the failure text when the detail matchesnot found, which is string matching on an upstream message. A typed error code, or a search token the grab must present, would be the real fix.subtitle_fileshas no uniqueness on (media_file_id, language, path). A manual grab of a second pt-PT subtitle writes a second row; only the sidecar path collision (claim_path) stops a duplicate today, and that is a filesystem check, not a constraint.DELETE /api/subtitles/{id}exists and §15 says replacement is a manual action from the UI, but nothing in the panel or the chip line offers it. Replacing a machine translation with a real subtitle therefore still needs a curl.