Single-binary Rust replacement for Sonarr + Radarr
Find a file
Alan a00ff54e9d
Some checks are pending
CI / check (push) Waiting to run
CI / deny (push) Waiting to run
CI / plan-file-gate (push) Waiting to run
release-artifacts / appimage (push) Waiting to run
release-artifacts / deb (push) Waiting to run
release-artifacts / docker (push) Waiting to run
release-artifacts / publish (push) Blocked by required conditions
feat(m148): Import lists II — Plex watchlist, exclusion API, WebUI settings, auto-add candidates, CLI exclusion commands
PlexWatchlistProvider fetches user's Plex watchlist via metadata.provider.plex.tv,
parses XML Video/Directory elements with Guid children for TMDB/TVDB/IMDB external
IDs (12 unit tests). Exclusion API: GET/POST/DELETE /api/v3/importlistexclusion with
validation (4 unit tests). WebUI SettingsImportLists.tsx: full CRUD for both lists
and exclusions with per-implementation form fields. ImportListSyncTask auto-add
candidate detection logs items not yet in library. CLI importlist-exclusion list/delete.
Scheduler tests updated for 9-task registry (ImportListSync added).

[gstack-context] milestone-cycle m148
2026-07-06 08:56:19 -04:00
.github/workflows m133: distribution — Docker multi-arch + .deb + AppImage + release pipeline (v0.133.0) (#10) 2026-06-12 04:01:30 +02:00
.sqlx chore(ci): reserve .sqlx/ offline-query dir + set SQLX_OFFLINE=true 2026-04-21 19:13:43 -04:00
crates feat(m148): Import lists II — Plex watchlist, exclusion API, WebUI settings, auto-add candidates, CLI exclusion commands 2026-07-06 08:56:19 -04:00
docs feat(m148): Import lists II — Plex watchlist, exclusion API, WebUI settings, auto-add candidates, CLI exclusion commands 2026-07-06 08:56:19 -04:00
packaging m133: distribution — Docker multi-arch + .deb + AppImage + release pipeline (v0.133.0) (#10) 2026-06-12 04:01:30 +02:00
resources chore(resources): rename desktop entry and update AppRun for avatarr binary 2026-04-11 17:45:20 -04:00
scripts feat(m141): release + delay profiles (v0.141.0) (#17) 2026-07-06 07:15:59 +02:00
tests feat(api): Bazarr/Prowlarr compat + OpenAPI spec + Docker integration (m93) 2026-05-20 01:00:47 -04:00
.cgcignore fix(parser): REGEX_73 separator class, Naruto-Kun website exclusion, season boundary checks 2026-04-30 00:11:58 -04:00
.dockerignore m133: distribution — Docker multi-arch + .deb + AppImage + release pipeline (v0.133.0) (#10) 2026-06-12 04:01:30 +02:00
.gitignore m133: distribution — Docker multi-arch + .deb + AppImage + release pipeline (v0.133.0) (#10) 2026-06-12 04:01:30 +02:00
Cargo.lock feat(m148): Import lists II — Plex watchlist, exclusion API, WebUI settings, auto-add candidates, CLI exclusion commands 2026-07-06 08:56:19 -04:00
Cargo.toml feat(m148): Import lists II — Plex watchlist, exclusion API, WebUI settings, auto-add candidates, CLI exclusion commands 2026-07-06 08:56:19 -04:00
CHANGELOG.md feat(m148): Import lists II — Plex watchlist, exclusion API, WebUI settings, auto-add candidates, CLI exclusion commands 2026-07-06 08:56:19 -04:00
CLAUDE.md m133: distribution — Docker multi-arch + .deb + AppImage + release pipeline (v0.133.0) (#10) 2026-06-12 04:01:30 +02:00
CONTRIBUTING.md m133: distribution — Docker multi-arch + .deb + AppImage + release pipeline (v0.133.0) (#10) 2026-06-12 04:01:30 +02:00
deny.toml m131: library hardening — rustdoc/doc-tests/examples + semver policy + cargo-deny + config riders (v0.131.0) (#8) 2026-06-12 02:09:47 +02:00
docker-compose.yml m133: distribution — Docker multi-arch + .deb + AppImage + release pipeline (v0.133.0) (#10) 2026-06-12 04:01:30 +02:00
Dockerfile m133: distribution — Docker multi-arch + .deb + AppImage + release pipeline (v0.133.0) (#10) 2026-06-12 04:01:30 +02:00
LICENSE chore: prepare v0.3.0 public release 2026-02-19 22:54:19 -05:00
README.md docs: core-loop closure audit — +3 rows (m137/m138/m142), Phase B renumbered to m135-m154 2026-07-01 22:48:45 -04:00
ROADMAP.md feat(m148): Import lists II — Plex watchlist, exclusion API, WebUI settings, auto-add candidates, CLI exclusion commands 2026-07-06 08:56:19 -04:00
rust-toolchain.toml m109: Test discipline restoration (v0.109.0) (#1) 2026-06-10 18:25:11 -04:00
THANKS.md m128: contributor onboarding — CONTRIBUTING, setup + worktree scripts, per-crate READMEs, window icon (v0.128.0) (#5) 2026-06-11 23:17:18 +02:00
TODOS.md feat(m142): metadata refresh + daemon truth (#18) 2026-07-06 09:26:06 +02:00

This is the main branch — active development (v0.55.0+, SQLite-backed).

For the v0.42.0 legacy line (JSONL + TOML persistence), check out the legacy branch. Both branches share crate names, binary names, and the ~/.config/avatarr/ config file — run only one version at a time, and back up ~/.local/share/avatarr/ before cross-version experiments.

🎬 Avatarr

CI

A single-binary Rust media manager. Search torrents, download via qBittorrent, organize into a Plex-compatible library — GUI and CLI in one portable executable.

Working toward a leaner, unified replacement for the Sonarr + Radarr stack: TV and movies in one binary, one database, three UI surfaces (Slint desktop GUI for power users, React WebUI for homelab/Docker, avatarr CLI for headless). Phase A of the rewrite arc (m40 rename → m133 distribution + signed releases) is shipped; the Phase B parity extension arc (m135m154 — authored by the 2026-06-28 m134 parity audit, restructured by the 2026-07-01 core-loop audit) is in progress. See ROADMAP.md for the friendly status mirror, or docs/plans/2026-04-30-avatarr-roadmap-post-m53b-redraft.md for the strategic spec with full reasoning + decision log. The 2026-05-25 post-m108 drift audit (docs/audit-2026-05-25-m108-drift.md) restructured the original m109-m113 polish arc into m109-m130, extended to m109-m133 by the 2026-06-09 librarification rewrite (componentization arc: avatarr-media extraction + boundary ledger + gated crates.io publish checkpoint + library hardening) — see ROADMAP for the current arc layout. Earlier 2026-04-21 + 2026-04-11 roadmaps are preserved for historical context.

Design reference: visual, interaction, and information design for the unified rebuild lives at docs/design/DESIGN.md (v1 handoff). The bundled HTML mockups under docs/design/hifi/ are pixel reference — implementation recreates them in Slint rather than lifting code. Start with docs/design/README.md for onboarding order.

Renamed from plex-pipeline-rs in v0.40.0 (April 2026). The project migrated to fresh repos at https://codeberg.org/alan090/avatarr and https://github.com/alan09086/avatarr. The old plex-pipeline-rs repos have been archived. See CHANGELOG.md for the migration entry.

🧭 Status

Today (v0.70.0):

  • Working torrent search across TPB, YTS, EZTV, and RuTracker with score-based ranking
  • qBittorrent monitor that auto-organizes completed downloads into Plex folder structures
  • Movies + TV (seasons, episodes, ranges, complete-series packs)
  • Explicit Movie/TV mode selector in the Search tab — no more guessing what the parser will infer
  • TV Season Picker dialog when a bare TV title is searched — TMDB resolves the canonical title, the picker offers S01S10 or a Complete Pack shortcut
  • avatarr search --type {movie,tv} is strict; --complete and --season N provide direct-dispatch shortcuts for TV
  • Smart per-episode quality upgrades (size + resolution comparison)
  • IMDB/TMDB metadata resolution with year-aware caching
  • clap-based CLI, Slint desktop GUI (m71-m85 rebuild), and React WebUI (m92+)
  • Cross-platform: Linux, Windows, macOS
  • SQLite catalog foundation (m43): hardened sqlx pool, 49-table unified schema from Sonarr v4 + Radarr v6, automatic migrations at startup, fatal-error exit with actionable messages on pool-open failure
  • Series / Season / Episode domain models (m44): typed Rust structs mirror the schema column-for-column; enums with Unknown(i32) fallthrough round-trip unknown DB values without erroring; SeriesRepository / SeasonRepository / EpisodeRepository as #[async_trait] + object-safe surfaces; Episode::has_file() + is_missing(now) computed helpers. Migration 0002 triggers keep the legacy Series.Seasons JSON column coherent with the Seasons TABLE by construction inside the triggering transaction.
  • Movie / MovieMetadata / MovieFile domain models (m45): Movies side of the port — 18-field Movie with schema-mirrored denormalisation, 29-field content-side MovieMetadata bound via MovieMetadataId FK, and MovieFile with has_media_info() helper. MovieStatusType (Deleted=-1, Tba=0, Announced=1, InCinemas=2, Released=3), MovieMonitorType, AddMovieMethod enums with Unknown(i32) fallthrough. MovieRepository / MovieMetadataRepository / MovieFileRepository as #[async_trait] + object-safe surfaces with sqlx-backed impls. Migration 0003 triggers keep Movies.HasFile / Movies.MovieFileId in sync with the MovieFiles table on every INSERT/UPDATE/DELETE.
  • Repository layer polish + path newtypes + remote-path resolver (m46): IBasicRepository<T, NewT> supertrait bundles find_by_id/insert/update/delete_by_id across Series/Episode/Movie/MovieFile — each entity repo now inherits the shared CRUD surface instead of redeclaring it (Season + MovieMetadata stay on upsert, so they opt out). 6 strongly-typed path newtypes (SeriesPath, MoviePath, MovieFilePath, RelativePath, LocalPath, RemotePath) replace raw String / PathBuf on the TV + Movie domain structs. RemotePathMapping + SqliteRemotePathMappingRepository + RemotePathResolver translate between remote and local path namespaces using longest-prefix-match with Path::components() normalization and a TTL + AtomicU64 version-counter invalidated cache (4 error variants, .. escape-path rejection). No new migration — RemotePathMappings table was already declared in 0001_initial.sql.
  • MoveLogs SQLite migration + dual-writer (m47): filesystem-move audit trail migrates from JSONL-only to a SQLite + JSONL dual-writer over a 9-release window (v0.47.x → v0.55.x). Typed MoveLog / NewMoveLog / FileMove domain models with MoveLogStatus / MoveLogCategory enums carrying #[serde(other)] Unknown for forward-compat. MoveLogWriter trait abstraction with DualWriter (JSONL first — rollback-safe, then SQLite — warn-on-error) and SpyWriter for tests. Startup sentinel probe (transaction-rollback INSERT) catches broken write paths at boot (CLI exit code 3). Periodic parity check every 100 writes logs error! on drift beyond ±2 rows. Migration 0004_move_logs.sql ships the MoveLogs table + 3 indexes; SHA-256 pinned. JSONL writer removal is scheduled for v0.56.0.
  • RootFolder domain + repository (m48): RootFolder (12 fields) and NewRootFolder (11 fields) domain types mapping the RootFolders table. RootFolderRepository trait extends IBasicRepository with find_by_path (UNIQUE path, byte-equal) and find_all (insertion-order). SqliteRootFolderRepository with private RootFolderRow + From boundary pattern. No new migration — table was declared in 0001_initial.sql.
  • Parser baseline measurement (m49): Sonarr v4.0.17.2952 parser fixture corpus (1,416 cases, 12 categories) vendored at tests/sonarr-fixtures/. crates/parser-baseline/ harness benchmarks hunch v2.0.0 and the existing bespoke parser side-by-side. Verdict: path C — port Sonarr's QualityParser.cs + LanguageParser.cs (hunch 30.3% / bespoke 0.9% weighted, both below the 60% fork threshold). Report at docs/research/hunch-baseline.md. m50 reframes as Sonarr parser port.
  • Sonarr parser port (m50): crates/parser/ workspace member with full GPL-3.0 port of Sonarr v4.0.17.2952's QualityParser.cs (15 regexes, 21 Quality variants via Resolution + QualitySource cascade) and LanguageParser.cs (6 regexes, 47 Language variants). avatarr-parser scores 94.5% quality / 100.0% language on the fixture corpus, well above the m51 ≥70% gate. 4-way bake-off in crates/parser-baseline/ (Sonarr ground-truth + hunch + bespoke + avatarr-parser). Library only at m50; m51 wires into release matching. scripts/build-appimage.sh restored. Report at docs/research/m50-port-baseline.md.
  • Episode regex cascade + release-group pipeline (m52-m53b): 89 Sonarr ReportTitleRegex[] cascade entries ported — 42 standard (m52), 9 daily, 34 absolute (m53). Post-cascade release-group pipeline hand-rolls Sonarr's ParseReleaseGroup (anime [SubGroup], exception lists, trailing -GROUP). Fixture pass rates: single 171/171, multi 69/69, season 49/49, daily 42/42, absolute 155/155 (100%), release_group 112/112. m53b closed the absolute fixture gap via mask-and-retry lookahead emulation, batch-range fallback, dual-capture range routing, trailing-year reattachment, and post-handler tail enrichment. extract.py fixture extractor fixed for paren-balancing and comment-skipping.
  • Series add + TVDB resolver (m61): avatarr series search queries TheTVDB for series by name; avatarr series add --tvdb-id N --path /tv/show fetches metadata and atomically inserts Series + Seasons + Episodes in a single transaction. 14-variant monitor strategy (--monitor all|none|future|first-season|…), root-folder default resolution, --json output for both commands. TVDB v4 API client with bearer-token caching and automatic 401 retry.
  • Movie add + TMDB resolver (m62): avatarr movie search queries TMDB for movies by title (optional --year filter); avatarr movie add --tmdb-id N --path /movies/dir fetches TMDB metadata and atomically inserts MovieMetadata + Movie in a single transaction. Configurable --monitored, --minimum-availability (announced/in-cinemas/released), root-folder quality profile resolution, --json output for both commands. Dedicated TMDB v3 API client with query-param auth.
  • Monitor / unmonitor state machine (m63): runtime MonitorService plus a CLI surface that toggles the Monitored flag on Series, Season, Episode, and Movie rows. avatarr series monitor/unmonitor, avatarr series apply-monitor --strategy <STRATEGY> (cascades through every season + episode using the same 14-variant strategy as m61's add-time picker, now including MonitorSpecials/UnmonitorSpecials), avatarr series set-monitor-new-items --mode all|none, avatarr series show/list, avatarr movie monitor/unmonitor, avatarr movie show/list, avatarr season monitor/unmonitor, and avatarr episode monitor/unmonitor (range syntax: 1-5, 1,3,5, 1-3,7,10-12). Repo set_monitored returns Result<usize, _> with a Monitored != ? guard to skip no-op writes; --dry-run on apply-monitor + episode commands distinguishes targeted rows from rows actually changing.
  • Missing detection + bulk-add (m64): avatarr series missing and avatarr movie missing surface monitored items without files. avatarr series bulk-add --file <path> and avatarr movie bulk-add --file <path> ingest JSON arrays for batch library population, with --dry-run local-DB validation and --json output. Final milestone of the Library Management cluster (m61-m64).
  • Scheduler subsystem (m65): tokio-cron-scheduler-backed scheduler daemon with TaskRegistry, CommandExecutor (500ms-poll, atomic-claim), PID lockfile single-instance enforcement, exponential backoff on task failures. MediaInfoBackfillTask as the first concrete task. CLI: avatarr scheduler run, avatarr scheduler list, avatarr commands list.
  • RSS polling + missing-search jobs (m66): Two new scheduled tasks that actively discover content. RssPollTask (15-min default) polls all RSS-enabled Newznab/Torznab indexers, runs releases through the decision engine, and lands accepted results in PendingReleases. MissingSearchTask (6-hour default) searches indexers for monitored-but-missing episodes and movies (10 series / 20 movies per run). Shared process_release pipeline bridges avatarr-parser quality parsing to the m59 decision engine. IndexerApi trait with NewznabClient HTTP client and MockIndexerApi test double. IndexerStatus backoff with escalating cooldowns (5m → 24h). Migration 0006 adds per-indexer LastRssSync timestamp, PendingReleases.Guid UNIQUE dedup index, and promotes IndexerStatus.IndexerId to UNIQUE for correct UPSERT. CLI: avatarr pending list/clear, avatarr indexer list. Final milestone of the Scheduler cluster (m65-m66).
  • IDownloadClient trait + qBittorrent refactor (m67): IDownloadClient async trait abstracts download clients behind Arc<dyn IDownloadClient>. QBitDownloadClient implements qBittorrent Web API v2. DownloadClientProvider resolves protocol + client ID to the correct implementation with backoff awareness. DownloadClient/DownloadClientStatus/DownloadHistory domain types with full repository layer. DownloadHistoryCleanupTask (24h) removes orphaned history rows. Monitor pipeline migrated from concrete &QbitClient to &dyn IDownloadClient. Old qbit.rs deleted. Migration 0007 promotes DownloadClientStatus.DownloadClientId index to UNIQUE. CLI: avatarr download-client list. First milestone of the Downloads + import cluster (m67-m70).
  • TrackedDownloads DB + broadcast event channels (m68): TrackedDownloads table (12 cols, migration 0008) persists downloads through their lifecycle (Downloading → ImportPending → Importing → Imported). TrackedDownloadState enum with Unknown(i32) fallthrough. EventBus + DownloadEvent (Grabbed/StateChanged/Removed) on tokio::sync::broadcast — A1 architectural lock for all future streaming surfaces (Slint m71+, Axum SSE m92). GrabReleasesTask (1-min) dequeues pending releases to download clients with crash-recovery dedup via exists_by_guid. DownloadSyncTask (1-min) polls clients for status. DownloadClientProvider caches resolved Arc<dyn IDownloadClient>. MockDownloadClient test double feature-gated for downstream consumers. CLI: avatarr queue list.
  • Hardlink import + recycle bin + RPM-aware paths (m69): ImportService drives TrackedDownload rows through Importing → (Imported | Failed) and broadcasts DownloadEvent::{Imported, ImportFailed}. place_file 3-tier cascade — reflink (FICLONE via libc::ioctl) → hardlink (std::fs::hard_link) → copy + size-verify — with stat-probe crash recovery (existing inode/size match short-circuits, mismatch returns DestExists rather than clobbering). RecycleBinService routes upgrade-replaced files to <data_dir>/recyclebin/<YYYY-MM-DD>/ with O(1) millisecond-suffix collision avoidance; RecycleBinRepository (append-only, no-update) backs restore + purge_expired. 3 new scheduled tasks wired into the daemon: ImportPendingTask (1-min, capped 50/tick), CleanupRecycleBinTask (weekly), RecoverStuckImportingTask (10-min, flips Importing rows wedged > 5 min back to ImportPending). Migration 0009 adds RecycleBin table + TrackedDownloads.ContentPath (D2 — snapshot save_path on Downloading → ImportPending so import doesn't depend on the torrent client still holding the row) + LastStateChangeAt (D8 — drives the stuck-importing recovery). Legacy push-driven pipeline deleted (~770 prod lines + 572 test lines): monitor::{process_completed, monitor_loop, process_torrent, process_movie, process_tv, cleanup_torrent} + organizer::{execute_movie, execute_tv} + 3 CLI handler call sites. CLI: avatarr import run, avatarr import list-pending [--json], avatarr recycle list [--json] / restore <ID> / empty --yes. Third milestone of the Downloads + import cluster (m67-m70).
  • Indexer abstraction + avatarr smoke self-test (m70): crates/core/src/indexer.rs (1114 LOC monolith) restructured into a directory module indexer/{mod.rs, newznab.rs, mock.rs} — peer of the m67 download_client/ layout. Trait surface unchanged; MockIndexerApi promoted from #[cfg(test)] to #[cfg(any(test, feature = "test-support"))] so release binaries opt into the test mocks. Three duplicate NullIndexerApi stubs collapsed across the scheduler tests. New avatarr smoke CLI subcommand runs six pipeline stages — parse → decide → mock-indexer → mock-download → hardlink → move-log — against a tempfile::TempDir-backed pool and prints a green/red ANSI table. Exit 0 on all-green / 1 otherwise. Hermetic by construction: never touches the user's real AVATARR_DATA_DIR, so it runs on a fresh install with no config, no DB, no network. Closes D10 from the 2026-04-30 rebuild spec as the backend-phase exit validator. Final milestone of the Downloads + import cluster (m67-m70).
  • 1,766 tests across 74 binaries, cargo clippy --workspace --all-targets --all-features -- -D warnings clean, full CLI binary smoke (17 subcommands / 28 probes) green
  • Auto-migration from ~/.config/plex-pipeline/~/.config/avatarr/ on first launch (idempotent, EXDEV-safe)

Phase A shipped through m133; Phase B parity arc (m135m154) underway (solo part-time at ~0.6× intensity):

Weekly milestone cadence (each ships v0.N.0), grouped into thematic clusters per the 2026-04-30 redraft:

Cluster Milestones Theme
Foundation m43 → m48 sqlx infra, domain models, repository layer, JSONL→SQLite migration, RootFolder
Parser quality m49 → m53b Sonarr fixture corpus + parser port + 100% fixture pass rate
Cleanup + parser arc m54 → m57 Legacy GUI deletion + binary migration to crates/cli, parser anime + year mismatch, parser wire-in, crates.io publish
Decision engine m58 → m60 Quality profiles + cutoff/upgrade + Accept/Reject/Upgrade engine
Library management m61 → m64 Series/Movie add workflows, monitor state machine, missing detection
Scheduler m65 → m66 tokio-cron-scheduler + RSS polling + missing-search
Downloads + import m67 → m70 IDownloadClient trait, tracked downloads, hardlink import, indexer abstraction, avatarr smoke end-to-end self-test
Slint desktop GUI rebuild m71 → m85 New crates/gui from DESIGN.md — tokens + 4-accent theme cycle + 22 components + 10 screens + cutover + soak
API v3 (Axum) m86 → m93 Load-bearing for both WebUI and Bazarr/Prowlarr; X-Api-Key auth, full endpoint coverage, OpenAPI spec
WebUI (React + TS) m94 → m103 Parallel implementation sharing tokens.toml with Slint, 10 screens mirroring DESIGN.md, Docker compose ship
Migration importer m104 → m105 avatarr-cli import {sonarr,radarr} with dry-run + idempotency
Notifications + health m106 → m107 Discord/Telegram/Gotify/webhook notifiers + health check framework + Codeberg update checker
Settings configuration m108 Full TOML config exposed in both GUIs via GET/PUT /api/v3/config
Test discipline m109 Fix 34 failing monitor_handlers tests (T1 violation on main), dead-code + stale-TODO audit, T1 plan-file CI gate. Post-audit insertion — see docs/audit-2026-05-25-m108-drift.md.
Componentization — library platform m110 → m111 (+ CP-1 ) avatarr-media extraction (dual-badged medialint M1, executes the committed extraction plan), evidence-driven library boundary ledger, gated crates.io publish checkpoint (sign-off only, never loop-walked). 2026-06-09 librarification insertion.
Invariant closure m112 Close two m86 deferrals (DownloadClientProvider cache invalidation + QualityProfile handler serialisation), Sonarr-compat stub policy
Settings CRUD — API layer m113 → m117 POST/PUT/DELETE for Indexer, DownloadClient, RootFolder, QualityProfile, Notification (one entity per milestone). Closes the m81 "CRUD editing deferred" deferral that the audit found unresolved at m108.
Settings CRUD — WebUI management m118 → m122 Add/edit/delete forms in React Settings tabs, mirroring the API layer (one entity per milestone)
First-run + end-to-end QA m123 → m126 First-run wizard, search-to-import walkthrough QA, GUI dogfooding round 2 (82 empty-DB-deferred items), UX polish (empty states, loading, validation feedback)
Resilience m127 Network failure handling, disk-space exhaustion, SIGTERM graceful shutdown in scheduler + API server
Contributor + user docs + library hardening m128 → m131 CONTRIBUTING.md + dev-setup + per-crate READMEs, getting-started + Sonarr/Radarr migration guide, API + config reference, library hardening (rustdoc + examples + semver policy + cargo-deny for the published crates — CP-1 prerequisite)
Size budgets + distribution m132 → m133 CI size budgets (AppImage ≤30 MB, WebUI bundle ≤500 KB gzipped) + Docker multi-arch + .deb + signed AppImage

Pre-scheduled 1.0 release removed — Avatarr's 1.0 tag happens organically if and when earned, not as a roadmap checkpoint. Shipped-milestone signals (m93 = Bazarr/Prowlarr work, m103 = WebUI ships, m104-m105 = Sonarr/Radarr import works) communicate readiness more precisely than a version number ceremony.

Distribution discipline — AppImage, Docker multi-arch, .deb, and signed releases all ship at m133 (deferred from m108 in the original roadmap to past the CRUD + QA + docs arc per the 2026-05-25 audit restructure). There is no per-commit AppImage rebuild during the m54-m132 development arc; cargo run / cargo test are the dev iteration loop.

Versioning: milestone N ships as v0.N.0; v0.N.1+ is strictly for post-ship bug fixes.

Features (today)

  • 🔍 Multi-source torrent search: TPB, YTS, EZTV, RuTracker (fallback chain — RuTracker disabled by default, requires account)
  • 🌟 Quality preference: Select "Any" to prefer 1080p while accepting all qualities, or choose a specific quality (720p, 1080p, 2160p) to only show results matching that quality
  • 📊 Smart scoring: Ranks results by title match, year, quality, seeders, file size
  • 📺 TV support: Season packs, episode fallback, auto-discovery (S01+), season ranges (S01-S05), complete series packs
  • 📽️ UHD routing: 2160p/4K movies automatically routed to UHD Movies library
  • ⬆️ Smart quality upgrade: Incoming files compared by resolution and size — upgrades lower quality, fills missing episodes, replaces zero-byte files
  • 👀 Scheduler-driven automation: RSS polling, missing-content search, grabbing, download-client sync, and hardlink-first import run as scheduled tasks (avatarr-cli scheduler run)
  • 🏷️ IMDB/TMDB metadata: Resolves missing years and canonical titles via API lookup (year-aware caching, acronym-aware matching)
  • Bare title resolution: Titles without a year or season tag are automatically resolved via TMDB — movies and TV shows correctly identified and categorized
  • 💾 Persistent move logs: every import/move recorded in the SQLite MoveLogs table — survives restarts, queryable
  • 🎁 Extras collection: Extras/bonus folders in TV packs automatically moved to Show/Featurettes/
  • 🎥 Movie extraction: Movie files found in TV packs processed as regular movies
  • 🌐 Cross-platform: Linux, Windows, macOS
  • ⌨️ Three surfaces: Full-featured CLI, Slint desktop GUI, and React WebUI
  • 🚀 First-run setup: Auto-generates config template on first launch
  • 📄 Batch mode: Process list files with movies, TV seasons, season ranges, and discover mode
RSS / Search ──> Score & Rank ──> Grab ──> Download Sync ──> Import ──> Plex
(indexers)       (decision engine)  (qBittorrent)  (scheduler)   (hardlink/rename)

📚 Documentation

Guide What it covers
Getting started Build (desktop + headless), the three surfaces, first run, the two-process model
Migrating from Sonarr/Radarr Importing your library — with an honest account of what carries over
API reference The Sonarr/Radarr-v3-compatible REST API: spec, auth, all 37 surfaces, WebSocket events, Bazarr/Prowlarr status
Configuration reference Every config.toml key with its default and whether current code actually reads it
Troubleshooting Symptom-indexed reference: health checks, backoff, stuck states, the smoke test
CONTRIBUTING Development setup, gates, PR flow

📦 Install

Docker (server)

The single-container deployment: API + WebUI + scheduler in one process (server --with-scheduler), state under one /data volume, healthcheck on /ping. Multi-arch (amd64 + arm64).

git clone https://codeberg.org/alan090/avatarr.git && cd avatarr
docker compose up -d --build     # WebUI at http://localhost:7878

docker-compose.yml parameterizes the media and downloads mounts (AVATARR_MEDIA_ROOT, AVATARR_DOWNLOADS_ROOT — default to local ./media + ./downloads). A switch-test profile adds real qBittorrent + Bazarr for full-stack evaluation. Images are not yet on a public registry — registry publishing arrives with v1.0.

.deb (Debian/Ubuntu server)

Built by cargo deb -p avatarr-cli (or grab the release-artifacts CI artifact): installs /usr/bin/avatarr-cli plus a hardened systemd unit (DynamicUser, state in /var/lib/avatarr) running the same single-process server. Not auto-enabled:

sudo dpkg -i avatarr_*.deb
sudo systemctl enable --now avatarr

The unit documents the ReadWritePaths drop-in you need to grant your media library to the sandboxed service.

AppImage (desktop GUI)

scripts/build-appimage.sh produces dist-appimage/avatarr-<version>-x86_64.AppImage (~13 MB) — see packaging/appimage/README.md for the build-host and run-host requirements. Signed public releases arrive with v1.0.

From source

Full walkthrough: docs/user/getting-started.md.

Prerequisites:

  • Rust toolchainrust-toolchain.toml pins the version; rustup picks it up automatically
  • Node.js 18+ — required even for CLI-only builds: the API crate embeds the WebUI bundle at compile time
  • A C compiler (gcc/clang/MSVC) — required by some dependencies
  • Desktop GUI only: fontconfig development headers

🐧 Linux

# Debian/Ubuntu
sudo apt install build-essential pkg-config libssl-dev nodejs npm
sudo apt install libfontconfig1-dev   # only if building the GUI

# Arch/CachyOS
sudo pacman -S base-devel openssl nodejs npm
sudo pacman -S fontconfig             # only if building the GUI

git clone https://codeberg.org/alan090/avatarr.git
cd avatarr
(cd crates/webui && npm ci && npm run build)   # REQUIRED before any cargo build
cargo build --release                          # or: -p avatarr-cli (headless, skips GUI deps)
# Binaries: target/release/avatarr (GUI), target/release/avatarr-cli (CLI + server)

🪟 Windows

# Requires Visual Studio Build Tools with "Desktop development with C++"
# Install Rust: https://rustup.rs/ (use default MSVC toolchain)
# Install Node.js 18+: https://nodejs.org/

git clone https://codeberg.org/alan090/avatarr.git
cd avatarr
cd crates\webui; npm ci; npm run build; cd ..\..   # REQUIRED before any cargo build
cargo build --release
# Binaries: target\release\avatarr.exe (GUI), target\release\avatarr-cli.exe (CLI + server)

🍎 macOS

xcode-select --install
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
brew install node

git clone https://codeberg.org/alan090/avatarr.git
cd avatarr
(cd crates/webui && npm ci && npm run build)   # REQUIRED before any cargo build
cargo build --release
# Binaries: target/release/avatarr (GUI), target/release/avatarr-cli (CLI + server)

Note: macOS builds are unsigned. Right-click and choose Open to bypass Gatekeeper on first launch.

Migrating from plex-pipeline-rs

If you previously ran plex-pipeline-rs, the v0.40.0 binary auto-migrates your ~/.config/plex-pipeline/ directory to ~/.config/avatarr/ on first launch. No manual action required. The migration is idempotent (it only fires when the new directory does not yet exist) and falls back to a recursive copy on EXDEV if your config and home directories live on different filesystems. See the v0.40.0 changelog entry for the full migration notes, including how to roll back.

🕹️ Usage

Two binaries ship from this workspace: avatarr is the desktop GUI; avatarr-cli carries every command below, plus avatarr-cli server (REST API + WebUI on :7878) and avatarr-cli scheduler run (the automation daemon — RSS polling, searching, grabbing, importing). Nothing automatic happens until the scheduler daemon is running — see the getting started guide for the full picture.

# Search
avatarr-cli search "Big Adventure 1985" --type movie    # Single movie
avatarr-cli search "Sunset Valley S01" --type tv        # TV season
avatarr-cli search "Echo Company S05E14" --type tv      # Single episode
avatarr-cli search --file list.txt --auto               # Batch from file
avatarr-cli search --file list.txt --auto --dry-run     # Preview only
avatarr-cli search "Night Runner 2003" --type movie --quality 1080p

# Daemons
avatarr-cli server                  # REST API v3 + WebUI (default port 7878)
avatarr-cli scheduler run           # Automation daemon (RSS, search, grab, import)
avatarr-cli scheduler list          # Show every scheduled task + cadence

# Movie library
avatarr-cli movie search "The Matrix" --year 1999        # Search TMDB
avatarr-cli movie add --tmdb-id 603 --path /media/Movies/Matrix --monitored
avatarr-cli movie add --tmdb-id 603 --path /media/Movies/Matrix --minimum-availability in-cinemas
avatarr-cli movie list                                    # List all movies with monitor flag
avatarr-cli movie show --tmdb-id 603                      # Show one movie row
avatarr-cli movie monitor --tmdb-id 603                   # Mark monitored
avatarr-cli movie unmonitor --tmdb-id 603                 # Mark unmonitored

# Series monitor state machine
avatarr-cli series list                                              # List all series
avatarr-cli series show --tvdb-id 12345                              # Show series + per-season subtable
avatarr-cli series monitor --tvdb-id 12345                           # Toggle master Monitored on
avatarr-cli series apply-monitor --tvdb-id 12345 --strategy pilot    # Cascade strategy through seasons + episodes
avatarr-cli series set-monitor-new-items --tvdb-id 12345 --mode all  # Auto-monitor newly imported items

# Season + episode monitor toggles
avatarr-cli season monitor --tvdb-id 12345 --season 1
avatarr-cli episode monitor --tvdb-id 12345 --season 1 --range 1,3,5,7-9
avatarr-cli episode unmonitor --tvdb-id 12345 --season 1 --range 1-3,7,10-12 --dry-run

# Quality profiles
avatarr-cli quality list                                  # List quality definitions
avatarr-cli profile show Any                              # Show profile detail + items
avatarr-cli profile create --clone Any --name "HD Only"   # Clone a profile
avatarr-cli profile set "HD Only" --cutoff Bluray-1080p   # Edit profile settings
avatarr-cli profile items add "HD Only" "HDTV-2160p"      # Add quality to profile

# Migration from Sonarr/Radarr (see docs/user/migration.md first!)
avatarr-cli import sonarr /path/to/sonarr.db --dry-run
avatarr-cli import radarr /path/to/radarr.db --dry-run

# Diagnostics
avatarr-cli health              # Run all five health checks now
avatarr-cli smoke               # Hermetic end-to-end pipeline self-test
avatarr-cli --test-parse        # Test torrent name parsers

Every command group has more — avatarr-cli --help is the full surface (queue, pending, indexers, download clients, recycle bin, notifications, OpenAPI spec, …).

📄 Input file format

# Movies — title with year
Big Adventure (1985)
Night Runner 2003

# TV — show name with S## tag
Sunset Valley S01
Echo Company S01-S03
The Drift S01+

# TV — individual episode
Sunset Valley S05E14

# TV — complete series pack (no season specified)
The Drift

# Bare title — resolved via TMDB (movie or TV detected automatically)
Big Adventure

⚙️ Configuration

Config file: ~/.config/avatarr/config.toml (auto-generated on first use). The sample below shows the most-used keys — newer sections ([api] key/port, [gui], recycle-bin retention, [tvdb]) are present in the generated file with their defaults. The configuration reference documents every key, its default, and whether current code actually reads it.

[qbittorrent]
url = "http://localhost:8080"
username = ""
password = ""

[paths]
staging_dir = "/tmp/staging"
movie_dir = "/media/Movies"
uhd_movie_dir = "/media/UHD Movies"
tv_dir = "/media/TV Shows"

[sources.tpb]
enabled = true
api_base = "https://apibay.org"
request_delay_ms = 2000
min_seeders = 1

[sources.yts]
enabled = true

[sources.eztv]
enabled = true

[sources.rutracker]
enabled = false

[tmdb]
api_key = ""

🏷️ Metadata resolution

When a torrent name lacks the year (common for TV shows), the monitor resolves the canonical title and year automatically. This ensures Plex-standard folder naming:

Movie Name (Year)/Movie Name (Year).ext
Show Name (Year)/Season N/Show Name (Year) - S01E01.ext

Resolution chain (first match wins):

  1. 🎞️ IMDB Suggestion API — free, no key needed
  2. 🎥 TMDB Search API — year-aware filtering (primary_release_year for movies, first_air_date_year for TV); add api_key under [tmdb] in config
  3. 📝 Torrent name — if the year is already present
  4. 📁 Existing library folder — parsed name matched against disk, with automatic folder rename when the resolver provides a corrected canonical title

Cache keys include title, media type, and year — same-title entries from different years are resolved independently.

💾 Database

Avatarr uses SQLite via sqlx. The database file lives at:

  • Linux: ~/.local/share/avatarr/avatarr.db
  • macOS: ~/Library/Application Support/avatarr/avatarr.db
  • Windows: %LOCALAPPDATA%\avatarr\avatarr.db (non-roaming — avoids profile-sync thrash on growing SQLite files)

Override with the AVATARR_DATA_DIR environment variable for isolated dev/test installs:

AVATARR_DATA_DIR=/tmp/avatarr-dev avatarr

Schema lives in crates/core/migrations/*.sql. The pool opens with PRAGMA foreign_keys=ON, WAL journal mode, and busy_timeout=5s; migrations run automatically at CLI/GUI startup with exponential-backoff retry on transient busy errors. Failure to open is fatal with a per-variant error message (stderr for CLI; stderr + ~/.cache/avatarr/startup-error.log for GUI) and exit code 2.

Regenerating sqlx offline query cache (contributors)

After modifying a sqlx::query! macro:

cargo install sqlx-cli --no-default-features --features native-tls,sqlite
export DATABASE_URL="sqlite:///tmp/avatarr-prepare.db"
cargo sqlx database create
cargo sqlx migrate run --source crates/core/migrations
cargo sqlx prepare --workspace

Commit the resulting .sqlx/query-*.json files. CI runs with SQLX_OFFLINE=true — no DATABASE_URL required at build time.

🔧 Troubleshooting

Full symptom-indexed reference: docs/user/troubleshooting.md (build failures, startup exit codes, WebUI auth, backoff ladders, stuck downloads/imports, the five health checks, and the avatarr-cli smoke self-test). The three most common:

Issue Solution
Fresh-clone build fails referencing ../webui/dist/ Build the WebUI bundle first: (cd crates/webui && npm ci && npm run build)
qBittorrent connection failed Check URL in Settings (default: http://localhost:8080), ensure Web UI is enabled
Nothing downloads automatically The automation daemon isn't running — start avatarr-cli scheduler run alongside avatarr-cli server

🏗️ Architecture

Cargo workspace with six crates:

Crate Purpose
avatarr-core Library: domain models, repository layer, decision engine, scheduler, import pipeline, metadata, organizer, config
avatarr-cli CLI binary (avatarr) via clap
avatarr-parser Sonarr-ported parser library (episode/quality/language/release-group) — published on crates.io at 0.1.0
avatarr-gui Slint desktop GUI
avatarr-api Axum API v3 server — WebUI data layer + Bazarr/Prowlarr compatibility surface
avatarr-media TMDB/IMDB metadata resolution + pure Plex naming formatters, extracted from avatarr-core at m110 — independently versioned 0.1.0, consumed by core via re-export

Platform: the avatarr crate family

avatarr is componentizing into a published crate family consumed by its own app crates and by external tools (see the ROADMAP Componentization section — m110/m111/m131 + the CP-1 publish gate):

  avatarr-parser ─┐                      ┌─ avatarr-cli
  (crates.io)     ├──> avatarr-core ─────┼─ avatarr-gui
  avatarr-media ──┘    (app library)     └─ avatarr-api ──> React WebUI
  (extracted m110)                                          (embedded assets)
        │
        └──────> medialint (first external consumer — path-dep until the CP-1 crates.io publish)

The library crates (avatarr-parser, avatarr-media) are pure — no SQLite, no app config — so external consumers like medialint can lint a media library against the Plex naming standard without dragging in the full manager.

Tech stack today (v0.70.0): Rust 2021, Tokio async runtime, reqwest for HTTP, quick-xml for Newznab/Torznab parsing, regex for parsers, clap for CLI, TOML for config, sqlx 0.8 against SQLite for the catalog, recycle bin, and move log, tempfile for the m70 hermetic smoke and integration test fixtures, wiremock for API client integration tests. The schema (crates/core/migrations/, 9 migrations, 51 tables) is stable since m43; domain models, repository layer, TVDB/TMDB API clients, the per-entity monitor state machine, scheduler subsystem (9 registered tasks), RSS polling + missing-search jobs, download client abstraction (IDownloadClient trait), indexer abstraction (directory-module layout, IndexerApi trait, feature-gated mocks), tracked downloads DB + broadcast event channels, hardlink-based file placement, the queryable recycle bin, and the avatarr smoke end-to-end self-test are complete through m70. See the roadmap for the full migration plan.

🤝 Heritage and attribution

Avatarr will port code from Sonarr and Radarr starting in m44 (parser port phase 1). Both upstream projects are GPL-3.0; Avatarr is GPL-3.0-or-later, so the port is legally clean. Each ported file gets a header comment crediting the source commit, and THANKS.md is the canonical attribution list (currently a placeholder until m44 lands).

This project is also a Rust rewrite of the original Python plex-pipeline (archived). The Rust port is ~484× faster startup, ~30× faster parsing, ~5.6× less memory, and ~7.7× smaller binary than the PyQt6 original.

📜 Changelog

See CHANGELOG.md for the full release history. The most recent release is v0.70.0 (m70 indexer abstraction + avatarr smoke self-test).

⚖️ License

GPL-3.0-or-later — see LICENSE. This is required for the m44 parser port to be legally clean against Sonarr/Radarr's GPL-3.0; the license is locked and will not change.