Skip to content

Bazarr+ v2.4.0 Prism

Choose a tag to compare

@LavX LavX released this 09 Jun 10:06
da367bd

Bazarr+ v2.4.0 - Prism

Bazarr+ v2.4.0 (Prism)

Codename: Prism (opening the v2.4 line).

Bazarr+ v2.4.0, Prism, is the first feature release after v2.3.1 (Keystone). It splits the single OpenSubtitles-compatible endpoint into a multi-tenant control plane, turns the Provider Hub into a full built-in replacement system with startup auto-install, and adds two new subtitle outputs: composed bilingual/trilingual files and embedded-track translation. It also reworks subtitle synchronization around selectable engines with a side-by-side output compare, and expands the provider catalog with ten new providers and a host-side archive-extraction migration.

v2.4.0 is the largest Bazarr+ release to date by real code footprint, because it shipped as one coordinated change across two repositories: the host fork and the provider catalog. Sizing the host repo alone undercounts it badly, since the bulk of the work lived in the catalog. Counting both:

Repo Files Insertions Commits
bazarr (host) 254 22,027 158 *
bazarr-provider-catalog (#86) 162 16,710 100
Combined ~416 ~38,700 250+

* The host side was squash-merged, so its commit count understates the real development volume. Measured by host-repo commits alone, v2.2.0 "Synapse" (257) is still the largest cycle, but that figure is inflated by generated test fixtures: a single jellyfin-openapi.json and test data made up most of its ~91.6k insertions, and hand-written code was closer to ~13.9k lines.

The hero image and codename marker are added when the release is cut, mirroring how the v2.3.1 hero PNG and codename landed in their own commits.


Headline: Distribution Hub (multi-tenant subtitle API)

The OpenSubtitles-compatible /api/v1 endpoint is no longer a single shared token. The new Distribution Hub turns it into a multi-tenant backend: you issue named API keys, assign each one a tier with per-window limits, scope it to a subset of providers, and watch usage per key. It ships as a new top-level Distribution Hub menu item (faTowerBroadcast icon) with its own management page (#178).

Distribution Hub overview
Distribution Hub overview: per-key usage stat cards and the 30-day usage chart.

Named API keys, tiers, and metering

The hub introduces a hash-only named API-key store backed by keyring, with a short-TTL resolve cache and full create, rotate, update, delete, and list operations. The token is shown once at creation and the hash is never exposed afterward.

Usage is the source of truth, not a counter you can reset by accident:

  • New compat_api_keys and compat_usage ORM tables record usage in hourly buckets, written through a race-safe upsert with an in-memory window-sum cache.
  • Editable preset tiers define per-window limits, where 0 means unlimited for that window. Each key inherits a default tier or carries custom hourly, daily, weekly, and monthly limits.
  • Limit evaluation runs across multiple windows at once: a custom limit overrides the tier, and the tightest binding window wins.
  • Search is metered and rate-limited per key (HTTP 429); download is metered as well (HTTP 406). Metering is best-effort and never returns a 500 on a subtitle request.

Existing integrations keep working. An idempotent legacy seed migrates the shared compat_endpoint.token into an Unlimited Default key, and that legacy token is never throttled.

The control plane is an admin-authenticated REST namespace covering named-key CRUD and rotate (token shown once, hash never returned), editable tier limits and default tier, usage statistics, an excludable provider list, and endpoint settings with secret regeneration. The UI is organized into tabs:

  • Overview: stat cards plus a 30-day usage chart with a dialect-aware daily timeseries.
  • API Keys: a table with a per-window usage hover tooltip, create-once token reveal, rotate, disable, delete, and a per-key editor for tier, custom limits, provider allow/exclude lists, and request timeout.
  • Tiers: an editable limit grid and the default-tier selector new keys inherit.
  • Settings: enable and consent toggles, the OpenSubtitles affiliation disclaimer, global timeout, the search rate-limit toggle, and Regenerate secrets.

Hardening landed alongside the feature: JWT tokens are bound to their key by a kid claim (older kid-less tokens are accepted until they expire); search is metered on admission rather than success, so empty searches cannot be spammed to evade limits; per-request timeouts are clamped at the edge to avoid cache fragmentation; the legacy seed is safe under a concurrent-seed race; the usage-sum cache TTL is tightened from 5s to 1s; secrets shorter than 32 characters are rejected; corrupt JSON in key columns is tolerated; disabling the Default key actually revokes its token; rotating the legacy Default key through the generic rotate path is rejected and routed through regenerate; and a restart-required banner appears when the endpoint is enabled in config but the /api/v1 blueprint is not yet mounted.

Per-key provider scoping and allow-lists (only_providers, local subs)

Each key can be limited to a specific set of providers, both per request and as a stored default (#178, #180, #181, #182):

  • /api/v1/subtitles accepts a per-request only_providers allow-list that mirrors exclude_providers and can only narrow the result, never widen it.
  • Each key carries a default allowed_providers column (migration d15706c70b02). A request only_providers intersects the key's grant rather than replacing it.
  • An allow-list that resolves to nothing is sent as an active empty list and kept distinct in the cache key from None, so "no providers" and "all providers" never collide.
  • GET /api/v1/providers returns the providers a given key may actually reach.
  • Local on-disk subtitles are governed by a reserved local name in the same allow and exclude knobs, gated by serve_local_subs.
  • Compat hash mapping was added for Shooter and Napisy24 (#180).

Consolidation: one home for the OpenSubtitles-compatible endpoint

The Distribution Hub is now the single home for the OpenSubtitles-compatible endpoint (#182). The old Settings > External Integration page is removed, and /settings/external redirects to /distribution-hub.

What moved with it:

  • The legacy Default key gains an inline rotate (a regenerate flow with one-time token reveal) and a Reveal token action backed by GET /distribution-hub/legacy-token.
  • Regenerate secrets now reveals the new shared token through a shared TokenRevealModal instead of discarding it, so you can re-paste it into existing clients.
  • The consent toggle and the OpenSubtitles affiliation disclaimer are exposed in the Settings tab.
  • OMDB is offered as a selectable Provider Hub integration (like AniDB), mapping to the existing settings-omdb-apikey.

Provider Hub: built-in replacement & startup auto-install

The Provider Hub stops being an add-on layer beside the built-in providers and becomes a system that can replace them.

Provider Hub Marketplace
The Provider Hub Marketplace: install, enable, and update providers as plugins.

Trusted catalog migrations for all built-ins

MIGRATED_BUILT_IN_PROVIDER_IDS expanded from gestdown-only to the full built-in provider set (#176). A trusted or official catalog entry may now replace any built-in, collapsing the built-in and its catalog mirror into a single registered provider. Untrusted catalogs still cannot shadow a built-in.

The same PR adds compat support for moviebytesize searches, AniDB-ID pass-through, and hash-checked source dependency installs (#173).

The compat moviehash to NapiProjekt change explored in this PR was reverted in review, because NapiProjekt keys on a different 32-character hash rather than the 16-character OpenSubtitles hash. NapiProjekt moviehash search is not a shipped capability in this release.

The trusted-migration path was hardened separately (#172):

  • The built-in shadow swap is atomic: the proxy class is built before the registry is mutated, so a proxy-build failure can never leave a built-in deleted.
  • Migrated built-in ids stay in the shadow denylist after registration, so a later untrusted install of the same id still requires trust.
  • Registry-layer trust-gate regression tests were added.

Local .zip package installs + origin tracking

You can install a provider from an uploaded .zip (#177, #179). POST /provider-hub/installations/local (multipart) runs stage_install_local with zip-slip protection, manifest validation, hash verification, a venv build, and a smoke test.

  • Each install records its origin (catalog or local) and the resolved catalog source_id. A stored local origin is authoritative.
  • Local packages are always untrusted and can never shadow a built-in.
  • The Marketplace shows a separate Local / manually installed section with a Local badge and an Install local package upload button.

Follow-up fixes (#179): local installs render even when there are no catalog sources, are skipped by catalog update discovery and apply_update, and extraction is zip-bomb guarded with a bounded member count and uncompressed-size cap.

Startup auto-install, same-id collapse, and the catalog submodule

When you opt in, Bazarr+ stages the official-catalog versions of your enabled built-ins on startup (#183, #184, #201). Auto-install is off by default: enable it in Settings > General, and manual install from the Marketplace always works regardless. When enabled, a autoinstall_enabled_builtins() runs before activate and register in bazarr/init.py, with a forced catalog refresh and a guarded state load that never raises.

  • The supervisor shows an Installing providers startup stage.
  • When no providers are enabled, the UI nudges you to the Marketplace.
  • Same-id catalog plugins collapse onto the built-in card under the replacement model: shipped keys the active plugin owns are dropped, and one option is appended per active plugin, so a same-id plugin yields a single plugin-backed card (#183).
  • The bazarr-provider-catalog clone is registered as a proper git submodule.

Startup auto-install depends on an enabled official or trusted catalog source and on the catalog submodule being present.

OpenSubtitles.org is now a native plugin

With built-in replacement in place, OpenSubtitles.org no longer needs the separate opensubtitles-scraper sidecar container. It ships as a native Provider Hub plugin (reusing the opensubtitles built-in id) that scrapes the site in-process with ai-cloudscraper and inline Anubis proof-of-work solving. Install OpenSubtitles.org from the Marketplace and the sidecar is retired.

FlareSolverr is strongly recommended. The plugin exposes a FlareSolverr URL setting (and a timeout) and uses it as a fallback whenever OpenSubtitles.org still returns a Cloudflare browser challenge. Run a FlareSolverr container and point the plugin at its /v1 endpoint. The old OPENSUBTITLES_USE_WEB_SCRAPER and OPENSUBTITLES_SCRAPER_URL environment variables are no longer used.

The opensubtitles-scraper submodule is removed from the repository and dropped from the releases page, since the sidecar no longer ships (#197, #199).


Provider Hub: host-side archive extraction (#194)

Zip, rar, and 7z subtitle extraction moves out of provider plugin bundles and into the host. A worker hands back the raw archive bytes plus the member it chose (or none, to let the host pick), and Bazarr+ extracts with its own rarfile/zipfile/py7zr stack, the same code native subliminal providers use, leaving the encoding to Subtitle.normalize()/chardet. The existing direct-content download mode is unchanged, so it is fully backward compatible.

What this fixes:

  • The aarch64, macOS, and Windows install failures from providers bundling an archive binary (py7zz). Extraction now happens host-side with the binary already in the image, so provider bundles carry zero archive dependencies, and several collapse to no dependencies at all.
  • The per-provider "wrong codepage to mojibake" class: encoding is detected once on the host instead of guessed inside each worker.
  • The 22 to 50x duplication of extraction, member-selection, and encoding logic across providers.

Hardening landed with it: a decompression-bomb guard checks declared uncompressed sizes via infolist() before any read, with hard caps on the archive (32 MB), each member (16 MB), and the total uncompressed size (64 MB); the base64 payload is rejected before decode; and the worker stdout reader now caps a single response line at the transport layer (48 MB) so a runaway worker cannot buffer a giant line in full and OOM the host. RAR and 7z are read in memory (no extraction to disk), which neuters archive path traversal, and the image uses a current 7-Zip plus py7zr rather than unrar/unar.

This pairs with the provider catalog's archive-mode migration, where the converted providers return archives instead of pre-extracted content.

Archive member selection is unified host-side as well (#205). When a download returns select_member, the host lists the zip/rar/7z members and calls back so the worker can language-pin the right one, with the season/episode context forwarded so series archives resolve to the correct file. Existing explicit-member and episode download branches are untouched, so it stays backward compatible.


Provider Catalog Expansion

The bundled provider catalog gets a large update in this release, landing alongside the host-side archive work above. The Provider Hub api_version (bazarr.provider-hub.v1) and the supported ABI tags are unchanged: the "v1.1" label on the catalog refers only to the new host-side archive contract, not an ABI bump.

Ten new providers join the catalog: Ktuvit, Napisy24, OpenSubtitles.com, Shooter, Subsarr, Titulky, TurkceAltyazi.org, WhisperAI, Wizdom, and YavkaNet. No providers were removed.

Provider catalog in the Marketplace
The expanded provider catalog browsable from the Marketplace.

Under the hood, the catalog's archive-based providers were migrated to the host-side extraction model: workers return the raw zip/rar/7z bytes (or hand member selection back to the host) instead of bundling their own extraction code, which is what makes the cross-platform install and encoding fixes in the section above possible. Per-language member selection is preserved for multilingual subtitle packs, so an archive that contains several languages no longer risks a silent wrong-language download. Episode matching across the archive selectors now recognizes separated tokens (Show.S01.E02, S01 E02, S01-E02) alongside the contiguous form, with a delimited-token guard so strings like 720p or x264 are never misread as episode numbers.

Transport retry with backoff was added to 21 providers for resilience against transient network and rate-limit errors, and host-side scoring now runs on a clean release_info string for sharper resolution, source, codec, and release-group matching.

Note

Update (post-release): the catalog is now live on main. The companion Provider Hub v1.1 catalog work merged into the catalog's main branch in bazarr-provider-catalog#86, so these providers are now generally available to v2.4.0 installs straight from the official catalog, with no dev_ref beta override needed.

This was a heavy lift: most of the original built-in providers have been migrated to the Provider Hub plugin model. The payoff is structural. Providers are now plugins that update independently of Bazarr+ releases, so a provider fix ships as a catalog update instead of waiting for a Bazarr+ version bump. And contributing a provider no longer means standing up an entire Bazarr+ development environment: you write and submit a single self-contained plugin against the catalog.


Combined Subtitle Output (bilingual/trilingual SRT + ASS)

Bazarr+ can now compose existing on-disk SRTs into a single bilingual or trilingual subtitle, as SRT or ASS, per language profile or on demand (#174).

Combined subtitle in Jellyfin
A combined bilingual subtitle playing in Jellyfin.

This is pure composition. It never triggers translation, and it is skipped silently if not all source languages are present on disk.

What landed:

  • A combined-filename parser and composer, plus an index-based alignment detector (sibling, offset, overlap).
  • SRT and ASS composers, with ASS supporting Bottom, Top, and Middle positioning.
  • Profile combine-rule validation, disk-based source-path resolution, and try_combine_for_video hooked into process_subtitle.
  • Source priority is plain, then hi, then forced.
  • Combined outputs are indexed alongside sync outputs for both movies and series.

New endpoints:

  • POST /api/movies/<id>/subtitles/combine
  • POST /api/episodes/<id>/subtitles/combine
  • POST /api/series/<id>/subtitles/combine (batch)

UI surfaces:

  • A CombineRuleEditor in the language profile form (toggle combined output, pick 2 to 3 languages, choose SRT or ASS).
  • A CombinedSubtitleBadge, a CombineForm modal, and a per-row Rebuild / View / Delete overflow menu.
  • A Combine Subtitles toolbox button on movie, episode, and series detail pages, plus mass-combine actions on the Movies and Series list pages.

Embedded Subtitle Scoring & Translate-From-Embedded

In-container (embedded) subtitle tracks are now recorded as source quality in history (action=7, score 100%) and can be translated directly from an embedded track (#175).

The backend extracts the text stream to {config_dir}/extracted_subs/ via ffmpeg, then runs the configured translator. Bitmap codecs (PGS and VobSub) are rejected with a clear error, since there is no text to translate. ffmpeg must be available for extraction.

On the frontend, an embedded track shows a 100% score and a translate-only action menu. The Movies table getRowId keys missing and embedded rows by language to stop sentinel-path collisions, and sync and combined outputs are kept out of the translate-from source list.

Fixes that came with it:

  • Hi and forced embedded history deduplicate against a canonical hi-priority code for both lookup and write.
  • buildEmbeddedScoreKey produces the correct score display.
  • Extraction selects the right track via per-track from_hi and from_forced.

Multi-Engine Subtitle Synchronization

Subtitle synchronization is reworked around selectable engines (#158). A new bazarr/subtitles/tools/subsync_engines.py plus a reworked subsyncer back a SyncOutputCompareModal for side-by-side output comparison in the editor, along with MassSyncForm and SyncSubtitleForm UI.

  • Editor sync now fails explicitly when no output is produced, instead of silently keeping the original.
  • Sync outputs are indexed in movies and series alongside combined outputs.
  • A subsync_engine_failures migration (6c9f1b8d2e3a) records engine failures.

Subtitle Editor: fullscreen and combined editing (#193)

The subtitle editor's video preview gains a fullscreen toggle: a button in the player controls plus the f shortcut (Esc exits). It fullscreens the whole player so the rendered subtitle overlay and the transport stay visible, not just the bare video, and scales the overlay up for readability.

Subtitle editor
The subtitle editor with keyboard shortcuts and the fullscreen video preview.

Combined subtitles are now editable in the editor. A combined SRT opens as one stacked cue per timestamp (each language on its own line); a combined ASS opens as its separate positioned cues (one per language: Bottom, Top, Middle) without the false overlap warnings the shared timestamps would otherwise trigger. The combine pipeline keeps a single file per output, building one format removes a stale sibling left in another format, and the editor resolves a double-indexed combined language to the most recently written file.


"What's New" Wizard (#202)

A paginated What's New modal now announces per-release highlights, one change per slide, with Back/Next, progress dots, an optional icon or image, and an optional "Take me there" deep-link. It auto-opens once per new version (a localStorage seen-token compared against the latest), gated on settings being loaded so it never appears over the login screen, and it is re-openable any time from System > Status. Content is authored per release in a version-keyed frontend file, so each release adds its own slide set. The v2.4.0 highlights ship seeded.

What's New wizard
The What's New wizard, auto-opening once per new version.


Other Improvements & Fixes

Held-back v2.3.1 review findings (#161):

  • Real episode and movie metadata is now passed to translate_subtitles_file. Passing None was crashing postprocess on Plex and Jellyfin refresh.
  • Translation is re-queued when the previously translated file no longer exists on disk.
  • SZProviderPool exclusion compares by alpha3, country, and script, so excluding base eng also excludes eng:hi and eng:forced.
  • save_settings resets the compat pool when provider_languages changes.
  • Embedded history rows use the DB-side original_path for path-mapped installs.
  • ProviderTool no longer seeds a stray provider_languages staging key in add mode.

Other fixes:

  • The updates panel now tracks per-provider apply state, so applying one update no longer spins every card, and an Apply all button fans out applies in parallel (#155).
  • The compat pool is reset on Provider Hub settings changes (#163).
  • The SQLAlchemy 2 declarative-base import is used (#164).
  • Provider Hub worker results now union in release-derived matches in get_matches, mirroring native providers, so the right subtitle wins and good matches no longer fall under min_score (#196).
  • Search falls back to the on-disk filename when the release/scene name can't be parsed (for example URL-encoded indexer titles), instead of swallowing the GuessingError and returning zero results (#200).
  • Montenegrin (cnr) is registered explicitly, and Provider Hub providers that declare a language code missing from the bundled babelfish snapshot now survive registration instead of being dropped wholesale (#203).
  • Provider Hub venv creation is serialized to prevent OOM on bulk installs, and the install-lock wait is bounded by the remaining budget (#204).

CI / Docker (release-only Docker Hub publishing)

Release builds now publish to Docker Hub (lavx/bazarr), gated to v* tag pushes and release workflow_dispatch only (#159). Non-release builds still go to ghcr.io only. Release-build publishing requires the DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets.

The Provider Hub test suite now runs in CI (#195).


Dependency Updates

Python:

  • pysubs2 1.8.0 to 1.8.1 (#171)
  • apprise 1.9.8 to 1.10.0 (#170)
  • lxml >=6.1.0 to >=6.1.1 (#169)
  • cachetools >=7.1.1 to >=7.1.4 (#166)
  • click-option-group >=0.5.6 to >=0.5.9 (#186)
  • plexapi >=4.16.1 to >=4.18.1 (#187)
  • certifi 2026.2.25 to 2026.5.20 (#189)
  • ffsubsync 0.4.31 to 0.4.32 (#190)
  • charset-normalizer 3.4.6 to 3.4.7 (#188)

Frontend and CI:

  • ruff 0.15.13 to 0.15.15 (#168)
  • mantine group (core, dropzone, form, hooks, modals, notifications) 9.2.1 to 9.2.2 (#167)
  • mantine group 9.2.2 to 9.3.0 (#191)
  • actions/cache 4 to 5 (#165)
  • docker/build-push-action 6 to 7 (#185)

Database Migrations

The following migrations run on boot in create-all-then-migrate order:

  • compat_api_keys and compat_usage tables for the Distribution Hub key store and hourly-bucketed usage meter.
  • d15706c70b02: the per-key allowed_providers column.
  • 6c9f1b8d2e3a: the subsync_engine_failures table for multi-engine sync.

Included Pull Requests

  • #155: Track per-provider apply state and add Apply all in the updates panel.
  • #158: Multi-engine subtitle synchronization.
  • #159: Publish to Docker Hub on release builds only.
  • #161: Address review findings from v2.3.1 PRs.
  • #163: Reset compat pool on Provider Hub settings changes.
  • #164: Use the SQLAlchemy 2 declarative-base import.
  • #165: Bump actions/cache 4 to 5.
  • #166: Bump cachetools to >=7.1.4.
  • #167: Bump mantine group 9.2.1 to 9.2.2.
  • #168: Bump ruff 0.15.13 to 0.15.15.
  • #169: Bump lxml to >=6.1.1.
  • #170: Bump apprise 1.9.8 to 1.10.0.
  • #171: Bump pysubs2 1.8.0 to 1.8.1.
  • #172: Allow trusted Provider Hub migrations.
  • #173: Hash-checked source dependency installs.
  • #174: Combined subtitle output (bilingual/trilingual SRT + ASS).
  • #175: Embedded subtitle scoring and translate-from-embedded tracks.
  • #176: Provider Hub built-in replacements for all built-ins plus compat features.
  • #177: Provider Hub local package installs and origin tracking.
  • #178: Distribution Hub (multi-tenant subtitle API backend).
  • #179: Address Codex re-review of Provider Hub local installs.
  • #180: Map compat hashes for Shooter and Napisy24.
  • #181: Enforce allow-list narrowing and gate local subs.
  • #182: Consolidate the external endpoint into the Distribution Hub.
  • #183: Same-id catalog collapse onto the built-in card.
  • #184: Startup auto-install of enabled built-ins and the catalog submodule.
  • #185: Bump docker/build-push-action 6 to 7.
  • #186: Bump click-option-group to >=0.5.9.
  • #187: Bump plexapi to >=4.18.1.
  • #188: Bump charset-normalizer 3.4.6 to 3.4.7.
  • #189: Bump certifi 2026.2.25 to 2026.5.20.
  • #190: Bump ffsubsync 0.4.31 to 0.4.32.
  • #191: Bump mantine group 9.2.2 to 9.3.0.
  • #193: Subtitle editor fullscreen player and combined-subtitle editing.
  • #194: Provider Hub host-side archive extraction (zip/rar/7z) with bomb and oversized-payload hardening.
  • #195: Run the Provider Hub test suite in CI.
  • #196: Derive release-based matches in the Provider Hub get_matches.
  • #197: Remove the opensubtitles-scraper submodule.
  • #199: Drop opensubtitles-scraper from the releases page.
  • #200: Fall back to the on-disk filename when the scene name can't be parsed.
  • #201: Make startup auto-install of catalog providers opt-in.
  • #202: Add the "What's New" version wizard.
  • #203: Register Montenegrin (cnr) and survive unknown language codes.
  • #204: Serialize venv creation to prevent OOM on bulk installs.
  • #205: Host-list / worker-select archive members (select_archive_member op).

Upgrade / Migration Notes

Drop-in. Pull the new image and restart.

Schema migrations run on boot: the compat_api_keys and compat_usage tables, the allowed_providers column (d15706c70b02), and the subsync_engine_failures table (6c9f1b8d2e3a). They run in create-all-then-migrate order. Existing settings, language profiles, providers, and the shared compat token carry over.

Two changes need a closer look:

  • Distribution Hub seeding is transparent. Your old shared compat token is auto-seeded as an Unlimited Default key, so existing Jellyfin and VLSub clients keep working unthrottled.
  • The Settings > External Integration page is removed. /settings/external now redirects to /distribution-hub. If you bookmarked it or scripted against it, move to the Distribution Hub. Recover the current shared token with the Reveal token action on the Default key (API Keys tab), and rotate it through the inline regenerate flow.
  • The OpenSubtitles.org sidecar scraper is retired. OpenSubtitles.org is now a native Provider Hub plugin, so you can drop the opensubtitles-scraper container and its OPENSUBTITLES_USE_WEB_SCRAPER / OPENSUBTITLES_SCRAPER_URL environment variables. Install OpenSubtitles.org from the Marketplace, keep (or add) a FlareSolverr container, and set the plugin's FlareSolverr URL to your FlareSolverr /v1 endpoint.

If you want per-key control plane behavior, open the Distribution Hub menu item, create a key in the API Keys tab (copy the token immediately, it is shown once), then set a tier or custom limits, a request timeout, and an allowed or excluded provider list (including the reserved local name to allow or block on-disk subtitles for that key).


Docker

docker pull ghcr.io/lavx/bazarr:2.4.0
docker pull ghcr.io/lavx/bazarr:latest

After upgrade, confirm the UI loads and /api/system/status reports 2.4.0.


Contributors

Thanks to @davidgut1982 for the auto-translation work that this release builds on, and the embedded subtitle history foundation behind embedded-track scoring and translate-from-embedded.

Thanks to @LavX for the Distribution Hub, the Provider Hub built-in replacement and startup auto-install system, combined subtitle output, multi-engine synchronization, and the surrounding hardening and release work.


Full Changelog: v2.3.1...v2.4.0