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: 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_keysandcompat_usageORM 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
0means 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/subtitlesaccepts a per-requestonly_providersallow-list that mirrorsexclude_providersand can only narrow the result, never widen it.- Each key carries a default
allowed_providerscolumn (migrationd15706c70b02). A requestonly_providersintersects 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/providersreturns the providers a given key may actually reach.- Local on-disk subtitles are governed by a reserved
localname in the same allow and exclude knobs, gated byserve_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
TokenRevealModalinstead 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.

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
moviehashto 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 (
catalogorlocal) and the resolved catalogsource_id. A storedlocalorigin 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-catalogclone 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.

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).

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_videohooked intoprocess_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/combinePOST /api/episodes/<id>/subtitles/combinePOST /api/series/<id>/subtitles/combine(batch)
UI surfaces:
- A
CombineRuleEditorin the language profile form (toggle combined output, pick 2 to 3 languages, choose SRT or ASS). - A
CombinedSubtitleBadge, aCombineFormmodal, 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.
buildEmbeddedScoreKeyproduces the correct score display.- Extraction selects the right track via per-track
from_hiandfrom_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_failuresmigration (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.

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.

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. PassingNonewas crashing postprocess on Plex and Jellyfin refresh. - Translation is re-queued when the previously translated file no longer exists on disk.
SZProviderPoolexclusion compares by alpha3, country, and script, so excluding baseengalso excludeseng:hiandeng:forced.save_settingsresets the compat pool whenprovider_languageschanges.- Embedded history rows use the DB-side
original_pathfor path-mapped installs. ProviderToolno longer seeds a strayprovider_languagesstaging 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 undermin_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
GuessingErrorand 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.0to1.8.1(#171) - apprise
1.9.8to1.10.0(#170) - lxml
>=6.1.0to>=6.1.1(#169) - cachetools
>=7.1.1to>=7.1.4(#166) - click-option-group
>=0.5.6to>=0.5.9(#186) - plexapi
>=4.16.1to>=4.18.1(#187) - certifi
2026.2.25to2026.5.20(#189) - ffsubsync
0.4.31to0.4.32(#190) - charset-normalizer
3.4.6to3.4.7(#188)
Frontend and CI:
- ruff
0.15.13to0.15.15(#168) - mantine group (core, dropzone, form, hooks, modals, notifications)
9.2.1to9.2.2(#167) - mantine group
9.2.2to9.3.0(#191) - actions/cache
4to5(#165) - docker/build-push-action
6to7(#185)
Database Migrations
The following migrations run on boot in create-all-then-migrate order:
compat_api_keysandcompat_usagetables for the Distribution Hub key store and hourly-bucketed usage meter.d15706c70b02: the per-keyallowed_providerscolumn.6c9f1b8d2e3a: thesubsync_engine_failurestable 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_memberop).
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/externalnow 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-scrapercontainer and itsOPENSUBTITLES_USE_WEB_SCRAPER/OPENSUBTITLES_SCRAPER_URLenvironment variables. Install OpenSubtitles.org from the Marketplace, keep (or add) a FlareSolverr container, and set the plugin's FlareSolverr URL to your FlareSolverr/v1endpoint.
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:latestAfter 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
