Skip to content

feat(providers): add Bazarr integration - #58

Merged
itz4blitz merged 7 commits into
itz4blitz:masterfrom
vbrzezina:feat/bazarr-integration
Oct 16, 2026
Merged

itz4blitz merged 7 commits into
itz4blitz:masterfrom
vbrzezina:feat/bazarr-integration

Conversation

@vbrzezina

@vbrzezina vbrzezina commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a Bazarr integration (@logarr/provider-bazarr). Bazarr's API isn't an *arr v3 API (/api/..., X-API-KEY, { data } envelope, pipe-delimited log format), so this is a standalone provider that depends only on @logarr/core.

What it ingests

  • Activity: episode and movie subtitle history, /api/system/health issues, and failed jobs from /api/system/jobs.
    • Action labels (download, upgrade, manual, upload, sync, delete, translated) come from the history_log call sites in Bazarr's source.
    • Bazarr's history API returns no row id, so activity ids are stable content hashes. Re-polling doesn't create duplicates.
    • History is fetched in pages of 50, at most 3 pages per poll, and filtered against the last sync time.
  • Log files: tails bazarr.log.
    • The parser handles the ts|LEVEL|logger|message| format, keeps | characters inside messages, and unescapes inline 'Traceback…' reprs into exception and stackTrace.

Wiring

  • Backend: provider registration in IngestionService and ServersService.
  • Env seeding: BAZARR_URL, BAZARR_API_KEY, BAZARR_LOGS_PATH, mounted at /bazarr-logs.
  • Proxy path /api, both Dockerfiles, compose files, turbo.json env, .env.example and .env.tpl.
  • Frontend:
    • The bazarr integration flips from coming_soon to available. Its capability flags match what is implemented: activity and real-time logs (file tailing, the same as Sonarr) yes; webhooks and metrics no.
    • Adds the provider icon and an "open in app" link from the logs page.
  • README: supported servers table, env vars, container paths and log locations.

Notes and limitations

  • Error handling: each endpoint fails on its own with a [bazarr] Failed to get … log line, and the poll fails only when every endpoint fails. Health activities use the health_warning type, matching the *arr base.
  • Timestamps: Bazarr writes naive local times in both history and logs. The README asks users to run Logarr in the same timezone (TZ). Because of this, the history window is widened by 24h, and duplicates are removed by the stable ids.
  • Health issues: each distinct issue is recorded once.
  • Out of scope: issue_source has no bazarr value, so issues fall back to system, the same as Plex, Emby and Whisparr today.

Testing

  • @logarr/provider-bazarr has 136 unit tests covering:
    • the parser: inline and multi-line exceptions, pipes inside messages, CRLF;
    • the client: headers, envelope handling, retries, and the API key never appearing in error messages;
    • the provider: id stability, since filtering, paging caps, and partial failure of individual endpoints;
    • the timestamp utilities.
  • I fed realistic multi-line Bazarr logs through the real LogFileProcessor end to end.
  • pnpm build, pnpm lint, pnpm typecheck and pnpm test all pass. pnpm install --frozen-lockfile is clean, and the lockfile diff only adds the new package.
  • Not yet tested against a live Bazarr instance.

pnpm format:check already fails on master (pre-existing unformatted files, including one line in server-seed.service.ts). All files added or changed here are prettier-clean.

@vbrzezina

Copy link
Copy Markdown
Contributor Author

Part of a set of integration PRs. Overview, merge order and design notes: #64

@vbrzezina
vbrzezina marked this pull request as ready for review October 12, 2026 16:43

