Skip to content

Bazarr+ v2.6.0 (Clockwork)

Latest

Choose a tag to compare

@LavX LavX released this 28 Aug 22:52
83de09c

Bazarr+ v2.6.0 - Clockwork

Bazarr+ v2.6.0 (Clockwork)

Codename: Clockwork

Clockwork is a correctness pass across the whole surface: 79 pull requests that make the machinery keep honest time. Databases created by upstream Bazarr are adopted instead of crash-looping, sync results are held to the offset you configured instead of merely bounded by it, providers are scored on what they actually offer, and your subtitles are finally downloadable straight from the UI, single files or whole seasons at once. Most of this release exists because users filed precise, reproducible reports, on GitHub and on Discord, and they are named below where their reports became fixes.


Headline: Coming from upstream Bazarr just works now

If you pointed Bazarr+ at a config directory created by upstream Bazarr, it crash-looped on boot with Can't locate revision identified by '0124f9e278fb': the upstream database carries migration revisions that were never part of the fork's chain, and Alembic refused to move.

Bazarr+ now detects an upstream-created database at startup and adopts it (#352): the migration stamp is rewritten onto the fork's own chain and the one column upstream dropped is restored, after which the normal migration path proceeds. Nothing in your data is altered by the adoption itself.

The first fix targeted the one revision named in the report; the follow-up generalized it (#356): adoption now handles any upstream revision, not a hard-coded list, so a database carrying an upstream migration that does not exist yet today still adopts cleanly tomorrow.

Take a backup before pointing Bazarr+ at an upstream config directory, as you would for any migration. The adoption is one-way: once stamped onto the fork's chain, the database is a Bazarr+ database.

This was the single largest barrier to trying Bazarr+ on an existing install, and it is gone. Thanks to szab100 for the report that pinned the exact revision (#302), and to SirCaralot and Alex, who hit the same wall from their own setups and reported it on Discord. SirCaralot also caught a fresh-PostgreSQL startup failure on the same path, addressed in the release candidate; PostgreSQL is first-class here and the adoption work covers it.


Download your subtitles

Every subtitle in the UI now carries a Download action, and it works for everything Bazarr+ can produce, not just the file a provider gave you (#384).

Single files, any variant

The row menu downloads the exact file on disk: plain subtitles, hearing-impaired and forced variants, synchronized outputs (en:hi:sync-ffsubsync and friends), and combined dual-language subtitles (en:combined-hu). The variant addressing is strict on purpose: an unknown or wrong-case modifier is rejected rather than resolved to the nearest file, so you always get the file you asked for.

The endpoint went through seven review rounds and is hardened accordingly:

  • The file is opened with symlink-following disabled at the final component, so a subtitle swapped for a symlink between validation and read cannot leak a file outside your library. Platforms without O_NOFOLLOW get an identity check on the opened descriptor instead.
  • The resolved real path must sit under the media folders Bazarr+ actually manages, and the extension allowlist runs on the resolved target, not the link name.
  • Download filenames are sanitized of control characters, responses carry the guessed subtitle MIME type, and X-Content-Type-Options: nosniff is set on everything.

Whole seasons and languages as zip bundles

The series and movie detail pages gained a bundle download in the header: one zip with every external subtitle, organized into Season NN/ folders for series, with optional season and language filters. Bundles are built in a spooled buffer with a hard size budget enforced while reading (a file growing mid-build cannot overshoot it), duplicate archive names are deduplicated case-insensitively, timestamps outside the zip format's range are clamped instead of crashing, and files that vanish between indexing and read are skipped rather than corrupting the archive.

The viewer is readable in light mode again

Cue timestamps in the subtitle viewer were nearly invisible in light mode; the contrast now holds in both themes (#384).

The UI tells you when a download is not a subtitle

If a download ever comes back as a web page instead of a subtitle file (an out-of-date backend behind an updated UI, for instance), the frontend now detects the payload type and raises a clear error instead of saving junk with a .srt name. Bulk downloads from the subtitle tools modal run sequentially with per-item error handling, so one failure no longer aborts the batch.

The What's New wizard gained a slide for the feature (#392), and two post-RC corrections keep the in-app text honest: the mass-translate slide now names the real setting ("Treat Embedded Subtitles as Downloaded", #393) and the embedded-parser options stopped claiming mediainfo needs a manual install that the Docker image already ships (#394).


Sync that tells the truth

Subtitle synchronization had a habit of reporting success it had not earned. Clockwork makes the whole pipeline honest, and the credit for forcing the issue goes to illusive888's report on GitHub (#308) and genuphobia's traceback-laden reports on Discord.

Maximum offset is now an acceptance threshold

The configured maximum offset used to bound ffsubsync's search window, so a subtitle minutes out of sync could come back "synced" within the window and overwrite a good file. It is now an acceptance threshold (#336): ffsubsync and autosubsync results beyond it are rejected outright, the rejection is recorded as the failure it is, and the next engine in your fallback order gets its turn. alass reports no comparable offset, which is documented rather than papered over.

Expect more honest failures. A file that previously "synced" into a ruined state now fails loudly, and files already ruined by the old behavior are not repaired by this release. Sync history entries now name the engine and the measured offset, so you can see which engine did what.

autosubsync reports what it measured

autosubsync used to discard its completed sync behind an opaque quality-threshold failure. It now reports the shift it actually measured under the same key the other engines use, so its results participate in the acceptance threshold like everyone else's (#349).

alass survives real-world MKVs

alass died on MKVs whose attachment streams ffprobe reports without a codec_long_name: the wrapper now shims the ffprobe output alass sees, so attachment streams stop killing the run (#334). This came straight from genuphobia's Discord traceback (a WEBRip AV1 anime episode with Opus audio).

Release-type mismatches are detected, notified, and cleaned up

Bazarr+ detects when a downloaded subtitle's release type (WEB-DL versus BluRay and friends) does not match your media file and notifies you (#342). This grew out of WIZARD's detailed Discord feature request about Hebrew subtitles from Ktuvit and Wizdom, where a subtitle often exists for exactly one release type and the wrong grab means no sync will ever fit; the original request's "at least alert the user" is what shipped. The indexer clears stale mismatch records when the situation is resolved on disk, not only when a download fixes it (#369), and the indexer also stopped indexing whichever database row it found first instead of the row the caller named (#360).

The engines are documented

The wiki now documents all three sync engines, what each is good at, and what their failure modes look like (#351), because genuphobia looked for that page and it did not exist.


Providers: scored, gated, and steady

Candidates are scored on their release information

Provider Hub candidates lost release-based scoring entirely when Provider Priority was disabled, so a perfect-release match and a wild guess scored the same. Hub candidates now match their release information against your file the way built-in providers always did (#347), with the follow-up reserving the release_info key so a plugin cannot clobber it (#369). Reported by illusive888 (#305), whose ticket restated the defect almost word for word before the code confirmed it.

Weight one provider up or down

Each provider can carry a score modifier from -100% to +100%, applied before the minimum-score check (#355). It is a percentage of the maximum score rather than raw points, so 25% on an episode is worth roughly 90. Built for keeping something like WhisperAI as a genuine last resort without lowering the bar for everyone else, which is exactly what Aracnoss asked for (#301).

Related scoring correction: movie editions detected by GuessIt were dropped on the way into scoring, so a "Director's Cut" subtitle scored as if editions matched (#337). Editions now carry through, which means subtitles for edition-tagged movies can score up to 30 points lower than before. If a wanted movie suddenly stalls under your minimum score, this is why: the old score was inflated, not the new one wrong. Reported by illusive888 (#307).

RAR archives extract again

RAR extraction had silently broken in the shipped image, taking providers like prijevodionline with it. The community fix from luminoso restored it (#313), the image ships unar, and the CI split that had hidden the failure was removed so the two test trees run together again with the RAR configuration pinned (#346). Two corrective follow-ups tightened the same area: the LegendasDivx built-in stopped returning the wrong episode from multi-episode archives and CLI extraction was bounded (#328), and pool adoption gained the gate described below. Reported by illusive888 (#304).

Archive matching stopped being picky

Subtitle members inside archives are matched case-insensitively, and the accepted extension list covers the canonical set including MicroDVD .sub, so Titlovi-style archives stop being rejected for cosmetic reasons (#338). Reported by illusive888 (#306).

The pool stops resurrecting the dead

Two long-standing pool defects are fixed. Pool re-registration could silently resurrect a provider you disabled or that was throttled; adoption now refuses discarded providers and consults an enabled-and-not-throttled gate, quietly, without poisoning throttle bookkeeping (#385). And a configuration change leaked the provider instance it replaced; the replaced provider is now terminated (#332).

Gestdown, and the built-ins' road to the catalog

The Gestdown built-in formats its release information from the API episode object with the show name and episode tag (#314, another luminoso contribution), and keeps the series title out of release-group matching so titles with dashes stop producing phantom release groups (#330). The built-in providers are now marked legacy, pointing at the Provider Hub catalog where fixes actually land (#325), and the catalog submodule tracks the catalog's main again after drifting 17 commits behind (#319).

Four dead providers removed, a third anti-captcha vendor added

hosszupuska (#324), podnapisi, subscenter and xsubs (#326) no longer exist as services and no longer exist in Bazarr+. On the other side of the ledger, CaptchaAI joins Anti-Captcha and DeathByCaptcha as a supported anti-captcha vendor (#368), and embedded subtitle tracks that report bare alpha3 string language values are handled instead of dropped (#367).

Connection tests take the key from the header only

The Sonarr and Radarr connection-test endpoints accepted Bazarr's API key from the query string, where it leaks into logs and proxies; they now take it from the header only (#335), and the connection-tester test harness authenticates the way the frontend actually does (#322).


Combined subtitles: custom language tags

Combined dual-language output learned to resolve custom language tags like zh-TW as sources (#311, contributed by tylergibson), with a follow-up resolving them as an isolated-tag fallback so zh-TW matches when only zh variants are indexed (#327). Combined subtitles also render like every other pill in the subtitle row instead of a special case (#341).


Translate from an embedded track

If a release only carries its English subtitles inside the video container, mass translate can now extract and translate them (#357). Enable "Treat Embedded Subtitles as Downloaded" so embedded tracks are indexed, then pick the source language as usual: when no external source file exists, the matching embedded track is extracted and used automatically. Each variant is handled separately, so a normal and a hearing-impaired track produce their own outputs.

The extraction path agrees with the indexer about which tracks exist (#362), so the track you see indexed is the track that gets extracted. This one came from a Discord user with anime libraries where the only English subtitles are embedded, who wanted Dutch across a whole series; that is now a two-click operation.


Multi-instance refinements

The multi-instance foundation from Murmuration gets a correctness pass of its own:

  • Per-instance default language profile (#340). Each Sonarr or Radarr instance can name the language profile its new media receives, which is what Sidar [FMHY] asked for on Discord: anime instances get the anime profile, everything else gets the standard one. Setting it changes nothing for existing media and nothing for single-instance installs; it does not retroactively reassign a library.
  • Manual upload resolves episodes by their local id (#316) and decides episode ownership by the owning series, not list membership (#318). Before this, uploading to a series whose upstream and local ids had drifted could offer the wrong series' episodes and silently write a subtitle into the wrong show. Files already misfiled are not repaired: the symptom to look for is a subtitle present for an episode you never uploaded one for. Reported by minhkhoi489-spec and two more users on the thread (#312), with chipvlz describing the same episode-selection failure on Discord.
  • Mass download maps series episode paths with the series path mapping; it used the movie mapping (#333).
  • The ffprobe metadata cache is keyed by the owning instance (#358), and media cache invalidations key on local ids by prefix instead of the upstream id (#320, #344), so same-titled media on different instances stop cross-contaminating each other's metadata and UI state.

Lighter and steadier

A Discord user asking, entirely reasonably, whether 710 MB of RAM was normal (thanks, ★ [GI]) triggered a proper investigation, and the answer became three fixes:

  • The web server thread pool is right-sized and configurable (#388). Bazarr+ ran 100 waitress threads and held memory to match; the default is now 32, configurable as web_server_threads (valid 4 to 100). On a real four-instance library this took the process from 123 threads to 49. Note that each open browser tab parks one worker on a Socket.IO long-poll, so very tab-heavy setups can raise the value back.
  • glibc malloc arenas are capped in the image (#365), and three code paths that held more memory than they were using were reworked (#366): together with the thread-pool change, resident memory on the reference box dropped from roughly 1.2 GiB at its worst to a few hundred MB under normal load.
  • The Jobs Manager respects its concurrency limit (#350). A check-then-act race let it stably run one job more than configured; the slot is now reserved before the worker spawns. Reported by pkoretic with screenshots that made the race obvious (#331).
  • A frontend hook leaked its throttle timer on unmount (#323), fixed alongside the rest.

Other Improvements & Fixes

  • The Swagger UI serves its assets from the installed flask_restx again instead of 404-ing (#383).
  • The feature request issue template works again, so people can actually file one (#354).
  • The public site was rebuilt newcomer-first (#363) with the animated Clockwork hero, and the README was brought in line with what v2.6.0 actually ships (#386), then corrected again where its sync claims overreached and the Jellyfin paragraph lagged upstream reality (#391).
  • The release process itself is now tooled (#389): a three-file version stamp (#364), a notes scaffold whose Included Pull Requests list is computed from the git log rather than typed, a pandoc render check that has already caught broken release pages in the past, and a post-cut verifier that checks the image tags, the site, and the release page. The notes archive was backfilled for v2.5.0 through v2.5.2 (#387), and the animated hero pipeline that produced the image at the top of this page lives in the repo (#345).
  • Pre-release triage fixed its own findings rather than filing them: the two low-severity findings (#361), the Codex findings left unaddressed across the cycle (#369), and the release-blocking repo debris (#359).

Tests that actually run

CI now runs every backend test file or excludes it by name with a stated reason (#317): 66 test files that silently never ran are running. The exclusion list drops entries for files that no longer exist (#329), the SQLite maintenance-pragma tests assert the pragmas took effect rather than the SQL issued (#321), the pragma listener registration is proven rather than assumed (#343), and the provider pool suite was repaired and put in CI (#348).

Provider catalog, on its own cadence

The Provider Hub catalog shipped alongside this cycle: LegendasDivx joins as a new provider (a cloudscraper port with credentials support), Gestdown builds scene-style release names from the API episode object and leaves season packs' own release names alone, and OpenSubtitles.org resolves Serbian instead of guessing English. Catalog updates reach you through the Provider Hub directly, no Bazarr+ upgrade required.


CI / Docker

  • The image ships unar for RAR extraction and mediainfo alongside ffprobe.
  • glibc malloc arenas are capped via MALLOC_ARENA_MAX in the image (#365).
  • The backend test workflow runs the previously split test trees together again, with the RAR configuration pinned (#346), and the backend test collection is exhaustive-or-excluded-by-name (#317).
  • actions/setup-python moved to v7 (#370).
  • Docker publishing is unchanged: versioned tags plus latest on release, sha-* pointers for every build.

Dependency Updates

The dependency backlog was cleared deliberately rather than drip-fed (#339), and the frontend's open dependency advisories were closed (#353). Notable bumps across the cycle:

  • alembic 1.19.1 (#374), dynaconf 3.3.5 (#378), chardet 7.6.0 (#375), rarfile 4.5 (#379), knowit 0.6.1 (#372), certifi 2026.7.22 (#380), setuptools 84 (#381), aiohttp 3.14.3 (#382), plexapi 4.18.2 (#373), numpy pinned within 2.5.x (#376).
  • The frontend Mantine group moved together in one coordinated bump (#371).

Database Migrations

No schema changes. The upstream-database adoption step stamps an adopted database onto the fork's migration chain and restores one column upstream dropped; it does not alter your data, and existing Bazarr+ databases are untouched.


Included Pull Requests

  • #311: fix(combine): resolve custom-lang tags like zh-TW as sources.
  • #313: Fix/legendasdivx brotli login.
  • #314: fix(gestdown): format release_info with show name and episode tag.
  • #316: fix(upload): resolve series upload episodes by local id.
  • #317: ci: run every backend test file, or exclude it on purpose.
  • #318: fix(upload): decide episode ownership by owning series, not list membership.
  • #319: chore: point the provider catalog submodule at main.
  • #320: fix(cache): stop keying series invalidations on the upstream id.
  • #321: test(sqlite): assert the maintenance pragmas took effect, not the SQL issued.
  • #322: test(connection-tester): authenticate the way the frontend does.
  • #323: fix(hooks): clear the throttle timer on unmount.
  • #324: chore(providers): retire the dead hosszupuska provider.
  • #325: docs(providers): mark the built-in providers legacy and point at the catalog.
  • #326: chore(providers): retire the dead podnapisi, subscenter and xsubs providers.
  • #327: fix(combine): resolve custom-lang tags as an isolated-tag fallback.
  • #328: fix(legendasdivx): stop returning the wrong episode, and bound CLI extraction.
  • #329: ci: drop the exclusion for a test file that no longer exists.
  • #330: fix(gestdown): keep the series title out of release-group matching.
  • #332: fix(providers): terminate the provider a config change replaces.
  • #333: fix(mass download): map a series episode with the series path mapping.
  • #334: fix(sync): shim ffprobe for alass so attachment streams stop killing it.
  • #335: fix(security): take the connection-test API key from the header only.
  • #336: fix(subsync): treat the maximum offset as an acceptance threshold, and report per-engine outcomes.
  • #337: fix(video): carry edition and other through fromguess.
  • #338: fix(archives): match subtitle members case-insensitively and accept the canonical extensions.
  • #339: chore(deps): clear the dependency update backlog.
  • #340: feat(arr instances): per-instance default language profile.
  • #341: fix(ui): render a combined subtitle like every other pill in the row.
  • #342: feat(subtitles): detect and notify release-type subtitle mismatches.
  • #343: test(database): prove the SQLite pragma listener is actually registered.
  • #344: fix(ui): invalidate media caches by prefix, not by the upstream id.
  • #345: feat(release): animated hero pipeline, and the v2.6.0 Clockwork motif.
  • #346: ci: run the two test trees together again, and pin the RAR configuration.
  • #347: fix(provider hub): score hub candidates on their release information.
  • #348: test(core): repair the pool suite and run it in CI.
  • #349: fix(sync): report the numbers autosubsync measured.
  • #350: fix(jobs): reserve the slot before spawning the worker.
  • #351: docs(wiki): document the three sync engines.
  • #352: fix(db): start against a database created by upstream Bazarr.
  • #353: chore(frontend): clear the open dependency advisories.
  • #354: fix(github): let people file a feature request.
  • #355: feat(scoring): add a per-provider score modifier.
  • #356: fix(db): adopt any upstream database, not the one revision we had listed.
  • #357: feat(translate): let mass translate use an embedded subtitle track.
  • #358: fix(metadata): key the ffprobe cache by the owning instance.
  • #359: chore: clear the release-blocking repo debris.
  • #360: fix(indexer): index the row the caller named, not whichever one is first.
  • #361: fix: the two low findings from the pre-release triage.
  • #362: fix(translate): extraction must agree with indexing about the tracks.
  • #363: site: rebuild the public site newcomer-first.
  • #364: release: stamp v2.6.0 Clockwork, and make the stamp reachable.
  • #365: perf: cap glibc's malloc arenas in the image.
  • #366: perf: stop three code paths holding more than they are using.
  • #367: Accept bare alpha3 string language values on embedded tracks.
  • #368: feat(captcha): add CaptchaAI as an anti-captcha vendor.
  • #369: fix: the Codex findings that were left unaddressed across the v2.6.0 cycle.
  • #370: [workflow]: bump actions/setup-python from 6 to 7.
  • #371: [bot]: bump the mantine group in /frontend with 6 updates.
  • #372: [bot]: bump knowit from 0.5.11 to 0.6.1.
  • #373: [bot]: update plexapi requirement from >=4.18.1 to >=4.18.2.
  • #374: [bot]: bump alembic from 1.18.5 to 1.19.1.
  • #375: [bot]: bump chardet from 7.4.3 to 7.6.0.
  • #376: [bot]: update numpy requirement from <2.6.0,>=2.5.0 to >=2.5.2,<2.6.0.
  • #378: [bot]: bump dynaconf from 3.3.2 to 3.3.5.
  • #379: [bot]: bump rarfile from 4.2 to 4.5.
  • #380: [bot]: bump certifi from 2026.6.17 to 2026.7.22.
  • #381: [bot]: update setuptools requirement from >=82.0.1 to >=84.0.0.
  • #382: [bot]: update aiohttp requirement from >=3.14.1 to >=3.14.3.
  • #383: fix(api): serve the swagger UI assets from the installed flask_restx.
  • #384: Subtitle downloads and viewer light-mode contrast.
  • #385: Stop provider-pool adoption resurrecting excluded providers.
  • #386: Bring the README in line with what v2.6.0 actually ships.
  • #387: Backfill the release-notes archive for v2.5.0 through v2.5.2.
  • #388: Right-size the waitress thread pool and make it configurable.
  • #389: Release tooling: version stamp, notes scaffold and render check, post-cut verifier.
  • #391: Qualify the README's sync claims and finish the Jellyfin correction.
  • #392: Add the subtitle downloads slide to the 2.6.0 What's New.
  • #393: Fix the mass translate What's New slide to name the real setting.
  • #394: Correct the embedded-subtitles parser option descriptions.

Upgrade / Migration Notes

  • No breaking changes. Update and restart as usual.
  • Coming from upstream Bazarr: point Bazarr+ at your existing config directory and it adopts the database on first boot. Take a backup first; the adoption is one-way.
  • Edition-tagged movies may score lower than before (up to 30 points), because editions now participate in scoring instead of being dropped. If an upgrade stalls under your minimum score, the old score was inflated; adjust the minimum if needed.
  • Sync failures are more honest. A sync beyond your configured maximum offset now fails and falls through to the next engine instead of writing a bad file. Files ruined by the old behavior are not repaired retroactively.
  • Check your library for misfiled uploads if you used manual upload on series before this release: the symptom is a subtitle present for an episode you never uploaded one for.
  • Web server threads default to 32. Raise web_server_threads (Settings > General, 4 to 100) if you keep many browser tabs open; each tab parks one worker.
  • The per-instance default language profile is opt-in and only affects newly added media.
  • PostgreSQL remains fully supported and first-class.

Docker

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

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


Contributors

This release was steered by its reporters, and they deserve the credit:

  • @illusive888 filed five precise, reproducible reports (#304, #305, #306, #307, #308) that became five fixes in this release: RAR extraction, hub release scoring, archive matching, edition scoring, and the sync acceptance threshold. That is a remarkable contribution to one cycle.
  • @luminoso contributed two fixes directly: the RAR extraction restoration (#313) and the Gestdown release-info formatting (#314).
  • @tylergibson contributed the combined-subtitles custom language tag fix (#311).
  • @szab100 pinned the upstream migration crash (#302) that became the headline feature.
  • @minhkhoi489-spec and the other reporters on #312 surfaced the wrong-show upload bug.
  • @pkoretic caught the Jobs Manager running past its limit, with screenshots that made the race obvious (#331).
  • @Aracnoss asked for provider weighting (#301) and got the per-provider score modifier.
  • genuphobia (Discord) reported the sync engines' dishonest reporting with full tracebacks, which became the acceptance threshold, the autosubsync fix, the alass shim, and the new wiki page.
  • WIZARD (Discord) wrote the detailed release-type feature request that became mismatch detection and notification.
  • SirCaralot and Alex (Discord) reported the upstream migration wall from their own setups, and SirCaralot caught the fresh-PostgreSQL startup failure on top.
  • chipvlz (Discord) described the manual-upload episode-selection failure that pointed at the local-id bug.
  • ★ [GI] (Discord) asked whether 710 MB of RAM was normal, which it was not, and the memory work answered.
  • Sidar [FMHY] (Discord) asked for per-instance anime profiles, which became the per-instance default language profile.
  • A Discord user with anime libraries full of embedded-only subtitles motivated translate-from-embedded.

@LavX : the adoption path, the download endpoints, the sync honesty work, the pool and scoring fixes, the multi-instance refinements, the memory and threading work, the rebuilt site, and the release tooling this page was produced with.

And thanks to everyone testing the release candidates and reporting from Discord.


Full Changelog: v2.5.2...v2.6.0