Commit graph

36 commits

Author SHA1 Message Date
Breadway
73df2a4c3c docs: bakery/homelab distribution, single-trunk CONTRIBUTING, drop leftover tarball
Some checks failed
check / check (push) Failing after 34s
State that breadarr is bakery-distributed (not in the BOS ISO, not a GTK
desktop-shell app, no bread events), add CONTRIBUTING.md for the
single-trunk + RC-tag model, ignore leftover src.tar.xz artifacts, and
delete breadarrd/src/src.tar.xz.
2026-08-15 21:35:31 +08:00
Breadway
3e566a193d pushing for dev release
All checks were successful
dev release / build (push) Successful in 39s
2026-08-12 09:17:20 +08:00
Breadway
1084be86cd Fix nyaa grabs being silently dropped as false magnet rejections
Some checks failed
dev release / build (push) Failing after 38s
add_torrent_response_is_rejected treated success_count == 0 alone as
an outright rejection. qBittorrent returns exactly that (with
pending_count: 1) for every URL-based add while it fetches the
torrent asynchronously — the shape nyaa's RSS feed always uses, since
it hands over a .torrent download URL, never a magnet. Every nyaa
grab was therefore marked MagnetRejected and silently dropped (no
release row, no log line) while the torrent downloaded successfully
in the background. With no release row, best_existing_score always
saw None, so the same episode got re-grabbed from every new feed
entry — the repeated duplicate downloads sitting in
/mnt/media/downloads/ (Mushoku Tensei, Tenki no Ko, Code Geass) traced
back to this. Root-caused by an Opus 5 investigation.

Also stop treating a genuine rejection as silent: process_item now
records a failed release row and logs a warning, matching the
existing HashCaptureFailed pattern, instead of just marking the item
seen and moving on with no trace.
2026-08-12 09:07:40 +08:00
Breadway
b49a8597e2 Delete leftover download folders once their video has been imported
move_or_copy_file only ever relocates the single video file it locates
inside a torrent's content_path — a multi-file release's own folder
(sample clips, .nfo/.srt/.jpg sidecars) was left behind with nothing
pointing at it anymore once the torrent got removed from qBittorrent.
Verified against production: 18 of ~30 entries in the downloads
directory were exactly this — husk folders with the video long since
moved into the library. run_import_cycle now removes content_path
itself (guarded against ever equalling the downloads root) alongside
the existing delete_torrent call.
2026-08-12 09:07:40 +08:00
fbf8d58587 Merge pull request 'feature/bakery-integration' (#1) from feature/bakery-integration into main
Some checks failed
dev release / build (push) Failing after 2m8s
Reviewed-on: #1
2026-08-06 10:53:15 +08:00
Breadway
5543485976 Fix all cargo clippy warnings across the workspace
Some checks failed
check / check (push) Failing after 53s
Removes genuinely dead code (unused import, no-op cast, an unused
PendingGrab accessor, and TmdbClient's TV-search methods now that TVDB
fully covers that path), tightens len()>0 checks to is_empty(), swaps
two fixed-size test vec!s for arrays, restructures a match to avoid an
unnecessary unwrap_err, fixes doc-comment list indentation, and hoists
a locked-connection call out of a match scrutinee. Struct fields/enum
variants that are still meaningful but not read by current callers
(TorrentInfo::save_path, GrabCycleStats::items_seen, X1337 fallback
route, BacklogCandidate::path) get #[allow(dead_code)] rather than
deletion, same for the two 8-argument functions (too_many_arguments).
2026-08-06 08:51:07 +08:00
Breadway
471ca884a6 gitignore: stop tracking graphify-out/ tool cache
graphify's local knowledge-graph output was never gitignored, so its
generated cache/graph files (75 tracked entries) were showing up as
dirty noise on every run. Add graphify-out/ to .gitignore and untrack
the existing files.
2026-08-06 08:46:00 +08:00
Breadway
6f1fe776ad CI: adopt the shared pinned-Arch-container build system
Some checks failed
check / check (push) Failing after 35s
Adds ci/build.sh + ci/bread-ecosystem.rev (pinned to bread-ecosystem
147cfbbf96ae4b171027defa1130d2caddb934b1), points the three release
workflows' build step through it, and adds check.yml as breadarr's first
lint/test gate on feature/fix branches. No ci/deps.txt: verified ort-sys's
build.rs has no system build-time requirements beyond rust/base-devel
(its download-binaries feature is pure-Rust ureq/lzma-rust2/hmac-sha256).
2026-08-05 19:15:47 +08:00
Breadway
3cbac5ffe9 CI: onboard breadarr onto the bakery distribution system
Adds bakery.toml (system_deps verified via ldd + Command::new grep against
breadarrd/src, not guessed) and dev-release.yml/rc-release.yml/release.yml
following bread-ecosystem's current single-trunk + RC-tag CI model. No
GitHub Release step anywhere — this repo has no GitHub mirror.
2026-08-05 19:04:22 +08:00
Breadway
7e1b7450cf Fix TUI Add flow landing new movies/shows flat in the library root
add_selected_search_result passed the raw configured default_root_folder
straight through as root_folder for both movies and series, with no
per-item subfolder computed. import_one/season_dir both expect
root_folder to already be the item's own folder, so new grabs landed
directly in the shared library root instead of their own folder,
invisible to Jellyfin's per-category libraries. Split default_root_folder
(series) from a new movies_root_folder, and have the TUI build the
"{Title} (Year)" subfolder itself before sending the add request.
2026-08-05 19:02:27 +08:00
Breadway
15b5b12a06 can't be bothered writing a commit message 2026-08-03 09:35:47 +08:00
Breadway
15cb0b6d81 Gitignore local repo-hygiene CLAUDE.md notes 2026-08-03 08:44:13 +08:00
Breadway
d110556a4d Add oversized-AV1 remediation and orphaned-file relink commands
- transcode_job gains is_anime/force_reencode so a job can be
  re-encoded even though it's already AV1 - needed for the rate-control
  bug that left ~476 files larger than their originals; the normal
  backfill query skips already-AV1 files, so this is find_oversized_
  av1_candidates plus a dedicated retranscode-oversized CLI command
- relink-orphaned-files: read-only reconciliation for episode_file rows
  that lost their association (DB-recovery incident) despite the real
  file still sitting where the importer would have put it
- qbit: delete finished torrents from qBittorrent after import instead
  of relocating them with set_location, since nothing is left seeding
  from the old save path after the move
2026-08-03 08:44:07 +08:00
Breadway
109b29ee55 Continue transcode/import feature work and fix bugs found by audit
These four files mix two things too interleaved to commit separately:
in-progress work on anime pipeline tuning (quality/preset/thread
config, per-pipeline parallelism caps), sampled decode verification,
and subtitle-codec-aware remuxing that predates this commit, plus a
set of correctness fixes from an independent Opus 5 review applied
directly on top of it:

- Attached-pic (cover art) streams could be probed as the real video
  stream when they came first, silently replacing the actual
  codec/height (ffprobe.rs)
- probe_failed rows (null codec/height) were enqueued for transcode
  and could never be claimed again, due to the unique partial index -
  should_enqueue now rejects them
- tokio::try_join! cancelled the sibling pipeline's in-flight
  spawn_blocking encode on the first Err instead of letting it finish
- Duplicate episode_file rows for the same media only had one swept on
  an upgrade swap, leaving stale rows/files behind
- File-swap and DB-write on transcode completion weren't atomic;
  wrapped in a transaction and made probe-refresh failure non-fatal
- In-progress transcode temp files were visible to library scans
  (video extension, no dotfile prefix) and could get imported mid-encode
- Remux temp files leaked on error paths; a Jellyfin refresh failure
  failed the whole import cycle instead of just logging
- insufficient_space could false-positive when the download and
  library dirs are on the same filesystem (rename is free there)
- Config validation for reference_height and min_size_reduction_pct,
  which previously could silently divide-by-zero or overflow deep
  inside an encode
2026-08-03 08:43:50 +08:00
Breadway
0f609aa4cc Fix bugs found by an Opus 5 audit: races, parsing, and CDATA gaps
- Atomic claim on review-queue approval, closing a double-approve race
  that could grab the same release twice (scheduler.rs)
- Constant-time comparison for the daemon API token, closing a timing
  side channel
- RSS items wrapped in CDATA (common for titles with '&') were
  silently dropped - only Event::Text was ever handled
- Reject malformed apibay info_hash values before building a magnet
  link that extract_btih can't parse back out
- Parse sizes with no space before the unit ("38.1GiB")
- Fix "Season N - NN" episode parsing and stop misreading a
  YYYY-MM-DD date as a bare episode range
- Query embeddings are no longer cached, fixing unbounded cache growth
  over the daemon's lifetime (only library-side candidates need caching)
2026-08-03 08:43:29 +08:00
Breadway
66d323b7f7 Fix real bugs found by an independent Opus review of the transcode feature
Requested and applied a full review of the AV1 transcode implementation.
Findings and fixes:

- Season-pack import bypassed upgrade_locked entirely: a season pack
  scored higher than a locally-transcoded file's stale release score
  would silently overwrite it. Fixed in import_season_pack_file (and
  mirrored into import_one for defense-in-depth) to check
  upgrade_locked before the score comparison, not after.
- The post-import enqueue hook only checked anime, silently skipping
  the HDR/2160p exclusions find_backlog_candidates applies to the
  backfill -- a freshly-grabbed HDR file would have gone through the
  unverified HDR-via-VAAPI path. Centralized all eligibility rules
  (anime, already-av1, HDR, height) into transcode::should_enqueue,
  used by both import_one and the newly-added season-pack enqueue
  hook (season packs previously had no transcode hook at all, despite
  being the actual headline use case -- 100GB+ season packs).
- find_backlog_candidates: a NULL-height row was included as a
  candidate but could never actually be claimed (claim_pending_jobs
  requires non-null height), permanently stuck pending. Fixed the
  WHERE clause.
- encode_and_verify now probes the real input file first and skips
  the encode entirely if it's already AV1 -- closes a narrow crash
  window where a rename-succeeded-but-DB-update-failed job would
  otherwise re-encode an already-transcoded file on retry.
- finalize_job's success path could leak a verified temp file and
  strand a job in 'running' forever if a filesystem operation failed
  partway through; now wrapped so any failure there cleans up and
  marks the job failed like every other error path.
- reset_orphaned_running_jobs now also sweeps each reset job's
  leftover temp file (job-id-scoped paths, so this lookup is
  unambiguous) rather than leaking them on a disk that's usually
  already tight on space.
- Added a unique index preventing two active jobs for the same file
  ever existing at once -- closes the remaining gap in last commit's
  concurrency fix (a daemon restart's reset could otherwise race a
  still-alive transcode-library backfill onto the same file).
- Made the VAAPI rate-control mode explicit (VBR) instead of
  driver-inferred.
- The daemon's transcode ticker awaited each cycle inline in the
  tokio::select! loop, blocking every other cycle (import, search,
  upgrade, reconcile) for the full multi-minute duration of an
  encode. Now spawns each cycle detached with a try-lock guard so
  overlapping ticks skip cleanly rather than stacking (the
  concurrency cap from the previous commit makes this safe).

Two items reviewed and deliberately left as documented, not fixed:
mp4 sources with mov_text subtitles will fail the encode cleanly (no
data loss, just no space saved) since Matroska can't hold that codec
via stream copy -- fixing this needs per-stream codec probing that
wasn't safe to add untested at this hour. Renaming an mp4 source's
extension to .mkv after transcoding is cosmetic (Jellyfin
content-sniffs fine) and left alone.
2026-07-25 00:38:51 +08:00
Breadway
576aad3bfe Fix a real production incident: unbounded concurrent transcode encodes
The daemon's steady-state transcode ticker and a manually-launched
transcode-library backfill process were both independently claiming
pending jobs from the same queue with no shared concurrency
awareness, each capping only at its own parallelism_max. Combined
with large files easily outlasting the 30s poll interval, this
stacked to 20+ simultaneous GPU encode/decode sessions and triggered
the kernel OOM killer on a shared 15GB host running a dozen+ other
containers (confirmed via dmesg; no services were lost, no library
files were touched — the original-file-safety design held under the
crash).

Fixes: claim_pending_jobs now treats its limit as a total concurrency
cap (subtracting already-running jobs, wrapped in a BEGIN IMMEDIATE
transaction so this is correct across concurrent processes touching
the same database, not just within one). Added reset_orphaned_running_jobs,
called on real daemon startup, so a crash never permanently strands
job slots in 'running'. Lowered the default parallelism_max from 4
to 2 given the observed real-world memory pressure.
2026-07-25 00:20:05 +08:00
Breadway
d533301880 Add native GPU-accelerated AV1 transcode capability
Post-grab async background swap, upgrade_locked flag to prevent the
upgrade loop from re-inflating a locally-transcoded file, and a
transcode-library CLI backfill for the existing catalog. Bitrate model
calibrated against Silicon Valley's real HEVC bitrate, scaled by
resolution and AV1's encoding efficiency. HDR/2160p and anime excluded
from this first pass. Jellyfin session polling throttles batch
encoding back to 1 stream during active playback.
2026-07-24 23:57:58 +08:00
Breadway
691fb39cbb Add grab_enabled kill switch for the passive RSS grab loop 2026-07-24 23:20:43 +08:00
Breadway
e179b9bf90 Apply quality gates before queuing for review, not just before auto-grab
The "reject sub-1080p when a better resolution exists in this batch" gate
(and every other quality gate) only ran on the confident auto-match path —
a low-confidence match skipped straight to queue_for_review before ever
reaching it. A human review decision is about whether this is genuinely
the right show, not a backdoor around quality standards; a title match
being ambiguous is no reason to let a worse-quality release through
untested. Verified live: several 720p season-pack releases were sitting in
the review queue for shows that also had 1080p+ alternatives in the same
search batch. Moved the gate check ahead of the review-queue branch so it
applies uniformly.
2026-07-21 21:52:40 +08:00
Breadway
2f0d8300ce Check eligibility before queuing a low-confidence match for review
process_item queued anything landing in the matcher's "needs review"
confidence band immediately, without ever checking whether the show/
season/episode/movie actually needed anything — that check only ran on
the auto-match path. A low-confidence match against an already-complete
show gained nothing from a human's yes/no, it was just noise that
reappeared every cycle the source kept re-listing the same old release
(verified live: fully-complete shows' season-pack re-releases piling up
in the review queue indefinitely). Reordered so the eligibility check
(movie/season-pack/episode) runs first regardless of confidence, and only
a genuinely-needed release ever reaches the queue-for-review decision.
2026-07-21 21:38:00 +08:00
Breadway
d1909f3083 Recognize season-pack titles beyond the literal "(S?N Complete)" shape
SEASON_PACK_RE only matched an explicit parenthesized "(S01 Complete)"
form, so any other real-world season-pack naming convention — bare
"S10.COMPLETE", spelled-out "Season 8 Complete", "[Season 4 Four
Complete]", or no "Complete" marker at all ("Game of Thrones - Season 8
S08 - 2019") — came back with season = None entirely, not just an
unresolved episode. That fed straight into review-queue approvals
returning "could not resolve which episode this release is" for releases
that were genuine, resolvable season packs. Broadened to a bare season
marker, safe here since this is only reached after SXXEXX_RE/SXX_DASH_EP_RE
have already failed to find a real episode number.
2026-07-21 21:12:12 +08:00
Breadway
6e7be67f0b Overhaul breadarr-tui UX: filter/sort, color, help overlay, cross-nav
Library tab gets a cycling filter (all/series/movies/missing/unmonitored)
and sort (title/missing/kind), color-coded kind tags and missing-count
severity, and monitor-toggle without opening detail. Keybinding hints move
out of cramped block titles into a context-aware status bar plus a `?`
help overlay, both driven by one shared keybinding table so they can't
drift apart. Episode status icons and review-queue confidence get the same
severity coloring. Stuck tab gains real selection/focus and can jump
straight to a show's Library detail (needed adding media_item_id to
StalledGrab's query — the one small backend touch in this pass). Adds a
manual refresh-now key.
2026-07-21 20:55:04 +08:00
Breadway
99604a7e55 Prefer season packs over individual episodes in search results
Search results were sorted purely by seeder count, so a season pack only
won out over a single-episode release by accident. Season packs already
clear every missing episode of that season in one grab and go through the
same seeder/quality gate as anything else (an unviable pack still gets
rejected there and iteration falls through to the next candidate) — so
sorting packs first, seeders as the tiebreaker, is a strict improvement
with no new risk. Shared by both the automated search cycle and the
manual-search candidate list.
2026-07-21 20:33:59 +08:00
Breadway
ba05a3cb0c Default TVDB "season 0" specials to unmonitored
Season 0 is TVDB's catch-all for specials/recaps/shorts and often a tie-in
movie already tracked as its own separate media_item (verified live:
Chainsaw Man's season 0 included a movie already present as its own entry).
Monitoring these by default means the library never actually "completes"
and inflates the missing-episode count with content nobody asked to
acquire as an episode. Regular seasons are unaffected.
2026-07-21 20:22:27 +08:00
Breadway
17d82b84c2 Fetch TVDB's English-translated episode names instead of original-language
Plain /episodes/default returns names in the show's original airing
language — for a lot of anime that's Japanese with no English name at all,
which is what was ending up embedded in generated filenames. TVDB carries
real crowd-sourced English translations at /episodes/default/eng (same
numbering/air-date fields, just a better name) — try that first and only
fall back to the original-language endpoint if a show has no English data.
2026-07-21 20:09:24 +08:00
Breadway
dcf9ee241c Don't embed CJK episode titles in generated filenames
TVDB has no English episode title at all for some shows (entire seasons of
otherwise-English-titled shows came back Japanese-only) — using it verbatim
put unreadable-to-most-tooling script into an otherwise Latin-script library.
Falls back to the no-title filename shape instead.
2026-07-21 19:45:30 +08:00
Breadway
60d01657eb Allow a trailing fansub revision tag in SxxExx parsing
"S01E01v2" (a fixed re-release of an episode) glued the version marker
directly onto the episode number with no separator, so the word-boundary
check after the digits never matched and the whole file fell through as
unparsed. Verified live: some shows had zero episode files linked because
every release happened to be a v2.
2026-07-21 19:32:13 +08:00
Breadway
54818f5f05 Fix qBittorrent WebUI API compat and season-folder placement in library scan
qBittorrent's newer WebUI API returns 204 (not 200 "Ok.") on login success,
and a JSON success/failure summary (not plain "Ok."/"Fails." text) from
torrents/add — both broke against the currently deployed version. Also had
scan_tv_root move already-tracked episode files into their Season NN
subfolder instead of just recording wherever they already sat on disk.
2026-07-21 19:17:42 +08:00
Breadway
9d8a59e3a8 Switch to tag-pinned bread-ecosystem deps; bump version to v0.1.0 2026-07-19 03:53:09 +08:00
Breadway
bb4576915e Switch to tag-pinned bread-ecosystem deps; bump version to v1.0 2026-07-19 03:27:37 +08:00
Breadway
2e32488b26 Remove orphaned bakery.toml — nothing serves it
breadarr has no .forgejo/workflows at all (no mirror, release, or
package workflow) and no PKGBUILD, and it isn't listed in
bread-ecosystem's registry/bread-ecosystem.toml, so gen-index.sh would
never pick it up even if a release workflow existed. The bakery.toml
here was pure dead metadata.

Not wiring up a release workflow instead: breadarr isn't in the
registry and its distribution channel (bakery, pacman, both, neither)
hasn't actually been decided, so adding one here would be inventing
a channel commitment I have no signal for. If/when breadarr is ready
to ship, follow docs/release-channels.md in bread-ecosystem to add it
properly (bakery.toml + registry entry + release.yml, or a PKGBUILD +
package.yml, or both).
2026-07-17 14:02:59 +08:00
Breadway
830e80622e Fix literal-tilde fallback bug in breadarr-shared's expand_home
expand_home() fell through to PathBuf::from(input) — the literal,
unexpanded "~/..." string — whenever the HOME env var itself wasn't set,
same bug class as breadclip-core/breadpad-shared/breadmon (found during
this pass's own crate-migration sweep, in a different shape here: the bug
was in this crate's own tilde-expansion helper rather than a
dirs::xxx().unwrap_or_else() chain). Fixed via bread_utils::xdg::home_dir,
which resolves a real home directory before ever needing to fall back.

Builds and tests clean: 205 passed, 1 pre-existing network-dependent test
ignored, 0 failed.
2026-07-17 10:13:06 +08:00
Breadway
8a2936b8fd Migrate embedding pipeline and model download to bread-onnx
OrtEmbedder's tokenize -> tensor build -> mean-pool -> L2-normalize
pipeline was near-byte-identical to breadmill's own OrtEmbedder (same
truncation, same actual_seq.min(mask.len()) padding guard, same 1e-10
epsilon) — now both share bread_onnx::embedding::EmbeddingSession (path
dependency for now, see the TODO in breadarrd/Cargo.toml). This crate
stays CPU-only (Provider::Cpu), matching its existing documented rationale.

ensure_model's reqwest-based download function is replaced with
bread_onnx::download::ensure_file (sync/ureq, matching breadmill's own
downloader and this workspace's bakery convention) dispatched via
spawn_blocking from this async context.

Builds and tests clean across the whole breadarr workspace: 205 passed, 1
pre-existing network-dependent test ignored, 0 failed.
2026-07-17 09:37:55 +08:00
Breadway
d35d9a1703 importer: rename old file aside instead of deleting it before placing the upgrade replacement
Both import_one and import_season_pack_file deleted the existing
episode_file row and unlinked the on-disk file *before* link_or_copy_file
placed the new one. If the link/copy (or the free-space check) then
failed, the original content and its DB row were already gone with
nothing to fall back to.

Now the stale file is renamed to a  sibling (freeing dest for the
hardlink fast path, same as before) and the DB row is left alone. Only
after the replacement is confirmed on disk are the  file and the
old row cleaned up; any failure in between restores the original file
before returning the error.
2026-07-17 06:51:06 +08:00
Breadway
697b009627 can't be bothered writing a commit message 2026-07-16 22:22:53 +08:00