github LavX/bazarr v2.7.1
Bazarr+ v2.7.1 (Atlas)

2 hours ago

Bazarr+ v2.7.1 (Atlas)

Codename: Atlas (patch on the v2.7 line).

A stability, security and integration-hardening patch, closing the gaps found after v2.7.0. API keys move out of request URLs, post-processing commands stop running through a shell, and Provider Hub trust follows the catalog source an install came from. Around that, syncs, jobs, uploads and PostgreSQL installs get the corrections they were missing, and two small schema changes run on the first start after upgrading.


Headline: A hardening pass over keys, commands, trust and uploads

The Gemini key travels in a header

The AI Translator now sends the Gemini API key in a request header instead of the URL query string (#644). The key no longer sits in URLs, where logs, proxies and other intermediate systems can keep a copy. Log redaction widens to more key shapes, and the translation request gains a bounded timeout, ten seconds to connect and ten minutes to read, so a hung call fails the job after its retries instead of being held forever.

If you use the AI translator, rotate your Gemini and OpenRouter keys after upgrading. Keys that traveled in URLs before this patch may already sit in a log or an intermediate system you cannot reach.

The Subtitle Editor sends its key in a header too

The editor's four data fetches, file info, peaks, subtitles and the sync poll, now send the API key in the X-API-KEY request header instead of the URL (#642). This lands on Chrome and Firefox. Native HLS playback keeps the URL key for now, because that player reads its key from the address, and a second part of this change is planned to move it.

Post-processing commands run as argument lists

Post-processing now runs its commands as an argument list instead of handing a string to a shell (#648). The saved command is validated when the settings are saved, and some unsafe templates are refused: the interpreter and its option are matched as a pair (python -c, node -e, perl -E), a placeholder after inline code is refused because those interpreters keep reading options there, and a versioned interpreter name such as perl5.38.2 is folded onto its family, so a version suffix cannot step around the per-family rules. A command that was already stored and does not pass now raises a health issue instead of waiting to hold a job. A save that sets the command is checked whatever the post-processing toggle says, because an enabled instance can inherit the global command while the global toggle is off.

Provider Hub trust follows the catalog source

Official-source trust is now granted only when the catalog source an install came from resolves to main, beta or an ancestor of them (#641). A provider installed from a community source no longer inherits official trust. Installs are addressed by id and bound to the exact source, provider id and version that was resolved, and updates only come from that bound source. Reserved provider ids are protected against unofficial reuse, and a failed catalog refresh no longer removes the entries that are already installed. If you test a catalog commit through dev_ref, that source is treated as untrusted, and community catalog responses carry no size cap.

Delete and blacklist check ownership first

A script can no longer delete or blacklist a subtitle the item does not own or cannot find (#616). The routes check ownership first and answer 403 or 409 instead of acting on the request. The request path now selects only one of the target item's own indexed subtitles, mapped through the owning instance's path mappings, and the check is repeated under the subtitle write locks right before the file is removed, so a reindex or instance reassignment in between cannot redirect the delete. Blacklisting is recorded by the delete itself, after the file is gone, so a refused or failed delete no longer leaves a blacklist entry behind. Episode job labels are corrected alongside.

Uploads are bounded, and refusals say so

The episode, movie and sports upload routes and the Provider Hub's local package install no longer read a whole file into memory before checking its size (#630). A Provider Hub package above 100 MiB and a subtitle file above 150 MiB are refused with HTTP 413 before anything is spooled, and a 256 MiB request-size backstop bounds the rest. The upload buffer is released once its job has finished, so a failed job no longer pins the bytes in memory, and the form key is read only for urlencoded request bodies, so multipart uploads are no longer parsed under the wrong reader (#649).


Other Improvements & Fixes

Providers and search

  • A provider's own failure is kept when the throttle callback raises: the status, the backoff and the provider health record come from the provider's cause, a failed download still discards the provider, and a teardown no longer leaves a terminated instance behind (#612).
  • The search no longer stops at the first provider that cannot deliver: that provider is skipped with a recorded reason and the search continues with the rest (#650).
  • A queued search request rechecks its admission when it actually starts, so a provider that recovered while the request waited can serve it, and one that became throttled in the meantime is skipped (#652).
  • Each provider pool failure records its traceback once, instead of a doubled, stringified traceback that buried the real frames (#651).
  • The movie and episode indexers keep embedded subtitle tracks that report no codec name, instead of dropping a real embedded subtitle from the index, and read wider stored sports entries (#613).
  • A malformed arr browse reply answers an empty listing instead of a 500, and the announcements reader tolerates a feed it cannot show (#615).

Media servers and libraries

  • Refresh libraries reports a scope that fails for an unexpected reason, instead of answering 200 with failed: 0 and sending you to check a setting that was fine. A server-wide refusal stops the run at once, and Plex Pass webhook refusals are reported as refusals (#627).
  • A Plex connection test or Autopulse configuration answers 409 sign_in_required when the only thing missing is the Plex sign-in, instead of a generic failure (#656).
  • Deleting a synced Sonarr or Radarr instance now takes an explicit confirmation to remove its library with it, in the same transaction: the series, episodes, movies, history, exclusions, root folders and release type flags synced from that instance (#617). Only database rows go, never files on disk, and nothing changes in the arr itself.
  • The ids of deleted Sonarr, Radarr and Sportarr instances are remembered, and a new instance's id is chosen above them, so a write that was already past its instance lookup cannot name the deleted instance and leave the new one owning the row (#628).

Sports

  • Sports subtitle changes no longer ask Sportarr to rescan its video library. A published subtitle notifies the configured media servers directly, path-scoped, covering downloads, uploads, deletes, syncs and post-processing (#622, #632).
  • Sports subtitle changes are announced after the save releases its locks, so a listener cannot read a library mid-write (#633).
  • A waiting Sportarr sync gets ahead of the scheduled recording index after a restart, instead of sitting behind a library scan for up to half an hour (#624).
  • On the shipped PostgreSQL driver, two overlapping Sportarr exclusions for the same event retry on their serialization error instead of raising (#629).

Jobs and queues

  • The Sonarr sync job fetches a failed series once, logs one warning, finishes the sync and reports (N skipped) in its name, instead of crashing on the empty answer (#647).
  • The scheduled Sportarr library sync now appears in System > Jobs as a queued job, with a completion summary beside its final rename, so it no longer sits in Completed looking unfinished (#657, #662).
  • A job is a translation when it runs the translation code, not when its name happens to contain the letters, so a Discover download for Lost in Translation no longer waits behind real translations. Duplicate job ids are returned atomically (#621).
  • A job whose socket is lost is still reported through the poll path and resolves with its final outcome instead of hanging, and the Manual search modal gains a per-row download spinner (#658).
  • A completed job that was enqueued without progress reporting gets its green ring and summary line like the cards around it, and a late Stop after a committed sync cannot flip a finished sync to Cancelled (#662).

Settings and setup

  • A settings save acts on the submitted values only after the write succeeds: the library scans, syncs, provider resets and missing-subtitle recalculation no longer start from values a refused write then rejects, and a boolean timeout is refused (#618).
  • A save whose configuration reached the disk but whose follow-up failed keeps its answer: the request's rows are written and the endpoint answers 503 with settings_refresh_failed, instead of a plain 500 with half the rows (#631).
  • Every field of the PostgreSQL configuration now resolves in order: the environment first, then POSTGRES_URL, then config.yaml (#646). An empty value in the URL leaves that setting unset rather than overriding it, so a URL that differs in one field no longer drops the others, and the installer now reads URL-only stacks.
  • Connection test failures are classified with constant reasons, such as blocked, timed out or connection refused, instead of echoing the underlying exception (#645).
  • The onboarding connection tests run under an isolated base URL, so they cannot leak into other routes while the wizard is open, and Back is locked while Finish writes (#653).

Container and installer

  • The container listens on the configured port: the supervisor takes --port when given, otherwise the Port under Settings > General, otherwise 6767, instead of always 6767 (#619). A second instance on a shared network namespace no longer fails with "address already in use", and if the port cannot be bound the supervisor stops with one log line naming where the port came from, rather than falling back to 6767. The backend binds a free loopback port, since the supervisor is its only client.
  • The one-line installer never leaves a half-made backup that looks complete: the backup is built in a .partial folder and only renamed once every step succeeded (#626).

Logging

  • Expected noise no longer writes ERROR rows: a library cover that breaks off mid-transfer logs one debug line instead of a traceback, and other expected socket and cover events are quieted. Prune failures are reported instead of passing silently, the unbounded ffsubsync.log is capped, and the System > Logs pager tightens its edge cases (#625).

Interface and docs

  • A movie's history shows the score of whatever last wrote the file on disk, read newest first, so a download followed by a sync keeps its score and provider. The toolbar count keeps its width while a whole-library read is on its way (#654).
  • The integration guides qualify the download-link lifetime: the in-memory link store drops entries after its TTL, and restarts flush the store (#655).
  • The README, the site and the install path lead with standalone search, with a reusable social card and clearer install, support and pull request guidance (#609).

Tests

  • The ownership revision race tests get room under load, with waits that only bound a hang (#620).
  • The end-to-end run covers every Discover feed without TMDB, each with its own terminal-state wait, and reads the What's New version from the app itself (#659).

Provider catalog updates

Provider bundles have a separate release channel from the application, and catalog fixes ship through the provider catalog, not the app: a stable install receives them when the catalog's main moves, through a normal catalog update in the Subtitle Hub. The promotion of the beta catalog to main is part of this release's delivery through the catalog. The promotion includes the prijevodionline 0.3.0 rewrite, whose sign-in, purchase and balance paths are unit-tested only; live verification of those paths has not been run. When you opt in, its downloads spend the prijevodionline account's tokens.


CI / Docker

The PostgreSQL lanes and the local mirror now install psycopg2 from the image's own requirements, instead of psycopg 3 installed by hand, so a defect only the shipped driver shows cannot pass every run; one such defect shipped in v2.7.0 and is fixed here (#629). The end-to-end Discover suite covers every feed without TMDB and pins the What's New version to the app's own value (#659). No Docker base-image change is included; the runtime image change in this release is the port behavior described above (#619).


Dependency Updates

  • SQLAlchemy moves from the 2.0 line to 2.1.1 (#634). This is a major-line bump, with the paired runtime pin changes.
  • apprise moves from the 1.x line to 2.0.0 (#636), also a major bump, with the paired pin changes.
  • The Mantine group in the frontend takes six updates (#635).
  • The plexapi and PyJWT version floors move up, with their runtime pins (#661).

Database Migrations

Schema changes run on boot, on SQLite and PostgreSQL alike, chained onto the v2.7.0 head:

  • Library link indexes (f8c3d1a7b926): history and blacklist rows point at the series, episode or movie they are about, and an upgrade's history row points at the one it replaced. None of those columns had an index, so deleting a row read the whole table to find what pointed at it. The indexes arrive with the synced-instance library delete, which made those deletes matter (#617).
  • Retired arr instance ids (a7d3e9c1f428, the current Alembic head): the ids of deleted Sonarr, Radarr and Sportarr instances are kept, and a new instance's id is chosen above them, so a write that was already past its instance lookup when the delete committed cannot name the deleted instance and leave the new one owning the row (#628).

Both run on the first start after upgrading. Back up your configuration and database first.


Included Pull Requests

  • #609: Present the project around standalone search, with a reusable social card and clearer install and support paths.
  • #612: Keep the provider's own failure when the throttle callback raises.
  • #613: Keep embedded tracks that report no codec, and read wider stored sports entries.
  • #614: End the retry loops of stopped SignalR clients, and recover from refused websockets.
  • #615: Answer an empty listing instead of a 500 when an arr browse reply or the announcements feed is malformed.
  • #616: Answer 403 and 409 when a script deletes or blacklists a subtitle it does not own, and label episode jobs correctly.
  • #617: Delete a synced Sonarr or Radarr instance together with its library.
  • #618: Act on a settings save only once it is written, and refuse boolean timeouts.
  • #619: Listen on the configured port instead of a fixed 6767.
  • #620: Give the ownership revision race tests room under load.
  • #621: Pick the translation lane by function, and return duplicate job ids atomically.
  • #622: Publish sports subtitle changes to media servers, without a Sportarr video-library rescan.
  • #624: Let a waiting Sportarr sync get ahead of the scheduled recording index.
  • #625: Quiet the expected socket and cover noise, report prune failures, and tighten the Logs pager.
  • #626: Never leave a half-made installer backup that looks complete.
  • #627: Report library refresh faults and Plex Pass webhook refusals.
  • #628: Keep deleted instance ids and owners out of later writes.
  • #629: Test on psycopg2 as the image ships it, and retry Sportarr exclusions on their error code.
  • #630: Bound subtitle and Provider Hub package uploads.
  • #631: Keep a saved settings answer when an event after the save fails.
  • #632: Re-land the sports publication fix, with its tests brought in line.
  • #633: Announce sports subtitle changes after the save releases its locks.
  • #634: Bump SQLAlchemy 2.0.54 to 2.1.1.
  • #635: Bump the mantine group in the frontend with 6 updates.
  • #636: Bump apprise 1.13.1 to 2.0.0.
  • #641: Bind Provider Hub trust and installs to the resolved catalog source.
  • #642: Send the Subtitle Editor's API key in a header instead of the URL.
  • #644: Send the Gemini key in a header, and bound the request.
  • #645: Classify connection test failures with constant reasons.
  • #646: Resolve POSTGRES_URL per field across the app, the backups and the installer.
  • #647: Fetch a failed series once, and finish the Sonarr sync with a skip count.
  • #648: Run post-processing commands as argument lists, with save-time validation.
  • #649: Release the upload buffer after its job, and read the form key only for urlencoded bodies.
  • #650: Continue the search when a provider cannot deliver.
  • #651: Record each provider pool failure traceback once.
  • #652: Recheck admission when a queued request starts.
  • #653: Isolate the wizard's connection tests under a base URL, and lock Back while Finish writes.
  • #654: Show the newest relevant history score, and keep the count's width.
  • #655: Qualify the download-link lifetime in the integration guides.
  • #656: Answer 409 sign_in_required when only the Plex sign-in is missing.
  • #657: Run the scheduled Sportarr library sync as a queued job.
  • #658: Report lost-socket job outcomes, and add the Manual search download spinner.
  • #659: Cover every Discover feed without TMDB in the e2e run, and read the What's New version from the app.
  • #661: Raise the plexapi and PyJWT floors and their runtime pins.
  • #662: Give completed jobs without progress their ring and summary.

Upgrade / Migration Notes

  • Back up your configuration and database before upgrading. The two schema changes in Database Migrations run on the first start after the upgrade.
  • If you use the AI translator, rotate your Gemini and OpenRouter keys after upgrading. Before this patch, keys could travel in request URLs, where logs, proxies and other intermediate systems can keep a copy.
  • Every field of the PostgreSQL configuration now resolves in order: the environment first, then POSTGRES_URL, then config.yaml. If you relied on config.yaml overriding a field of the URL, check the effective values after upgrading.
  • The container now listens on the configured port instead of always 6767. If your configuration names a different port and you publish 6767, check your port mapping after upgrading.
  • Deleting a synced Sonarr or Radarr instance now removes its library rows together with it, when you confirm removing the library. Only database rows go, never files on disk.
  • If you script against the API: an upload above the limit answers 413 (100 MiB for a Provider Hub package, 150 MiB for a subtitle file), delete and blacklist answer 403 or 409 for subtitles you do not own or cannot find, and the system log's paging has one more field than the v2.7.0 notes describe, below.
  • If you test a catalog commit through dev_ref, that source is now untrusted: official trust follows only main, beta or an ancestor of them.
  • Native HLS playback in the Subtitle Editor still sends its API key in the URL; a second part of the header change is planned.

Errata for v2.7.0

Three sentences in the v2.7.0 notes overstated or omitted details. The corrected facts:

  • The search deadline migration moves any saved 20 to 40 once, whether the value was the old default or one you chose yourself. The v2.7.0 notes said a value you chose yourself was kept; that is wrong. Setting 20 again afterwards stays, as the notes said.
  • GET /api/system/logs paging has one more field than the v2.7.0 notes describe: later pages must pass the first page's total as baseline_total, so pages do not shift as new entries arrive.
  • Log pruning goes by file count, not bytes. An older daily file larger than the size cap can keep the folder above 256 MB until it ages out, and the folder settles at about 256 MB once older files roll out; the v2.7.0 notes' "at most by default" overstated it.

Docker

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

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


Contributors

Thanks to the reporters who made this release concrete:


Full Changelog: v2.7.0...v2.7.1

Don't miss a new bazarr release

NewReleases is sending notifications on new releases.