@itz4blitz itz4blitz left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes. Registration, the client, and the log parser look solid. History sync drops events and will not retry them.

  1. collectHistory (packages/provider-bazarr/src/bazarr.provider.ts) stops after 3 pages of 50 once since is set, and the first poll (no since) reads only one page. pollServer then sets lastActivitySync to the max timestamp of the returned activities (ingestion.service.ts). That max is the newest row, so the next poll starts at the head again and the skipped tail is never stored. Health rows make this worse: normalizeHealthIssue stamps new Date(), and one inserted history row beside a standing health issue moves the cursor to now. Page until the oldest parsed row is older than the cutoff, or when the cap hits, leave lastActivitySync at the oldest fetched history time. Keep health timestamps out of that cursor.

  2. parseDateString (packages/provider-bazarr/src/bazarr.utils.ts) treats slash dates as month-first unless the first number is greater than 12. Bazarr's parsed_timestamp is Python %x %X. Day-first locales (en_GB, fr_FR) swap the day and month for days 1–12, the cutoff drops those rows, and paging can stop early. Dotted dates are already day-first. Add a day-first fixture (05/10/26 as 5 October) and do not advance the cutoff from a timestamp that failed to parse.

Also, when you rebase: this PR conflicts with the other integration branches on the shared registration files. #57 and #60 are landing first.

Follow-ups, not blocking: Bazarr warn/error issues are stored as system because issue_source has no bazarr value (same gap as Whisparr). The new op:// refs in .env.tpl will fail op inject until those vault items exist.

Add @logarr/provider-bazarr, a standalone provider for Bazarr's /api.

- X-API-KEY client with `data` envelope unwrapping; connection test via
  /api/system/status
- Activity from episode and movie subtitle history (action labels from
  Bazarr's history_log codes), system health and failed jobs, with stable
  content-hash ids (the history API exposes no row id), since-filtering and
  bounded paging
- File tailing of bazarr.log with a parser for Bazarr's pipe-delimited
  format, preserving `|` inside messages and unescaping inline tracebacks
- Backend registration, env seeding (BAZARR_URL/BAZARR_API_KEY,
  /bazarr-logs), proxy path, Dockerfiles, compose, turbo env
- Frontend: Bazarr integration flipped to available with truthful capability
  flags, provider icon and logs deep link
- README updates, including a note about Bazarr's naive local timestamps
…ates

History sync now pages until the oldest parseable row is older than the cutoff
(40 page safety cap; 7 day window on the first poll). When the cap is hit, only
rows at or before the oldest fetched history row may advance the sync cursor.
Health, failed-job and unparsed rows never advance it (advancesSyncCursor: false).

Slash dates are read per response: a number above 12 proves day-first or
month-first, the order is remembered per server, and rows that stay ambiguous
are treated as unparsed instead of guessed.
…d polls progress

Locate the first row older than the cutoff by probing (exponential then binary
search on start), then read pages oldest to newest with an overlap that detects
rows inserted at the head mid-read. If the page cap is hit the rows are the
oldest ones above the cutoff and the cursor advances only to the newest row read
with certainty, so each poll gains ground until the backlog is caught up.

History ids hash the raw timestamp text (independent of the inferred date
order), the date order is inferred over every row of the poll before
normalizing, a failing history source keeps the other source's rows off the
cursor, rows never move the cursor backwards past since, and year-first
dates (2026/10/05) are read as Y/M/D.
@vbrzezina
vbrzezina force-pushed the feat/bazarr-integration branch from 1503280 to 4f6ab79 Compare October 13, 2026 16:54
@vbrzezina

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I've rebased onto current master and pushed the changes.

  • .env.tpl: the Bazarr entries are now commented out until the 1Password items exist.
  • Conventions: the provider is aligned with the shared provider conventions, and the barrel test is restored.
  • Synthetic timestamps: health rows no longer advance the sync cursor (builds on fix(ingestion): keep synthetic activity timestamps out of the sync cursor #67).
  • History paging: history is now paged back to the cursor, and day-first dates (%x) are read correctly. A capped poll reads from the cursor forward, so it always makes progress. Tests cover this.

Known limitation: a server with more than 50 subtitle events per day costs extra requests per poll. If one of the two history endpoints keeps failing, the cursor stays put. Happy to tighten either in a follow-up.

@itz4blitz
itz4blitz merged commit a14e66a into itz4blitz:master Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants