All notable changes to Sublarr are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Health status — Ollama no longer critical — removed Ollama connectivity from the overall health flag; Ollama is an optional translation backend and its unavailability only affects translation, not core subtitle management; the status bar now correctly shows Online when Sublarr itself is reachable
- Preview Player — Firefox subtitle crash (definitive fix) — replaced
createTrack("/sub.ass")withcreateTrackMem(content, length)in the libass-wasm worker'sonRuntimeInitialized; bypassesass_read_file()(which returns NULL in Firefox even with valid WASM FS content) by passing the placeholder ASS directly in memory viaass_new_track+ass_process_data; real subtitle continues to load post-init viasetTrackByUrl()
- Preview Player — Firefox subtitle crash — fixed
ass_read_filereturning NULL in the libass-wasm worker (Firefox); the worker'sonRuntimeInitializedalways callscreateTrack("/sub.ass")— now initialised with a valid placeholder ASS so the init-time call succeeds; real subtitle is loaded post-init viasetTrackByUrl()through the worker's message buffer; also fixes CSPwasm-unsafe-evaland fallback font (default.woff2viafonts-liberationin Docker)
- Preview Player — subtitle rendering — subtitles now render correctly in the preview player; fixed canvas overlay positioning (libass canvasParent inserted inside relative wrapper), worker auth (subContent instead of unauthenticated subUrl), and CJS constructor interop for libass-wasm
- Preview Player — subtitle toggle latency — eliminated 10–20 s reappearance delay when toggling subtitles off/on; worker is now kept alive across track changes and reuses
setTrack()/freeTrack()instead of a full WASM worker restart
- Scoring — video_codec weight — x264/x265/AV1 codec match adds +2 points to episode and movie scores (Bazarr parity)
- Language Profiles — mustContain / mustNotContain — AND-logic filter: only accept subtitles matching ALL mustContain terms; any mustNotContain term rejects (Bazarr parity); new DB columns on
language_profiles - Language Profiles — cutoff — stop searching for a language once a subtitle is already present on disk
- Language Profiles — audioExclude — skip downloading a subtitle if the audio track is already in the target language
- Provider Infrastructure — CircuitBreaker persistence — CB OPEN state written to
ProviderStats.disabled_until; survives application restarts;is_openproperty added - Provider Infrastructure — rate-limit throttle — configurable extended throttle on
ProviderRateLimitErrorviaprovider_rate_limit_throttle_minutes - Download Quality — upgrade chain tracking —
upgraded_from_idforeign key onsubtitle_downloadsrecords which subtitle was replaced; enables full upgrade audit trail - Download Quality — post-download command —
post_download_commandconfig executes an arbitrary shell command after each successful download; supports{subtitle_path},{language},{provider},{score}variable substitution - Sync — manual alass endpoint —
POST /api/v1/sync/alasstriggers alass subtitle synchronisation on demand
- Standalone Mode — Auto-activation —
is_standalone_mode()helper auto-activates standalone mode when no *arr is configured;StandaloneStatusextended witharr_configuredandauto_activatedfields - Connections — Standalone scan button — manual scan button added to the Standalone section in Connection Settings
- Settings — Connections — removed central API Keys section; API keys are now managed inline within each connection's own settings panel
- Translation — Beta marking — Translation card on Settings overview now shows "BETA" pill; Translation Settings page shows a warning banner
- Language Profiles — mustContain AND logic — corrected to require ALL terms instead of ANY term (Bazarr parity fix)
- Post-download hook — guard added via
getattr(self, 'settings', None)to prevent crash when settings are not available - OpenSubtitles — Anime season-1 collapse — fallback search now maps S02+ episodes to Season 1 with the original episode number (not absolute episode);
moviehashstripped from fallback params to allow title-based lookup - UI — WebSocket events — corrected event names (
upgrade_complete,wanted_scan_complete); addedwanted_item_searchedhandler - UI — Wanted page — per-row independent loading state (shared
isPendingwas spinning all rows simultaneously) - UI — Episode Search Panel — null-safety guards on
target_resultsandsource_results
- Movie Detail — Subtitle Management — wanted items section below file info shows missing subtitles per language; inline Search / Skip / Re-enable buttons; wired to
/wanted?movie_id=filter - Backend —
/wantedmovie filter — new?movie_id=query param filters wanted items bystandalone_movie_id; enables movie detail subtitle management without loading the full wanted list
- Series Detail — Episode Grid — restored full feature set: per-row checkboxes, SubBadge per subtitle language (teal = ASS optimal, purple = SRT upgradeable, orange = missing), audio-track badges, sidecar subtitle actions (delete, download, NFO export, subtitle menu, health badge, preview, edit), batch toolbar (Search / Extract / Translate / Cleanup), Skip / Accept inline actions wired to
useUpdateWantedStatus - Dashboard — AutomationBanner — subtitle line now shows live "Last completed: X ago" derived from
scannerStatus.last_scan_at; replaces hardcoded placeholder text - Library — fixed
anime_only=Falsefilter that was hiding non-anime content; all library entries now visible regardless of type
- Settings — API Keys — removed duplicate TMDB and TVDB entries; fixed
updateApiKeyrequest body format that was causing 400 errors on save - Security — CSP / Permissions-Policy —
Content-Security-PolicyandPermissions-Policyresponse headers added to all responses (F-23) - Security — Webhook SSRF —
validate_service_url()applied to webhook create and update endpoints; blocks dangerous URL schemes (F-21) - Security — Auth warning — startup
SECURITY WARNINGlog emitted when both API key and UI auth are disabled, alerting operators to the open-API exposure (F-17/F-18 root cause)
- Providers — Subf2m — new subtitle provider supporting 60+ languages via Subf2m.co
- Providers — Subsource — new subtitle provider (multi-language, movie & TV)
- Providers — YIFY Subtitles — movie-only provider using IMDB-based JSON API
- Providers — Zimuku — Chinese subtitle provider (simplified & traditional)
- Providers — BetaSeries — French subtitle provider for TV series
- Providers — Titlovi — Balkan subtitle provider (Croatian, Serbian, Bosnian, Slovenian, Macedonian)
- Providers — EmbeddedSubtitles — integrates embedded subtitle tracks from media files directly into the search and scoring pipeline
- Subtitle Processing Pipeline — post-download processing hook; 18 fix functions (HI removal, common formatting corrections, OCR artifact cleanup); configurable per-series via series detail panel
- Settings — Processing Pipeline — new settings section for configuring post-processing behavior (fix modules, interjection list)
- Series Detail — Batch Process — button to run post-processing on all existing subtitles for a series; progress log modal
- Settings — Fansub / Release Groups — global release-group preference fields moved from Wanted tab to Scoring tab where they belong conceptually
- Series Detail — Fansub Preferences — replaced the always-visible card with a compact toolbar button; active overrides highlighted in accent color; per-series settings in a modal dialog
- Security — SSRF — URL validation in
PUT /api/v1/confignow covers dot-notation extension keys (e.g.whisper.subgen.url) that previously bypassed the_URL_FIELDScheck - Security — SocketIO log sanitization —
SocketIOLogHandlernow strips DB-internal error details (table names, column names, query fragments) before emitting to WebSocket clients - Backend — startup crash —
validate_service_urlwas imported inroutes/config.pybut never implemented; added full SSRF-safe implementation
- Settings — Navigation — Restructured from 7 groups / 23 tabs to 5 logical groups (Connections, Languages & Subtitles, Providers, Automation, System); no tabs removed
- Providers — Priority — Replaced move-up/down buttons in edit modal with drag & drop handles on provider tiles
- Score Breakdown — Hover tooltip on score badges in search results shows per-component point breakdown (series title, season, episode, format bonus, provider modifier, etc.)
- Wanted — Failure Details — Failed items now show inline error reason, attempt count, and next retry countdown
- Wanted — Batch Progress — Progress bar with found/failed counters during "Search All" operation
- Dashboard — Automation Widget — New widget showing automation status (enabled/disabled), today's found/failed subtitle stats, last/next run times, and Run Now button
- Onboarding — Language Step — New wizard step to configure target and source language during first-time setup
- Onboarding — Automation Step — New wizard step to configure automatic search interval and subtitle upgrade behavior
- Backend — Test Foundation — added 29 new tests covering
WantedSearchService,ProviderManager, and quality-validation logic; total suite now 736 tests at 47.76% coverage - Backend — Type Safety + Lint — resolved all
rufferrors andmypytype warnings across the entire backend; no new ignores added - Backend — File Splits — 8 oversized files (800–2921 lines) decomposed into focused packages:
routes/hooks/,routes/library/,routes/wanted/,routes/translate/,routes/system/,routes/tools/; service packagestranslator/andwanted_search/; shared batch state extracted toroutes/batch_state.py - Backend — Architecture —
providers/registry.pywithPROVIDER_METADATAdict replaces three class-level dicts; nestedSettingsviews (GeneralSettings,TranslationSettings,ProviderSettings,MediaServerSettings,ScanningSettings) with read-only delegation; singleton lifecycle viaget_scanner()/get_provider_manager()checkingapp.extensions - Frontend — SyncControls split —
SyncControls.tsxdecomposed intoOffsetTab,SpeedTab,FramerateTab,ChapterTab,StandardActions,SyncTabBar; orchestrator retains all state and handlers - Frontend — useApi split —
useApi.tsdecomposed into six domain files:useLibraryApi,useWantedApi,useTranslationApi,useProvidersApi,useIntegrationApi,useSystemApi; barrel re-exports all public hooks - Frontend — Error Boundaries —
ErrorBoundarycomponent wraps Library, Wanted, and Settings routes; runtime errors are caught per-route instead of crashing the full app
- Backend — monkeypatch targets — updated
test_wanted_search_reliability.pypatch paths to point to the submodule where each function is called after the Phase 3 package split - Frontend — verbatimModuleSyntax — added
import typeto all interface-only imports inVideoPlayer.tsx,PlayerModal.tsx,SubtitleTrackSelector.tsxto satisfyverbatimModuleSyntax: truein tsconfig - Frontend — TypeScript strict errors — fixed all errors from
tsc --project tsconfig.app.json: toast call signature (toast.success/error→toast(msg, type)),'warning'toast type (→'error'), missingRefreshCwimport,handleDeleteSidecarreturn type, duplicatestyleJSX attribute, RechartsFormattertype mismatch, duplicatesubsceneprovider key, implicitanyin Logs filter callback,useSeriesDetailnullable parameter, missing libass-wasm type declaration
- Standalone — NFO metadata integration — standalone scanner reads
.nfosidecar files to resolve series/movie title, year, TVDB/TMDB ID, and episode metadata without requiring an API lookup; falls back to filename parsing when no NFO is present - Standalone — Skip extra files — trailers, featurettes, samples and other non-episode extras are now excluded from subtitle discovery during standalone filesystem scan; follows Jellyfin/Kodi naming convention (
-trailer,-featurette,-behindthescenes,-deleted,-interview,-scene,-short,-sample,-theme); configurable viastandalone_skip_extrastoggle in Settings → Library Sources (advanced)
- Standalone — symlinks and SQLAlchemy text() compatibility —
os.walk(followlinks=True)now follows symlinked directories; raw SQL wrapped insqlalchemy.text()to fix deprecation warnings - Standalone — app context — scanner operations that write to DB now correctly run inside Flask app context to avoid
RuntimeError: No application context - Standalone — library view — standalone series/movies now appear in Library with correct poster URLs and breadcrumb navigation
- Standalone — series detail fallback — SeriesDetail page gracefully handles episodes without a Sonarr instance; subtitle sidecar endpoint falls back to standalone path resolution
- Standalone — poster endpoint — path security enforced via
is_safe_path(); URL generation updated to use/api/v1/prefix consistently - Standalone — NFO/poster lookup in Season subfolder — scanner now finds
poster.jpgand.nfofiles insideSeason XX/subdirectories, not only in the series root - Settings — nav redirect — Setup page correctly redirects to
/settingsafter initial configuration;NavLinkisActiveprop removed (invalid in React Router v6) - Wanted — scroll list layout — replaced hardcoded
calc(100vh - 300px)withflex-1 / min-h-0chain; list now fills the full remaining viewport at any window size
- Dependencies — jsdom 28 → 29; 13 npm minor/patch updates
- Web Player — Streaming endpoint —
GET /api/v1/media/stream?path=serves video files with HTTP 206 range-request support;is_safe_path()enforced;Content-Typeresolved by extension;SUBLARR_STREAMING_ENABLEDsetting (default true) allows disabling the endpoint - Web Player — PlayerModal — portal-based HTML5
<video>player with play/pause/seek/volume/fullscreen; opens via "Preview" button on episode cards in SeriesDetail - Web Player — ASS/SRT subtitle overlay — SubtitleOctopus (libass WASM) renders styled ASS subtitles natively in-browser;
subtitles-octopus-worker.jsand.wasmserved from/public/ - Web Player — Subtitle track selector — dropdown to switch between all available sidecar subtitle files for the episode; "Off" option disables overlay
- Web Player — Seek-to-cue — clicking a cue row in SubtitleEditorModal jumps the player to that timestamp via
onSeekRequestbridge - Web Player — Settings toggle —
streaming_enabledtoggle in Settings → Automation (advanced section)
- AI Glossary Builder — DB schema — adds
term_type(character/place/other),confidence(float 0–1),approved(boolean) columns toglossary_entries; Alembic migrationf1a2b3c4d5e6 - AI Glossary Builder — Extractor service —
glossary_extractor.pyperforms frequency analysis over subtitle sidecar files to surface recurring proper-noun candidates without requiring an LLM - AI Glossary Builder — Suggest endpoint —
POST /api/v1/series/<id>/glossary/suggesttriggers auto-detection and returns ranked candidates for human review - AI Glossary Builder — TSV export —
GET /api/v1/glossary/exportdownloads all approved glossary terms as a tab-separated file for external use - AI Glossary Builder — CRUD extended — existing
POST/PUT /api/v1/glossaryendpoints accept the newterm_type,confidence, andapprovedfields - AI Glossary Builder — Config —
SUBLARR_GLOSSARY_ENABLED(default true) andglossary_max_termsper-series cap (default 100) in Settings → Translation (advanced section) - AI Glossary Builder — LLM injection — approved terms injected as
<glossary>system prompt prefix during translation; capped at 50 terms; V8-compatibleterm → translationcomma format retained; single-line fast-path added (Translate to German: {line}) when subtitle contains exactly one cue - AI Glossary Builder — GlossaryPanel UI — Suggest button (Wand2 icon) triggers candidate detection; candidate list with approve/pre-fill/reject actions;
TermTypeBadge(character/place/other); Export TSV button; all wired via newsuggestGlossaryTermsandexportGlossaryTsvhooks
- NFO Export — Auto sidecar —
auto_nfo_exportconfig flag (off by default) writes an XML.nfofile alongside every downloaded or translated subtitle; contains provider, source/target language, score, translation backend, BLEU score, timestamp, and Sublarr version - NFO Export — API routes —
POST /api/v1/subtitles/export-nfo?path=<path>for single-subtitle export;POST /api/v1/series/<id>/subtitles/export-nfofor bulk export of all subtitles in a series; per-fileis_safe_path()validation enforced on all paths - NFO Export — Settings toggle —
auto_nfo_exporttoggle in Settings → Automation (advanced section); expert feature, hidden behind "Show advanced" - NFO Export — SeriesDetail button —
FileCodebutton on each subtitle sidecar badge in SeriesDetail triggers single-file NFO export with toast feedback
- Single-Account Login — First-run setup wizard — on first visit,
/setuppresents two choices: set a password or leave the UI open; no forced registration - Single-Account Login — Flask session auth —
before_requesthook enforces session-or-X-Api-Keyon all/api/routes when enabled; session secret auto-generated and persisted inconfig_entries; bcrypt password hashing - Single-Account Login — Auth API —
GET /api/v1/auth/status,POST /auth/setup(first-run),POST /auth/login,POST /auth/logout,POST /auth/change-password,POST /auth/toggle; API key auth (X-Api-Key) remains independent - Single-Account Login — React routing —
AuthGuardcomponent redirects to/setupor/loginas needed; auth pages render full-screen without Sidebar - Settings → Security tab — toggle UI auth on/off; change-password form (shown only when auth enabled)
- Sidebar — Logout button — shown when
auth.enabled && auth.authenticated; navigates to/loginon success
- List Virtualization — Library table view — replaced client-side pagination (25/page) with
@tanstack/react-virtualvirtual scroll using the padding-row technique;<table>/<tr>DOM structure preserved; sticky header; scroll resets on filter/sort; grid view retains pagination;VirtualLibraryTable+LibrarySharedcomponents extracted tofrontend/src/components/library/ - List Virtualization — Wanted list — Wanted now fetches all matching items in a single request (up to 9 999) and renders with virtual scroll;
useWantedVirtualizerhook infrontend/src/components/wanted/VirtualWantedTable.tsx; removes multi-page navigation
- Subtitle Diff Viewer — Per-cue accept/reject —
POST /tools/diffcomputes a cue-level diff using pysubs2 + difflib.SequenceMatcher; returns structured diff entries (unchanged/modified/added/removed) with timing in seconds.POST /tools/diff/applyrecomputes the diff server-side, merges accepted/rejected changes into the modified SSAFile (preserving header and styles), creates a.bakbackup, and writes atomically viaos.replace. FrontendSubtitleDiff.tsxrewritten from CodeMirror merge view to a filterable per-cue table; users can accept or reject each change individually or via Accept All / Reject All; applying navigates back to preview and invalidates the subtitle-content cache.
- CLI —
sublarr search— search subtitle providers for all wanted items in a series via--series-id <id>; callsGET /wanted+POST /wanted/batch-search - CLI —
sublarr translate— translate a subtitle file viaPOST /translate/sync; supports--forceflag; prints output path (sync) or job ID (queued) - CLI —
sublarr sync— sync subtitle timing to a video file viaPOST /tools/auto-sync;--engine ffsubsync|alass - CLI —
sublarr status— show active translation jobs and background task state;--runningto filter in-progress jobs only - CLI — Entry point —
backend/sublarr_cli.py; configure viaSUBLARR_URLandSUBLARR_API_KEYenv vars or--url/--api-keyflags
- Jellyfin — Play-start webhook — Sublarr now triggers the subtitle search+translate pipeline automatically when Jellyfin starts playing an episode; receives
PlaybackStartevents from the Jellyfin Webhook Plugin; resolves item path via configured Jellyfin/Emby media server instances - Settings → Automation — Jellyfin play-translate — new toggle enables automatic translation on Jellyfin playback start (
SUBLARR_JELLYFIN_PLAY_TRANSLATE_ENABLED, default off)
- Chapter Detection — ffprobe-based chapter list — Sublarr reads chapter metadata from video files; results cached per-file (mtime-invalidated) to avoid repeated
ffprobecalls; path validated viais_safe_path() - Advanced Sync — Chapter Range — offset operations can now be scoped to a chapter window; only subtitle events within the selected chapter are shifted; preview mode samples only in-range events
- SyncControls — Chapter Tab — new "Chapter" tab visible when chapters are detected; chapter dropdown (title + timestamps), ±offset presets, preview, and two-step confirm-apply flow
- Fansub Preferences — per-series preferred and excluded groups — configure preferred and excluded fansub groups per series; preferred groups receive a configurable score bonus, excluded groups are effectively filtered out; accessible from Series Detail
- SeriesFansubPrefsPanel — new panel in SeriesDetail with comma-separated preferred/excluded group inputs, bonus score field, and Save/Reset buttons
- SeriesSettings — per-series Whisper audio track — pin a preferred audio track index for Whisper transcription per series; clearing the setting (set to null) resumes automatic track selection
- SeriesAudioTrackPicker — new component in SeriesDetail; lazy-loads available audio tracks via ffprobe; dropdown sets the per-series Whisper transcription preference
- OP/ED Detector — detects Opening and Ending cue regions in subtitle files using ASS style name matching and position/duration heuristics; read-only detection returns
{type, start_ms, end_ms, event_count, method}without modifying the file; configurable detection window viaSUBLARR_OP_WINDOW_SEC(default 300 s)
- SubtitleEditorModal — Quality Tools — added Detect OP/ED button after Remove Credits button
- Credit Remover —
credit_remover.py— detects and removes credits-only subtitle lines from ASS/SSA/SRT files using 4 independent heuristics: role markers ((Translator),(QC), etc.), credit prefix patterns (Credits:,Staff:, etc.), duration heuristic (events near end of file), and isolated capitalized names (John Smith);dry_runmode for preview without modification POST /api/v1/tools/remove-credits— new endpoint to strip detected credits;dry_run=truereturns preview of lines that would be removed (capped at 50);dry_run=falsecreates.bakbackup then writes cleaned file; returnsoriginal_lines,cleaned_lines,removed,backed_up- Config —
credit_threshold_sec— new setting (SUBLARR_CREDIT_THRESHOLD_SEC, default 90s) controls how many seconds from the end of a file are considered the credits region
- SubtitleEditorModal — Quality Tools — added Remove Credits button alongside existing Remove HI button
- Batch Translate —
POST /wanted/batch-translate— re-translate multiple subtitle files in one request; acceptsitem_idsarray; returns per-item success/failure map - Batch Search Extended —
POST /wanted/batch-searchnow acceptsseries_idsarray for multi-series search in a single call - Library — Series Checkboxes — multi-select series in Library view with floating batch toolbar (Search All Missing)
- SeriesDetail — Episode Checkboxes — multi-select episodes with floating batch toolbar (Search / Extract)
- Filter Presets — save, load, and delete named filter configurations on Library, Wanted, and History pages; persisted in
filter_presetsDB table viaGET|POST|DELETE /api/v1/filter-presets - Global Search (Ctrl+K) — fuzzy search across series, episodes, and subtitles; keyboard-accessible command palette
- Auto-Extract on Scan —
scan_auto_extract+scan_auto_translatesettings; scanner automatically extracts embedded subs on first detection
- Marketplace — GitHub Plugin Discovery — new Settings → Providers → Marketplace tab; discovers community plugins via
topic:sublarr-providerGitHub topic search; caches results inmarketplace_cacheDB table with 1-hour TTL - Marketplace — Official/Community Badges — plugins from
official-registry.jsonreceive a verified "Official" badge; community plugins show a neutral "Community" label;is_officialflag persisted in DB - Marketplace — SHA256 Integrity Verification —
install_plugin_from_zip()verifies SHA256 hash before extraction; SHA256 is required (empty string rejected with HTTP 400); prevents install of corrupted or tampered plugins - Marketplace — Capability Warnings —
CapabilityWarningModalwarns users before installing non-official plugins that declarefilesystemorsubprocesscapabilities; confirmation required before proceeding - Marketplace — Installed Plugins DB —
installed_pluginstable tracks name, version, capabilities, SHA256, plugin dir, and install timestamp; persists across restarts - Marketplace — Hot-Reload —
POST /marketplace/installhot-reloads the plugin manager after successful installation viamanager.reload()+invalidate_manager() - Marketplace — Refresh —
POST /marketplace/refreshforce-fetches latest plugin list from GitHub, bypassing the 1-hour cache TTL - Marketplace — Update Detection — UI compares installed version against registry version; highlights available updates with a yellow badge
- Config —
github_token— new optionalSUBLARR_GITHUB_TOKENsetting; used for authenticated GitHub API requests to avoid rate limiting - DB Migration
a2b3c4d5e6f7— addsmarketplace_cacheandinstalled_pluginstables via Alembic
- SSRF Prevention —
zip_urlvalidated to be HTTPS-only before download (urlparsescheme check) - Path Traversal —
is_safe_path()applied to all install/uninstall plugin directory operations - XSS Prevention —
github_urlvalidated withstartsWith('https://')before rendering as<a href>
- Accessibility — Toast
aria-live—ToastContainernow hasrole="status",aria-live="polite", andaria-atomic="true"; screen readers announce toast messages without interrupting focus - Accessibility — Skip-to-Main Link — visually-hidden skip link added as first focusable element in the render tree; activating it moves focus to
#main-content; visible on keyboard focus - Accessibility — Modal
role="dialog"— all 7 modals (SubtitleEditorModal,WidgetSettingsModal,GlobalSearchModal,SubtitleCleanupModal,SyncModal,AddProviderModal,ProviderEditModal) now haverole="dialog",aria-modal="true",aria-labelledbypointing to the modal title, andautoFocuson the close button - Accessibility — Semantic Tables — all
<th>elements in Library, History, Blacklist, and Wanted tables havescope="col"; Library sort headers updatearia-sortdynamically (ascending/descending/none) - Accessibility — Form Labels —
AddProviderModalandProviderEditModalinputs havearia-labelor<label htmlFor>associations;SettingRowrenders a semantic<label>whenhtmlForis provided - Accessibility — Client-Side Validation —
AddProviderModalandProviderEditModalvalidate required fields on blur and on submit attempt; inline<p role="alert">error messages witharia-invalid/aria-describedbyon inputs - StatusBadge — Lucide Icons — each status now renders a lucide icon alongside the color dot for colorblind accessibility (
CheckCircle2,XCircle,Clock,Loader2,AlertCircle,Search,MinusCircle);Loader2animates withanimate-spin; color dot removed - Page-Specific Skeletons —
LibrarySkeleton,TableSkeleton,ListSkeleton,FormSkeletonadded toPageSkeleton.tsx; Library, History, Queue, Blacklist, Wanted, and Settings Suspense boundaries use their matching skeleton instead of the generic one prefers-reduced-motion— CSS media query added toindex.css; overrides all animation/transition durations to0.01msfor users who opt out of motion
- Library Grid — Tablet Breakpoint — added
md:grid-cols-5betweensm:grid-cols-4andlg:grid-cols-6; smooths the column jump at 768px viewport - Stagger Animation — 300ms Cap — Library grid cards and Wanted list rows apply
animationDelay: Math.min(i * 30, 300)ms; late items on large lists no longer appear broken - CSS Hover — Remove JS State —
RecentActivityWidgetandProviderTilereplaceduseState/onMouseEnter/onMouseLeavebackground-color handlers with a.hover-surface:hoverCSS utility class; eliminates unnecessary re-renders
- PostgreSQL — First-Class Support — full migration guide, PG-compatible Alembic migrations, dialect-aware health endpoints (
GET /database/health), VACUUM guard (returns 501 on PostgreSQL);docker-compose.postgres.ymlfor batteries-included PG stack;docs/POSTGRESQL.mdcovers fresh install, SQLite→PG migration via pgloader, pool tuning, backup/restore - Incremental Metadata Cache — ffprobe results cached persistently in DB with mtime-based invalidation;
GET /api/v1/cache/ffprobe/statsandPOST /api/v1/cache/ffprobe/cleanupendpoints; batch wanted-scanner probes now use cache (use_cache=True); eliminates redundant ffprobe calls on unchanged files - Background Wanted Scanner — Batch Commits — scanner now batches all DB writes per series/movie into a single commit (instead of one commit per episode); thread-local
_batch_modeflag ensures batch mode in the scanner thread never blocks concurrent API request commits;SUBLARR_SCAN_YIELD_MSsetting (default: 0) adds optional CPU yield between series to reduce contention - Parallel Translation Workers — Configurable Count —
SUBLARR_TRANSLATION_MAX_WORKERSsetting (default: 4) controls the thread pool size of the in-memory job queue;/translateasync endpoint now routes through the shared job queue (same as/translate/sync) so concurrency is always bounded and observable viaGET /api/v1/jobs - Redis Job Queue —
backend/worker.pyRQ worker entry point withAppContextWorkersubclass — each job runs inside a Flask app context;docker-compose.redis.ymlstack with Redis 7 + Sublarr +rq-worker; scale workers with--scale rq-worker=N; graceful fallback toMemoryJobQueuewhen Redis is unreachable
- Remux Engine — mkvmerge wrong track ID —
_remux_mkvmergewas referencing an undefinedstream_indexvariable (NameError) and the call site was passingsubtitle_track_index(0-based subtitle-only index, e.g.0) instead of the global ffprobe stream index (e.g.2); mkvmerge's--subtitle-tracks !Nflag uses global Track IDs matching ffprobe'sstream_index— passing!0targeted the video track and left the subtitle untouched; now_remux_mkvmergereceives and uses the correct globalstream_index; validated with mkvmerge v92.0 inside Docker
- Dockerfile — mkvtoolnix missing — added
mkvtoolnixto the Docker image apt-get install step; without itmkvmergewas unavailable inside the container and all MKV stream removal jobs failed with "mkvmerge not found"
- Stream Removal — Safe Remux Engine — remove embedded subtitle streams from video containers without re-encoding; mkvmerge used for MKV/MK3D, ffmpeg for all other containers (MP4, AVI, etc.); backend auto-detected by file extension; ffprobe verification after remux validates duration (±2s), video/audio stream counts, subtitle count (exactly -1), and file size (≥50% of original)
- Trash-Folder Backups — Configurable Retention — original video moved to centralized
<media_root>/<remux_trash_dir>/trash/<YYYY-MM-DD>/<file>.<ts>.bakbefore each remux (TinyMediaManager-style); absolute trash path supported; falls back to sibling.bakon permission error; CoW reflink attempted first on Btrfs/XFS for near-instant copies;remux_trash_dir(default.sublarr) andremux_backup_retention_days(default 7) configurable in Settings → Automation - Async Remux Jobs —
POST /api/v1/library/episodes/<ep_id>/tracks/<index>/remove-from-containerstarts a background job;GET /api/v1/remux/jobsandGET /api/v1/remux/jobs/<job_id>expose status; real-time updates via Socket.IOremux_job_updateevents; optional Sonarr/Radarr folder-monitoring pause during remux - Backup Management API —
GET /api/v1/remux/backupslists all.bakfiles in trash directories;POST /api/v1/remux/backups/cleanupdeletes backups older than retention period (supportsdry_runmode) - Undo / Restore —
POST /api/v1/remux/backups/restoreatomically restores backup to original video path viaos.replace(); both paths validated withis_safe_path()to prevent path traversal; "Rückgängig" button appears in TrackPanel after successful stream removal and restores in one click
- HI Support — Hearing Impaired Preference — new
hi_preferencesetting (include/prefer/exclude/only); provider results scored accordingly:preferadds +30,exclude/onlyapply ±999 penalty;hi_removal_enabledtoggle for future HI-tag stripping - Forced Subtitle Support — Forced Preference — new
forced_preferencesetting (include/prefer/exclude/only) with same ±30/±999 scoring logic; bonuses stack when both HI and forced preferences match - TRaSH Scoring Presets — Importable Community Profiles —
backend/scoring_presets/package with three bundled presets (anime,tv,movies);GET /api/v1/scoring/presets,GET /api/v1/scoring/presets/<name>,POST /api/v1/scoring/presets/importendpoints; Settings → Events & Hooks → Scoring tab shows preset selector and custom JSON import; import validates schema and callsinvalidate_scoring_cache() - Anti-Captcha Integration — Provider 403 Bypass — new
CaptchaSolverclass supporting Anti-Captcha.com and CapMonster via identicalcreateTask/getTaskResultREST API;anti_captcha_provider+anti_captcha_api_keysettings; Kitsunekko calls_try_solve_captcha_and_retry()on HTTP 403 — submits reCAPTCHA v2 token and retries; falls back gracefully if no solver configured; Anti-Captcha section added to Providers tab in Settings
- Duplicate Detection — SHA-256 download dedup — skips provider downloads when SHA-256 hash matches an existing subtitle in the same directory; stale hash entries are auto-cleaned on startup; toggleable via
SUBLARR_DEDUP_ON_DOWNLOAD; hash registered on every successful file write - Smart Episode Matching — multi-episode + OVA/Special — multi-episode filenames (
S01E01E02) parsed to full episode list; OVA/Special/SP detection via guessit + filename regex;release_group,source,resolution,absolute_episodepropagated toVideoQueryfor all providers - Video Hash Pre-Compute —
file_hashcomputed once inbuild_query_from_wanted()and reused across all providers; eliminates redundant file reads when multiple providers are queried in parallel - Release Group Filtering — include/exclude subtitle results by release group, codec, or source tag; score bonus for preferred groups; release metadata auto-extracted from filename via guessit; configurable at Settings → Wanted
- Provider Result Re-ranking — auto-adjusts per-provider score modifiers from download history; formula: success rate + avg score vs. global average + consecutive failure penalty; throttled hourly; preview endpoint and manual trigger available
- Subtitle Upgrade Scheduler — periodic re-check for higher-quality subtitles; eligibility: score < 500 OR non-ASS format; configurable
upgrade_scan_interval_hoursat Settings → Automatisierung; manual trigger via/tasks/upgrade-scan/trigger - Translation Quality Dashboard — daily quality trend chart (avg score + issue count) and per-series quality table (sortable, color-coded bars) added to Statistics page
- Custom Post-Processing Scripts —
subtitle_downloadedevent —subtitle_downloadedevent now emitted fromsave_subtitle(); shell hooks at Settings → Events & Hooks receiveSUBLARR_SUBTITLE_PATH,SUBLARR_PROVIDER_NAME,SUBLARR_SCORE,SUBLARR_LANGUAGE, andSUBLARR_SERIES_TITLEenvironment variables
- Activity — Parsed media titles — file column now shows parsed series/episode name and episode number instead of raw filename; full path still accessible in the expanded row;
parseMediaTitle()utility added tolib/utils.ts - History — Blacklist confirmation dialog — ban icon on history entries now opens a confirmation modal showing provider and title instead of blacklisting immediately; optional "Also delete subtitle file" checkbox deletes the sidecar file and invalidates the history cache in one atomic flow
- SeriesDetail — Delete confirmation dialog — deleting a subtitle sidecar now opens a confirmation modal with an "Also add to blacklist?" checkbox; when checked, the provider record is looked up from
subtitle_downloadsand added to the blacklist before the file is moved to trash - Activity — Expanded row layout — expanded detail row redesigned with cleaner label/value grid, stats section, and better visual hierarchy
- Wanted —
wanted_auto_translate=Falsenot respected —process_wanted_item()always started a translation job regardless of thewanted_auto_translatesetting; now the flag is checked and translation is skipped when disabled - Backend —
DELETE /library/subtitles— accepts optionalblacklist: boolbody parameter; whentrue, looks up the provider record insubtitle_downloads(LIKE-match on video base path + language) and callsadd_blacklist_entry()before trashing the sidecar
- App — SPA 404 on page reload —
static_url_path=""caused Flask's built-in static file route to intercept/wanted,/libraryetc. and return 404 before theserve_spa()catch-all; fixed by settingstatic_folder=Noneso only the custom handler runs - App — PostgreSQL startup warnings —
rowidinwanted_itemsdedup query replaced withid(primary key);MIN(title)aggregate added to search index rebuild query to satisfy PostgreSQL GROUP BY rules;_patch_pre_alembic_columns()detects and adds thesourcecolumn tosubtitle_downloadsfor databases created before Alembic was introduced - Scoring —
_DEFAULT_EPISODE_WEIGHTSimport — re-exported fromdb.scoringsoroutes/hooks.pycan import them without reaching into the repository layer
- Sidebar — Update available badge — a pulsing badge appears in the sidebar when a newer GitHub release is available; the version is fetched from the GitHub Releases API once on load and cached; clicking opens the release page directly
- Wanted — Search and download — provider search and download were broken due to missing Flask app context in background threads and stale cache; fixed by passing the app instance explicitly and resetting the provider cache on each call
- Wanted — Extracted status — extracting an embedded subtitle no longer removes the item from Wanted; instead it stays visible with a new teal
Extractedbadge so the user can see what was extracted and trigger translation or cleanup as a follow-up step - Wanted — Sidecar Cleanup — new
POST /api/v1/wanted/cleanupendpoint and matching UI button (with confirmation dialog) that deletes non-target-language.ass/.srtsidecar files next to media files of extracted items; supportsdry_runmode and optionalitem_idsfilter; path-traversal protected viais_safe_path() - Wanted — Extracted filter tab — new filter tab in the status row allows filtering the Wanted list to show only items with status
extracted
- Wanted — Extract behavior —
PUT /wanted/<id>/statusnow acceptsextractedas a valid status value in addition towanted,ignored,failed
- Library — Grid/Thumbnail view — toggle button (table ↔ grid) next to series/movies tabs; grid renders poster images from Sonarr/Radarr with missing-count badge; preference persisted to
localStorage; fallback film-slate SVG when no poster available - Library — Status and profile filters — dropdown to filter items by status (all / has missing / complete) and by profile name; filtering applied client-side via
useMemowith no additional API calls - Wanted — Error and retry display — failed wanted items now show the failure reason as a truncated
⚠ messagetooltip in the status column; upcoming retry time shown asRetry: Xm/Xhbelow the badge whenretry_afteris set - Settings — Search field — text input at the top of the settings sidebar filters tabs by name in real-time; Migration tab is excluded from search results regardless of the Advanced toggle
- SeriesDetail — EpisodeActionMenu — replaces 8 unlabelled icon-only action buttons with two primary labelled buttons (Search, Edit) and a
⋯ Moredropdown grouped by category (Preview/Compare, Timing, Analyse, History); extracted into standaloneEpisodeActionMenucomponent
- Sidebar — Version display — version fallback changed from the hardcoded
v0.1.0tov…while the health endpoint is loading; version now always reflectsbackend/VERSIONcorrectly - i18n — SeriesDetail action buttons — all 12 episode action button tooltips (Preview, Edit, Compare, Sync Timing, Auto-Sync, Video Sync, Health Check, Embedded Tracks, Search, Interactive Search, History, Back) were hardcoded English; replaced with
t('library:episode_actions.*')keys available in both DE and EN - i18n — Wanted page — "Scan Embedded" button label, "Scanning…" state text, and "Upgrades Only (N)" filter badge were hardcoded; replaced with
t('library:wanted.*')keys - i18n — FilterBar / FilterPresetMenu — "Add filter", "Clear all", "Presets", "No saved presets", "Preset name…", "Save current filters" were hardcoded English; now use
t('common:filters.*')keys - Settings — Migration tab visibility — Migration tab was always visible in the System group; now only rendered when the Advanced toggle is active and the settings search field is empty
- Statistics — empty state message — placeholder text updated to mention subtitle searches in addition to translations so users understand both workflows populate the chart
- Statistics — download tracking —
record_subtitle_download()indb/providers.pynow also writes to thedaily_statstable viarecord_stat(); provider downloads were previously invisible on the Statistics page (only translation jobs were tracked)
- Provider UI — Deaktivieren vs. Entfernen — Power button grays out a provider tile in-grid (50% opacity, "Deaktiviert" badge) while Trash button removes it to the
+pool entirely; newproviders_hiddenconfig key separates "off but visible" from "removed from grid" - Provider — Subscene — 55-language community subtitle database, no account required; HTML scraping with BeautifulSoup4, rate limit 10/60 s
- Provider — Addic7ed — 36 languages, TV-series specialist with episode-exact matching; optional login credentials increase daily download limit; BeautifulSoup4, rate limit 10/60 s
- Provider — TVSubtitles — 35 languages, TV-series only, no auth; BeautifulSoup4, rate limit 15/60 s
- Provider — Turkcealtyazi — Turkish subtitle community site, login required; BeautifulSoup4, rate limit 10/60 s
- Language expansion —
_LANGUAGE_TAGSexpanded from 25 to ~70 ISO 639-1 codes;SUPPORTED_LANGUAGESconstant with 63 ordered entries served viaGET /api/v1/languages(cached 1 h) - LanguageSelect component — searchable dropdown for source/target language settings that updates both the language code and
_namefields simultaneously
- Settings — source/target language — fields now use the new
LanguageSelectdropdown instead of plain text inputs - Provider reactive health checks — status is fetched on-demand only (no background polling);
ProviderManager.update_providers()does selective enable/disable without full reinit;providers_hiddenkey excluded from provider reinit trigger - Provider UI grid — complete tile-grid redesign: ProviderTile shows status badge, success rate, language count, and credential type; AddProviderModal replaces flat list with searchable cards; ProviderEditModal uses structured config_fields; header shows
N aktiv / M konfiguriertcounts;+tile only visible when hidden providers exist - CI —
actions/checkout,actions/setup-node,actions/setup-pythonbumped to v6
- Path traversal hardening —
is_safe_path()fromsecurity_utilsnow enforced on all 8 remaining routes that accepted user-supplied file paths:tools.py,video.py,whisper.py,spell.py,integrations.py,webhooks.py,translate.py(4 endpoints + batch directory),subtitles.py; inline ad-hocos.path.abspath().startswith()checks replaced throughout (CRITICAL) - WebSocket authentication — Socket.IO
connecthandler now rejects connections with an invalid or missing API key whenSUBLARR_API_KEYis set; frontendWebSocketContextpasses the key via socketauthdict (HIGH) - Secret masking in API responses —
get_safe_config()extended to deep-mask JSON blob fields (sonarr_instances_json,radarr_instances_json,media_servers_json) — credential sub-keys (api_key,password,token,secret,pin) replaced with"***";notification_urls_jsonalways masked;routes/config.pyblocklist extended with 8 additional sensitive keys (HIGH) - Request size limit —
MAX_CONTENT_LENGTH = 16 MBadded to Flask app factory to prevent DoS via oversized request bodies (HIGH) - Hook script path restriction —
create_hookandupdate_hooknow validatescript_pathagainst/config/hooks/usingis_safe_path(); arbitrary filesystem execution blocked (HIGH) - SQL injection in Bazarr migrator — table names read from the Bazarr SQLite file validated with
^[a-zA-Z_][a-zA-Z0-9_]*$regex before interpolation into queries; invalid names skipped with a warning (HIGH) - XZ decompression bomb protection —
AnimeTosho._decompress_xz()now enforces a 10 MB limit on decompressed output; payloads exceeding the limit raiseValueError(MEDIUM) - Container hardening — port binding changed from
0.0.0.0to127.0.0.1;read_only: true+tmpfs: [/tmp]added todocker-compose.yml(MEDIUM)
- Dev/prod requirements split — test and lint tools (
pytest,ruff,mypy,bandit,locust, etc.) moved fromrequirements.txtto newrequirements-dev.txt; production image no longer installs dev dependencies - CI — backend job now installs
requirements-dev.txtalongsiderequirements.txtso lint and test tools are available
- Sidecar discovery APIs —
GET /api/v1/library/series/<id>/subtitlesscans all episode files in parallel (ThreadPoolExecutor) and returns sidecar metadata keyed by Sonarr episode ID;GET /api/v1/library/episodes/<id>/subtitlesfor single-episode scan; response includes path, language, format, size, and mtime for each sidecar file - Sidecar delete API —
DELETE /api/v1/library/subtitlesmoves one or more sidecar files to a.sublarr_trash/folder (manifest.json per entry) instead of permanently deleting; only files insideSUBLARR_MEDIA_PATHare accepted — path-traversal attempts return 403 - Trash management APIs —
GET /api/v1/library/trashlists recoverable files;POST /api/v1/library/trash/<id>/restoremoves the file back;DELETE /api/v1/library/trash/<id>permanently removes it; auto-purge of entries older thansubtitle_trash_retention_days(default: 7 days) runs on every delete call - Batch delete API —
POST /api/v1/library/series/<id>/subtitles/batch-deleteremoves sidecars across all episodes of a series filtered by language and/or format; all deletions go through the trash system - Inline sidecar badges — SeriesDetail episode rows now show a badge for every sidecar file found on disk (language + format label); non-target-language sidecars are displayed in a dimmed style with a × delete button; clicking × soft-deletes the file and immediately refreshes the row
- Subtitle Cleanup Modal — series-level "Clean up" button opens a modal grouped by language showing file count and total size per language; "Keep target languages only" quick action pre-selects all non-target languages for deletion; preview shows file count and MB to be moved to trash before confirming
- Live extraction progress —
batch-extract-tracksemits abatch_extract_progressWebSocket event after each episode; SeriesDetail shows a progress banner (file name +X / N episodes) with a progress bar and animated spinner while extraction is running; Extract button is disabled during the operation - Activity page visibility —
batch-extract-tracksnow creates a DB job record (running→completed/failed) so every extraction run appears on the Activity page with succeeded, failed, and skipped episode counts; the job is visible within one poll cycle (~3 s) of starting - Always-visible series toolbar — new action row pinned to the SeriesDetail hero header containing three buttons: "Extract Tracks" (triggers
batch-extract-tracksfor the whole series, shows live X/N counter), "Clean up" (opens Subtitle Cleanup Modal), and "Search N missing" (moved here from the language row); all three actions are available without selecting individual episodes - Auto-cleanup settings — three new config fields:
auto_cleanup_after_extract(boolean toggle),auto_cleanup_keep_languages(comma-separated ISO 639-1 codes, e.g.de,en),auto_cleanup_keep_formats(ass/srt/any); when enabled, sidecars not matching the keep rules are moved to trash automatically at the end of eachbatch-extract-tracksrun - Settings UI — three new fields added to the Automation tab;
subtitle_trash_retention_daysfield also added to control automatic trash purge interval - Wanted Batch Search card —
useWantedBatchStatus()was previously wired but never rendered; now shown as an amber card with a progress bar and found/failed/skipped item counts while a batch search is running - Batch Probe card — live progress card appears while
batch-probeis running; shows total tracks scanned, found, extracted, and failed counts plus the currently processed file path; teal accent with animatedLayersicon - Wanted Scanner card — new
GET /api/v1/wanted/scanner/statusendpoint exposes the full live state of the background wanted scanner (is_scanning,is_searching, phase label, current/total progress, added/updated counters); rendered as a green card with an optional phase badge and progress bar; adaptive polling — 3 s while active, 30 s idle - The Queue page now shows all four background operations simultaneously: Batch Translation, Wanted Batch Search, Batch Probe, and Wanted Scanner — each with a distinct colour accent and its own progress indicator
- Subtitle badge semantics — three visual states: teal = ASS/embedded-ASS (optimal), violet = SRT/upgradeable, orange = missing; non-target-language sidecar files shown in a separate dimmed group with × delete button
- Language code normalisation —
normLang()maps ISO 639-2 three-letter codes (ger,eng,jpn,fre, …) to ISO 639-1 two-letter codes (de,en,ja,fr, …) so MKV track tags and sidecar filenames no longer generate duplicate badges for the same language - SeriesDetail subtitle column — changed from a fixed
w-40(160 px) width toflex-1 min-w-[200px]so badge rows expand to fill available space and avoid excessive wrapping on wide screens - Sidecar query live refresh —
['series-subtitles']TanStack Query polls every 4 s while extraction is running; on completion both['series-subtitles']and['series']are invalidated so episode rows update without a manual reload - Queue page polling — job list refetch interval reduced from 15 s to 3 s so short-lived translation jobs are reliably visible while the Queue page is open
- Batch-extract series_id 400 —
batch_extractreadpage.get("items", [])butget_wanted_items()returns{"data": [...]}, causing every series-level extraction triggered from SeriesDetail to return 400 "item_ids or series_id required"; fixed topage.get("data", []) - Batch-probe deadlock — a database error inside
get_wanted_items()during a probe run leftprobe.running = Truepermanently until process restart; the call is now wrapped in try/except so the flag is always cleared on failure - wanted_item_searched event dropped — the
wanted_item_searchedsignal was emitted inroutes/wanted.pybut never registered inevents/catalog.py, causing the event to be silently discarded by the unknown-name guard inemit_event(); catalog entry and signal registration added - Duplicate language badges —
gerMKV track tag and target languagedepreviously rendered as two separate badges;normLang()now normalises both sides before comparison so they collapse to a single badge
- ZIP Slip —
marketplace.pyplugin installation now usessafe_zip_extract()that validates every entry before extraction (CRITICAL) - Git clone SSRF/RCE —
validate_git_url()enforces HTTPS + domain allowlist (github.com, gitlab.com, codeberg.org) for plugin installs (CRITICAL) - Path traversal —
is_safe_path()guard added to video segment, audio waveform/extract and OCR endpoints (HIGH) - Symlink deletion bypass —
dedup_engine.pynow skips symlinks and validates paths againstmedia_pathbefore deletion (HIGH) - Hook env injection —
sanitize_env_value()strips newlines and null-bytes from event data before passing to shell scripts (HIGH) - CORS wildcard Socket.IO — replaced
"*"with configurableSUBLARR_CORS_ORIGINS(default: localhost dev origins) (MEDIUM) - New
backend/security_utils.py— canonical security utilities used by all of the above
- CI — paths-filter skips backend/frontend jobs when only the other side changed; concurrency cancels duplicate runs
- Claude Code Review — project context in review prompt; concurrency cancels stale reviews on new commits
- Settings UX Redesign — card-based sub-grouping in all tabs; each logical block has a header with icon, title, description and optional connection badge
- SettingsCard component — reusable card wrapper with divided body rows and ConnectionBadge slot
- ConnectionBadge component — 4-state indicator (connected/error/unconfigured/checking) for Sonarr, Radarr and media server tabs
- Advanced Settings toggle — global "Erweitert" checkbox in the Settings header persisted to localStorage; hides annotated advanced fields by default with orange left-border marker
- SettingRow descriptions — all 38 config fields now show always-visible description text beneath each label; 10 fields marked as advanced
- InfoTooltip improvements — ESC-key dismiss, keyboard focus/blur handlers, full ARIA accessibility (
aria-describedby,role="tooltip",useId),motion-safe:animation prefix - Dirty-state Save button — Save button disabled and grayed when no changes exist; enabled with amber indicator when fields differ from loaded config
- Navigation warning —
useBlocker(React Router v6) +window.beforeunloadprevent accidental navigation away with unsaved changes - ProvidersTab descriptions — credential and endpoint fields annotated with contextual help text
- MediaServersTab & WhisperTab descriptions — all SettingRow fields annotated
- TranslationTab descriptions — backend credential fields annotated; PromptPresetsTab shows available template variables
- MigrationTab improvements — hardcoded Tailwind color classes replaced with CSS custom properties; context header added
- Scan Auto-Extract —
wanted_auto_extract+wanted_auto_translatesettings; scanner extracts embedded subs immediately on first detection when enabled - Batch Extract Endpoint —
POST /api/v1/wanted/batch-extractextracts embedded subs for multiple wanted items in one request - Multi-Series Batch Search —
POST /api/v1/wanted/batch-searchnow acceptsseries_idsarray to trigger search across multiple series at once - SeriesDetail Batch Toolbar — episode checkboxes with Search / Extract bulk actions
- Library Batch Toolbar — series checkboxes with Search All Missing bulk action
- Track Manifest (Phase 29) — list all embedded subtitle/audio streams in MKV files, extract them as standalone files, or use one as the translation source; TrackPanel component in Library/Series Detail
- Video Sync Backend (Phase 30) —
POST /api/v1/tools/video-syncstarts async ffsubsync/alass job;GETpolls progress; fallback timeout 300s - Video Sync Frontend (Phase 31) — SyncModal with engine selector (ffsubsync / alass), live progress bar; auto-sync after download configurable per-download
- Waveform Editor (Phase 32) — Waveform tab in the subtitle editor: wavesurfer.js visualization with per-cue region markers; backend extracts audio via ffmpeg with in-memory waveform cache
- Format Conversion (Phase 33) — convert ASS ↔ SRT ↔ SSA ↔ VTT via pysubs2; convert dropdown in TrackPanel for any non-image subtitle track
- Batch OCR Pipeline (Phase 34) — async
POST /api/v1/ocr/batch-extract+GET /api/v1/ocr/batch-extract/<job_id>for extracting text from PGS/VobSub image-based subtitle tracks via Tesseract; parallel 4-worker frame processing - Quality Fixes Toolbar (Phase 35) — one-click editor buttons: Overlap Fix, Timing Normalize, Merge Lines, Split Lines, Spell Check; all endpoints create
.bakbackup before modifying
- ESLint
react-hooks/set-state-in-effectinSubtitleEditorModal— replaced synchronoussetStatecalls inuseEffectwith React's "adjust during render" pattern
- Context Window Batching (Phase 19) — subtitle cues grouped into context-window-aware chunks for coherent LLM translation
- Translation Memory Cache (Phase 20) — SHA-256 exact-match + difflib similarity cache avoids retranslating identical/near-identical lines;
.quality.jsonsidecar file tracks per-line scores - Per-Line Quality Scoring (Phase 21) — LLM scores each translated line 0–10; low-scoring lines retried automatically; quality badge in Library/Series Detail
- Bulk Auto-Sync (Phase 22) — auto-sync buttons in Library, Series Detail, and subtitle editor;
POST /api/v1/tools/bulk-auto-syncbatch endpoint - Machine Translation Detection (Phase 23) — detects OpenSubtitles
mt/aiflags; orange MT badge on search results and in Library - Uploader Trust Scoring (Phase 24) — 0–20 score bonus based on provider uploader rank; emerald Trust badge for top-ranked uploaders
- AniDB Absolute Episode Order (Phase 25) —
anidb_sync.pyfetches anime-lists XML weekly; providers queryabsolute_episodefor correct numbering; routes/anidb_mapping.py + db/repositories/anidb.py - Whisper Fallback Threshold (Phase 26) — configurable minimum Whisper confidence score; subs below threshold fall back to LLM retry
- Tag-Based Profile Assignment (Phase 27) — Sonarr/Radarr series/movie tags automatically assign language profiles via
TagProfileMappingtable; processed in webhook handler - LLM Backend Presets (Phase 28) — 5 built-in prompt templates (Anime, Documentary, Casual, Literal, Dubbed); Settings UI "Add from Template" button; user-editable custom presets
_translate_with_manager:batch_sizechunking now applied correctly (regression in v0.9.6)- Prompt presets:
{source_language}/{target_language}placeholders substituted at runtime, not stored pre-substituted
- Zombie jobs: jobs stuck in "running" state after backend restart are cleaned up on startup
- Wanted page: pagination counter now reflects active filter, not full DB total
- Duplicate
wanted_items:UniqueConstraint(file_path, target_language, subtitle_type)prevents race-condition duplicates get_series_missing_counts(): excludesexisting_sub = 'srt'and'embedded_srt'(upgrade candidates) from "missing" count
- Global Glossary — per-language term overrides applied during all translations; configurable in Settings → Translation
- Per-Series Glossary — series-specific term overrides; accessible from Series Detail
- Provider test: works without explicit
Content-Type: application/jsonheader (force=TrueJSON parsing)
-
Plugin architecture with hot-reload for custom subtitle providers
-
Plugin discovery from
/config/plugins/with manifest validation -
Plugin-specific configuration stored in
config_entriesdatabase table -
Watchdog-based hot-reload with 2-second debounce (opt-in via
plugin_hot_reload) -
Plugin developer template and documentation
-
Gestdown — Addic7ed proxy with REST API, covers both Addic7ed and Gestdown content
-
Podnapisi — Large multilingual database with XML API and lxml parsing
-
Kitsunekko — Japanese anime subtitles via HTML scraping (BeautifulSoup optional)
-
Napisy24 — Polish subtitles with MD5 file hash matching (first 10MB)
-
Whisper-Subgen — External ASR integration, returns low-score placeholder in search
-
Titrari — Romanian subtitles via polite scraping (no auth required)
-
LegendasDivx — Portuguese subtitles with session authentication and daily limit tracking
-
Per-provider response time tracking with weighted running average
-
Auto-disable after consecutive failure threshold (default: 10 failures)
-
Configurable cooldown period (
provider_auto_disable_cooldown_minutes, default: 30 min) -
Provider health dashboard with success rate, response time, and download counts
-
DeepL backend with glossary caching by (source, target) language pair
-
LibreTranslate backend for self-hosted translation (line-by-line for 1:1 mapping)
-
OpenAI-compatible backend supporting any OpenAI API endpoint with CJK hallucination detection
-
Google Cloud Translation backend with fresh client per call for credential rotation
-
Per-profile backend selection in language profiles
-
Automatic fallback chains with configurable backend priority
-
Circuit breakers per translation backend (reuses provider circuit breaker pattern)
-
Translation quality metrics tracked per backend
-
Plex support with lazy
plexapiconnection (optional dependency) -
Kodi support with JSON-RPC
VideoLibrary.Scan(directory-scoped) -
Unified media server settings page with multi-server configuration
-
MediaServerManager.refresh_all()notifies all configured servers after subtitle changes -
Legacy Jellyfin configuration auto-migrated to new multi-server format
-
faster-whisper backend with lazy model loading and device/compute_type caching
-
Subgen backend for external Whisper API integration
-
Case D translation pipeline: automatic Whisper fallback when all providers fail
-
Whisper job queue with configurable max concurrency and progress via WebSocket
-
Audio extraction via ffmpeg pipe (no temp files)
-
Language detection validation against expected source language
-
Folder-watch operation without Sonarr/Radarr dependency
-
TMDB metadata lookup (requires API key)
-
AniList metadata lookup (no API key required, 0.7s rate limiting)
-
TVDB metadata lookup with 24h JWT token caching
-
Anime detection via multi-signal heuristic (bracket groups, fansub groups, CRC32, absolute numbering)
-
guessit-based filename parsing with anime-aware mode -
MediaFileWatcherwith per-path debounce and file stability checks -
StandaloneScannergroups files by series for efficient metadata lookup -
Standalone items integrate with existing Wanted pipeline
-
Multi-signal forced subtitle detection (ffprobe flags, filename patterns, title analysis, ASS style analysis)
-
Per-series forced subtitle preference (disabled/separate/auto) in language profiles
-
OpenSubtitles
foreign_parts_onlyfilter for native forced search -
Post-search forced classification for providers without native support
-
Forced subtitle type badges and filter buttons in Wanted UI
-
Internal event bus using
blinkerwith signal isolation namespace -
22+ business events published (subtitle_downloaded, translation_complete, provider_failed, etc.)
-
Shell script hooks with environment variable payload and configurable timeouts
-
Outgoing webhooks with HTTP POST, JSON payload, and retry logic on failure
-
Event catalog with versioned payload schemas (CATALOG_VERSION=1)
-
SocketIO bridge for real-time event forwarding to frontend
-
Configurable scoring weights (hash, series, year, season, episode, release_group, ASS bonus)
-
Per-provider score modifiers (-100 to +100 range)
-
Scoring cache with 60s TTL and config-change invalidation
-
English and German translations for entire UI
-
react-i18nextwith static JSON imports (no HTTP backend) -
Language preference stored in localStorage (
sublarr-language) -
LanguageSwitchercomponent in header -
Dark/light theme toggle with system preference detection
-
Theme stored in localStorage (
sublarr-theme) with 3 states: dark, light, system -
Inline script in
index.htmlprevents flash of wrong theme before React hydration -
CSS variable-based theming
-
Full backup (config + database as ZIP) with in-memory buffer
-
Scheduled automatic backups with configurable interval
-
Restore from ZIP upload via Settings UI
-
Backup rotation with configurable retention count
-
Recharts-based charts with responsive containers
-
Time-range filters (7d, 30d, 90d, all)
-
Daily stats, provider usage, translation backend performance, format distribution
-
Subtitle download and upgrade history visualization
-
Timing adjustment (centisecond precision, H:MM:SS.cc format)
-
Encoding fix (detect and convert to UTF-8)
-
Hearing impaired tag removal
-
Style stripping (ASS to plain text)
-
All tools create
.bakbackup before modification -
Path traversal prevention via
os.path.abspathvalidation -
OpenAPI 3.0.3 specification at
/api/v1/openapi.jsonwith 65+ documented paths -
Swagger UI at
/api/docsfor interactive API exploration -
apispec+apispec-webframeworksfor YAML docstring-based spec generation -
X-Api-Key security scheme for authenticated endpoints
-
Incremental wanted scan with timestamp tracking (only rescans modified items)
-
Full scan forced every 6th cycle as safety fallback
-
Parallel ffprobe via
ThreadPoolExecutor(max 4 workers per series) -
Parallel wanted search processing (removed 0.5s inter-item delay)
-
Route-level code splitting with
React.lazyfor all 13 page components -
PageSkeletonloading component for Suspense fallback -
Extended
/health/detailedwith 11 subsystem categories -
Translation backend health checks per instance
-
Media server health checks per instance
-
Whisper backend health reporting
-
Sonarr/Radarr connectivity checks across all configured instances
-
Scheduler status reporting
- Architecture — Application Factory pattern (
create_app()) with 15 Flask Blueprints (from monolithicserver.py) - Database — Split
database.pyintodb/package with 9 domain modules (from monolithic 2153-line file) - Frontend — React 19 + TypeScript + Tailwind v4 (upgraded from React 18 + Tailwind CSS)
- Translation — Ollama configuration moved from dedicated tab to unified Translation Backends tab
- Settings — Split 4703-line
Settings.tsxmonolith into 7 focused tab modules underSettings/directory - Version numbering — Changed from v1.0.0-beta to v0.9.0-beta (standard pre-release convention -- v1.0.0 reserved for stable release)
- Gunicorn — Single worker mode required for Flask-SocketIO WebSocket state consistency
- Case-sensitive email uniqueness in provider configurations
- Hardcoded version strings ("0.1.0") replaced with centralized
version.py - SPA fallback route now returns correct version string
- Toast message and ThemeToggle label i18n gaps closed
- Pre-existing integration test expectations updated for health endpoint response format
- Provider System — Direct subtitle sourcing from AnimeTosho, Jimaku, OpenSubtitles, and SubDL
- Wanted System — Automatic detection of missing subtitles via Sonarr/Radarr integration
- Search & Download Workflow — End-to-end subtitle acquisition without Bazarr
- Upgrade System — Automatic SRT-to-ASS upgrades with configurable score delta
- Language Profiles — Per-series/movie target language configuration with multi-language support
- LLM Translation — Integrated subtitle translation via Ollama (ASS and SRT formats)
- Glossary System — Per-series translation glossaries for consistent terminology
- Prompt Presets — Customizable translation prompt templates with default preset
- Blacklist & History — Track downloads and block unwanted subtitle releases
- HI Removal — Hearing impaired marker removal from subtitles before translation
- Embedded Subtitle Detection — Extract and translate subtitles embedded in MKV files
- AniDB Integration — TVDB-to-AniDB ID mapping for better anime episode matching
- Webhook Automation — Sonarr/Radarr webhooks trigger scan-search-translate pipeline
- Multi-Instance Support — Configure multiple Sonarr/Radarr instances
- Notification System — Apprise-based notifications (Pushover, Discord, Telegram, etc.)
- Onboarding Wizard — Guided first-time setup
- Provider Caching — TTL-based search result caching per provider
- Re-Translation — Detect and re-translate files when model/prompt/language changes
- Config Export/Import — Backup and restore application configuration
- Docker Multi-Arch — Builds for linux/amd64 and linux/arm64
- Unraid Template — Community Applications template for Unraid