From 2f129904e360e1443946a6f183205eab37d7751b Mon Sep 17 00:00:00 2001 From: sprooty Date: Mon, 17 Aug 2026 01:14:36 +0000 Subject: [PATCH] Re-baseline docs against the ratified unified-arr plan UNIFIED-ARR-PLAN.md becomes the single active roadmap: records the 2026-08-05 decision resolutions (D5 MariaDB delivery, D8 Indexarr boundary, D9 logical instances/versions) and the current 54-issue state. PLAN.md, IMPLEMENTATION_PLAN.md, and TODO3.md are reduced to historical pointers since their content predates and is superseded by the canonical plan. Topic docs (API-COMPATIBILITY, DATABASE, ARCHITECTURE, etc.) are updated to distinguish current PostgreSQL-era implementation state from the settled MariaDB 11.4 target state, with cross-references corrected to match the plan's renumbered sections. --- AGENTS.md | 6 + IMPLEMENTATION_PLAN.md | 319 +------ PLAN.md | 1528 +----------------------------- README.md | 24 +- TODO3.md | 227 +---- docs/API-COMPATIBILITY.md | 90 +- docs/ARCHITECTURE.md | 13 +- docs/CONFIGURATION.md | 7 + docs/CRATE-GUIDE.md | 5 + docs/DATABASE.md | 13 +- docs/DEPLOYMENT.md | 8 + docs/DEVELOPMENT.md | 6 + docs/DOMAIN-MODELS.md | 5 + docs/NOTIFICATIONS.md | 6 + docs/UNIFIED-ARR-PLAN.md | 1510 ++++++++--------------------- docs/backlog.json | 40 +- docs/clientapp.md | 6 + docs/phase1-user-system.md | 4 + docs/phase2-watch-progress.md | 4 + docs/phase3-media-requests.md | 5 + docs/phase4-watchlist-ratings.md | 5 + docs/phase5-notifications-pwa.md | 5 + docs/streaming.md | 6 + 23 files changed, 583 insertions(+), 3259 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 51045073..4b2332f5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,12 @@ compatibility pointer to this file. Read `README.md`, `CONTRIBUTING.md`, `docs/UNIFIED-ARR-PLAN.md`, and the issue being implemented before changing behavior. +`docs/UNIFIED-ARR-PLAN.md` is the only active roadmap and owns future state, +phase order, and architecture decisions. Topic documents describe the current +implementation unless they explicitly identify a target state. `PLAN.md`, +`IMPLEMENTATION_PLAN.md`, `TODO3.md`, and the client phase documents are not +backlogs. + ## Workspace The following list is checked against `Cargo.toml` in CI. diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index e4278fdf..008e6791 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -1,314 +1,9 @@ -# StackArr — Implementation Plan +# Historical implementation-plan pointer -## Current State (updated 2026-03-26) +This file previously tracked the pre-unification StackArr build. Its completion +percentages, vendored-engine description, PostgreSQL deployment, and remaining +phase list are historical and must not be used to select work. -**29 crates**, 74K lines of Rust, 686 tests passing. React UI with 15 pages, 6K lines TypeScript. Full Docker build + CI/CD pipeline. Deployed to Node B (192.168.1.75). - -**Architecture change**: rustTorrent and rustnzbd engines vendored directly into the workspace (not sidecars). Single binary includes media management + torrent engine + usenet engine. No external download clients required (but still supported via qBit/Transmission/SABnzbd/NZBGet API clients). - -### Crate Status - -| Crate | % Done | State | -|-------|--------|-------| -| stackarr-core | 100% | Models, config with defaults, DB pool, migrations, enabled modules | -| stackarr-parser | 100% | Release name parser, 75 tests | -| stackarr-media | 100% | Series/Movie/Episode CRUD + calendar + wanted + metadata refresh services | -| stackarr-web | 100% | All routes working (50+ endpoints), search/grab fully wired | -| stackarr-download | 90% | qBit, Transmission, SABnzbd, NZBGet + embedded torrent/usenet clients; DownloadClientManager exists but not in AppState | -| stackarr-indexer | 90% | Newznab/Torznab client working; IndexarrClient substantially complete; IndexerManager implemented but not in AppState | -| stackarr-metadata | 100% | TMDB client with rate limiting (4 req/s) + LRU cache (2000 entries, TTL), shared in AppState | -| stackarr-migrate | 100% | Sonarr/Radarr/Prowlarr SQLite readers + Postgres writer with ID mapping | -| stackarr-import | 90% | Disk scan + naming tokens + process_completed_download pipeline | -| stackarr-scheduler | 80% | Real metadata refresh task; import scan task; RSS sync is no-op stub; missing_search_task absent | -| stackarr-notify | 80% | Webhook + Discord; Telegram/Slack/email missing | -| stackarr-quality | 100% | CRUD works; 9/9 specs implemented with real logic; 46 tests passing | -| **Torrent engine** (12 crates) | 100% | Vendored from rustTorrent — librtbit, DHT, trackers, bencode, etc. | -| **Usenet engine** (5 crates) | 100% | Vendored from rustnzbd — NNTP, yEnc, par2, post-processing | - -### React UI Status - -| Page | Status | -|------|--------| -| First Boot Wizard | Done | -| Series List + Detail | Done | -| Movie List + Detail | Done | -| Calendar | Done | -| Queue | Done | -| Torrents (full client UI) | Done | -| Usenet (queue + history + NNTP servers) | Done | -| History | Done | -| Wanted/Missing | Done | -| Settings (7 tabs) | Done | -| Migration | Done | - -### Infrastructure - -| Component | Status | -|-----------|--------| -| Docker multi-stage build (Node + Rust + slim runtime) | Done | -| docker-compose.yml (dev) | Done | -| docker-compose.prod.yml (Node B with media mounts RO) | Done | -| GitHub Actions CI/CD (check → build → smoke test → deploy) | Done | -| Repo: github.com/TheDancingDeveloper-org/NGMS | Done | -| GHCR: ghcr.io/thedancingdeveloper-org/ngms | Done | - ---- - -## ~~Phase 0 — Make It Boot~~ COMPLETE - -- ~~`src/main.rs` — DATABASE_URL env, auto-generate default config~~ -- ~~`config.rs` — Default impls, generate_default()~~ -- ~~`db.rs` — Migration runner, enabled_modules queries~~ -- ~~`docker/Dockerfile` — Multi-stage build~~ -- ~~`docker/docker-compose.yml` — StackArr + Postgres 17~~ -- ~~`config.example.toml`~~ - -Verified: `docker compose up --build` → API + UI served on port 9111. - ---- - -## ~~Phase 1 — First-Boot Wizard + Core CRUD~~ COMPLETE - -- ~~POST /api/v1/setup/init — persist modules, root folders, API key~~ -- ~~GET /api/v1/system/status — real first_boot from DB~~ -- ~~CRUD routes: root folders, tags, naming config, download clients, indexers~~ -- ~~Health check verifies DB connectivity~~ - ---- - -## ~~Phase 2 — *arr Migration + Library Import~~ COMPLETE - -- ~~stackarr-migrate crate: sonarr.rs, radarr.rs, prowlarr.rs, writer.rs~~ -- ~~CLI: `stackarr migrate --sonarr --radarr --prowlarr [--dry-run]`~~ -- ~~API: POST /api/v1/system/migrate (multipart upload)~~ -- ~~Disk scan: POST /api/v1/command {"name":"DiskScan"}~~ -- ~~TMDB lookup: GET /api/v1/series/lookup, /movies/lookup~~ - -Real *arr backup DBs at TestData/arr-backups/ (535 series, 1212 movies, 6 indexers). - ---- - -## ~~Phase 3 — Library Views + Metadata Refresh~~ COMPLETE - -- ~~GET /api/v1/calendar?start=&end=~~ -- ~~GET /api/v1/wanted/missing, /wanted/cutoff~~ -- ~~GET /api/v1/series/{id}/episodes~~ -- ~~PUT /api/v1/episode/{id}, /episode/monitor (bulk)~~ -- ~~CalendarService, WantedService, MetadataRefreshService~~ -- ~~metadata_refresh_task — real TMDB integration~~ -- ~~Commands: RefreshSeries, RefreshMovie, RefreshAll~~ - ---- - -## ~~Phase 4 — Search + Decision Engine + Grab~~ COMPLETE - -- ~~All 9 decision specifications implemented~~ (QualityAllowedSpec, CutoffSpec, MinSizeSpec, MaxSizeSpec, BlocklistSpec, QueueConflictSpec, MinimumSeedersSpec, CustomFormatScoreSpec, AlreadyImportedSpec) -- ~~`rank_releases()` — quality → seeders → age → indexer priority~~ -- ~~IndexerManager + DownloadClientManager in AppState, loaded from DB at startup~~ -- ~~Search handler: indexer fanout → decision engine → ranking → JSON response~~ -- ~~Grab handler: download client dispatch → queue entry → history entry~~ -- ~~46 quality tests passing~~ - ---- - -## ~~Phase 5 — Download Import Pipeline~~ COMPLETE - -- ~~naming.rs — token system ({Series Title}, S{season:00}E{episode:00}, etc.) with 14 tests~~ -- ~~process_completed_download() — scan → parse → rename → move → DB update~~ -- ~~import_scan_task — polls queue for completed items, runs import, cleans up~~ - ---- - -## Phase 6 — RSS Automation - -**Goal**: Auto-monitor indexer RSS feeds and grab new releases for monitored media. - -**Status**: NOT STARTED. Depends on Phase 4 (needs decision engine). - -**What needs to happen**: -- **6.1** Implement rss_sync_task (fetch RSS → parse → match to media → decision engine → grab) -- **6.2** Add missing_search_task (periodic search for missing media) -- **6.3** Command endpoints: RssSync, SeriesSearch, EpisodeSearch, MovieSearch, MissingSearch -- **6.4** Add `commands` table for async command tracking - ---- - -## ~~Phase 7 — Indexarr Sidecar Integration~~ COMPLETE - -**Goal**: Optional Indexarr as a torrent indexer source. - -- ~~Complete IndexarrClient (Torznab passthrough + REST + health)~~ — torznab_search, rest_search, status, health_check all implemented (crates/stackarr-indexer/src/indexarr.rs) -- ~~GET /api/v1/indexarr/status route~~ — exists and works (crates/stackarr-web/src/routes/indexarr.rs) -- ~~docker-compose optional Indexarr service~~ — exists with `profiles: [indexarr]` (docker/docker-compose.yml) -- ~~**7.1** IndexarrClient integrated into search fanout~~ — SearchService includes Indexarr via `with_indexarr()`, /search page in sidebar - ---- - -## ~~Phase 8 — Embedded Download Clients~~ SUPERSEDED - -~~Original plan: feature-flagged optional embedded clients.~~ - -**Decision changed**: rustTorrent (12 crates) and rustnzbd (5 crates) vendored directly into the workspace. No feature flags — always built in. Single binary includes everything. - -- ~~embedded_torrent.rs wrapping librtbit::Session~~ -- ~~embedded_usenet.rs wrapping nzb_web::QueueManager~~ -- ~~Feature flags removed~~ - -**Additional work done beyond original plan**: -- Full Torrents page in React UI (stats, sortable table, add modal, expand details) -- Full Usenet page in React UI (queue + history + NNTP server management) -- API routes: /api/v1/torrent/* and /api/v1/usenet/* (20 endpoints) -- Sidebar nav updated with Torrents + Usenet tabs -- ~~Wire API stubs to actual librtbit::Session and nzb_web::QueueManager in AppState~~ — engines initialized on boot if enabled in config (src/main.rs:179-293) - ---- - -## ~~Phase 9 — React UI~~ COMPLETE - -- ~~15 pages, 6K lines TypeScript~~ -- ~~Dark theme, sidebar nav, TanStack Query~~ -- ~~First-boot wizard, series/movie CRUD, calendar, queue, history, wanted~~ -- ~~Settings (7 tabs), migration upload, torrents, usenet~~ -- ~~Axum serves UI dist with SPA fallback~~ - ---- - -## ~~CI/CD + Docker~~ COMPLETE - -- ~~GitHub Actions: check → build → smoke test → deploy to Node B~~ -- ~~GHCR: ghcr.io/thedancingdeveloper-org/ngms~~ -- ~~docker-compose.prod.yml with all media mounts READ ONLY~~ -- ~~Port 9111~~ - ---- - -## Phase 10 — Security Hardening - -**Goal**: Secure the stack before any network exposure beyond localhost. - -**Status**: NOT STARTED. - -**Security audit completed 2026-03-26.** Updated 2026-03-28 with auth middleware, secret redaction, Plex TLS config, and upload validation. - -### 10a — Authentication + Authorization (CRITICAL) — DONE - -- [x] **10a.1** `require_auth_middleware` applied via `from_fn_with_state` to all 35+ protected routes — validates session cookie, device token, API key, or first-boot bypass -- [x] **10a.2** `/api/v1/setup/init` already protected (checks `enabled_count == 0`) -- [x] **10a.3** `redact_sensitive_fields()` applied to indexer, download client, and Plex server GET/POST/PUT responses — masks API keys, auth tokens, and passwords -- [x] **10a.4** Full user-based auth with session cookies, device tokens, and admin API key — `RequireUser` extractor handles all auth methods - -### 10b — CORS + CSRF + Headers (CRITICAL) - -- [x] **10b.3** Security response headers already present: `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `X-XSS-Protection`, `Referrer-Policy`, `Permissions-Policy` -- [ ] **10b.1** Replace `CorsLayer` mirror_request() with explicit origin allowlist -- [ ] **10b.2** Add CSRF protection on state-changing endpoints (POST/PUT/DELETE) - -### 10c — Input Validation + Error Handling (HIGH) — DONE - -- [x] **10c.1** Path traversal — already uses `std::fs::canonicalize()` + `is_dir()` in medialibraryfolders.rs -- [x] **10c.2** URL encoding — already uses `urlencoding::encode()` in newznab.rs -- [x] **10c.3** Error responses — already return generic "internal server error" with full details in `tracing::error!` -- [x] **10c.4** File upload validation — 500 MB size limit + SQLite header check on migrate endpoint - -### 10d — TLS + Transport Security (HIGH) — DONE - -- [x] **10d.1** Plex TLS — `verify_tls` column added to `plex_servers` (migration 009), configurable per-server, `from_server()` uses `server.verify_tls`, new servers default to `true` -- [x] **10d.2** Usenet NNTP — `ssl_verify` already defaults to `true` -- [x] **10d.3** Usenet credentials — `send_command` trace only logs command verb, not arguments - -### 10e — Rate Limiting (HIGH) - -- [ ] **10e.1** Add rate limiting middleware — `governor` is already in Cargo.toml but unused on the main API -- [ ] **10e.2** Priority: auth endpoints first, then general API - -### 10f — Docker + Deployment (MEDIUM) - -- [ ] **10f.1** Add `USER` directive to Dockerfile — currently runs as root -- [ ] **10f.2** Audit docker-compose credential handling — database URL with `stackarr:stackarr` in plain text - ---- - -## Phase 11 — Polish + Features - -**Goal**: Production-ready for daily use. - -**Status**: NOT STARTED. - -**Work items**: -- ~~**11.1** Wire embedded torrent/usenet engines to AppState (start session on boot)~~ — DONE (src/main.rs:179-293) -- ~~**11.15** TMDB rate limiting + caching~~ — DONE: shared `TmdbClient` in AppState (rate-limited 4 req/s + 2000-entry LRU cache), used by discover routes and scheduler tasks -- **11.2** Integration tests (Postgres in Docker, full flow testing) -- **11.3** Real cutoff comparison in wanted/cutoff endpoint (needs quality profile parsing) -- **11.4** Backup/restore (export/import DB as JSON) -- **11.5** Health check system (DB, disk space, client connectivity, indexer health) -- **11.6** Import lists (TMDB popular, Trakt watchlist, IMDB list) -- **11.7** Disk scan on schedule -- **11.8** Scene name mapping / XEM integration for anime -- **11.9** Custom format specification engine (full regex) -- **11.10** Blocklist management UI -- **11.11** Log viewer (WebSocket streaming) -- **11.12** OpenAPI/Swagger docs (utoipa) -- **11.13** Prometheus metrics endpoint -- **11.14** Notification providers: Telegram, Slack, email -- **11.15** TMDB rate limiting / caching - ---- - -## What's Left — Priority Order - -| Priority | Work | Phase | Effort | -|----------|------|-------|--------| -| ~~WI-1~~ | ~~**Finish decision engine** (AlreadyImportedSpec, CustomFormatScoreSpec, managers in AppState)~~ | ~~4~~ | ~~DONE~~ | -| ~~WI-2~~ | ~~**Wire search + grab handlers** (indexer fanout → decision engine → download client → queue/history)~~ | ~~4~~ | ~~DONE~~ | -| WI-3 | **RSS automation** (rss_sync_task, missing_search_task, command endpoints, commands table) | 6 | Medium | -| ~~WI-4~~ | ~~**Auth middleware on all protected routes** + secret redaction in responses~~ | ~~10a~~ | ~~DONE~~ | -| ~~WI-5~~ | ~~**Input validation** (path traversal, URL encoding, error sanitization, upload validation)~~ | ~~10c~~ | ~~DONE~~ | -| ~~WI-6~~ | ~~**TLS verification** (Plex configurable per-server, Usenet default-on, credential redaction)~~ | ~~10d~~ | ~~DONE~~ | -| WI-7 | **Rate limiting** (wire existing `governor` dep to API middleware) | 10e | Small | -| WI-8 | **CORS allowlist + CSRF + Docker non-root** | 10b,f | Small | -| ~~WI-9~~ | ~~**Indexarr search fanout** (already wired into search flow + /search page)~~ | ~~7~~ | ~~DONE~~ | -| WI-10 | **Integration tests** | 11 | Medium | -| ~~WI-11~~ | ~~**TMDB rate limiting + caching** (shared client in AppState)~~ | ~~11~~ | ~~DONE~~ | -| WI-12 | **Import lists, scene mapping, custom formats** | 11 | Large | -| WI-13 | **Additional notifications** (Telegram, Slack, email) | 11 | Small | -| WI-14 | **OpenAPI/Swagger + Prometheus metrics** (dependencies present, need wiring) | 11 | Medium | - -### Security Audit Summary (updated 2026-03-28) - -| Category | Severity | Status | -|----------|----------|--------| -| API Authentication | ~~CRITICAL~~ | **FIXED** — `require_auth_middleware` on all protected routes (session, device token, API key, first-boot bypass) | -| CORS | CRITICAL | `mirror_request()` allows any origin (TODO: explicit allowlist) | -| CSRF | CRITICAL | No tokens, no origin validation | -| Path Traversal | ~~HIGH~~ | **OK** — canonicalize + is_dir check | -| TLS Verification | ~~HIGH~~ | **FIXED** — Plex configurable per-server (verify_tls column), Usenet defaults to true | -| Sensitive Data in Responses | ~~HIGH~~ | **FIXED** — `redact_sensitive_fields()` applied to indexer, download client, Plex responses | -| Error Leakage | ~~HIGH~~ | **OK** — Generic "internal server error" returned, full details in tracing::error! | -| Rate Limiting | HIGH | `governor` in Cargo.toml, keyed rate limiter created but not applied to API middleware | -| URL Injection | ~~HIGH~~ | **OK** — `urlencoding::encode()` used on all Newznab params | -| Security Headers | ~~MEDIUM~~ | **PARTIAL** — X-Frame-Options, X-Content-Type-Options, XSS-Protection present; CSP + HSTS missing | -| Docker Root | MEDIUM | No USER directive in Dockerfile | -| Credentials Storage | MEDIUM | Usenet creds in plaintext config | -| File Upload | ~~MEDIUM~~ | **FIXED** — 500 MB size limit + SQLite header validation on migrate endpoint | -| **Frontend XSS** | **OK** | No dangerouslySetInnerHTML, eval, innerHTML | -| **SQL Injection** | **OK** | Parameterized sqlx throughout | -| **Dependencies** | **OK** | All current, no known CVEs | -| **Cryptography** | **OK** | Proper libraries (rustls, rand 0.9, aws-lc-rs) | -| **.gitignore** | **OK** | .env, config.toml, secrets excluded | -| **Committed Secrets** | **OK** | None found | - ---- - -## Reference: *arr Database Backups - -| App | Database Path | -|-----|--------------| -| Sonarr | `TestData/arr-backups/sonarr/sonarr.db` (535 series, 31967 episodes, 9113 files) | -| Radarr | `TestData/arr-backups/radarr/radarr.db` (1212 movies, 1128 files) | -| Prowlarr | `TestData/arr-backups/prowlarr/prowlarr.db` (6 indexers) | - -## Repo - -- GitHub: https://github.com/TheDancingDeveloper-org/NGMS -- GHCR: ghcr.io/thedancingdeveloper-org/ngms -- Deploy: Node B (192.168.1.75), port 9111 +Use [`docs/UNIFIED-ARR-PLAN.md`](docs/UNIFIED-ARR-PLAN.md) for the approved target +state and phase gates. Use the 54-issue coverage ledger in that document and the +open GitHub issues for live work status. diff --git a/PLAN.md b/PLAN.md index 5cb26d63..b3acad39 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,1523 +1,9 @@ -# Arz — Unified Media Manager +# Historical plan pointer -## Context +This file contained the original Arz greenfield design. It is retained at this +path only so old links do not fail; its architecture, PostgreSQL choice, crate +names, and phases are superseded. -Replace Sonarr + Radarr + Prowlarr with a single Rust application. The core is a **media manager** — TV series and movie library management with automated search, grab, and organization. Download clients (rustTorrent, rustnzbd) and indexing (Indexarr) are optional embedded modules that can be swapped for external equivalents (qBittorrent, SABnzbd, Prowlarr, etc.). - -**Key design principles:** -- Media manager first, download clients second -- Everything optional except the core media library -- First-boot wizard configures enabled modules -- Disabled modules are hidden from UI entirely -- PostgreSQL database (avoids SQLite contention) -- Single binary, single container + optional Indexarr sidecar - ---- - -## 1. Cargo Workspace Structure - -``` -arz/ -├── Cargo.toml # Workspace root -├── crates/ -│ ├── arz-core/ # Domain models, DB, config, error types -│ ├── arz-media/ # Media library: series, movies, episodes, files -│ ├── arz-parser/ # Release name parser (quality, episode, language) -│ ├── arz-quality/ # Quality profiles, custom formats, decision engine -│ ├── arz-indexer/ # Indexer hub: Newznab/Torznab client, Indexarr integration -│ ├── arz-download/ # Download client abstraction layer -│ ├── arz-import/ # Post-download import: match, rename, move, organize -│ ├── arz-scheduler/ # Background jobs: RSS, search, import scan, housekeeping -│ ├── arz-metadata/ # External metadata: TMDB, TVDB, OMDB clients -│ ├── arz-web/ # Axum HTTP server, REST API, WebSocket, auth -│ └── arz-notify/ # Notifications: webhook, Telegram, Discord, email, etc. -├── src/ -│ └── main.rs # Binary: CLI args, startup, signal handling -├── ui/ # React frontend (Vite + TypeScript) -├── migrations/ # PostgreSQL migrations (sqlx) -├── docker/ -│ ├── Dockerfile -│ └── docker-compose.yml # arz + optional indexarr sidecar -└── config.example.toml -``` - -### Crate dependency graph - -``` -main.rs - └── arz-web - ├── arz-media - │ ├── arz-core - │ ├── arz-parser - │ └── arz-metadata - ├── arz-quality - │ ├── arz-core - │ └── arz-parser - ├── arz-indexer - │ ├── arz-core - │ └── arz-parser - ├── arz-download - │ └── arz-core - ├── arz-import - │ ├── arz-core - │ ├── arz-media - │ ├── arz-parser - │ └── arz-quality - ├── arz-scheduler - │ ├── arz-core - │ ├── arz-media - │ ├── arz-indexer - │ ├── arz-download - │ ├── arz-import - │ └── arz-quality - └── arz-notify - └── arz-core -``` - -### Feature flags (Cargo features on workspace) - -```toml -[features] -default = ["ui"] -ui = [] # Embed React SPA -torrent-embedded = ["librtbit"] # Embedded rustTorrent engine -usenet-embedded = ["nzb-core", "nzb-web", "nzb-nntp", "nzb-decode", "nzb-postproc"] -indexarr-sidecar = [] # Indexarr HTTP client integration -``` - ---- - -## 2. Domain Model - -### 2.1 Media Library (arz-media) - -```rust -// --- TV --- -pub struct Series { - pub id: i64, - pub title: String, - pub clean_title: String, // normalized for matching - pub sort_title: String, - pub overview: Option, - pub status: SeriesStatus, // Continuing, Ended, Upcoming, Deleted - pub network: Option, - pub air_time: Option, - pub first_aired: Option, - pub year: Option, - pub runtime: Option, // minutes - pub path: PathBuf, // library root folder for this series - pub root_folder_id: i64, - pub quality_profile_id: i64, - pub season_folder: bool, - pub monitored: bool, - pub use_scene_numbering: bool, - pub series_type: SeriesType, // Standard, Daily, Anime - // External IDs - pub tvdb_id: Option, - pub imdb_id: Option, - pub tmdb_id: Option, - pub tvmaze_id: Option, - pub mal_id: Option, - pub tags: Vec, - pub added_at: DateTime, - pub last_info_sync: Option>, -} - -pub struct Episode { - pub id: i64, - pub series_id: i64, - pub episode_file_id: Option, - pub season_number: i32, - pub episode_number: i32, - pub absolute_number: Option, - pub scene_season_number: Option, - pub scene_episode_number: Option, - pub scene_absolute_number: Option, - pub title: Option, - pub overview: Option, - pub air_date: Option, - pub air_date_utc: Option>, - pub runtime: Option, - pub monitored: bool, - pub grabbed: bool, // currently being downloaded - pub last_search_time: Option>, -} - -// --- Movies --- -pub struct Movie { - pub id: i64, - pub title: String, - pub clean_title: String, - pub sort_title: String, - pub overview: Option, - pub year: Option, - pub studio: Option, - pub path: PathBuf, - pub root_folder_id: i64, - pub quality_profile_id: i64, - pub monitored: bool, - pub minimum_availability: Availability, // Announced, InCinemas, Released - pub movie_file_id: Option, - // External IDs - pub tmdb_id: Option, - pub imdb_id: Option, - // Dates - pub in_cinemas: Option, - pub physical_release: Option, - pub digital_release: Option, - pub tags: Vec, - pub added_at: DateTime, - pub last_info_sync: Option>, - pub collection_tmdb_id: Option, -} - -// --- Shared --- -pub struct MediaFile { - pub id: i64, - pub media_type: MediaType, // Series, Movie - pub relative_path: String, - pub size: i64, - pub date_added: DateTime, - pub quality: QualityModel, - pub languages: Vec, - pub scene_name: Option, - pub release_group: Option, - pub release_hash: Option, - pub edition: Option, // movies: Director's Cut, etc. - pub media_info: Option, - pub indexer_flags: i32, -} - -pub enum MediaType { Series, Movie } -pub enum SeriesStatus { Continuing, Ended, Upcoming, Deleted } -pub enum SeriesType { Standard, Daily, Anime } -pub enum Availability { Announced, InCinemas, Released } -``` - -### 2.2 Parser (arz-parser) - -```rust -pub struct ParsedRelease { - pub title: String, - pub release_title: String, // original full name - pub year: Option, - pub quality: QualityModel, - pub languages: Vec, - pub release_group: Option, - pub release_hash: Option, - pub edition: Option, - // TV-specific (Option — absent for movies) - pub season_number: Option, - pub episode_numbers: Vec, - pub absolute_episode_numbers: Vec, - pub air_date: Option, // daily shows - pub is_full_season: bool, - pub is_multi_season: bool, - pub is_special: bool, - // Movie-specific - pub imdb_id: Option, - pub tmdb_id: Option, -} - -pub struct QualityModel { - pub quality: Quality, - pub revision: Revision, - pub source: QualitySource, // where detected from -} - -pub enum Quality { - Unknown, - SDTV, DVD, WEBDL480p, WEBRip480p, Bluray480p, - HDTV720p, WEBDL720p, WEBRip720p, Bluray720p, - HDTV1080p, WEBDL1080p, WEBRip1080p, Bluray1080p, Remux1080p, - HDTV2160p, WEBDL2160p, WEBRip2160p, Bluray2160p, Remux2160p, - Raw, -} - -pub struct Revision { - pub version: i32, // 1 = original, 2+ = proper/repack - pub real: i32, // 0 = normal, 1+ = REAL tag - pub is_repack: bool, -} -``` - -### 2.3 Quality Profiles (arz-quality) - -```rust -pub struct QualityProfile { - pub id: i64, - pub name: String, - pub cutoff: Quality, // stop upgrading at this level - pub items: Vec, - pub upgrade_allowed: bool, - pub min_format_score: i32, - pub cutoff_format_score: i32, -} - -pub struct QualityProfileItem { - pub quality: Option, // None = group header - pub allowed: bool, - pub items: Vec, // nested groups -} - -pub struct CustomFormat { - pub id: i64, - pub name: String, - pub specifications: Vec, -} - -pub struct FormatSpecification { - pub field: FormatField, // ReleaseName, Quality, Language, IndexerFlag, etc. - pub pattern: String, // regex or enum match - pub negate: bool, - pub required: bool, -} - -pub struct CustomFormatScore { - pub profile_id: i64, - pub format_id: i64, - pub score: i32, -} -``` - -### 2.4 Decision Engine (arz-quality) - -```rust -pub struct DownloadDecision { - pub remote_release: RemoteRelease, - pub rejections: Vec, -} - -pub struct Rejection { - pub reason: String, - pub rejection_type: RejectionType, // Permanent, Temporary -} - -/// Each spec checks one concern. Run in order, short-circuit on permanent reject. -pub trait DecisionSpecification: Send + Sync { - fn is_satisfied(&self, decision: &DecisionContext) -> SpecificationResult; -} - -// Specifications: -// - QualityAllowedSpec -// - QualityCutoffSpec (already have good enough?) -// - SizeLimitSpec (min/max per quality) -// - AgeLimitSpec (usenet retention) -// - BlocklistSpec -// - AlreadyImportedSpec -// - QueueConflictSpec (already downloading?) -// - CustomFormatScoreSpec -// - ProtocolSpec (usenet vs torrent preference) -// - MinimumSeedersSpec (torrents) -// - RepackSpec (prefer repacks of same release) -// - RawDiskSpec (reject raw disk images) -// - SampleSpec (reject samples) -// - LanguageSpec -``` - -### 2.5 Indexer Hub (arz-indexer) - -```rust -pub struct IndexerConfig { - pub id: i64, - pub name: String, - pub indexer_type: IndexerType, - pub base_url: String, - pub api_key: Option, - pub protocol: DownloadProtocol, // Usenet, Torrent - pub categories: Vec, // Newznab category IDs - pub enabled: bool, - pub priority: i32, - pub supports_search: bool, - pub supports_rss: bool, - pub proxy: Option, -} - -pub enum IndexerType { - Newznab, // Standard Newznab (NZB indexers) - Torznab, // Standard Torznab (torrent indexers) - IndexarrTorznab, // Indexarr sidecar via Torznab - IndexarrApi, // Indexarr sidecar via REST API -} - -pub struct ReleaseInfo { - pub guid: String, - pub title: String, - pub download_url: Option, - pub info_url: Option, - pub indexer_id: i64, - pub protocol: DownloadProtocol, - pub size: i64, - pub age: i64, // days - pub publish_date: DateTime, - // Torrent-specific - pub info_hash: Option, - pub magnet_url: Option, - pub seeders: Option, - pub leechers: Option, - // Usenet-specific - pub nzb_url: Option, - // IDs from indexer - pub tvdb_id: Option, - pub imdb_id: Option, - pub tmdb_id: Option, - // Parsed - pub categories: Vec, // Newznab category IDs - pub indexer_flags: Vec, -} - -pub enum DownloadProtocol { Usenet, Torrent } -``` - -### 2.6 Download Client Abstraction (arz-download) - -```rust -/// Unified interface — works for embedded AND external clients -#[async_trait] -pub trait DownloadClient: Send + Sync { - fn protocol(&self) -> DownloadProtocol; - fn name(&self) -> &str; - - async fn add(&self, release: &GrabRequest) -> Result; // returns download_id - async fn get_items(&self) -> Result>; - async fn remove(&self, download_id: &str, delete_files: bool) -> Result<()>; - async fn pause(&self, download_id: &str) -> Result<()>; - async fn resume(&self, download_id: &str) -> Result<()>; - async fn test(&self) -> Result<()>; - async fn status(&self) -> Result; -} - -pub struct GrabRequest { - pub release: ReleaseInfo, - pub category: String, - pub download_url: String, // NZB URL or magnet/torrent URL -} - -pub struct DownloadItem { - pub download_id: String, - pub title: String, - pub status: DownloadStatus, - pub total_size: i64, - pub remaining_size: i64, - pub output_path: Option, - pub category: Option, - pub can_move_files: bool, - pub can_be_removed: bool, - pub protocol: DownloadProtocol, -} - -pub enum DownloadStatus { - Queued, Downloading, Paused, - PostProcessing, // usenet: par2/extract - Completed, Failed, Warning, -} - -// --- Implementations --- - -// External clients (HTTP API): -pub struct QBittorrentClient { /* qBit WebUI API */ } -pub struct TransmissionClient { /* Transmission RPC */ } -pub struct SabnzbdClient { /* SABnzbd API */ } -pub struct NzbgetClient { /* NZBGet API */ } - -// Embedded clients (library calls): -#[cfg(feature = "torrent-embedded")] -pub struct EmbeddedTorrentClient { - session: Arc, -} - -#[cfg(feature = "usenet-embedded")] -pub struct EmbeddedUsenetClient { - queue_manager: Arc, -} -``` - -### 2.7 History & Queue (arz-core) - -```rust -pub struct HistoryEvent { - pub id: i64, - pub media_type: MediaType, - pub media_id: i64, // series_id or movie_id - pub episode_id: Option, // TV only - pub event_type: HistoryEventType, - pub quality: QualityModel, - pub languages: Vec, - pub source_title: String, // release name - pub download_id: Option, - pub indexer_id: Option, - pub download_client: Option, - pub data: serde_json::Value, // event-specific metadata - pub occurred_at: DateTime, -} - -pub enum HistoryEventType { - Grabbed, Imported, DownloadFailed, - FileDeleted, FileRenamed, DownloadIgnored, -} - -pub struct QueueItem { - pub id: i64, - pub media_type: MediaType, - pub media_id: i64, - pub episode_id: Option, - pub quality: QualityModel, - pub languages: Vec, - pub size: i64, - pub title: String, - pub status: DownloadStatus, - pub time_left: Option, - pub download_id: String, - pub download_client: String, - pub protocol: DownloadProtocol, - pub indexer: String, - pub error_message: Option, - pub added_at: DateTime, -} - -pub struct Blocklist { - pub id: i64, - pub media_type: MediaType, - pub media_id: i64, - pub source_title: String, - pub quality: QualityModel, - pub languages: Vec, - pub indexer_id: Option, - pub info_hash: Option, // torrent matching - pub message: Option, - pub added_at: DateTime, -} -``` - -### 2.8 Modules/Features Configuration (arz-core) - -```rust -/// Persisted in DB — set during first-boot, changeable in settings -pub struct EnabledModules { - pub tv_management: bool, // Series/Episodes - pub movie_management: bool, // Movies - pub torrent_embedded: bool, // Embedded rustTorrent - pub usenet_embedded: bool, // Embedded rustnzbd - pub torrent_external: bool, // External torrent clients - pub usenet_external: bool, // External usenet clients - pub indexarr_sidecar: bool, // Indexarr integration - pub external_indexers: bool, // Newznab/Torznab indexers - pub notifications: bool, -} -``` - ---- - -## 3. Database Schema (PostgreSQL) - -```sql --- Core -CREATE TABLE app_config ( - key TEXT PRIMARY KEY, - value JSONB NOT NULL -); - -CREATE TABLE enabled_modules ( - id SERIAL PRIMARY KEY, - module TEXT UNIQUE NOT NULL, - enabled BOOLEAN NOT NULL DEFAULT false, - config JSONB -); - -CREATE TABLE root_folders ( - id SERIAL PRIMARY KEY, - path TEXT NOT NULL UNIQUE, - media_type TEXT NOT NULL, -- 'series' | 'movie' - free_space BIGINT, - last_checked TIMESTAMPTZ -); - -CREATE TABLE tags ( - id SERIAL PRIMARY KEY, - label TEXT NOT NULL UNIQUE -); - --- Quality system -CREATE TABLE quality_profiles ( - id SERIAL PRIMARY KEY, - name TEXT NOT NULL, - cutoff INTEGER NOT NULL, - upgrade_allowed BOOLEAN NOT NULL DEFAULT true, - min_format_score INTEGER NOT NULL DEFAULT 0, - cutoff_format_score INTEGER NOT NULL DEFAULT 0, - items JSONB NOT NULL -- ordered quality items tree -); - -CREATE TABLE custom_formats ( - id SERIAL PRIMARY KEY, - name TEXT NOT NULL, - specifications JSONB NOT NULL -); - -CREATE TABLE custom_format_scores ( - profile_id INTEGER REFERENCES quality_profiles(id) ON DELETE CASCADE, - format_id INTEGER REFERENCES custom_formats(id) ON DELETE CASCADE, - score INTEGER NOT NULL, - PRIMARY KEY (profile_id, format_id) -); - --- TV -CREATE TABLE series ( - id SERIAL PRIMARY KEY, - title TEXT NOT NULL, - clean_title TEXT NOT NULL, - sort_title TEXT NOT NULL, - overview TEXT, - status TEXT NOT NULL DEFAULT 'continuing', - series_type TEXT NOT NULL DEFAULT 'standard', - network TEXT, - air_time TIME, - first_aired DATE, - year INTEGER, - runtime INTEGER, - path TEXT NOT NULL, - root_folder_id INTEGER REFERENCES root_folders(id), - quality_profile_id INTEGER REFERENCES quality_profiles(id), - season_folder BOOLEAN NOT NULL DEFAULT true, - monitored BOOLEAN NOT NULL DEFAULT true, - use_scene_numbering BOOLEAN NOT NULL DEFAULT false, - tvdb_id INTEGER, - imdb_id TEXT, - tmdb_id INTEGER, - tvmaze_id INTEGER, - mal_id INTEGER, - images JSONB, - genres TEXT[], - tags INTEGER[], - added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - last_info_sync TIMESTAMPTZ -); -CREATE INDEX idx_series_tvdb ON series(tvdb_id); -CREATE INDEX idx_series_tmdb ON series(tmdb_id); -CREATE INDEX idx_series_imdb ON series(imdb_id); -CREATE INDEX idx_series_clean_title ON series(clean_title); - -CREATE TABLE seasons ( - id SERIAL PRIMARY KEY, - series_id INTEGER NOT NULL REFERENCES series(id) ON DELETE CASCADE, - season_number INTEGER NOT NULL, - monitored BOOLEAN NOT NULL DEFAULT true, - UNIQUE(series_id, season_number) -); - -CREATE TABLE episodes ( - id SERIAL PRIMARY KEY, - series_id INTEGER NOT NULL REFERENCES series(id) ON DELETE CASCADE, - season_number INTEGER NOT NULL, - episode_number INTEGER NOT NULL, - absolute_number INTEGER, - scene_season_number INTEGER, - scene_episode_number INTEGER, - scene_absolute_number INTEGER, - title TEXT, - overview TEXT, - air_date DATE, - air_date_utc TIMESTAMPTZ, - runtime INTEGER, - monitored BOOLEAN NOT NULL DEFAULT true, - episode_file_id INTEGER REFERENCES media_files(id) ON DELETE SET NULL, - last_search_time TIMESTAMPTZ, - UNIQUE(series_id, season_number, episode_number) -); -CREATE INDEX idx_episodes_air_date ON episodes(air_date_utc); - --- Movies -CREATE TABLE movies ( - id SERIAL PRIMARY KEY, - title TEXT NOT NULL, - clean_title TEXT NOT NULL, - sort_title TEXT NOT NULL, - overview TEXT, - year INTEGER, - studio TEXT, - path TEXT NOT NULL, - root_folder_id INTEGER REFERENCES root_folders(id), - quality_profile_id INTEGER REFERENCES quality_profiles(id), - monitored BOOLEAN NOT NULL DEFAULT true, - minimum_availability TEXT NOT NULL DEFAULT 'released', - movie_file_id INTEGER REFERENCES media_files(id) ON DELETE SET NULL, - tmdb_id INTEGER, - imdb_id TEXT, - in_cinemas DATE, - physical_release DATE, - digital_release DATE, - images JSONB, - genres TEXT[], - tags INTEGER[], - collection_tmdb_id INTEGER, - added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - last_info_sync TIMESTAMPTZ -); -CREATE INDEX idx_movies_tmdb ON movies(tmdb_id); -CREATE INDEX idx_movies_imdb ON movies(imdb_id); -CREATE INDEX idx_movies_clean_title ON movies(clean_title); - -CREATE TABLE alternative_titles ( - id SERIAL PRIMARY KEY, - media_type TEXT NOT NULL, - media_id INTEGER NOT NULL, - title TEXT NOT NULL, - clean_title TEXT NOT NULL, - scene_name BOOLEAN NOT NULL DEFAULT false -); -CREATE INDEX idx_alt_titles_clean ON alternative_titles(clean_title); - --- Media files (shared between TV and movies) -CREATE TABLE media_files ( - id SERIAL PRIMARY KEY, - media_type TEXT NOT NULL, - relative_path TEXT NOT NULL, - size BIGINT NOT NULL, - date_added TIMESTAMPTZ NOT NULL DEFAULT NOW(), - quality JSONB NOT NULL, - languages JSONB NOT NULL, - scene_name TEXT, - release_group TEXT, - release_hash TEXT, - edition TEXT, - media_info JSONB, - indexer_flags INTEGER NOT NULL DEFAULT 0 -); - --- Episode-to-file join (multi-episode files) -CREATE TABLE episode_files ( - episode_id INTEGER NOT NULL REFERENCES episodes(id) ON DELETE CASCADE, - media_file_id INTEGER NOT NULL REFERENCES media_files(id) ON DELETE CASCADE, - PRIMARY KEY (episode_id, media_file_id) -); - --- Indexers -CREATE TABLE indexers ( - id SERIAL PRIMARY KEY, - name TEXT NOT NULL, - indexer_type TEXT NOT NULL, - base_url TEXT NOT NULL, - api_key TEXT, - protocol TEXT NOT NULL, - categories INTEGER[], - enabled BOOLEAN NOT NULL DEFAULT true, - priority INTEGER NOT NULL DEFAULT 25, - supports_search BOOLEAN NOT NULL DEFAULT true, - supports_rss BOOLEAN NOT NULL DEFAULT true, - config JSONB, -- proxy, extra settings - last_rss_sync TIMESTAMPTZ -); - --- Download clients -CREATE TABLE download_clients ( - id SERIAL PRIMARY KEY, - name TEXT NOT NULL, - client_type TEXT NOT NULL, -- 'embedded_torrent', 'embedded_usenet', 'qbittorrent', 'sabnzbd', etc. - protocol TEXT NOT NULL, - config JSONB NOT NULL, -- host, port, api_key, category mappings - enabled BOOLEAN NOT NULL DEFAULT true, - priority INTEGER NOT NULL DEFAULT 1 -); - --- Queue (tracked downloads in progress) -CREATE TABLE queue ( - id SERIAL PRIMARY KEY, - media_type TEXT NOT NULL, - media_id INTEGER NOT NULL, - episode_id INTEGER, - title TEXT NOT NULL, - quality JSONB NOT NULL, - languages JSONB, - size BIGINT, - status TEXT NOT NULL, - download_id TEXT NOT NULL, - download_client_id INTEGER REFERENCES download_clients(id), - indexer_id INTEGER REFERENCES indexers(id), - protocol TEXT NOT NULL, - error_message TEXT, - added_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); -CREATE INDEX idx_queue_download_id ON queue(download_id); - --- History -CREATE TABLE history ( - id BIGSERIAL PRIMARY KEY, - media_type TEXT NOT NULL, - media_id INTEGER NOT NULL, - episode_id INTEGER, - event_type TEXT NOT NULL, - quality JSONB NOT NULL, - languages JSONB, - source_title TEXT NOT NULL, - download_id TEXT, - indexer_id INTEGER, - download_client TEXT, - data JSONB, - occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); -CREATE INDEX idx_history_media ON history(media_type, media_id); -CREATE INDEX idx_history_occurred ON history(occurred_at DESC); -CREATE INDEX idx_history_download_id ON history(download_id); - --- Blocklist -CREATE TABLE blocklist ( - id SERIAL PRIMARY KEY, - media_type TEXT NOT NULL, - media_id INTEGER NOT NULL, - source_title TEXT NOT NULL, - quality JSONB NOT NULL, - languages JSONB, - indexer_id INTEGER, - info_hash TEXT, - message TEXT, - added_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); -CREATE INDEX idx_blocklist_media ON blocklist(media_type, media_id); -CREATE INDEX idx_blocklist_hash ON blocklist(info_hash); - --- Naming conventions -CREATE TABLE naming_config ( - id SERIAL PRIMARY KEY, - media_type TEXT NOT NULL UNIQUE, - rename_files BOOLEAN NOT NULL DEFAULT true, - standard_format TEXT, -- token pattern: {Series Title} - S{season:00}E{episode:00} - daily_format TEXT, - anime_format TEXT, - season_folder_format TEXT, - movie_format TEXT, - movie_folder_format TEXT, - colon_replacement TEXT NOT NULL DEFAULT 'smart' -); - --- Notifications -CREATE TABLE notification_providers ( - id SERIAL PRIMARY KEY, - name TEXT NOT NULL, - provider_type TEXT NOT NULL, -- 'webhook', 'telegram', 'discord', etc. - config JSONB NOT NULL, - on_grab BOOLEAN NOT NULL DEFAULT false, - on_import BOOLEAN NOT NULL DEFAULT false, - on_upgrade BOOLEAN NOT NULL DEFAULT false, - on_health_issue BOOLEAN NOT NULL DEFAULT false, - on_failure BOOLEAN NOT NULL DEFAULT false, - enabled BOOLEAN NOT NULL DEFAULT true -); - --- Import lists (external sources of media to add) -CREATE TABLE import_lists ( - id SERIAL PRIMARY KEY, - name TEXT NOT NULL, - list_type TEXT NOT NULL, -- 'tmdb_popular', 'trakt_watchlist', 'imdb_list', etc. - media_type TEXT NOT NULL, - config JSONB NOT NULL, - quality_profile_id INTEGER REFERENCES quality_profiles(id), - root_folder_id INTEGER REFERENCES root_folders(id), - monitored BOOLEAN NOT NULL DEFAULT true, - enabled BOOLEAN NOT NULL DEFAULT true, - poll_interval_secs INTEGER NOT NULL DEFAULT 3600 -); -``` - ---- - -## 4. REST API Design - -``` -# First-boot & system -GET /api/v1/system/status # Version, uptime, enabled modules -GET /api/v1/system/health # Health checks -POST /api/v1/setup/init # First-boot: set modules + basic config -PUT /api/v1/setup/modules # Enable/disable modules (restart required for embedded clients) - -# Series (hidden if tv_management disabled) -GET /api/v1/series # List all -POST /api/v1/series # Add series (lookup + add) -GET /api/v1/series/{id} -PUT /api/v1/series/{id} -DELETE /api/v1/series/{id} -GET /api/v1/series/lookup?term= # Search TVDB/TMDB -GET /api/v1/series/{id}/episodes -GET /api/v1/episode/{id} -PUT /api/v1/episode/{id} # Toggle monitored, etc. - -# Movies (hidden if movie_management disabled) -GET /api/v1/movie # List all -POST /api/v1/movie # Add movie -GET /api/v1/movie/{id} -PUT /api/v1/movie/{id} -DELETE /api/v1/movie/{id} -GET /api/v1/movie/lookup?term= # Search TMDB - -# Calendar -GET /api/v1/calendar?start=&end= # Upcoming episodes + movie releases - -# Wanted -GET /api/v1/wanted/missing # Monitored, no file -GET /api/v1/wanted/cutoff # Have file, below cutoff quality - -# Media files -GET /api/v1/mediafile # All files -GET /api/v1/mediafile/{id} -DELETE /api/v1/mediafile/{id} -PUT /api/v1/rename # Preview rename -POST /api/v1/rename # Execute rename - -# Manual import -GET /api/v1/manualimport?folder= # Scan folder, return matches -POST /api/v1/manualimport # Execute import of selected files - -# Quality profiles -GET /api/v1/qualityprofile -POST /api/v1/qualityprofile -PUT /api/v1/qualityprofile/{id} -DELETE /api/v1/qualityprofile/{id} - -# Custom formats -GET /api/v1/customformat -POST /api/v1/customformat -PUT /api/v1/customformat/{id} -DELETE /api/v1/customformat/{id} - -# Indexers -GET /api/v1/indexer -POST /api/v1/indexer -PUT /api/v1/indexer/{id} -DELETE /api/v1/indexer/{id} -POST /api/v1/indexer/{id}/test - -# Download clients -GET /api/v1/downloadclient -POST /api/v1/downloadclient -PUT /api/v1/downloadclient/{id} -DELETE /api/v1/downloadclient/{id} -POST /api/v1/downloadclient/{id}/test - -# Releases (search results) -GET /api/v1/release?episodeId= # Search indexers for episode -GET /api/v1/release?movieId= # Search indexers for movie -POST /api/v1/release # Grab a release (send to download client) -POST /api/v1/release/push # External push (webhook from indexer) - -# Queue -GET /api/v1/queue # Current downloads -DELETE /api/v1/queue/{id} # Remove + optional blocklist -POST /api/v1/queue/grab/{id} # Force grab pending item - -# History -GET /api/v1/history # Paginated event log -POST /api/v1/history/failed/{id} # Mark as failed → blocklist + re-search - -# Blocklist -GET /api/v1/blocklist -DELETE /api/v1/blocklist/{id} -DELETE /api/v1/blocklist/bulk - -# Commands (async job triggers) -POST /api/v1/command # { name: "SeriesSearch", seriesId: 1 } -GET /api/v1/command/{id} # Check command status - -# Supported commands: -# SeriesSearch, SeasonSearch, EpisodeSearch -# MovieSearch -# RssSync -# RefreshSeries, RefreshMovie -# DiskScan (rescan library) -# MissingSearch, CutoffSearch -# Housekeeping - -# Config -GET /api/v1/config/naming -PUT /api/v1/config/naming -GET /api/v1/config/general -PUT /api/v1/config/general - -# Embedded torrent client status (hidden if not enabled) -GET /api/v1/torrent/status # rustTorrent session stats -GET /api/v1/torrent/list # Active torrents - -# Embedded usenet client status (hidden if not enabled) -GET /api/v1/usenet/status # rustnzbd queue stats -GET /api/v1/usenet/servers # NNTP server config - -# Notifications -GET /api/v1/notification -POST /api/v1/notification -PUT /api/v1/notification/{id} -DELETE /api/v1/notification/{id} -POST /api/v1/notification/{id}/test - -# Tags -GET /api/v1/tag -POST /api/v1/tag -PUT /api/v1/tag/{id} -DELETE /api/v1/tag/{id} - -# Root folders -GET /api/v1/rootfolder -POST /api/v1/rootfolder -DELETE /api/v1/rootfolder/{id} - -# Import lists -GET /api/v1/importlist -POST /api/v1/importlist -PUT /api/v1/importlist/{id} -DELETE /api/v1/importlist/{id} - -# Log -GET /api/v1/log # Paginated log entries -WS /api/v1/log/stream # Real-time log WebSocket - -# Indexarr sidecar (hidden if not enabled) -GET /api/v1/indexarr/status # Sidecar health + stats -GET /api/v1/indexarr/search # Proxy search through sidecar -``` - ---- - -## 5. Integration Architecture - -### 5.1 Embedded rustTorrent - -```rust -#[cfg(feature = "torrent-embedded")] -pub struct EmbeddedTorrentClient { - session: Arc, - api: librtbit::Api, -} - -impl EmbeddedTorrentClient { - pub async fn new(config: &TorrentConfig) -> Result { - let opts = SessionOptions { - persistence: Some(SessionPersistenceConfig::Json { - folder: Some(config.data_dir.join("torrent-state")), - }), - listen: Some(ListenerOptions { /* ... */ }), - ..Default::default() - }; - let session = Session::new_with_opts(config.download_dir.clone(), opts).await?; - let api = Api::new(Arc::clone(&session), None); - Ok(Self { session, api }) - } -} - -#[async_trait] -impl DownloadClient for EmbeddedTorrentClient { - async fn add(&self, req: &GrabRequest) -> Result { - let opts = AddTorrentOptions { - category: Some(req.category.clone()), - ..Default::default() - }; - let resp = self.session.add_torrent( - AddTorrent::from_url(&req.download_url), - Some(opts), - ).await?; - Ok(resp.into_handle().unwrap().info_hash().to_string()) - } - - async fn get_items(&self) -> Result> { - let list = self.api.api_torrent_list(); - // Map TorrentListResponse → Vec - } - // ... -} -``` - -### 5.2 Embedded rustnzbd - -```rust -#[cfg(feature = "usenet-embedded")] -pub struct EmbeddedUsenetClient { - queue_manager: Arc, -} - -impl EmbeddedUsenetClient { - pub async fn new(config: &UsenetConfig) -> Result { - let startup = nzb_web::startup::initialize( - StartupConfig { - config_path: config.config_path.clone(), - data_dir: Some(config.data_dir.clone()), - ..Default::default() - }, - None, - ).await?; - Ok(Self { queue_manager: startup.queue_manager }) - } -} - -#[async_trait] -impl DownloadClient for EmbeddedUsenetClient { - async fn add(&self, req: &GrabRequest) -> Result { - let nzb_data = reqwest::get(&req.download_url).await?.bytes().await?; - let mut job = nzb_core::nzb_parser::parse_nzb(&req.release.title, &nzb_data)?; - job.category = req.category.clone(); - self.queue_manager.add_job(job.clone(), Some(nzb_data.to_vec())).await?; - Ok(job.id) - } - - async fn get_items(&self) -> Result> { - let jobs = self.queue_manager.get_jobs(); - // Map NzbJob → DownloadItem - } - // ... -} -``` - -### 5.3 External clients - -```rust -// qBittorrent WebUI API -pub struct QBittorrentClient { base_url: String, session: reqwest::Client } -// SABnzbd API -pub struct SabnzbdClient { base_url: String, api_key: String, session: reqwest::Client } -// NZBGet JSON-RPC -pub struct NzbgetClient { base_url: String, session: reqwest::Client } -``` - -### 5.4 Indexarr Sidecar - -```rust -pub struct IndexarrClient { - base_url: String, - api_key: String, - client: reqwest::Client, -} - -impl IndexarrClient { - /// Use Torznab API (Sonarr/Radarr compatible) - pub async fn torznab_search(&self, params: &TorznabQuery) -> Result> { - // GET {base_url}/api/torznab?t=search&q=...&apikey=... - } - - /// Use native REST API for richer results - pub async fn search(&self, query: &str, filters: &SearchFilters) -> Result> { - // GET {base_url}/api/v1/search?q=... - } - - pub async fn status(&self) -> Result { - // GET {base_url}/api/v1/stats - } -} -``` - ---- - -## 6. Search Flow - -``` -User triggers search (manual or scheduled) - │ - ▼ -SearchService - ├── Build search criteria from media (Series+Episode or Movie) - │ - Extract IDs: tvdb_id, imdb_id, tmdb_id - │ - Build query terms: "Show Name S01E05" / "Movie Name 2024" - │ - Determine categories: TV HD (5040) / Movies HD (2040) etc. - │ - ├── Fan out to all enabled indexers (parallel) - │ ├── Newznab indexers → GET /api?t=tvsearch&tvdbid=&season=&ep= - │ ├── Torznab indexers → GET /api?t=tvsearch&tvdbid=&season=&ep= - │ ├── Indexarr (Torznab) → GET /api/torznab?t=tvsearch&... - │ └── Indexarr (REST) → GET /api/v1/search?q=... - │ - ├── Aggregate all ReleaseInfo results - │ - ├── Parse each release name → ParsedRelease - │ - Extract quality, language, release group - │ - Match to correct media (by title similarity + IDs) - │ - ├── Run Decision Engine on each release - │ - Check against quality profile - │ - Filter by size limits - │ - Check blocklist - │ - Check if already in queue - │ - Check if already imported at same/better quality - │ - Score custom formats - │ - Reject or approve - │ - ├── Sort approved releases by preference - │ - Quality rank (from profile) - │ - Custom format score - │ - Protocol preference (usenet vs torrent) - │ - Indexer priority - │ - Age (prefer newer for usenet) - │ - Seeders (prefer more for torrent) - │ - └── Grab best release (or return ranked list for interactive search) - ├── Select download client (by protocol + priority) - ├── client.add(GrabRequest) → download_id - ├── Insert queue record - ├── Insert history record (Grabbed) - └── Send notification (on_grab) -``` - ---- - -## 7. Download → Import Flow - -``` -Background: CompletedDownloadService (polls every 60s) - │ - ├── For each enabled download client: - │ └── client.get_items() → Vec - │ - ├── Match DownloadItem.download_id to queue records - │ - ├── For completed items: - │ │ - │ ▼ - │ ImportService - │ ├── Scan output_path for media files - │ ├── Parse each filename → ParsedRelease - │ ├── Match to Series+Episode or Movie (using queue record as hint) - │ ├── Run import decision engine (quality upgrade check) - │ │ - │ ├── For each approved file: - │ │ ├── Build target path using naming config tokens - │ │ │ TV: {root}/{Series}/{Season 01}/{Series - S01E05 - Title [Quality]} - │ │ │ Movie: {root}/{Movie (Year)}/{Movie (Year) - [Quality]} - │ │ ├── Move/hardlink/copy file to library - │ │ ├── Create/update MediaFile record - │ │ ├── Link to Episode or Movie - │ │ ├── Delete old file if upgrade - │ │ ├── Insert history record (Imported) - │ │ └── Send notification (on_import) - │ │ - │ └── Remove queue record - │ - └── For failed items: - ├── Insert history record (DownloadFailed) - ├── Add to blocklist (optional, based on settings) - ├── Remove from download client - ├── Remove queue record - ├── Trigger re-search if configured - └── Send notification (on_failure) -``` - ---- - -## 8. Configuration - -```toml -# config.toml - -[general] -instance_name = "Arz" -bind_addr = "0.0.0.0" -port = 8989 -data_dir = "/config" -log_level = "info" - -[database] -url = "postgresql://arz:password@localhost:5432/arz" -max_connections = 20 - -[auth] -method = "forms" # none, forms, basic, external -api_key = "auto-generated" - -# --- Optional embedded modules --- - -[torrent] -enabled = false # set true via first-boot -download_dir = "/downloads/torrents/incomplete" -complete_dir = "/downloads/torrents/complete" -listen_port = 6881 -dht_enabled = true -peer_limit = 200 -upload_limit_bps = 0 -download_limit_bps = 0 - -[usenet] -enabled = false # set true via first-boot -incomplete_dir = "/downloads/usenet/incomplete" -complete_dir = "/downloads/usenet/complete" -max_active_downloads = 3 - -[[usenet.servers]] -name = "Primary" -host = "news.example.com" -port = 563 -ssl = true -username = "" -password = "" -connections = 8 -priority = 0 - -[indexarr] -enabled = false # set true via first-boot -url = "http://indexarr:8080" -api_key = "" -mode = "peer" # peer (sync only), full (DHT + sync) - -[naming.series] -rename = true -standard = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} [{Quality Title}]" -daily = "{Series Title} - {Air-Date} - {Episode Title} [{Quality Title}]" -anime = "{Series Title} - S{season:00}E{episode:00} - {Absolute Episode} - {Episode Title} [{Quality Title}]" -season_folder = "Season {season:00}" - -[naming.movie] -rename = true -standard = "{Movie Title} ({Release Year}) [{Quality Title}]{[Edition Tags]}" -folder = "{Movie Title} ({Release Year})" -``` - ---- - -## 9. Background Services (arz-scheduler) - -All run as tokio tasks inside the single process: - -| Service | Interval | Purpose | -|---------|----------|---------| -| **RssSyncService** | 15 min | Poll all indexers' RSS feeds, auto-grab matching releases | -| **CompletedDownloadService** | 60s | Poll download clients, trigger import for completed items | -| **RefreshSeriesService** | 12 hr | Refresh metadata from TVDB/TMDB for all series | -| **RefreshMovieService** | 12 hr | Refresh metadata from TMDB for all movies | -| **DiskScanService** | on-demand | Scan library folders, detect new/removed files | -| **HousekeepingService** | 24 hr | Clean old history, expired blocklist entries, orphaned records | -| **ImportListSyncService** | 1 hr | Poll import lists, auto-add new media | -| **HealthCheckService** | 5 min | Check download clients, indexers, disk space | -| **QueueCleanupService** | 15 min | Detect stale queue items (download client no longer has them) | -| **SearchScheduler** | configurable | Scheduled searches for missing/cutoff media | -| **EmbeddedTorrentMonitor** | 30s | (if enabled) Track rustTorrent session health | -| **EmbeddedUsenetMonitor** | 30s | (if enabled) Track rustnzbd queue health | -| **IndexarrHealthCheck** | 5 min | (if enabled) Check sidecar connectivity | - ---- - -## 10. First-Boot Flow - -``` -1. User opens http://localhost:8989 for first time -2. App detects no config in DB → serves first-boot wizard - -Wizard steps: - ┌─────────────────────────────────────────────┐ - │ Step 1: Welcome │ - │ "What do you want to manage?" │ - │ [x] TV Series │ - │ [x] Movies │ - │ [ ] (future: Music, Books) │ - ├─────────────────────────────────────────────┤ - │ Step 2: Download Clients │ - │ Torrent: │ - │ ( ) None │ - │ ( ) Built-in (rustTorrent) │ - │ ( ) External (qBittorrent/Transmission) │ - │ │ - │ Usenet: │ - │ ( ) None │ - │ ( ) Built-in (rustnzbd) │ - │ ( ) External (SABnzbd/NZBGet) │ - │ │ - │ [If external selected → config fields] │ - ├─────────────────────────────────────────────┤ - │ Step 3: Indexers │ - │ ( ) None (add later) │ - │ ( ) Built-in Indexarr sidecar │ - │ └── [auto-detect http://indexarr:8080] │ - │ ( ) External indexers (add later) │ - ├─────────────────────────────────────────────┤ - │ Step 4: Library Folders │ - │ TV root: [/media/tv ] [Browse] │ - │ Movie root: [/media/movies] [Browse] │ - ├─────────────────────────────────────────────┤ - │ Step 5: Quality Profile │ - │ [Create default profiles] │ - │ Or: [Import from existing Sonarr/Radarr] │ - ├─────────────────────────────────────────────┤ - │ Step 6: Authentication │ - │ Username: [________] │ - │ Password: [________] │ - └─────────────────────────────────────────────┘ - -3. POST /api/v1/setup/init with all selections -4. App initializes enabled modules, starts background services -5. Redirect to dashboard -``` - ---- - -## 11. Implementation Phases - -### Phase 1: Foundation (MVP — media library + manual search) -- [ ] Cargo workspace scaffold with all crates (empty) -- [ ] PostgreSQL schema + sqlx migrations -- [ ] `arz-core`: config loading, DB pool, error types, enabled modules -- [ ] `arz-metadata`: TMDB client (movies + TV) -- [ ] `arz-media`: Series/Episode/Movie CRUD, root folders -- [ ] `arz-parser`: Release name parser (quality, episodes, title extraction) -- [ ] `arz-web`: Axum server, auth, REST API for media CRUD -- [ ] First-boot API endpoint -- [ ] React UI: first-boot wizard, series/movie list, add/search media -- [ ] Docker: single container with Postgres - -### Phase 2: Search & Grab -- [ ] `arz-quality`: Quality profiles, custom formats, decision engine -- [ ] `arz-indexer`: Newznab/Torznab client, Indexarr client -- [ ] `arz-download`: Download client trait + external client implementations (qBit, SABnzbd) -- [ ] Search flow: manual search → decision engine → grab -- [ ] Queue tracking (poll download clients) -- [ ] History + blocklist -- [ ] UI: interactive search, queue view, history - -### Phase 3: Automated Workflows -- [ ] `arz-scheduler`: RSS sync, completed download polling -- [ ] `arz-import`: Completed download → scan → match → rename → move -- [ ] Naming config with token system -- [ ] Auto-search for missing/cutoff -- [ ] Calendar view -- [ ] Wanted views (missing + cutoff unmet) - -### Phase 4: Embedded Download Clients -- [ ] `arz-download`: EmbeddedTorrentClient wrapping `librtbit` -- [ ] `arz-download`: EmbeddedUsenetClient wrapping `nzb-*` crates -- [ ] First-boot options for embedded vs external -- [ ] UI: embedded client status panels -- [ ] Usenet server configuration in UI - -### Phase 5: Indexarr Integration -- [ ] Indexarr sidecar docker-compose -- [ ] `arz-indexer`: IndexarrClient (Torznab + REST) -- [ ] Auto-detect sidecar on startup -- [ ] Indexarr defaults to peer-only mode (sync, no crawling) -- [ ] UI: Indexarr status panel - -### Phase 6: Polish & Parity -- [ ] `arz-notify`: Notification providers (webhook, Discord, Telegram) -- [ ] Import lists (TMDB popular, Trakt watchlist, IMDB) -- [ ] Manual import (scan folder, match, import) -- [ ] Disk scan (detect files added outside the app) -- [ ] Custom format specifications (full regex engine) -- [ ] Scene name mapping / XEM integration -- [ ] Backup/restore -- [ ] API key auth for external tool integration -- [ ] Health check system - -### Phase 7: Migration Tools -- [ ] Import from Sonarr (SQLite → PostgreSQL migration) -- [ ] Import from Radarr (SQLite → PostgreSQL migration) -- [ ] Import from Prowlarr (indexer definitions) - ---- - -## 12. Key Dependencies - -```toml -[workspace.dependencies] -# Async -tokio = { version = "1", features = ["full"] } -# Web -axum = "0.8" -tower = "0.5" -tower-http = { version = "0.6", features = ["cors", "trace", "fs"] } -# Database -sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono", "json", "uuid"] } -# HTTP client -reqwest = { version = "0.12", features = ["json", "rustls-tls"] } -# Serialization -serde = { version = "1", features = ["derive"] } -serde_json = "1" -toml = "0.8" -quick-xml = "0.37" # Newznab/Torznab XML parsing -# Logging -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } -# Error handling -thiserror = "2" -anyhow = "1" -# Util -chrono = { version = "0.4", features = ["serde"] } -uuid = { version = "1", features = ["v4", "serde"] } -regex = "1" -async-trait = "0.1" -# Embedded clients (optional) -librtbit = { path = "../rustTorrent/crates/librtbit", optional = true } -nzb-core = { path = "../rustnzbd/crates/nzb-core", optional = true } -nzb-web = { path = "../rustnzbd/crates/nzb-web", optional = true } -nzb-nntp = { path = "../rustnzbd/crates/nzb-nntp", optional = true } -nzb-decode = { path = "../rustnzbd/crates/nzb-decode", optional = true } -nzb-postproc = { path = "../rustnzbd/crates/nzb-postproc", optional = true } -``` - ---- - -## 13. Docker Deployment - -```yaml -# docker-compose.yml -services: - arz: - build: . - ports: - - "8989:8989" - volumes: - - ./config:/config - - /media/tv:/media/tv - - /media/movies:/media/movies - - /downloads:/downloads - environment: - - ARZ_DATABASE_URL=postgresql://arz:password@postgres:5432/arz - - ARZ_TORRENT_ENABLED=true - - ARZ_USENET_ENABLED=true - - ARZ_INDEXARR_ENABLED=true - - ARZ_INDEXARR_URL=http://indexarr:8080 - depends_on: - - postgres - - postgres: - image: postgres:17-alpine - volumes: - - pgdata:/var/lib/postgresql/data - environment: - - POSTGRES_USER=arz - - POSTGRES_PASSWORD=password - - POSTGRES_DB=arz - - # Optional sidecar - indexarr: - image: indexarr:latest - environment: - - INDEXARR_WORKERS=http_server,sync # peer-only mode - - INDEXARR_DB_BACKEND=sqlite - ports: - - "8080:8080" - profiles: - - indexarr - -volumes: - pgdata: -``` - ---- - -## Verification Plan - -### Phase 1 verification: -```bash -# Run migrations -sqlx database create && sqlx migrate run - -# Start app -cargo run -- --config config.toml - -# Test first-boot -curl http://localhost:8989/api/v1/system/status -# → should return { "firstBoot": true } - -# Complete setup -curl -X POST http://localhost:8989/api/v1/setup/init -d '...' - -# Add a series -curl -X POST http://localhost:8989/api/v1/series -d '{"tvdbId": 121361}' - -# Add a movie -curl -X POST http://localhost:8989/api/v1/movie -d '{"tmdbId": 550}' -``` - -### Integration test: -```bash -cargo test --workspace -# + docker compose test environment with Postgres -``` +The only active roadmap and approved future state is +[`docs/UNIFIED-ARR-PLAN.md`](docs/UNIFIED-ARR-PLAN.md). Current implementation +details live in the topic documents under `docs/`. diff --git a/README.md b/README.md index 688ee65a..3c533129 100644 --- a/README.md +++ b/README.md @@ -26,16 +26,24 @@ Current status and client-by-client progress are tracked in [API compatibility](docs/API-COMPATIBILITY.md). The full execution sequence and objective gates are in the [Unified Arr plan](docs/UNIFIED-ARR-PLAN.md). +The Unified Arr plan is the only active roadmap. It records every settled +architecture decision and maps all 54 issues that were open at the 2026-08-05 +review. Older top-level and client phase plans are retained only as historical +references. + ## v1 definition of done -- [ ] Overseerr adds a series and a movie, sees them appear, and tracks availability. -- [ ] Bazarr discovers the library and fetches subtitles. -- [ ] Recyclarr syncs a TRaSH config without error. -- [ ] nzb360 connects, browses, and manages the queue, including SignalR updates. -- [ ] Homepage and Homarr widgets show correct counts. -- [ ] A real Sonarr, Radarr, and Prowlarr installation migrates in one command. -- [ ] A single container operates without an external download client. -- [ ] Resident memory under load remains below 150 MiB. +- [ ] Overseerr adds TV and film through the logical Sonarr/Radarr façades and + tracks availability. +- [ ] Bazarr discovers both libraries and completes a subtitle workflow. +- [ ] Recyclarr reads and writes quality definitions, profiles, and custom formats. +- [ ] nzb360 searches, mutates the queue, runs commands, and receives SignalR events. +- [ ] Homepage and Homarr show correct health, media, and queue counts. +- [ ] One command imports real Sonarr, Radarr, Prowlarr, and SABnzbd data safely. +- [ ] Stock Sonarr downloads and imports through StackArr's legacy client protocols. +- [ ] Standard/external and standalone/bundled MariaDB 11.4 images both pass smoke tests. +- [ ] Conformance, live-MariaDB, and coverage-ratchet gates are green. +- [ ] StackArr stays below 150 MiB resident memory in the specified mixed workload. ## Development diff --git a/TODO3.md b/TODO3.md index b4aff2c7..d4ce6d5c 100644 --- a/TODO3.md +++ b/TODO3.md @@ -1,223 +1,8 @@ -# TODO3 — Media Import + Archive features +# Historical task-note pointer -Status snapshot from the session that landed the first cut of Feature 1 -(import recommendations) and Feature 2 (.torrent/.nzb archival). +The task notes formerly stored here predate the canonical unified-arr roadmap. +They are available in Git history but are not an active backlog. -Backend: `cargo check --workspace`, `cargo clippy -D warnings` (on touched -crates), and `cargo test -p stackarr-core -p stackarr-download --p stackarr-import --lib` (137 tests) all green. -Frontend: `npm run build` clean. - ---- - -## Feature 2 — .torrent / .nzb archive (shipped) - -- New `[storage.archive]` config section — `crates/stackarr-core/src/config.rs` - (`StorageConfig`, `ArchiveConfig`, `resolved_*_dir` helpers). -- Default dirs rooted at `{general.data_dir}/archive/{Torrents,Usenet/NZBs,Usenet/NZBs/failed}`. -- NZB archival hook in `EmbeddedUsenetClient::add()` - (`crates/stackarr-download/src/embedded_usenet.rs`) — saves raw bytes after - decompress + XML validation, before queue submission. Best-effort; logged - but never blocks a grab. -- Torrent archival hook in `EmbeddedTorrentClient::add()` - (`crates/stackarr-download/src/embedded_torrent.rs`) — fetches HTTP(S) - `.torrent` once, saves it, passes bytes to librtbit via - `AddTorrent::TorrentFileBytes` to avoid double-download. Magnets skipped - silently. Post-add, file is renamed to include the info_hash for - correlation. -- `archive_cleanup` scheduler task in `crates/stackarr-scheduler/src/lib.rs` - alongside `recycle_bin_cleanup`. Count-based, mtime-sorted. Runs on an - interval from `ArchiveCleanupConfig`. Emits activity row like other tasks. -- Scheduler wiring in `src/main.rs` — builds `ArchiveCleanupConfig` from - current `AppConfig`, creates the three dirs at startup. -- Failed NZB move hook in `download_sync_task` — when a queue row transitions - to `Failed` for a `DownloadProtocol::Usenet` item, calls - `stackarr_download::embedded_usenet::move_archive_to_failed(nzb_dir, - failed_dir, download_id)`. Archive paths captured from scheduler struct. -- Read-path + write-path API at `/api/v1/config/storage` - (`crates/stackarr-web/src/routes/general.rs`). DB keys `archive_*` override - TOML; PUT returns `restartRequired: true`. -- Settings UI → **Storage / Archive** tab - (`ui/src/pages/Settings.tsx::StorageTab`). Enable toggle, editable paths - (with resolved defaults as placeholder), 3 count caps, cleanup interval, - amber "restart required" banner. - -### Decisions locked in -- Global count caps (not per-category/per-indexer). -- Sort key: mtime on disk (simpler; works for failed NZBs that may lack a - history row). -- Magnet torrents skipped silently — no archive row. -- Failed NZBs stored in a separate `failed/` bucket with its own cap so - debugging artefacts aren't evicted by normal churn. - -### Open follow-ups -- **Hot reload of archive settings.** Currently the `EmbeddedTorrentClient`, - `EmbeddedUsenetClient`, and the `archive_cleanup` scheduler task all - capture config snapshots at construction. Changing values via the UI - persists to DB but takes effect on next app restart. The UI warns the user. - Proper fix is an `Arc>>` inside each client so the - dirs can be swapped live, plus a config-reload hook that rebuilds the - scheduler task's interval without restarting the join set. -- **DB-override merge on startup.** `get_storage_config` merges DB over TOML - on read, but `main.rs` only reads TOML when constructing the scheduler + - clients. DB-persisted overrides are currently ignored at startup. Needs a - helper that merges `app_config` rows into `AppConfig` after load, or - exposes a `merged_archive_config()` getter. -- **Torrent `.torrent` bytes path for archive.** Only HTTP(S) grabs archive. - Magnets (`magnet:?xt=…`) never persist a file because there's nothing to - save until librtbit resolves the metadata. If you want magnet-archive - parity, hook librtbit's metadata-ready event and serialise the info dict - back to `.torrent` bytes. - ---- - -## Feature 1 — Media import with recommendations (MVP shipped) - -### Database -- `migrations/018_import_candidates.sql` — new `import_candidates` table with - parsed_* + suggested_* fields, status machine (`pending`/`accepted`/ - `rejected`/`ignored`/`failed`), FK to `media_library_folders`, - `target_series_id`/`target_movie_id`, JSONB `data` column. Partial unique - index `(discovered_path) WHERE status = 'pending'` so re-runs of the - scheduler dedupe. - -### Core -- `crates/stackarr-core/src/models/import_candidate.rs` — `ImportCandidate` - struct (FromRow, Serialize camelCase), `NewImportCandidate` input, async - CRUD: `insert_pending`, `list_pending`, `get`, `update_suggestion`, - `mark_accepted`, `mark_rejected`, `mark_failed`. Registered in - `crates/stackarr-core/src/models.rs`. - -### Disk scan -- `crates/stackarr-import/src/lib.rs::disk_scan` — kept as back-compat - wrapper around new `disk_scan_in_folder(pool, media_library_folder_id, - path, media_type)`. -- `scan_series` — now aggregates unmatched files into - `UnmatchedSeriesGroup` keyed by folder-name-lowercase. After the walk, - emits one `import_candidates` row per group with `match_kind` = - `season` (if one unique season parsed) or `series`. JSONB `data` contains - per-episode breakdown. -- `scan_movies` — emits one `import_candidates` row per unmatched file - (`match_kind = "movie"`), with parsed year. - -### TMDB match -- `crates/stackarr-import/src/tmdb_match.rs` — `suggest_series` / - `suggest_movie` (takes parsed title + year, returns - `Option`), `refresh_pending_suggestions` batch helper that - scans pending rows with `confidence = 0`, calls TMDB, updates rows. - Scoring = normalised Levenshtein similarity (85%) + year bonus (15%). - `MIN_CONFIDENCE = 0.45` filters noise. Exported from `stackarr-import` - lib.rs. 5 unit tests. -- Depends on `stackarr-metadata` — added to `stackarr-import/Cargo.toml`. - -### Web routes -- `crates/stackarr-web/src/routes/import_candidates.rs` — new file: - - `GET /api/v1/import-candidates?mediaType=...&limit=...` — list pending - ordered by confidence desc, discovered_at desc - - `POST /api/v1/import-candidates/{id}/accept` — body optionally overrides - `tmdbId`, `mediaLibraryFolderId`, `qualityProfileId`, `monitored` - - `POST /api/v1/import-candidates/{id}/reject` -- `accept_series` / `accept_movie` helpers — insert minimal row, inline TMDB - enrichment (overview, poster/fanart, genres, year, runtime, external ids, - episodes for all seasons), trigger immediate `disk_scan_in_folder` to link - discovered files, mark candidate accepted. Failure paths call - `mark_failed` with the error string. -- Module registered in `routes/mod.rs` and merged in `lib.rs`. - -### Scheduler integration -- `scheduled_disk_scan` and the two call sites in - `crates/stackarr-web/src/routes/system.rs` (initial-setup scan + manual - scan command) + `medialibraryfolders.rs` (on-add scan) now query - `(id, path, media_type)` and pass `Some(folder_id)` through - `disk_scan_in_folder`. The old `disk_scan()` wrapper is still used by the - "rescan a specific series" path (where the folder id isn't meaningful). - -### Frontend -- `ui/src/pages/Import.tsx` — new page. Media-type filter chips - (All / Series / Movies), refresh button, "Scan library now" button that - fires `system/command` with `RescanMediaLibrary`. Grid of - `CandidateCard`s with poster, title, confidence %, file count, total - size, overview, accept/reject buttons, busy spinners, inline toast. - Accept is disabled when no TMDB suggestion is set (tells user why). -- Route added to `ui/src/App.tsx` (`/import`) as lazy import. -- Nav link added to `ui/src/components/Sidebar.tsx` under Downloads, using - the `FolderInput` lucide icon. - -### Decisions locked in -- Prefer series-level / season-level recommendations; fall back to - episode-level only when no series grouping is confident. -- "Add as new media folder vs move into existing" wizard is deferred — user - said all data lives in media folders, which are already periodically - scanned. -- Confidence never auto-accepts. User reviews and clicks. -- Accept creates the Series/Movie entity pointing at the discovered_path - in-place — no file moves. The series path IS the on-disk folder. - -### Open follow-ups -- **TMDB refresh isn't scheduled.** `refresh_pending_suggestions` exists and - is tested but nothing calls it on an interval. Candidates get suggestions - only if something calls the helper directly. Add a scheduler task - `import_candidates_tmdb_refresh` (hourly) alongside `importer` that calls - it, and optionally a manual `POST - /api/v1/import-candidates/refresh-suggestions` endpoint. ~15 LOC each. -- **Add-folder wizard UI** (tab 2 of the original plan). Would let user - pick a folder via FileBrowser, preview detected candidates, and choose - "create new media_library_folder" vs "move files into existing folder". - Deferred because periodic scan of existing media folders covers the - common case. -- **Bulk accept** — endpoint `POST /api/v1/import-candidates/bulk-accept` - with filter params (e.g. `minConfidence`, `mediaType`) for "accept all - ≥90%". Useful once you have trust in the confidence score. -- **"Register in place" vs "move into canonical layout"** distinction on - accept. Current implementation only does in-place. For users who want - Sonarr-style layout, we'd need to wire `stackarr-import::naming` into the - accept flow. -- **Episode-level fallback emission.** Currently if a series folder parses - as one unified group, we emit one candidate. If the parser can't infer - the show title, we still emit a single `series`-kind candidate with - whatever we have. We never emit per-episode rows today. If users hit - cases where a single folder mixes multiple shows, we'd want to split. -- **Accept flow assumes `media_library_folder_id` is set on the candidate.** - Safe because the only scenarios that write candidates now are via - `disk_scan_in_folder` with a known folder id. The legacy `disk_scan()` - wrapper (used by the per-series rescan command) doesn't emit candidates - at all, so there's no null-id path to worry about. - ---- - -## Verification that ran green this session - -```bash -cargo check --workspace -cargo clippy -p stackarr-core -p stackarr-download -p stackarr-import \ - -p stackarr-scheduler -p stackarr-web --lib -- -D warnings -cargo test -p stackarr-core -p stackarr-download -p stackarr-import --lib -# 137 passed; 0 failed - -cd ui && npm install && npm run build -# built Import-*.js (8.47 kB / 2.81 kB gz) and updated Settings-*.js -``` - -Pre-existing clippy warnings in `crates/stackarr-postgres/src/lifecycle.rs` -(`unused_mut`, `unused_variables` on Windows-only code paths around lines -397 and 434) were left alone — they're not from this work and fixing them -is outside scope. CI that runs `clippy -D warnings` on the whole workspace -will surface them. - ---- - -## Suggested next-session shopping list - -1. Wire `refresh_pending_suggestions` into the scheduler (hourly task) + - add a manual-trigger endpoint. Without this, the accept flow shows - "No TMDB suggestion" on most candidates. -2. Verify the whole flow end-to-end on a real library. Plausible bugs: - media_files insert may fail when `media_library_folders.path` uses - Windows backslashes vs the walker's forward slashes (check - `stackarr-import::scan_series` around `components[0]` path matching). -3. Persistence of storage settings — either build the ArcSwap live-reload - path, or at minimum have `main.rs` merge `app_config` `archive_*` rows - over the TOML values before constructing clients + scheduler. -4. Consider whether the `import_candidates` cleanup should prune old - `accepted`/`rejected` rows (no cleanup today — they'll grow forever). - Candidate for `recycle_bin_cleanup`-style retention. +All planned work is covered by the issue ledger in +[`docs/UNIFIED-ARR-PLAN.md`](docs/UNIFIED-ARR-PLAN.md). New work requires a GitHub +issue with a governing specification and acceptance criteria. diff --git a/docs/API-COMPATIBILITY.md b/docs/API-COMPATIBILITY.md index 4cd4876b..46349c50 100644 --- a/docs/API-COMPATIBILITY.md +++ b/docs/API-COMPATIBILITY.md @@ -1,52 +1,62 @@ # Arr API compatibility -StackArr's native `/api/v1` API is independent of the compatibility work and is -not a claim of arr compatibility. Compatibility is implemented as additive, -thin façades over the shared core. +**Current state:** no legacy arr façade is implemented on `main`. -## Pinned targets +**Target state:** settled in [the product plan](UNIFIED-ARR-PLAN.md). -| Façade | Target contract | Status | -| --- | --- | --- | -| Sonarr | v3 API from Sonarr v4.0.13.2931 | Not implemented | -| Radarr | v3 API from Radarr v6.2.0.10390 | Not implemented | -| Prowlarr | v1 API from the 2025-10-04 reference snapshot | Not implemented | +StackArr's native `/api/v1` API is independent of compatibility work. Arr +compatibility is additive and is proved only by pinned contracts, golden +captures, and unmodified clients—not by similarly named native routes. + +## Frozen v1 targets + +| Façade | API | System-status version | Reference | +| --- | --- | --- | --- | +| Sonarr | v3 | `4.0.13.2931` | tag `v4.0.13.2931` | +| Radarr | v3 | `6.2.0.10390` | tag `v6.2.0.10390` | +| Prowlarr | v1 | `2.1.4.5212` | tag `v2.1.4.5212` (source commit `574721bfb5e5c929b1e585bd5d4d144665dd7a05`) | + +Sonarr v5 is outside v1. P2 copies each OpenAPI document into `contracts/` +with its source revision, license, and SHA-256. A target changes only through a +reviewed plan and fixture update. + +## Logical instances -Sonarr v5 is explicitly out of scope for v1. Target versions change only with -an intentional contract update and reviewed golden-file diffs. +One StackArr process exposes persisted logical Sonarr, Radarr, and Prowlarr +instances. An instance has a stable ID and slug, its own API-key hash, and +root-folder/tag/profile scope over the shared domain. A canonical path prefix is +always available; an optional dedicated listener maps to the same identity. +Multiple quality tiers therefore use multiple scoped façades, not duplicated +libraries or processes. -The governing wire specifications are the checked-in reference OpenAPI files. -The conformance harness will generate tests for every operation and compare -recorded responses structurally. Matching a resource name in `/api/v1` does not -count as implementing its arr counterpart. +System-status reports the pinned upstream version above so client feature +detection is deterministic. Native `/api/v1/system/status` reports the actual +StackArr version. -## Required compatibility details +## Required shared behavior -- `X-Api-Key` header and `?apikey=` query authentication, plus forms-auth cookie - behavior used by legacy UIs; -- arr error response shapes and status codes; -- `ProviderResource.fields[]`, preserving option shape, privacy, visibility, - and ordering; -- SignalR negotiation and JSON hub messages; -- deliberately selected version values from each system-status endpoint; and -- per-façade API keys with both dedicated-port and path-prefix deployment modes. +- `X-Api-Key`, `?apikey=`, and captured forms-auth cookie behavior; +- exact arr status codes, error bodies, pagination, and date/time formats; +- ordered `ProviderResource.fields[]`, including options, privacy, and hidden + fields; +- SignalR negotiation and JSON hub queue/command events; and +- stable behavior through both the instance path prefix and dedicated listener. -## Client support matrix +Prowlarr `application` and `appprofile` resources are intentional deviations: +the shared core eliminates cross-app synchronization. Their captured +unsupported/not-found behavior is tested and published rather than silently +omitted. -No client is supported yet. A client moves out of “Not implemented” only after -an unmodified client passes its recorded end-to-end flow. +## Client acceptance -| Client | Required flow | Status | +| Client | Required flow | Current status | | --- | --- | --- | -| Overseerr | Connect both façades; add series/movie; track availability | Not implemented | -| Bazarr | Discover series/movie libraries and fetch subtitles | Not implemented | -| Recyclarr | Read and write quality/custom-format configuration | Not implemented | -| nzb360 | Browse, mutate, manage queues, receive SignalR updates | Not implemented | -| Homepage/Homarr | Read status and correct media/queue counts | Not implemented | - -## Not implemented - -All legacy arr façade endpoints are currently unimplemented. P2 builds the -capturing proxy, golden store, replay/diff runner, generated OpenAPI tests, and -traffic-ranked backlog. P3 and P4 then implement read and write behavior in -that measured order. See [UNIFIED-ARR-PLAN.md](UNIFIED-ARR-PLAN.md). +| Overseerr | Connect Sonarr/Radarr, add media, observe availability | Not implemented | +| Bazarr | Discover TV/film libraries and complete subtitle flow | Not implemented | +| Recyclarr | Read/write quality and custom-format configuration | Not implemented | +| nzb360 | Browse, search, mutate queue, trigger command, receive SignalR | Not implemented | +| Homepage/Homarr | Read health and correct media/queue counts | Not implemented | + +P2 builds the conformance evidence; P3 implements reads; P4 implements writes. +The phase gates and every owning issue are in the +[canonical plan](UNIFIED-ARR-PLAN.md#6-delivery-phases-and-gates). diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 14728428..381d1c26 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,8 +1,19 @@ # Architecture +> [!IMPORTANT] +> This document describes the implementation on `main`. The approved future +> architecture—including MariaDB 11.4, logical arr façade instances, the target +> schema, crate boundaries, and phase gates—is canonical in +> [UNIFIED-ARR-PLAN.md](UNIFIED-ARR-PLAN.md). PostgreSQL names below remain until +> P1 lands; they are current-state facts, not future-state decisions. + ## System Overview -StackArr is a monolithic Rust binary that embeds a web server, background scheduler, and optional download engines (torrent + usenet). It connects to PostgreSQL for persistence and serves a React SPA for the UI. +StackArr currently builds one Rust application binary containing the native web +server, background scheduler, and optional in-process torrent and Usenet engines. +It serves the React applications and currently connects to PostgreSQL. P1 changes +the persistence layer to MariaDB; P2-P4 add arr compatibility façades without +replacing `/api/v1`. ``` ┌──────────────┐ diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 6b1508e9..8493c31a 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -1,5 +1,12 @@ # Configuration +> [!IMPORTANT] +> Examples on this page match the current PostgreSQL implementation on `main`. +> The approved P1 target changes the application URL to `mysql://` for MariaDB +> 11.4 and removes `managed-postgres`/`embed-postgres`. The standard image uses +> external MariaDB; the standalone image supplies a private supervised MariaDB +> service. See [UNIFIED-ARR-PLAN.md](UNIFIED-ARR-PLAN.md#3-settled-decisions). + StackArr is configured via a TOML file, environment variables, and CLI flags. CLI/env overrides take precedence over the config file. ## Config File diff --git a/docs/CRATE-GUIDE.md b/docs/CRATE-GUIDE.md index eaafaef9..786a6627 100644 --- a/docs/CRATE-GUIDE.md +++ b/docs/CRATE-GUIDE.md @@ -1,5 +1,10 @@ # Crate Guide +> [!NOTE] +> This is a current-implementation reference. Future crate boundaries and the +> `stackarr-postgres` to MariaDB transition are governed by +> [UNIFIED-ARR-PLAN.md](UNIFIED-ARR-PLAN.md#crate-boundaries). + Every crate in the workspace, what it does, and how to use it. ## StackArr Application Crates diff --git a/docs/DATABASE.md b/docs/DATABASE.md index 633e0fcf..9a03a75d 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -1,6 +1,17 @@ # Database -PostgreSQL 17 is required. SQLite is only used for reading *arr migration databases (rusqlite in `stackarr-migrate`). +> [!IMPORTANT] +> This is the database reference for the implementation on `main`, where +> PostgreSQL 17 is still required. The settled target is MariaDB 11.4 LTS through +> the `sqlx` MySQL driver. The approved target schema and the required revision to +> the in-flight baseline are in +> [UNIFIED-ARR-PLAN.md](UNIFIED-ARR-PLAN.md#target-schema-contract-for-t20). +> Update this document atomically with the P1 database implementation; do not use +> the current PostgreSQL details to reopen the database decision. + +SQLite is used only to read arr migration databases (`rusqlite` in +`stackarr-migrate`) and by the independent bootstrap service. It is not an +application-database candidate for v1. ## Connection diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index e0386030..1b851f25 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,5 +1,13 @@ # Deployment +> [!IMPORTANT] +> The commands below describe the current PostgreSQL-based images on `main`. +> They are pre-alpha development artifacts, not the approved v1 deployment. +> P1 replaces them with two MariaDB 11.4 modes: a standard image using an +> external `mysql://` service and a standalone image supervising a private +> MariaDB service with s6. The application will not download database binaries. +> See [UNIFIED-ARR-PLAN.md](UNIFIED-ARR-PLAN.md#3-settled-decisions). + ## Docker ### Multi-Stage Build diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index aa3df8d2..78f85f3b 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,5 +1,11 @@ # Development +> [!IMPORTANT] +> This page describes development on the current PostgreSQL implementation. +> MariaDB 11.4 is the settled target and becomes the only application test +> service when P1 lands. Track that transition and its live-test gate in the +> [canonical plan](UNIFIED-ARR-PLAN.md#p1-merge-order). + ## Prerequisites - **Rust** 1.88+ nightly (edition 2024 requires nightly features) diff --git a/docs/DOMAIN-MODELS.md b/docs/DOMAIN-MODELS.md index f5495623..1a316730 100644 --- a/docs/DOMAIN-MODELS.md +++ b/docs/DOMAIN-MODELS.md @@ -1,5 +1,10 @@ # Domain Models +> [!NOTE] +> This page documents models on `main`. The mandatory generic media identity and +> logical compatibility-instance model approved for P1 are specified in the +> [target schema contract](UNIFIED-ARR-PLAN.md#target-schema-contract-for-t20). + Model structs live in `stackarr-core/src/models/` (split across `media.rs`, `download.rs`, `quality.rs`, `history.rs`, `discover.rs`, `user.rs`) and use `#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]`. All structs use `#[serde(rename_all = "camelCase")]` for JSON serialization. ## Enums diff --git a/docs/NOTIFICATIONS.md b/docs/NOTIFICATIONS.md index 131a961a..1f67a23c 100644 --- a/docs/NOTIFICATIONS.md +++ b/docs/NOTIFICATIONS.md @@ -1,5 +1,11 @@ # Notification System +> [!NOTE] +> This is a current-implementation reference. P7's settled future state moves +> eligible notification providers to a declarative schema; it does not change +> the existing event model. See issue #96 in the +> [canonical issue ledger](UNIFIED-ARR-PLAN.md#7-open-issue-coverage-ledger). + StackArr has two independent notification subsystems: 1. **External providers** -- outbound notifications sent to third-party services (Discord, Slack, Telegram, webhooks, email) when system events occur (grabs, imports, failures, etc.). diff --git a/docs/UNIFIED-ARR-PLAN.md b/docs/UNIFIED-ARR-PLAN.md index 54095e63..dfe08271 100644 --- a/docs/UNIFIED-ARR-PLAN.md +++ b/docs/UNIFIED-ARR-PLAN.md @@ -1,1142 +1,382 @@ -# NGMS → Unified Arr: Comprehensive Plan +# StackArr product plan and target state -**Status:** In execution -**Date:** 2026-08-02 (re-baselined against `github/main` the same day) -**Owner:** TheDancingDeveloper-org -**Scope:** Turn NGMS into a single Rust service that replaces Sonarr + Radarr + Prowlarr, -speaks their legacy APIs wire-compatibly, treats TRaSH Guides / Profilarr as first-class, -and keeps the embedded torrent + Usenet engines. - -Every number was measured on 2026-08-02 against `TheDancingDeveloper-org/NGMS@main` — not -the stale local checkout (see §0.1). -Measurement commands are in [Appendix E](#appendix-e--how-the-numbers-were-measured) so -they can be re-run and challenged. - ---- - -## 0. Execution handoff - -**Read this section first. It is written for the agent that will execute the plan.** - -### 0.1 Where the code is - -```bash -git clone https://github.com/TheDancingDeveloper-org/NGMS.git ngms-unified-arr -cd ngms-unified-arr -git switch -c feat/unified-arr-execution -``` - -**Do not work in the legacy shared NGMS checkout.** It is a disjoint 43-commit branch with no -common ancestor (`git merge-base main github/main` returns empty) and a HEAD that does not -exist on GitHub. The first draft of this plan was measured against it and was materially -wrong. Everything here has since been re-measured against canonical GitHub `main`. If you find a -discrepancy, trust the repo and correct this file. - -Naming differs deliberately: the stable **repository/image identity** is `NGMS`, the -**product and crates** are `StackArr` / `stackarr-*`, and the **torrent engine** ships as -`swarmforge` on crates.io. See resolved decision D7 (§10.4). - -### 0.2 What already exists — do not recreate - -Created on the repo on 2026-08-02 while drafting this plan: - -- **31 labels** — `phase:p0`…`phase:p7` (8), `area:*` (16), `type:*` (5), `risk:high`, - `blocked`. Stock GitHub and Dependabot labels also present. -- **8 milestones** — `P0`…`P7`, numbers **1–8**, descriptions matching §7. - -Completed during execution on 2026-08-02: - -- **76 issues** created from the authoritative manifest in - [Appendix F](#appendix-f--initial-github-issue-backlog). -- **Org Projects v2 board** created with the required custom fields and all issues added. - -Already true of the repo, so *not* work items despite appearing in older plans: GitHub -Actions is live, all engine crates come from crates.io, and `crates/usenet/` is gone. - -### 0.2b Decision log - -Ratified by the owner on 2026-08-02. These are **settled** — do not re-litigate them. - -| # | Decision | Ruling | -|---|---|---| -| D2 | Database target | **MariaDB.** Pin **11.4 LTS**. Unblocks all of P1. | -| D3 | Licence | **GPL-3.0.** Unblocks T3 and T27. | -| D4 | Product scope | **Freeze list ratified as written in §4.3.** | - -Consequence of D2 that the executing agent must respect: MariaDB 10.5+ *does* support -`RETURNING` on `INSERT`, and T24 may use it to reduce the 78-site rewrite. **New code must -still be written `RETURNING`-free** so a later SQLite or MySQL 8 backend stays cheap. If you -want to overturn that, raise it — do not just start using it. - -Resolved during execution on 2026-08-02: **D1** keeps GitHub repository `NGMS` and -the existing GHCR path; **D6** keeps exact engine pins and uses grouped weekly -Dependabot/Renovate pull requests; **D7** uses StackArr as the product/crate name while -leaving the repository and image identifiers stable. D5, D8, and D9 block later phases. - -### 0.3 Order of operations - -1. Read §1 (decision), §3 (principles), §4 (decisions required) in full. -2. **Resolve the blocking decisions before writing code.** D2 (MariaDB vs MySQL 8) blocks - all of P1. D4 (scope freeze) is the mitigation for the top risk in the register. -3. Create the issues (Appendix F). -4. P0, then P1. Do not begin P2 until P1's exit criteria are met and green. - -### 0.4 Rules of engagement - -Not style preferences — each one is why a specific past failure happened. - -1. **Test-first, from a spec.** §6.2 is binding. For compat work the spec is machine- - readable; generate the failing test from `openapi.json` before the handler exists. -2. **Never vendor a published crate.** §2.4 is the cautionary tale. Need a change in - `nzb-*` or `swarmforge`? Change it upstream and bump the pin. -3. **Façades contain no logic** — DTOs and translation only. Anything else goes in core. -4. **Do not widen scope silently.** §4.3 lists what is frozen. If a task appears to need a - frozen subsystem, stop and raise it. -5. **Correct this document when reality disagrees.** It is a working artefact, not a - record. Re-run Appendix E and update the numbers. -6. **Report honestly.** If an exit criterion is unmet, say so. Do not mark it done. - -### 0.5 What "done" means - -Appendix A is the v1 definition of done, and it is deliberately expressed as *third-party -software working unmodified* rather than internal completeness. If Overseerr, Bazarr, -Recyclarr, nzb360 and Homepage all work against it, the project has succeeded regardless of -what remains unimplemented. - ---- - -## 1. The decision - -**Do not start a fourth greenfield attempt. Extend NGMS.** - -The case rests on three findings, each of which was surprising enough to change the -recommendation: - -1. **It is real, and substantial.** 70,303 lines of own code, **1,060 tests** (15.1 per - 1k LOC) and 5 TODO/`unimplemented!()` markers. The hard parts — Cardigann engine with a - parity harness, 549 indexer definitions, release parser, custom-format scoring, import, - scheduler, and working Sonarr/Radarr/Prowlarr DB importers — already exist and are - tested. - -2. **The engine-dependency question is already settled.** All seven Usenet crates and the - torrent engine come pinned from crates.io. What remains is `crates/torrent/` — 44,197 - lines that are not a workspace member and never compile. Deleting it removes 39% of the - apparent tree at zero cost. - -3. **The gap to the target is additive.** The project already covers ~48% of Sonarr's - resource topology, 42% of Radarr's and 55% of Prowlarr's, under its own `/api/v1` - namespace. What is missing is a compatibility façade and a finite list of resources — - not a rewrite. - -The alternative — porting 542k lines of C# — is not the shape of the work. The three -reference apps are forks of one common ancestor (NzbDrone) and are ~70% the same program. -The port is *one* core parameterised by media type, with thin per-app façades. - -### What we are explicitly not doing - -- Not porting the C#. We reimplement against specs (see §3). -- Not porting the 405 FluentMigrator migrations. One importer reads final-state DBs. -- Not keeping the project's own 18 migrations either. It deploys fresh, so the schema collapses - to a single baseline and is redesigned once, now, for the unified model (§4.2). -- Not porting 243k lines of arr React. We keep and grow the NGMS UI. -- Not maintaining a Usenet or torrent fork. Both become upstream dependencies. - ---- - -## 2. Evidence base - -### 2.1 Reference sources - -Located at `Active/RefenceMaterials/reference/External Repos/`. All GPL-3.0. - -| Repo | Version | Prod C# LOC | Test LOC | Frontend LOC | DB migrations | OpenAPI | -|---|---|---|---|---|---|---| -| Sonarr | v4.0.13.2931 (2026-03-17) | 224,140 | 73,212 | 87,957 | 223 | v3: 162 paths / 136 schemas; v5 also in-tree | -| Radarr | v6.2.0.10390 (2026-04-19) | 191,008 | 59,418 | 101,159 | 140 | v3: 164 paths / 137 schemas | -| Prowlarr | (2025-10-04) | 127,064 | 19,054 | 54,082 | 42 | v1: 93 paths / 70 schemas | - -Sonarr core subsystem sizes, for effort calibration: - -| Subsystem | LOC | Notes | -|---|---|---| -| `Parser/` | 4,519 | plus 4,644 LOC of parser tests — the single richest spec asset | -| `DecisionEngine/` | 3,454 | **30 specifications**, order-sensitive | -| `Organizer/` | 2,088 | naming tokens | -| `CustomFormats/` | 906 | the TRaSH hinge | - -Provider long tail (per app): ~47 indexers, ~47 notifications, ~21–23 download clients, -~35 import lists. Deduplicated across the three: **~100 unique providers**. - -### 2.2 The project as it stands - -> **Baseline: `github.com/TheDancingDeveloper-org/NGMS`, branch `main`, 499 commits, last -> pushed 2026-07-31.** Measured 2026-08-02. -> -> ⚠️ **Do not use the legacy shared NGMS checkout as the source of truth.** That local -> directory is a disjoint 43-commit branch — `git merge-base main github/main` is empty and -> its HEAD `7dbec753` does not exist on GitHub. An earlier draft of this plan was measured -> against it and was wrong in several material respects (§2.4). Work from a fresh clone. - -Crates are named `stackarr-*`; the torrent engine is published on crates.io as -**`swarmforge`**, aliased to the historical `librtbit` names in `Cargo.toml`. - -| Crate | LOC | Tests | | Crate | LOC | Tests | -|---|---|---|---|---|---|---| -| stackarr-web | 24,867 | 62 | | stackarr-indexer | 2,202 | 38 | -| stackarr-core | 4,943 | 44 | | stackarr-media | 2,132 | 22 | -| stackarr-scheduler | 4,475 | 11 | | stackarr-parser | 1,838 | 175 | -| stackarr-import | 4,411 | 137 | | stackarr-plex | 1,798 | 22 | -| stackarr-migrate | 4,409 | 24 | | stackarr-metadata | 1,222 | 21 | -| stackarr-quality | 4,137 | 158 | | stackarr-postgres | 1,216 | 20 | -| stackarr-cardigann | 3,850 | 90 | | stackarr-notify | 1,110 | 31 | -| stackarr-stream | 3,645 | 111 | | stackarr-cardigann-parity | 989 | 0 | -| stackarr-download | 3,059 | 94 | | | | | - -**Own code: 70,303 LOC / 1,060 tests** (15.1 tests per 1k LOC). -Plus `crates/torrent/` — 44,197 LOC and 396 tests that **are not a workspace member and do -not compile or run**. - -- **API paths:** 246 distinct, 49 resources. -- **Cardigann definitions:** 549 YAML. -- **Database:** PostgreSQL 17 via `sqlx` 0.8.6; **18** migrations; **zero** compile-time - `query!` macros despite `macros`/`derive` features being enabled. See §4.2. -- **Engines:** all seven `nzb-*` crates pinned from crates.io at current versions; torrent - via `swarmforge` from crates.io. **No live vendored engine remains.** -- **CI:** GitHub Actions on self-hosted runners (`node-b`) — jobs `rust`, `ui`, - `container` → GHCR, including a "Verify public dependency boundary" step. -- **Docs:** 26 files in `docs/`. -- **TODO/`unimplemented!()` markers:** 5 across 70k LOC. - -### 2.3 API coverage gap - -Crude top-level-resource match against the checked-in OpenAPI specs (undercounts — -plural/singular and nested routers are missed). StackArr exposes **49** resources total. - -| Target | Resources | Present | Missing | -|---|---|---|---| -| Sonarr v3 | 42 | **20** | autotagging, customfilter, delayprofile, diskspace, episodefile, health, importlistexclusion, indexerflag, language, languageprofile, localization, manualimport, mediacover, metadata, parse, **qualitydefinition**, releaseprofile, remotepathmapping, rename, rootfolder, seasonpass, update | -| Radarr v3 | 43 | **18** | alttitle, collection, credit, exclusions, extrafile, movie, moviefile, + most of the above | -| Prowlarr v1 | 20 | **11** | applications\*, appprofile\*, customfilter, health, indexerproxy, indexerstats, indexerstatus, localization, update | - -\* `applications` / `appprofile` are **deleted, not ported** — unification removes the sync -problem they exist to solve. - -Note `customformat` **is** already present (migration `014_custom_format_fields.sql`), which -materially de-risks the TRaSH work in P5. `qualitydefinition` is not. - -### 2.4 The vendored-engine lesson - -**This is now history, not a task — but the lesson is the reason for several P0 guardrails.** - -An earlier NGMS line vendored `rustnzbd` in March 2026 and made 5 local commits. Every one -of them was subsequently absorbed upstream (ramp-up delay → `c7d8294`; the crate-dependency --direction refactor → `crates/nzb-nntp/src/config.rs`; the nzb-web re-exports → -`nzb-web/src/lib.rs:3-5`). The fork delta ended at **zero**, while the vendored copy drifted -~12,400 lines behind four months of upstream releases. It went unnoticed because the -vendored crates declared `version.workspace = true` and so carried **no version identity** — -nothing could have reported the drift. - -**The current repo has already fixed this.** All seven Usenet crates are pinned from -crates.io, and the torrent engine comes from `swarmforge`: - -| Crate | Pinned | | Crate | Pinned | -|---|---|---|---|---| -| nzb-web | =0.4.21 | | nzb-postproc | =0.2.7 | -| nzb-core | =0.2.17 | | nzb-news | =0.1.13 | -| nzb-nntp | =0.2.23 | | nzb-dispatch | =0.2.7 | -| nzb-decode | =0.1.3 | | swarmforge (torrent) | =0.1.0 | - -What survives as work: **`crates/torrent/` is still on disk** — 44,197 LOC and 396 tests -that are not workspace members and never build. Delete it (P1/T15). And add the guardrails -that make silent re-vendoring impossible (P0/T12) — the repo already has a "Verify public -dependency boundary" CI step to build on. - -### 2.5 Documentation drift found - -`CLAUDE.md` on `main` still documents a layout the `Cargo.toml` contradicts. Correct in -P0/T6, then enforce in CI (P1/T29). - -| Claim in `CLAUDE.md` | Reality | -|---|---| -| "`torrent/` — Vendored librtbit (12 crates, from rustTorrent)" | Not a workspace member. Consumed from crates.io as `swarmforge`. The directory is dead. | -| "`usenet/` — Vendored nzb engine (5 crates, from rustnzbd)" | **The directory no longer exists.** All seven crates come from crates.io. | -| "PostgreSQL 17 (required). **Never use SQLite for application data.**" | Superseded — the project is moving to MariaDB (§4.2). This line must change with it. | - ---- - -## 3. Guiding principles - -1. **Spec-driven, not source-driven.** Three specs, in priority order: - - the checked-in `openapi.json` files (the wire contract), - - the arr NUnit test corpus, ~151k LOC (the behavioural contract — mine it, don't read - the implementation), - - the TRaSH Guides JSON repo (the quality contract). -2. **Additive.** `/api/v1` is not touched. Compatibility arrives as new crates. -3. **Compatibility is the moat, not nostalgia.** The measure of success is that Overseerr, - Bazarr, Recyclarr, nzb360 and Homepage work unmodified. -4. **Depend, don't vendor.** Every shared engine is a published, versioned crate with - Renovate on it. The Usenet fork is the cautionary tale. -5. **Delete before adding.** P1 removes 44k dead lines before a feature is written. -6. **Tests first, always.** See §6. - ---- - -## 4. Decisions required before P1 - -These are cheap now and expensive later. Each needs an explicit answer. - -### 4.1 Licence — **DECIDED: GPL-3.0** (D3, 2026-08-02) - -`Cargo.toml` declares MIT. Sonarr, Radarr and Prowlarr are all GPL-3.0, and we intend to -derive from their OpenAPI specs and mine their test corpus as our behavioural spec. That -makes NGMS a derivative work. - -**Ruling: relicense to GPL-3.0.** Accepted with the consequence understood — this forecloses -a closed-source commercial edition. Executed in P1 by T3 (LICENSE file, `Cargo.toml` -`license` field) and T27 (source headers). - -Note the vendored Usenet crates are MIT; consuming them from crates.io under GPL-3.0 is -fine (MIT is GPL-compatible). - -### 4.2 Database — swap Postgres → MySQL, and drop all migrations - -**DECIDED (D2, 2026-08-02): MariaDB, pinned to 11.4 LTS.** The project deploys fresh, so -there is no upgrade path to preserve and all existing migrations are deleted in favour of a -single baseline schema. - -This is the right moment for both changes and the worst possible moment to defer them — -every query written from P1 onward locks the dialect in, and the façade work adds hundreds -of queries. - -#### The fresh-deploy dividend - -Because there is no installed base, the 18 existing migrations collapse to -one `001_baseline.sql`. That is worth more than the tidiness: **it means the schema can be -restructured freely, right now, for the unified media model** described in §5 — the -media-type-generic core, the profile-provenance tables needed by P5, and the explainable- -decision records needed by P6. Doing that schema work later costs a migration chain and a -data backfill. Doing it in P1 costs nothing. - -So P1 does not merely translate the schema — it designs the *target* schema once. - -#### Swap inventory (measured 2026-08-02) - -| Item | Count | Effort | -|---|---|---| -| **Compile-time `sqlx::query!` macros** | **0** | **None — the saving grace.** All queries are runtime `sqlx::query(...)` despite `macros`/`derive` being enabled. No `.sqlx` offline cache to regenerate, no live DB at compile time. | -| Positional placeholders `$1..$n` → `?` | **1,420** | Mostly scriptable — **but see the trap below** | -| `RETURNING` clauses | **78** | **The single largest cost.** MySQL has no `RETURNING`. | -| `ON CONFLICT` | **77** | → `ON DUPLICATE KEY UPDATE` / `INSERT IGNORE` | -| `jsonb` references in Rust | **56** | → `JSON` | -| `PgPool` / `Postgres` type refs | **173** | Mechanical → `MySqlPool`. Much of this is concentrated in the dedicated `stackarr-postgres` crate (1,216 LOC), which is a real advantage — it becomes `stackarr-mariadb`. | -| `BIGSERIAL` / `SERIAL` in schema | 20 / 16 | → `BIGINT AUTO_INCREMENT` / `INT AUTO_INCREMENT` | -| `JSONB` in schema | 27 | → `JSON` (MySQL 8 stores JSON binary; no `jsonb` keyword) | -| `gen_random_uuid()` | 2 | → `UUID()` (MySQL 8) or generate app-side (preferred — portable) | -| `ILIKE` | ~2 | → `LIKE` (MySQL collations are case-insensitive by default) | -| `sqlx` features | 1 line | `"postgres"` → `"mysql"` | - -**The placeholder trap.** `$1` is a *named* position and may legally repeat or appear out -of order within a query; `?` is positional-by-occurrence. Any query that reuses `$1` twice, -or binds out of order, silently breaks under a naive regex rewrite. The conversion script -must detect non-monotonic or repeated placeholders and fail loudly rather than convert -them. Assume a handful need hand-rewriting. +**Status:** Canonical and in execution -**`RETURNING` is the real work.** 78 sites, each currently doing insert-and-read-back in -one round trip. Under MySQL each becomes either `INSERT` + `SELECT LAST_INSERT_ID()` inside -a transaction, or an insert followed by a re-select on a natural key. Both are correct; -both are more code and one more round trip. +**Baseline:** `TheDancingDeveloper-org/NGMS@dd26a0aa` -#### MySQL or MariaDB? +**Issue inventory:** 54 open issues, reviewed 2026-08-05 -Worth an explicit choice, because it materially changes cost: - -- **MariaDB 10.5+ supports `RETURNING` on `INSERT`** (and 10.0+ on `DELETE`). That could - eliminate most of the 78-site rewrite. -- MySQL 8.0 does not support `RETURNING` at all. -- `sqlx`'s `mysql` driver targets both; MariaDB is wire-compatible. - -**Ruling: MariaDB 11.4 LTS.** T24 may use MariaDB's `RETURNING` support opportunistically -to reduce the 78-site rewrite, but **new code must be written `RETURNING`-free** so a later -SQLite or MySQL 8 backend stays cheap. Document the target in `CONFIGURATION.md` and -`docs/DATABASE.md`, and pin the version in the CI service container. - -#### What this does not solve - -MySQL/MariaDB is still a server process. The NAS/Raspberry Pi adoption concern that -motivated the earlier SQLite suggestion remains open — Sonarr and Radarr ship with SQLite -and need no external database. Two ways to close it, neither in P1: - -1. Ship MariaDB inside the container (s6-overlay already in use) so single-box installs are - still one `docker run`. **Recommended** — cheap, and preserves the "just works" story. -2. Add a SQLite backend later behind the same query layer. Much cheaper *if* the P1 rewrite - avoids dialect-specific constructs, which is another reason to avoid `RETURNING`. - -Track as open question §10.8. - -### 4.3 Product scope boundary — **RATIFIED** (D4, 2026-08-02) - -NGMS today is Sonarr + Radarr + Prowlarr + Overseerr + a media server + a P2P discovery -layer. Adding TRaSH/Profilarr widens it further. **Scope, not Rust, is the risk.** - -| Subsystem | LOC | Recommendation | -|---|---|---| -| Embedded torrent + Usenet engines | (external) | **Core.** The one thing Sonarr structurally cannot do. Single container, no SABnzbd/NZBGet/qBittorrent. | -| `stackarr-cardigann` + 549 defs | 3,797 | **Core.** This is the Prowlarr replacement. | -| `stackarr-migrate` | 3,726 | **Core.** Adoption depends on it. | -| `stackarr-stream` (HLS/transcode) | 3,477 | **Freeze.** Jellyfin's job. Pure maintenance surface. | -| `stremio` routes | — | **Freeze or spin out.** | -| `stackarr-plex` | 1,563 | **Keep, low priority.** Integration, not ownership. | -| `discover` / `trending` / `requests` / `watchlist` | — | **Defer.** Overseerr does this and we will be API-compatible with it anyway. | -| `stackarr-bootstrap` (UPnP, BIP39) | 1,200 | **Freeze.** Interesting, orthogonal, unfinished. | - -**This table is ratified and binding.** "Freeze" = keeps compiling and stays tested, -accepts no new features, revisited after P5. A PR that adds functionality to a frozen -subsystem should be rejected on those grounds alone — scope is the top entry in the risk -register, and this list is its only real mitigation. - -### 4.4 Versioning and release - -Adopt the `rustnzbd` model that is demonstrably working: independently versioned crates, -published, Renovate-managed. Any NGMS crate another project might consume -(`stackarr-cardigann` is the obvious candidate — 549 definitions is a community asset) gets -published. - -### 4.5 CI platform — **already GitHub Actions; extend it** - -Resolved by reality: the repo is already public on GitHub with GitHub Actions on -self-hosted runners. See §6.3 — the work is **extending** the pipeline (conformance, -coverage ratchet, multi-arch, MariaDB service), not migrating to it. - ---- - -## 5. Target architecture - -``` -stackarr-core ── media-type-generic domain, storage, config - stackarr-domain-tv series / season / episode adapter - stackarr-domain-film movie / collection adapter - stackarr-domain-* (future: music, books) — plugin-shaped, not forks - -stackarr-decision ── NEW. Ported decision engine, 30 specs, explainable -stackarr-quality ── custom formats, quality definitions, TRaSH scoring -stackarr-profiles ── NEW. TRaSH/Profilarr subscription, 3-way merge, provenance -stackarr-indexer ── Cardigann + Newznab/Torznab + Indexarr -stackarr-download ── embedded torrent (librtbit) + Usenet (nzb-web) + external clients -stackarr-import ── scan, import, rename, organise -stackarr-metadata ── TMDB/TVDB, scene numbering, XEM -stackarr-notify ── declarative HTTP providers (see §7.5) -stackarr-scheduler ── background tasks -stackarr-migrate ── Sonarr/Radarr/Prowlarr/SABnzbd importers - -stackarr-web ── /api/v1 (native, unchanged) -stackarr-compat-core ── NEW. Shared arr concerns: ProviderResource field reflection, - X-Api-Key + querystring auth, SignalR hub, error shapes - stackarr-compat-sonarr-v3 ── NEW. thin façade ─┐ - stackarr-compat-radarr-v3 ── NEW. thin façade ─┼─ all over the same core - stackarr-compat-prowlarr-v1 ── NEW. thin façade ─┘ -``` - -**Façade rule:** a compat crate contains DTOs, route wiring and translation *only*. Any -logic that appears in a façade belongs in the core. This is enforceable in review and is -the difference between one product and three. - -**Instance identity.** Overseerr must be able to point "Sonarr" at one endpoint and -"Radarr" at another. Serve each façade on its own port *and* its own path prefix, with a -per-façade API key, so both deployment styles work. - -### 5.1 Compatibility details that get missed - -- **SignalR.** `/signalr/messages` — negotiate handshake plus the JSON hub protocol over - WebSocket. nzb360, LunaSea and the arr web UIs depend on it. Not optional; goes in - `stackarr-compat-core`. -- **`ProviderResource` field reflection.** The UI and Prowlarr build settings forms from - the `fields[]` array returned by provider endpoints — including `selectOptions`, - `privacy`, `hidden` and **ordering**. Must match byte-for-byte in shape. -- **Auth.** `X-Api-Key` header *and* `?apikey=` querystring, plus the forms-auth cookie. -- **Version sniffing.** Clients gate features on `/api/v3/system/status`. Pick the reported - version deliberately and write it down. -- **Download clients over legacy protocols.** Expose the embedded engines via a - SABnzbd-compatible and qBittorrent-WebUI-compatible API. This gives the façade's - `downloadclient` resource something real to point at — *and* lets someone's existing - Sonarr use NGMS as its download client with zero changes. Cheapest possible on-ramp: - adopt the download half before committing to the arr half. - ---- - -## 6. Engineering practice - -### 6.1 Where NGMS already stands - -| | StackArr | rdpapp | -|---|---|---| -| Rust LOC (own) | 70,303 | 45,906 | -| Tests | 1,060 | 289 | -| Tests per 1k LOC | **15.1** | 6.3 | - -StackArr is already 2.4× denser in tests than rdpapp. What rdpapp has that NGMS lacks is not volume -— it is **discipline and gates**: - -- `CLAUDE.md` §4 mandates `cargo build && cargo fmt && cargo clippy -- -D warnings && - cargo test` before every commit, and explicitly requires new tests for new logic. -- **Contract/golden files** — `contracts/v1/library-snapshot.json`. A frozen, versioned - artefact that fails the build when the shape changes. -- **Live integration tests** — `tests/rdp-live/`. -- **A task runner** — `justfile` with `check`, `test-web`, `test-integration`, - `test-backup-restore`, `test-visual`, `smoke`. -- **Operational drills** — `ci/backup-restore-drill.sh`. - -### 6.2 The TDD standard to adopt - -Applied strictly from P1 onward. The compat work is uniquely well suited to it because the -specification *already exists in machine-readable form*. - -1. **Red first, from the spec.** For every façade endpoint, the test is generated from - `openapi.json` before the handler exists: request shape, response schema, status codes. - The endpoint is not "started" until a failing test names it. -2. **Golden files are the contract.** Adopt rdpapp's `contracts/` pattern at - `contracts/arr-v3/.json` — captured real responses from live Sonarr/Radarr. - A diff is a build failure, never a silent drift. -3. **Mine, don't invent, the behavioural tests.** Sonarr's 4,644 LOC of parser tests and - its 30 DecisionEngine specifications are the spec for `stackarr-parser` and `stackarr-decision`. - Port the *test cases* first; the implementation follows to make them pass. This is the - single highest-leverage activity in the whole plan. -4. **Property tests where the input space is hostile.** Release-name parsing and - custom-format scoring get `proptest`, not just examples. -5. **`mock-nntp-server`** (free with the crates.io move) makes Usenet integration tests - hermetic. Use it. -6. **Gates are non-negotiable.** `fmt --check`, `clippy -- -D warnings`, `test --workspace` - block merge. No exceptions, no `#[allow]` without a comment naming the reason. -7. **Coverage as a ratchet, not a target.** Record it per crate; the number may not go - down. `coverage-watchdog` already exists in this workspace — wire it in. -8. **A `justfile`**, mirroring rdpapp: `check`, `test`, `test-compat`, `test-e2e`, - `conformance`, `smoke`. - -### 6.3 CI — extend the existing pipeline - -**Current state.** `.github/workflows/ci.yml` runs on **self-hosted runners** -(`[self-hosted, node-b, linux, x64, rust]`) with jobs `rust`, `ui` and `container` → GHCR, -plus a "Verify public dependency boundary" step and Dependabot. This is far more mature -than the abandoned local branch suggested — the migration to GitHub happened on 2026-07-30 -(`dbfacaca feat: migrate NGMS to GitHub and SwarmForge`). - -**What is still missing.** Four gaps, in order of weight: - -1. **No conformance job.** The single most important gate in this plan (P2) has nowhere to - run yet. -2. **Self-hosted only.** Every job requires the `node-b` runner. An outside contributor - cannot get CI on a fork — which defeats the adoption argument for being public at all. - At minimum the `rust` and `ui` jobs should run on `ubuntu-latest`. -3. **No coverage ratchet.** `coverage-watchdog` exists in the workspace and is unused here. -4. **No multi-arch build.** linux/arm64 and musl static builds matter for the NAS audience. - -**Target pipeline** (`.github/workflows/ci.yml`): - -| Job | Contents | -|---|---| -| `lint` | `fmt --check`, `clippy --workspace -- -D warnings` — **move to `ubuntu-latest`** | -| `test` | `cargo test --workspace` against a MariaDB service container (pinned to the §4.2 target) — **`ubuntu-latest`** | -| `conformance` | replay recorded arr traffic against the façade; diff golden files | -| `build` | matrix: `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `x86_64-unknown-linux-musl` | -| `docker` | multi-arch buildx → GHCR (`ghcr.io//ngms`) | -| `release` | tag-triggered, binaries + image + changelog | -| `coverage` | ratchet check | - -Notes: -- Use `Swatinem/rust-cache` for the hosted jobs; keep sccache on the self-hosted ones. -- Add a `rust-toolchain.toml` so local and CI agree. -- Keep `container` and deploy on self-hosted — those legitimately need the homelab. -- No private registry credentials are needed: `swarmforge` and `nzb-*` are on crates.io. - ---- - -## 7. Phase plan - -Phases are sequential; work within a phase is parallelisable. Each has an exit criterion -that is objectively checkable. - -### P0 — Repository, guardrails, and work tracking - -No code changes. Stand up the place the work will be tracked, and the guardrails that stop -the next eighteen months drifting the way the Usenet fork did. This phase exists because -every problem found in §2.5 — dead vendored code, a four-month-stale fork, three false -claims in `CLAUDE.md`, a CI pipeline with no build step — is a *process* failure, not an -engineering one. Fix the process first. - -**Resolved:** source, issues, projects, and CI live in `TheDancingDeveloper-org/NGMS`. - -#### P0.1 — Repository - -**The repo already exists**: `TheDancingDeveloper-org/NGMS`, public, with GitHub Actions -live. Labels and milestones were pre-created; the 76 issues and roadmap board were added -during execution. P0.1 is *configure and populate*, not create. - -| Item | Detail | -|---|---| -| Description + topics | Set to an honest pre-alpha unified-arr description with `sonarr`, `radarr`, `prowlarr`, `bittorrent`, `trash-guides`, and `media-automation` topics. | -| Repo name | D7 resolved: StackArr product/crates; stable `NGMS` repository and GHCR identifiers (§10.4). | -| Source of truth | **GitHub owns code, issues and CI.** Forgejo keeps a mirror and the private deploy path only. Do not run two issue trackers. | -| Stale local checkout | The legacy shared checkout is a disjoint 43-commit branch (§2.2). Workspace policy requires it to remain untouched pending a separate deployment/deprecation decision. Work instead in a clean canonical GitHub checkout. | -| Licence | GPL-3.0-only per D3; executed by T3/T27. | - -#### P0.2 — Documentation set - -Written before the first issue, so contributors arrive to a repo that explains itself. - -| File | Contents | -|---|---| -| `README.md` | What it is, **honest status (pre-alpha, not usable yet)**, the compatibility promise, the Appendix A definition of done as a public checklist | -| `LICENSE` | GPL-3.0 (§4.1) | -| `CONTRIBUTING.md` | The §6.2 TDD standard as binding policy: red-first from spec, golden files, mandatory gates, no `#[allow]` without a named reason | -| `CODE_OF_CONDUCT.md` | Contributor Covenant | -| `SECURITY.md` | Disclosure path — this software holds indexer and download-client credentials | -| `AGENTS.md` / `CLAUDE.md` | Corrected. All three false claims from §2.5 fixed, plus MariaDB. | -| `docs/UNIFIED-ARR-PLAN.md` | This document | -| `docs/API-COMPATIBILITY.md` | New. Target versions, what is and is not implemented, per-client support matrix | -| `docs/TESTING.md` | New. How to run each tier; how to add a conformance golden file | -| `docs/DATABASE.md` | Rewritten for MariaDB + single baseline schema | -| `docs/ARCHITECTURE.md` | Updated for the §5 crate layout | - -#### P0.3 — Guardrails - -The point of this sub-phase is that none of §2.5 can silently recur. - -| Guardrail | Enforces | -|---|---| -| Branch protection on `main` | No direct push; PR + green CI required; linear history | -| Required checks | `lint`, `test`, `conformance`, `build` (§6.3) | -| `CODEOWNERS` | Self for now; makes review routing explicit when contributors arrive | -| `.github/workflows/ci.yml` | The §6.3 pipeline — the first real build the project has had | -| `renovate.json` | Extended to crates.io `nzb-*`. **This is the specific control that would have caught the four-month Usenet drift.** | -| **`no-vendored-crates` CI check** | Fails the build if a `crates/` subdirectory duplicates a published dependency. The direct lesson of §2.4. | -| **Doc-drift check** | CI asserts `CLAUDE.md`'s crate list and workspace-member claims match `Cargo.toml`. The direct lesson of §2.5. Lands in P1 as T29, once T6 has corrected the file. | -| Coverage ratchet | `coverage-watchdog`; the number may not go down | -| Issue templates | `bug`, `feature`, `compat-gap`, `provider`, `decision` | -| PR template | Checklist: tests added, gates pass, docs updated, no new vendored code | - -#### P0.4 — Tracking structure - -**Milestones** — `P0`…`P7`, **already created** (numbers 1–8). - -**Labels — already created (31).** For reference: - -``` -phase:p0 … phase:p7 -area:compat-sonarr area:compat-radarr area:compat-prowlarr area:compat-core -area:db area:parser area:decision area:quality area:trash area:indexer -area:download area:import area:metadata area:ci area:docs area:ui -type:epic type:task type:bug type:decision type:spike type:chore -risk:high good-first-issue help-wanted blocked -``` - -**Project (Projects v2)** — board with custom fields `Phase`, `Area`, `Size` (XS–XL), -`Risk`, `Spec source` (which of the three §3 specs governs the item). - -#### P0.5 — Initial work items - -The full enumerated backlog is in [Appendix F](#appendix-f--initial-github-issue-backlog): -**9 decision issues, 8 phase epics, and 59 concrete tasks — 76 items**, with full bodies -and acceptance criteria in the machine-readable manifest `docs/backlog.json`. - -Two rules for the backlog: - -1. **Decisions are issues.** Every item in §4 and §10 becomes a `type:decision` issue that - blocks its dependent work. D1/D2/D3/D4/D6/D7 are resolved; later decisions retain - their explicit phase dependencies. -2. **P3's issues are generated, not written.** The conformance harness (P2) emits one issue - per unimplemented endpoint from the 419 OpenAPI paths, ranked by recorded real-client - traffic. Hand-writing them now would be guessing — the whole point of P2 is that - priority is measured. Appendix B stays a placeholder until then. - -**Exit:** repo public with green CI; branch protection active; all 76 items created, -labelled, milestoned and on the board; §10.3 and §10.9 closed. - ---- - -### P1 — Consolidation (the cheapest, highest-ratio work) - -No new features. Shrink and correct the foundation. - -| Task | Detail | Δ LOC | -|---|---|---| -| Delete `crates/torrent/` | Dead — not a workspace member, never compiles. Torrent comes from crates.io `swarmforge`. Verify no path refs first. | −44,197 | -| ~~Delete `crates/usenet/`~~ | **Already done.** All seven `nzb-*` crates pinned from crates.io (§2.4). No action. | — | -| **Delete all migrations** | 18 files → one `001_baseline.sql`. Fresh deploy, no upgrade path to preserve. | — | -| **Design the target schema** | Not a translation. Bake in the media-type-generic model (§5), P5 profile-provenance tables, P6 decision records — while it is still free. | — | -| **Postgres → MariaDB** | 1,420 placeholders, 78 `RETURNING`, 77 `ON CONFLICT`, 173 `PgPool` refs, 56 `jsonb`. Zero `query!` macros. `stackarr-postgres` becomes `stackarr-mariadb`. See §4.2. | ~1,800 touched | -| Relicense | MIT → GPL-3.0 across workspace + headers | — | -| Correct `CLAUDE.md` | Three false claims (§2.5) + the database change | — | -| Toolchain | Add `rust-toolchain.toml`; unpin CI from 1.88 | — | -| `justfile` | Mirror rdpapp targets | — | -| GitHub Actions | **Extend** the existing pipeline (§6.3): conformance job, coverage ratchet, MariaDB service, multi-arch build | — | -| Renovate/Dependabot | Confirm the crates.io `nzb-*` and `swarmforge` pins are watched — they are `=`-pinned, so nothing bumps them automatically | — | - -**Result: ~70,300 lines, down from 114,500 — a 39% reduction with zero functionality lost.** -The Usenet dividend has already been banked upstream; what remains here is the dead torrent -tree and the database swap. - -**Ordering within P1 matters.** Do the deletions first (they shrink the surface the -database swap has to cross), then the schema design, then the dialect swap, then CI last so -it validates the finished state. Specifically: the 44k dead torrent lines are removed *before* anyone -counts queries to convert. - -**Exit:** -- green CI on GitHub Actions including a multi-arch build -- `cargo tree` shows no path dependency on a vendored engine -- all 1,060 tests pass against MariaDB -- a fresh `docker run` reaches `/health` from an empty database using only - `001_baseline.sql` - -**Risk:** low-to-medium. The deletions are near-zero risk. The database swap is the real -content of this phase — 1,420 placeholder conversions is where a silent bug hides, which is -why the conversion script must fail loudly on repeated or non-monotonic `$n` (§4.2) and why -the 1,060 existing tests are the gate. The workspace has also never been verified to build in -this session — see §10.1. - -### P2 — Conformance harness - -The measuring instrument. Nothing after this is guesswork. - -- Record real HTTP traffic from the live Sonarr/Radarr/Prowlarr instances (and from - Overseerr, Bazarr, Recyclarr, nzb360, Homepage hitting them) — a capturing proxy. -- Store as versioned golden files under `contracts/arr-v3/`. -- Build a replay harness: fire recorded requests at NGMS, diff JSON structurally - (schema + shape, tolerant of ids/timestamps). -- Generate a failing test per `openapi.json` path — 419 across the three specs — all red. -- **Produce the ranked backlog**: intersect "what real clients actually call" with the - missing-resource matrix (§2.3). This converts "port everything" into a finite ordered - list, and is the single most valuable output of the phase. -- Fixtures already available: `myotherrepos/StackArr/test-fixtures/sonarr_backup.zip` and - `radarr_backup.zip`. - -**Exit:** `just conformance` runs, reports a coverage percentage, and the ranked backlog -exists as a document. - -### P3 — Read-only compatibility - -Make the ecosystem *see* NGMS. - -- `stackarr-compat-core`: auth (header + querystring), error shapes, `ProviderResource` field - reflection, SignalR hub. -- `stackarr-compat-sonarr-v3` / `-radarr-v3` / `-prowlarr-v1`: all GET endpoints. -- Priority order comes from P2, but expect: `system/status`, `rootfolder`, `diskspace`, - `health`, `series`/`movie`, `episode`, `qualityprofile`, `customformat`, `tag`, - `queue`, `history`, `calendar`, `parse`. -- `customformat` + `qualitydefinition` are first among equals — they are the TRaSH hinge, - and the engine half already exists in `stackarr-quality/src/custom_formats.rs` (479 LOC) with - no API surface on it. - -**Exit:** Overseerr, Bazarr and Homepage connect and display correct data against NGMS -unmodified. Recyclarr can read. - -### P4 — Write path and migration - -- POST/PUT/DELETE across the façades. -- `manualimport`, `rename`, `command` (the arr task-queue endpoint), `release` (grab). -- Harden `stackarr-migrate`: real Sonarr/Radarr/Prowlarr DBs in, verified state out. Add the - SABnzbd importer already sitting in `nzb-core::sabnzbd_import`. -- Expose embedded engines as SABnzbd/qBittorrent-compatible clients (§5.1). - -**Exit:** a real Sonarr + Radarr + Prowlarr install migrates in one command and continues -operating. Recyclarr can write. nzb360 can manage. - -### P5 — TRaSH + Profilarr native - -Where the differentiated value is, and where nothing upstream can follow. - -- **Subscribed profiles as first-class objects**: upstream ref + local overrides + **3-way - merge** on update, with diff preview and changelog. Recyclarr and Configarr clobber; we - merge. This is the headline. -- **Provenance on every custom format**: "from TRaSH `2026-07-14`, score overridden - 500 → 350." -- **Git-backed profile packs** — Profilarr's real contribution, as a core sync source - rather than a sidecar service. -- **Compiled scoring**: `RegexSet`/Aho-Corasick over the whole TRaSH set in one pass, - replacing N formats × M regexes per release per indexer per RSS cycle. This is a genuine - hot path and it scales with how seriously a user takes TRaSH. -- **Simulation** — the feature nobody else can build: *"show me what would have been - grabbed differently over the last 90 days if I applied this profile change."* We have - history and the release cache in one database. Recyclarr cannot do this. Sonarr cannot - do this. -- Port `myotherrepos/ArrProfileGenerator` (ProfSync) wizard logic — TRaSH profile - generation already solved in Python, needs translating not designing. - -**Exit:** a user can subscribe to a TRaSH profile, take an upstream update without losing -local edits, and preview the historical impact before applying. - -### P6 — Decision engine and parser parity - -The correctness core. Deliberately after compatibility, because P2's harness is what makes -it verifiable. - -- Port Sonarr's **30 DecisionEngine specifications**, order-sensitive. -- Port the 4,644 LOC parser test corpus *first*, then grow `stackarr-parser` (currently 1,155 - LOC vs Sonarr's 4,519) to pass it. Expect this to be the largest single body of work in - the plan. -- **Explainable decisions**: emit a structured, replayable object with the full per-spec - score breakdown, and expose it as an API resource. Sonarr's rejection reasons are strings - in a log; "why didn't it grab this?" is the single most common user complaint in the arr - community. This is a headline feature disguised as a refactor. -- Metadata: TVDB/TMDB direct, **scene numbering and XEM mapping**. Budget as a project in - itself — this is where naive rewrites break and never recover. - -**Exit:** the ported arr test corpus passes. - -### P7 — Unification dividends - -Only possible once one core owns everything. - -- **Delete Prowlarr's `Applications/` subsystem** — no `AppIndexerMap`, no sync command, no - sync levels, no drift. An indexer is defined once. -- **Deduplicated indexer traffic** — one query planner, one cache, one rate limiter across - all media types. Today Sonarr and Radarr independently hammer the same tracker with - separate limiters. Fewer requests, materially lower ban risk. -- **One download queue, one disk budget, one bandwidth budget** — today the arrs fight each - other for client slots and free space. -- **Cross-seed internally** — unified file index + embedded torrent engine makes matching - existing files against a new tracker an internal query, not an external daemon. -- **Shared release cache with failure memory** — "seen this hash, failed 3× on import, - don't re-grab." -- **Media types as a type system, not forks** — anime becomes first-class rather than a - hack on the series model; music/books become plugins rather than Lidarr/Readarr forks. -- **Global dry-run** for naming, profile changes, upgrade sweeps. -- **Declarative notification providers** (§7.5 below). - -### 7.5 Collapsing the provider long tail - -~100 unique providers is the majority of the boring work and it never stops, because -upstream keeps adding. Mitigations, in order of leverage: - -| Category | Count | Strategy | -|---|---|---| -| Torrent indexers | ~47×3 | **Already solved** — Cardigann YAML, 549 definitions in tree | -| Usenet indexers | — | Newznab/Torznab generic | -| Notifications | ~47×3 | **Make declarative.** Most are an HTTP POST with a body template. YAML/JSON templates, not 47 Rust structs. | -| Import lists | ~35×3 | Mostly declarative too (Trakt/IMDb/TMDb list fetch + parse) | -| Download clients | ~21×3 | Hand-written, but only ~8 matter (qBit, Deluge, Transmission, rTorrent, SAB, NZBGet, + embedded) | - -Making notifications and import lists **data rather than code** collapses roughly 60% of -the tail. - ---- - -## 8. Risk register - -| # | Risk | Severity | Mitigation | -|---|---|---|---| -| 1 | **Scope.** NGMS is already 6 products fused; TRaSH/Profilarr widens it | **Highest** | §4.3 freeze list. Enforce at review. Scope is what kills this, not Rust. | -| 2 | **Metadata / scene numbering / XEM.** Where naive rewrites die | High | Treat as its own project in P6. Consider running a Skyhook equivalent. | -| 3 | **Parser parity.** 1,155 LOC vs Sonarr's 4,519 + 12 years of edge cases | High | Port the 4,644 LOC test corpus first. Never write parser code without a failing test from it. | -| 4 | **Upstream drift.** Sonarr v5 (`Api.V5`) already in-tree; 3 moving targets | Medium | Pin v3 as the compatibility target. v5 is a later façade, not a parallel one. | -| 5 | **Bus factor of one.** 43 commits, single branch, one contributor | Medium | Public GitHub (§6.3) is the mitigation. So is test density. | -| 6 | **Database swap introduces silent data bugs.** 1,420 placeholder rewrites, 78 `RETURNING` sites | Medium | Conversion script fails loudly on repeated/non-monotonic `$n`; 1,060 tests are the gate; hand-review every `RETURNING` site. §4.2. | -| 6b | **MariaDB still needs a server process** — the NAS "just works" story | Medium | Ship MariaDB in-container via s6-overlay. Keep new code free of `RETURNING` so a SQLite backend stays cheap later. §10.8. | -| 7 | **Re-vendoring temptation.** The Usenet fork happened once | Medium | Principle §3.4. Renovate. Never `version.workspace = true` on a vendored crate. | -| 8 | **Build unverified.** Modified `Cargo.lock`, private registry deps, no `target/` | Medium | First action of P1. | -| 9 | **Licence contamination** discovered late | Low but severe | Resolve in P1 (§4.1). | -| 10 | **SignalR underestimated** | Low | Scope it explicitly into `stackarr-compat-core`, not "later". | - ---- - -## 9. Effort shape - -Not a schedule — a shape. Relative weights. - -| Phase | Weight | Character | -|---|---|---| -| P0 Repo + guardrails | ▏ | Days. Governance, documentation, CI, and tracking. | -| P1 Consolidation | ▍ | Deletion is days. The MySQL swap and schema redesign are the real content — call it a couple of weeks, and do not rush the 1,420 placeholder conversions. | -| P2 Conformance harness | ▎ | Tooling. Pays for everything after. | -| P3 Read-only compat | ▍▍ | Broad, shallow, highly parallel. | -| P4 Write path + migration | ▍▍ | Broad, medium depth. | -| P5 TRaSH/Profilarr | ▍▍▍ | The differentiated work. | -| P6 Decision engine + parser | ▍▍▍▍▍ | **The bulk.** Deep, correctness-critical, test-led. | -| P7 Dividends | ▍▍ | Incremental, individually shippable. | - -After deduplication, dropping the frontends and skipping migration history, genuine core -parity is roughly 120–180k lines of Rust plus the provider tail — against ~47.5k that -already exists. A credible 80/20 (TV + film + indexers + TRaSH-native, ecosystem -compatible) is a focused multi-month effort. Full parity down to every provider and every -twelve-year-old edge case is a year-scale commitment. **Scope to the 80/20 and let the -conformance harness say when it is reached.** - ---- - -## 10. Open questions - -1. ~~**Does the workspace build?**~~ **RESOLVED 2026-08-02:** a clean canonical clone - completed `cargo build --workspace` on the stock execution host before P1 changes. -2. ~~**Does `librtbit` want to go to crates.io?**~~ **RESOLVED: already published** as - `swarmforge`, aliased to the `librtbit` names in `Cargo.toml`. Public CI needs no - private registry credentials. -3. ~~**Public repo — which org?**~~ **RESOLVED 2026-08-02: `TheDancingDeveloper-org`.** - The repo already exists there, public, with 499 commits and GitHub Actions. - The former source organization is not the canonical route for this repository. -4. ~~**Name.**~~ **RESOLVED 2026-08-02:** StackArr is the product and crate family; - `NGMS` and its GHCR path remain stable distribution identifiers. The README and - repository guidance document the deliberate divergence. -5. **Does Indexarr merge in or stay separate?** `stackarr-indexer/src/indexarr.rs` (152 LOC) - integrates it today. Overlaps heavily with the Prowlarr replacement story. -6. **Reported version string** for `/api/v3/system/status` — clients gate features on it. -7. **Multi-instance semantics.** Sonarr users often run two instances (e.g. 1080p and 4K). - Does one NGMS present as two Sonarrs, or does the unified model make that obsolete? - Affects the façade's instance-identity design in P3. -8. **How is MariaDB delivered to end users?** Bundled in-container via s6-overlay - (recommended — preserves one-command install), external only, or both? Sonarr and Radarr - need no external database at all; whatever we choose must not make NGMS harder to try - than the thing it replaces. See §4.2. -9. ~~**MariaDB or MySQL 8?**~~ **RESOLVED 2026-08-02: MariaDB 11.4 LTS**, with new code - kept `RETURNING`-free for portability. See §0.2b. - ---- - -## Appendix A — Definition of done for v1 - -Objectively checkable, no interpretation required: - -- [ ] Overseerr adds a series and a movie, sees them appear, tracks availability -- [ ] Bazarr discovers the library and fetches subtitles -- [ ] Recyclarr syncs a TRaSH config without error -- [ ] nzb360 connects, browses, manages the queue (validates SignalR) -- [ ] Homepage/Homarr widgets show correct counts -- [ ] A real Sonarr + Radarr + Prowlarr install migrates in one command -- [ ] Single container, no external download client required -- [ ] Memory under load < 150 MB RSS - -## Appendix B — Ranked missing-resource list - -Placeholder. **Generated by P2** from the intersection of the §2.3 matrix with recorded -real-client traffic. Not written by hand — the whole point is that priority is measured, -not guessed. - -## Appendix C — Assets already in the workspace - -| Asset | Location | Use | -|---|---|---| -| Sonarr/Radarr/Prowlarr source + OpenAPI + tests | `Active/RefenceMaterials/reference/External Repos/` | The three specs | -| Arr DB fixtures | `myotherrepos/StackArr/test-fixtures/{sonarr,radarr}_backup.zip` | Migration + conformance | -| TRaSH profile generator (Python) | `myotherrepos/ArrProfileGenerator` (ProfSync) | Port into `stackarr-profiles` (P5) | -| Usenet engine | crates.io `nzb-*` | Dependency, not fork | -| Torrent engine | crates.io `swarmforge` (aliased to `librtbit` names) | Dependency, already | -| Coverage tooling | `Active/apps/coverage-watchdog` | P1 ratchet | -| CI reference pipeline | `Active/rdpapp/.woodpecker/ci.yml` | Job structure worth mirroring | -| Woodpecker failure modes | `Active/rdpapp/ciblock.md` | Why the homelab CI path stays minimal | - -## Appendix D — Deletion checklist for P1 - -``` -crates/torrent/ 44,197 LOC — dead, not a workspace member -crates/usenet/ 16,869 LOC — replaced by crates.io nzb-web -migrations/001..011_*.sql 647 SQL — fresh deploy; → one 001_baseline.sql -Cargo.toml: 5 usenet workspace members -Cargo.toml: nzb-* path deps → nzb-web = "0.4.21" -Cargo.toml: sqlx features "postgres" → "mysql" -Cargo.toml: license MIT → GPL-3.0 -imports in stackarr-download, stackarr-web — 17 symbols, all verified present upstream -131 PgPool/Postgres type refs 22 files — → MySqlPool -1,420 $n placeholders — → ? (script must fail on repeats) - 63 RETURNING clauses — → LAST_INSERT_ID() / re-select - 55 ON CONFLICT — → ON DUPLICATE KEY UPDATE / INSERT IGNORE - 36 jsonb refs — → JSON -CLAUDE.md: 3 false claims + database change -``` - -## Appendix E — How the numbers were measured - -```bash -REF="Active/RefenceMaterials/reference/External Repos" - -# Reference sizes -find $REF/Sonarr/src -name '*.cs' -not -name '*.Test.cs' | xargs cat | wc -l -python3 -c "import json;d=json.load(open('$REF/Sonarr/src/Sonarr.Api.V3/openapi.json'));\ -print(len(d['paths']),len(d['components']['schemas']))" - -# NGMS -find Active/apps/NGMS -name '*.rs' -not -path '*/target/*' | xargs cat | wc -l -grep -rn '#\[test\]\|#\[tokio::test\]' --include='*.rs' crates src | grep -v '/torrent/\|/usenet/' | wc -l -grep -rho '"/api/v[0-9]*/[a-z0-9/_:{}.-]*"' crates/stackarr-web/src | sort -u | wc -l - -# Usenet fork delta -git -C Active/apps/NGMS log --numstat -- crates/usenet -diff -r Active/apps/NGMS/crates/usenet/nzb-core/src Active/apps/rustnzbd/crates/nzb-core/src - -# crates.io -curl -s https://crates.io/api/v1/crates/nzb-web | python3 -m json.tool - -# Database coupling (§4.2) -ls migrations | wc -l ; cat migrations/*.sql | wc -l -grep -rho '\$[0-9]\+' --include='*.rs' crates src | wc -l # 1420 -grep -rhoi 'returning' --include='*.rs' crates src | wc -l # 78 -grep -rhoi 'on conflict' --include='*.rs' crates src | wc -l # 77 -grep -rho 'PgPool\|sqlx::Postgres\|postgres::' --include='*.rs' crates src | wc -l # 173 -grep -rho 'sqlx::query[_a-z]*!' --include='*.rs' crates src | wc -l # 0 — no macros -grep -rhoi 'jsonb\|serial\|gen_random' migrations/*.sql | tr 'A-Z' 'a-z' | sort | uniq -c -``` - ---- - -## Appendix F — Initial GitHub issue backlog - -**76 items: 9 decisions, 8 epics, 59 tasks.** The authoritative, machine-readable manifest -is **`docs/backlog.json`** — title, body, labels and milestone for every item. This appendix -is the human-readable index of it. - -Labels (31) and milestones (`P0`–`P7`, numbers 1–8) **already exist on the repo** — see -§0.2. Do not recreate them. - -P3–P7 are deliberately coarser than P0–P1: fine-grained compatibility issues are *generated* -by the P2 harness (T34) from measured client traffic, not guessed at now. That is also why -Appendix B is a placeholder. - -### Creation script +**Owner:** TheDancingDeveloper-org -```bash -R=TheDancingDeveloper-org/NGMS -python3 - <<'EOF' > /tmp/mk-issues.sh -import json -for it in json.load(open('docs/backlog.json')): - labels = ','.join(it['labels']) - body = it['body'].replace("'", "'\''") - title = it['title'].replace("'", "'\''") - print(f"gh issue create -R $R --title '{title}' --body '{body}' " - f"--label '{labels}' --milestone '{it['milestone']}'") -EOF -bash /tmp/mk-issues.sh +This document is the single authority for product scope, architectural decisions, +phase order, and completion gates. GitHub issues describe bounded implementation +work; they do not override this plan. Detailed documents describe the current +implementation unless they explicitly say “target state.” `PLAN.md`, +`IMPLEMENTATION_PLAN.md`, `TODO3.md`, and the client phase documents are historical +references, not competing roadmaps. + +There are no unresolved product or architecture questions in this plan. New +information can change a decision through a pull request that updates this document, +the affected issue, and its tests together. + +## 1. Product outcome + +StackArr will be one self-hosted Rust service that replaces Sonarr, Radarr, and +Prowlarr over a shared media domain. It keeps the native `/api/v1` API and admin UI, +adds wire-compatible arr façades, embeds BitTorrent and Usenet engines, imports an +existing arr installation, and manages TRaSH/Profilarr profiles as native data. + +The repository and image remain `TheDancingDeveloper-org/NGMS` and +`ghcr.io/thedancingdeveloper-org/ngms`. The product and Rust crate family are named +StackArr. The license is GPL-3.0-only. + +### v1 completion contract + +v1 is complete only when all of the following are demonstrated with released +artifacts and unmodified clients: + +- Overseerr connects to logical Sonarr and Radarr façades, adds a series and a + movie, and observes availability changes. +- Bazarr discovers both libraries and completes a subtitle workflow. +- Recyclarr reads and writes quality definitions, quality profiles, and custom + formats without contract errors. +- nzb360 connects, searches, mutates the queue, triggers commands, and receives + SignalR updates. +- Homepage and Homarr show correct health, media, and queue counts. +- One command imports real Sonarr, Radarr, Prowlarr, and SABnzbd configuration and + data, with a dry-run report and no source mutation. +- A stock Sonarr can use StackArr through its qBittorrent-compatible and + SABnzbd-compatible download-client endpoints. +- The standard image works with external MariaDB 11.4; the standalone image starts + StackArr and a private MariaDB 11.4 service from one container and one persistent + `/config` volume. +- The conformance suite is green for the declared endpoint set, the MariaDB suite is + green against a live service, and line coverage does not regress below the + committed per-crate baseline. +- Resident memory remains below 150 MiB during the documented mixed TV/film search, + grab, and queue workload. MariaDB is measured and reported separately so the + application budget is not obscured by deployment mode. + +## 2. Scope and source hierarchy + +### In v1 + +- unified TV and film library management; +- Cardigann, Newznab, Torznab, and the optional Indexarr adapter; +- embedded `swarmforge` BitTorrent and published `nzb-*` Usenet engines; +- external download-client support and legacy qBittorrent/SABnzbd façades; +- Sonarr v3, Radarr v3, and Prowlarr v1 compatibility; +- arr/SABnzbd migration; +- quality definitions, custom formats, TRaSH subscriptions, three-way merge, + provenance, and profile-change simulation; +- parser, decision-engine, scene-numbering, and XEM parity; and +- unified indexer planning, resource budgets, and declarative providers. + +### Preserved but frozen through P5 + +`stackarr-stream`, Stremio routes, bootstrap discovery, and the existing client-app +features continue to compile and receive security, data-integrity, compatibility, +and test maintenance. They receive no new product behavior before P5 exits. + +### Deferred beyond v1 + +Discovery, trending, requests, watchlist, ratings, PWA expansion, music, books, and +ownership of a media-streaming server are not v1 roadmap work. Existing code and +native `/api/v1` routes are preserved; the deferral prevents further expansion. +Jellyfin/Plex integrations remain integrations, not capabilities StackArr attempts to +replace. + +### Governing sources + +Use these in descending order: + +1. checked-in, hash-pinned OpenAPI contracts and reviewed golden captures for wire + behavior; +2. upstream arr behavioral tests for domain behavior, ported as tests without copying + implementation code; +3. TRaSH Guides data and the ProfSync questionnaire for profile behavior; +4. this plan and the native `/api/v1` contract for StackArr-specific behavior; and +5. a GitHub issue for bounded acceptance criteria. + +When sources disagree, the higher source wins and the lower artifact is corrected in +the same change. + +## 3. Settled decisions + +| Area | Ruling | +| --- | --- | +| Product/source naming | StackArr is the product and crate family. NGMS remains the repository and GHCR path until an explicit rename migration. | +| License | GPL-3.0-only. Closed-source redistribution is not a product direction. | +| Database | MariaDB 11.4 LTS through `sqlx`'s MySQL driver. SQLite is read-only migration input and independent bootstrap storage, never the primary application database. | +| Database delivery | Publish two images: standard uses an explicit external `mysql://` URL; standalone supervises a private MariaDB 11.4 s6 service. The application never downloads database binaries. | +| Schema lifecycle | Before the first tagged release, edit the single `001_baseline.sql`. After the first tagged release, preserve it and add ordered forward migrations. No pre-MariaDB upgrade path is supported. | +| Native API | `/api/v1` is preserved. Arr façades are additive and thin. | +| Indexarr (#29) | Indexarr stays an independently deployable service. Its client/translation adapter remains inside `stackarr-indexer`; StackArr does not copy Indexarr internals. The Prowlarr façade presents Indexarr results through the same indexer domain as Cardigann/Newznab/Torznab. | +| Logical instances (#30) | One process supports multiple persisted logical façade instances. Each has a stable ID, name, kind, API-key hash, enable flag, listener and path-prefix settings, and root-folder/tag/profile scope. Instances are filtered views over shared media, queue, history, and providers—not duplicated libraries. One default Sonarr, Radarr, and Prowlarr instance is created. | +| Compatibility listeners | Every logical instance has a canonical path prefix on the main listener. A dedicated listener is optional. Both route to the same instance ID and contract tests. Path-prefix URLs are the durable identity; ports are deployment convenience. | +| Reported versions | System-status reports the contract snapshot, not the StackArr package version: Sonarr `4.0.13.2931`, Radarr `6.2.0.10390`, Prowlarr `2.1.4.5212`. StackArr's real version is exposed in the native status API and an additional compatibility response header where clients tolerate it. | +| Prowlarr applications | `application` and `appprofile` remain intentionally unsupported because one core removes cross-app sync. They return the captured arr-compatible unsupported/not-found behavior and are listed as intentional deviations. | +| Dependencies | Engine families stay exact-pinned on crates.io and receive grouped weekly reviewed updates. Published engines are never vendored or sourced privately. | +| CI runners | Organization jobs use explicit self-hosted labels. Untrusted fork code does not execute automatically on privileged self-hosted runners; a maintainer approves a safe run after review. | +| Coverage | Use pinned `cargo-llvm-cov`. Commit per-crate and workspace line-coverage baselines; CI rejects any decrease. `coverage-watchdog` is not the coverage tool. | +| Linux artifacts | Container manifest: `linux/amd64` and `linux/arm64`. musl: a separate `x86_64-unknown-linux-musl` binary build and smoke test, because musl is not a Docker platform. | + +## 4. Target architecture + +```text +unmodified clients + ├─ /api/v3/* Sonarr logical instances ─┐ + ├─ /api/v3/* Radarr logical instances ─┼─ thin DTO/route translators + ├─ /api/v1/* Prowlarr instances ───────┘ │ + └─ /api/v1/* StackArr native API ───────────────┤ + ▼ + media + quality + profiles + decisions + scheduler + │ │ │ + indexer planner download/import migration + ├─ Cardigann ├─ SwarmForge ├─ Sonarr + ├─ Newznab ├─ nzb-* ├─ Radarr + ├─ Torznab └─ external ├─ Prowlarr + └─ Indexarr adapter └─ SABnzbd + └──────────────┬───────────────┘ + ▼ + MariaDB 11.4 LTS ``` -Run it once, from the repo root of a fresh clone. Verify with -`gh issue list -R $R --limit 100 | wc -l` → 76. - -### Index - -#### P0 — Repository, guardrails and work tracking (20 items) - -| ID | Title | Type | Risk | -|---|---|---|---| -| D1 | Confirm repo identity and org | decision | | -| D2 | MariaDB 11.x or MySQL 8 as the pinned target [RESOLVED: MariaDB 11.4 LTS] | decision | ⚠️ | -| D3 | Relicense MIT to GPL-3.0 [RESOLVED: GPL-3.0] | decision | ⚠️ | -| D4 | Ratify the product scope freeze list [RESOLVED: scope freeze ratified] | decision | ⚠️ | -| D6 | Update policy for `=`-pinned crates.io dependencies | decision | | -| D7 | Align project naming | decision | | -| E0 | Repository, guardrails and work tracking | epic | | -| T1 | Configure repo metadata and retire the stale checkout | task | | -| T2 | README: honest status, compatibility promise, public DoD | task | | -| T3 | Add LICENSE (GPL-3.0) | task | | -| T4 | CONTRIBUTING.md encoding the TDD standard | task | | -| T5 | CODE_OF_CONDUCT.md and SECURITY.md | task | | -| T6 | Correct CLAUDE.md / AGENTS.md drift | task | | -| T7 | docs/API-COMPATIBILITY.md | task | | -| T8 | docs/TESTING.md | task | | -| T9 | Branch protection, required checks, CODEOWNERS | task | | -| T10 | Issue and PR templates | task | | -| T11 | Ensure dependency automation watches the pinned engine crates | task | ⚠️ | -| T12 | Extend the dependency-boundary CI check to forbid re-vendoring | task | ⚠️ | -| T13 | Create the Projects v2 board | task | | - -#### P1 — Consolidation (18 items) - -| ID | Title | Type | Risk | -|---|---|---|---| -| D5 | How is MariaDB delivered to end users | decision | | -| E1 | Consolidation: delete dead code, MariaDB swap, baseline schema | epic | | -| T14 | Verify a clean clone builds on a stock runner | task | | -| T15 | Delete crates/torrent/ | task | | -| T16 | Audit the `=`-pinned crates.io dependencies | task | | -| T17 | Add rust-toolchain.toml | task | | -| T18 | Add a justfile | task | | -| T19 | Extend ci.yml | task | | -| T20 | Design the target baseline schema | task | ⚠️ | -| T21 | Collapse 18 migrations to one baseline | task | | -| T22 | Swap sqlx driver and rename stackarr-postgres | task | | -| T23 | Convert 1,420 positional placeholders | task | ⚠️ | -| T24 | Rewrite 78 RETURNING sites | task | ⚠️ | -| T25 | Rewrite 77 ON CONFLICT clauses | task | | -| T26 | Convert JSON and identity column types | task | | -| T27 | Apply GPL-3.0 headers | task | | -| T28 | Wire coverage-watchdog as a ratchet | task | | -| T29 | CI doc-drift check | task | | - -#### P2 — Conformance harness (6 items) - -| ID | Title | Type | Risk | -|---|---|---|---| -| E2 | Conformance harness and measured backlog | epic | | -| T30 | Capturing proxy for live arr traffic | task | | -| T31 | Golden-file store at contracts/arr-v3/ | task | | -| T32 | Replay and structural-diff harness | task | | -| T33 | Generate a failing test per OpenAPI path | task | | -| T34 | Ranked backlog generator | task | | - -#### P3 — Read-only compatibility (11 items) - -| ID | Title | Type | Risk | -|---|---|---|---| -| D8 | Does Indexarr merge in or stay separate | decision | | -| D9 | Multi-instance semantics and reported version string | decision | | -| E3 | Read-only arr API compatibility | epic | | -| T35 | stackarr-compat-core skeleton | task | | -| T36 | Arr authentication | task | | -| T37 | ProviderResource field reflection | task | ⚠️ | -| T38 | SignalR hub | task | ⚠️ | -| T39 | Sonarr v3 façade — GET endpoints | task | | -| T40 | Radarr v3 façade — GET endpoints | task | | -| T41 | Prowlarr v1 façade — GET endpoints | task | | -| T42 | qualitydefinition resource | task | | - -#### P4 — Write path and migration (5 items) - -| ID | Title | Type | Risk | -|---|---|---|---| -| E4 | Write path, migration and download-client compatibility | epic | | -| T43 | Write path across all three façades | task | | -| T44 | manualimport, rename, command, release | task | | -| T45 | Harden the migration importers | task | | -| T46 | Expose embedded engines over legacy download-client protocols | task | | - -#### P5 — TRaSH + Profilarr (7 items) - -| ID | Title | Type | Risk | -|---|---|---|---| -| E5 | TRaSH + Profilarr native | epic | | -| T47 | stackarr-profiles crate | task | | -| T48 | Three-way merge on upstream profile update | task | | -| T49 | Custom-format provenance | task | | -| T50 | Compiled custom-format scoring | task | | -| T51 | Profile-change simulation | task | | -| T52 | Port ProfSync wizard logic | task | | - -#### P6 — Decision engine and parser (5 items) - -| ID | Title | Type | Risk | -|---|---|---|---| -| E6 | Decision engine and parser parity | epic | | -| T53 | Port the Sonarr parser test corpus first | task | ⚠️ | -| T54 | Port the 30 DecisionEngine specifications | task | ⚠️ | -| T55 | Explainable decisions | task | | -| T56 | Metadata: TVDB/TMDB direct, scene numbering, XEM | task | ⚠️ | - -#### P7 — Unification dividends (4 items) - -| ID | Title | Type | Risk | -|---|---|---|---| -| E7 | Unification dividends | epic | | -| T57 | Unified indexer query planner | task | | -| T58 | Declarative notification and import-list providers | task | | -| T59 | Unified download queue and resource budgets | task | | -### Blocking order - -``` -RESOLVED 2026-08-02: D2 = MariaDB 11.4 LTS D3 = GPL-3.0 D4 = scope freeze ratified - └─> P1 is unblocked; T3 and T27 are unblocked - -RESOLVED 2026-08-02: -D1 = retain TheDancingDeveloper-org/NGMS and the existing GHCR path -D6 = exact pins plus grouped weekly dependency-update pull requests -D7 = StackArr product/crates; stable NGMS repository/image identifiers - -T6 (fix CLAUDE.md) ──> T29 (enforce in CI) -T15 (delete torrent) ──> T12 check passes -T20 (schema design) ──> T21..T26 (the swap) -T30..T33 (harness) ──> T34 (ranked backlog) ──> all P3 issue generation -D9 ──> T39, T40, T41 -``` +### Crate boundaries + +- `stackarr-core`: configuration, storage primitives, logical instances, common + identifiers, errors, and domain events. +- `stackarr-media`: generic media identity with TV and film adapters. +- `stackarr-parser`: pure release parsing; no I/O or database dependency. +- `stackarr-decision` (new in P6): ordered specifications and replayable outcomes. +- `stackarr-quality`: quality definitions, profiles, custom formats, and scoring. +- `stackarr-profiles` (new in P5): sources, subscriptions, snapshots, overrides, + merge, provenance, and simulation. +- `stackarr-indexer`: query planning and adapters, including the external Indexarr + client. +- `stackarr-download`, `stackarr-import`, `stackarr-scheduler`, + `stackarr-metadata`, `stackarr-notify`, and `stackarr-migrate`: business services + named by their domains. +- `stackarr-web`: native `/api/v1` and UI delivery. +- `stackarr-compat-core` plus Sonarr/Radarr/Prowlarr crates: DTOs, route wiring, + authentication translation, SignalR, and error translation only. + +Compatibility crates may depend on core services. Core crates never depend on a +compatibility crate. A business rule found in a façade is moved into the appropriate +domain crate before merge. + +### Target schema contract for T20 + +The baseline proposed on `feat/mariadb-baseline` is **not accepted unchanged**. Its P5 +profile and P6 decision groups are the correct direction, but the following revision is +the approved T20 contract: + +1. `media_entities` is the mandatory owner of common identity. It stores media type, + source provider and source ID, title/sort title, year, monitored state, library + folder, quality profile, external IDs, and timestamps. The source key is unique by + `(media_type, source_provider, source_id)`. +2. `series.entity_id` and `movies.entity_id` are `NOT NULL UNIQUE` foreign keys with + cascade delete. Common fields are removed from adapter tables; adapters retain only + TV- or film-specific attributes. Writes cannot create an adapter without a generic + identity. +3. `media_files` has a stable owning `media_entity_id`, a unique normalized path within + its library folder, and explicit episode/movie association tables. Polymorphic + `media_type + media_id` references are not used where a foreign key is possible. +4. Add `compat_instances` and normalized scope tables for root folders, tags, and + profiles. Store only API-key hashes. Enforce unique instance slug and unique enabled + listener `(bind_address, port)`; validate unique path prefixes in application logic. +5. Queue, history, blocklist, decision records, and import candidates reference + `media_entities` where the entity is known. Raw external candidates may keep a null + entity reference plus their immutable input payload. +6. Retain the five P5 tables, with immutable profile snapshots keyed by source, + upstream key, revision, and content hash. Overrides use JSON Pointer keys and retain + base/local values for deterministic three-way merge. +7. Retain the two P6 tables, adding decision schema version, parser version, profile + snapshot/hash, and ordered step records. A stored decision must be replayable without + reading mutable current profile data. +8. Use InnoDB, `utf8mb4`, UTC `DATETIME(6)`, application-generated UUIDs where opaque + IDs are required, `JSON` only for genuinely variable payloads, and indexes for every + foreign key and measured list/filter path. +9. The baseline must install from empty MariaDB 11.4, reject invalid relationships, + and pass fixture import, concurrency/upsert, JSON path, and round-trip tests on a + live service before review acceptance. + +This ruling resolves the approval question in #58/#102: revise the branch to this +contract, review the resulting DDL and tests, then split the provisional T21–T26 work +into issue-scoped commits. Documentation does not claim that implementation has landed. + +## 5. Compatibility contract + +The source snapshots are frozen and copied into `contracts/` during P2 with their +license, source revision, and SHA-256. A later upstream release never changes v1 +silently. + +| Façade | API | Reported version | OpenAPI source | +| --- | --- | --- | --- | +| Sonarr | v3 | `4.0.13.2931` | Sonarr tag `v4.0.13.2931` | +| Radarr | v3 | `6.2.0.10390` | Radarr tag `v6.2.0.10390` | +| Prowlarr | v1 | `2.1.4.5212` | Prowlarr tag `v2.1.4.5212`, source commit `574721bfb5e5c929b1e585bd5d4d144665dd7a05` | + +Required shared behavior includes header and query-string API-key auth, browser forms +auth, exact error/status shapes, ordered `ProviderResource.fields[]`, SignalR negotiate +and JSON hub messages, pagination, date/time serialization, and stable logical-instance +identity. A same-named native endpoint is not compatibility evidence. + +## 6. Delivery phases and gates + +Phases are sequential. Work inside a phase may run in parallel only when its declared +dependencies and tests allow it. An epic closes only when its exit gate is met. + +| Phase | Outcome | Exit gate | +| --- | --- | --- | +| P0 | Repository, license, workflow, issue structure, and documentation guardrails | Complete on `main`; the canonical guidance and public project exist. | +| P1 | MariaDB target baseline and dialect swap; reproducible CI/artifacts | Revised T20 schema accepted; issue-scoped T21–T26 changes merged; live MariaDB tests, coverage ratchet, conformance gate wiring, amd64/arm64 containers, musl artifact, standard image, and standalone image all green. | +| P2 | Capturing proxy, redacted golden store, replay/diff harness, OpenAPI test generation, and measured endpoint backlog | Every pinned operation has a test; real-client captures replay; coverage percentage and traffic-ranked issues are generated reproducibly. | +| P3 | Read-only Sonarr/Radarr/Prowlarr façades | Default and additional logical instances work; auth/provider/SignalR contracts pass; client read flows pass; intentional Prowlarr omissions are explicit. | +| P4 | Write façades, commands, importers, and legacy download-client protocols | Overseerr, Recyclarr, and nzb360 write flows pass; real arr/SAB migration passes; stock Sonarr downloads and imports through StackArr. | +| P5 | Native TRaSH/Profilarr subscriptions and safe updates | Subscribe/apply, provenance, three-way merge preview, compiled scoring benchmark, 90-day simulation, and guided setup pass API/UI/E2E tests. Frozen subsystems may then be reconsidered only through a new plan revision. | +| P6 | Parser and ordered decision parity; direct metadata and scene numbering | Ported parser and 30-specification corpora pass; decisions are queryable/replayable; known-hard scene/XEM fixtures pass. | +| P7 | Benefits possible only in a unified service | Duplicate indexer traffic is measurably reduced; ten notification providers are data-only; one cross-media queue enforces disk/bandwidth reservations and global priority. | + +### P1 merge order + +1. Revise and accept T20 to the schema contract above. +2. Rebase the MariaDB rescue work; split T21–T26 and standalone delivery into + independently reviewable commits/PRs. +3. Run all ignored database tests against live MariaDB and retain CI evidence. +4. Merge license-header and coverage work only after each matches the decisions in + this document; replace the `coverage-watchdog` assumption with `cargo-llvm-cov`. +5. Complete T19 with the separate musl job and both container delivery modes. +6. Close duplicate handoff findings after their owning task contains the evidence. + +## 7. Open-issue coverage ledger + +This ledger covers all 54 issues that were open on 2026-08-05. “Fold into” means the +finding remains visible but its implementation and closure evidence belong to the named +task; it is not a new architecture decision. + +### P1 — consolidation and delivery (11) + +| Issue | Disposition | +| --- | --- | +| [#32 E1](https://github.com/TheDancingDeveloper-org/NGMS/issues/32) | Phase epic; close only at the P1 gate. | +| [#57 T19](https://github.com/TheDancingDeveloper-org/NGMS/issues/57) | CI owner: self-hosted policy, live MariaDB, conformance, amd64/arm64, separate musl, and safe fork handling. | +| [#58 T20](https://github.com/TheDancingDeveloper-org/NGMS/issues/58) | Revise the proposed baseline to §4; the required design is settled here. | +| [#59 T21](https://github.com/TheDancingDeveloper-org/NGMS/issues/59) | One fresh-deploy baseline; merge after T20. | +| [#60 T22](https://github.com/TheDancingDeveloper-org/NGMS/issues/60) | Diffuse `sqlx` driver swap plus rename; standalone database delivery is part of acceptance, not a stub crate. | +| [#61 T23](https://github.com/TheDancingDeveloper-org/NGMS/issues/61) | Convert placeholders with an audit that rejects repeats/out-of-order binds. | +| [#62 T24](https://github.com/TheDancingDeveloper-org/NGMS/issues/62) | Hand-review insert identity/concurrency; all new code remains `RETURNING`-free. | +| [#63 T25](https://github.com/TheDancingDeveloper-org/NGMS/issues/63) | Port upserts by behavior, with conflict/concurrency tests. | +| [#64 T26](https://github.com/TheDancingDeveloper-org/NGMS/issues/64) | Convert JSON/identity types and prove JSON query behavior on MariaDB. | +| [#65 T27](https://github.com/TheDancingDeveloper-org/NGMS/issues/65) | Apply consistent SPDX headers; tracked by PR #110. | +| [#66 T28](https://github.com/TheDancingDeveloper-org/NGMS/issues/66) | Implement the `cargo-llvm-cov` baseline and ratchet; rework PR #109 if it assumes `coverage-watchdog`. | + +### P2 — measured conformance (6) + +| Issue | Disposition | +| --- | --- | +| [#33 E2](https://github.com/TheDancingDeveloper-org/NGMS/issues/33) | Phase epic; close only at the P2 gate. | +| [#68 T30](https://github.com/TheDancingDeveloper-org/NGMS/issues/68) | Capture and redact traffic from the five named client families. | +| [#69 T31](https://github.com/TheDancingDeveloper-org/NGMS/issues/69) | Store contracts with provenance, hashes, review rules, and deterministic normalization. | +| [#70 T32](https://github.com/TheDancingDeveloper-org/NGMS/issues/70) | Replay with structural diffs and explicit dynamic-field matchers. | +| [#71 T33](https://github.com/TheDancingDeveloper-org/NGMS/issues/71) | Generate one initially-red case per pinned OpenAPI operation. | +| [#72 T34](https://github.com/TheDancingDeveloper-org/NGMS/issues/72) | Rank missing endpoints by observed client traffic and generate bounded issues. | + +### P3 — read compatibility (11) + +| Issue | Disposition | +| --- | --- | +| [#34 E3](https://github.com/TheDancingDeveloper-org/NGMS/issues/34) | Phase epic; close only at the P3 gate. | +| [#29 D8](https://github.com/TheDancingDeveloper-org/NGMS/issues/29) | Resolved by §3: Indexarr remains separate; adapter stays in `stackarr-indexer`. | +| [#30 D9](https://github.com/TheDancingDeveloper-org/NGMS/issues/30) | Resolved by §3: persisted logical instances and pinned reported versions. | +| [#73 T35](https://github.com/TheDancingDeveloper-org/NGMS/issues/73) | Shared compatibility crate and enforceable dependency boundary; tracked by PR #108. | +| [#74 T36](https://github.com/TheDancingDeveloper-org/NGMS/issues/74) | Header/query/cookie authentication per logical instance. | +| [#75 T37](https://github.com/TheDancingDeveloper-org/NGMS/issues/75) | Exact provider-field reflection including order and privacy. | +| [#76 T38](https://github.com/TheDancingDeveloper-org/NGMS/issues/76) | SignalR negotiation and live queue events. | +| [#77 T39](https://github.com/TheDancingDeveloper-org/NGMS/issues/77) | Traffic-ranked Sonarr v3 GET surface. | +| [#78 T40](https://github.com/TheDancingDeveloper-org/NGMS/issues/78) | Traffic-ranked Radarr v3 GET surface. | +| [#79 T41](https://github.com/TheDancingDeveloper-org/NGMS/issues/79) | Traffic-ranked Prowlarr v1 GET surface with the two declared omissions. | +| [#80 T42](https://github.com/TheDancingDeveloper-org/NGMS/issues/80) | Sonarr/Radarr quality-definition resource and Recyclarr read flow. | + +### P4 — writes, migration, and protocol compatibility (5) + +| Issue | Disposition | +| --- | --- | +| [#35 E4](https://github.com/TheDancingDeveloper-org/NGMS/issues/35) | Phase epic; close only at the P4 gate. | +| [#81 T43](https://github.com/TheDancingDeveloper-org/NGMS/issues/81) | POST/PUT/DELETE across all façades and logical-instance scopes. | +| [#82 T44](https://github.com/TheDancingDeveloper-org/NGMS/issues/82) | Manual import, rename, command, and release/search workflows. | +| [#83 T45](https://github.com/TheDancingDeveloper-org/NGMS/issues/83) | Real Sonarr/Radarr/Prowlarr plus SABnzbd importer, dry run, and rollback-safe failure. | +| [#84 T46](https://github.com/TheDancingDeveloper-org/NGMS/issues/84) | qBittorrent WebUI and SABnzbd protocols backed by embedded engines. | + +### P5 — profiles (7) + +| Issue | Disposition | +| --- | --- | +| [#36 E5](https://github.com/TheDancingDeveloper-org/NGMS/issues/36) | Phase epic; close only at the P5 gate. | +| [#85 T47](https://github.com/TheDancingDeveloper-org/NGMS/issues/85) | `stackarr-profiles` and subscribed profiles. | +| [#86 T48](https://github.com/TheDancingDeveloper-org/NGMS/issues/86) | Deterministic three-way merge with preview/conflict review. | +| [#87 T49](https://github.com/TheDancingDeveloper-org/NGMS/issues/87) | Source/revision/override provenance in API and UI. | +| [#88 T50](https://github.com/TheDancingDeveloper-org/NGMS/issues/88) | Compiled scoring with a checked-in representative benchmark corpus. | +| [#89 T51](https://github.com/TheDancingDeveloper-org/NGMS/issues/89) | Historical 90-day impact report before apply. | +| [#90 T52](https://github.com/TheDancingDeveloper-org/NGMS/issues/90) | Native guided setup equivalent to the pinned ProfSync questionnaire. | + +### P6 — parser, decisions, and metadata (5) + +| Issue | Disposition | +| --- | --- | +| [#37 E6](https://github.com/TheDancingDeveloper-org/NGMS/issues/37) | Phase epic; close only at the P6 gate. | +| [#91 T53](https://github.com/TheDancingDeveloper-org/NGMS/issues/91) | Port Sonarr parser tests first, then implementation. | +| [#92 T54](https://github.com/TheDancingDeveloper-org/NGMS/issues/92) | Port all 30 ordered decision specifications and tests. | +| [#93 T55](https://github.com/TheDancingDeveloper-org/NGMS/issues/93) | Persist/query/replay the complete decision breakdown. | +| [#94 T56](https://github.com/TheDancingDeveloper-org/NGMS/issues/94) | Direct TVDB/TMDB metadata, scene numbering, and XEM hard fixtures. | + +### P7 — unification dividends (4) + +| Issue | Disposition | +| --- | --- | +| [#38 E7](https://github.com/TheDancingDeveloper-org/NGMS/issues/38) | Phase epic; v1 closes only at the P7 gate and §1 contract. | +| [#95 T57](https://github.com/TheDancingDeveloper-org/NGMS/issues/95) | Shared query planner/cache/rate limiter with before/after request metrics. | +| [#96 T58](https://github.com/TheDancingDeveloper-org/NGMS/issues/96) | Declarative provider schema; at least ten notification providers use no provider-specific Rust. | +| [#97 T59](https://github.com/TheDancingDeveloper-org/NGMS/issues/97) | Cross-media priority plus enforceable disk and bandwidth reservations. | + +### Handoff findings without milestones (5) + +| Issue | Owning disposition | +| --- | --- | +| [#102](https://github.com/TheDancingDeveloper-org/NGMS/issues/102) | The approval question is resolved by the T20 revision contract in §4; close after #58 records and implements it. | +| [#103](https://github.com/TheDancingDeveloper-org/NGMS/issues/103) | After T20 revision, split the rescue branch by T21–T26 and delivery concern before review. | +| [#104](https://github.com/TheDancingDeveloper-org/NGMS/issues/104) | Database delivery was already decided under closed D5; fold implementation into #60/#57 and prove the standalone image. It is unrelated to Indexarr decision #29. | +| [#105](https://github.com/TheDancingDeveloper-org/NGMS/issues/105) | Fold coverage into #66 and musl/multi-arch into #57 under §3. | +| [#106](https://github.com/TheDancingDeveloper-org/NGMS/issues/106) | Fold live MariaDB evidence into #57/#58; passing ignored-only local tests is not acceptance. | + +Count check: 11 + 6 + 11 + 5 + 7 + 5 + 4 + 5 = **54**. + +## 8. Engineering and release policy + +- Start new behavior with a failing test derived from its governing source. +- Unit-test logic; integration-test database, filesystem, network, and process + boundaries; E2E-test user/client flows. +- Golden capture changes require source provenance and explicit review. Never update a + fixture merely to make CI green. +- Use runtime `sqlx` queries with audited binds. New application SQL is + `RETURNING`-free even though MariaDB supports insert `RETURNING`. +- Preserve user data and wire contracts. Migrations are forward-only after the first + release and must have rollback/recovery instructions even when SQL rollback is not + possible. +- Required Rust gates are `cargo fmt --all -- --check`, `cargo clippy --workspace + --all-features -- -D warnings`, and `cargo test --workspace --all-features`; phase + gates add MariaDB, conformance, coverage, UI, and artifact tests. +- Do not vendor published crates, add private sources, add undocumented `#[allow]`, or + add AI contributor/co-author attribution. + +## 9. Documentation maintenance + +- This file owns future state and phase order. +- `README.md` owns the public summary and v1 promise. +- `AGENTS.md` and `CONTRIBUTING.md` own contributor workflow. +- `API-COMPATIBILITY.md` owns pinned wire targets and measured client status. +- `ARCHITECTURE.md`, `DATABASE.md`, `DEPLOYMENT.md`, and `CONFIGURATION.md` distinguish + current implementation from approved target state until P1 lands. +- `docs/backlog.json` is the historical 76-item seed manifest, not live issue state. +- GitHub is the live work-state authority. This ledger is refreshed whenever issues are + added, closed, split, or materially re-scoped. diff --git a/docs/backlog.json b/docs/backlog.json index 29e1ee2a..672abad6 100644 --- a/docs/backlog.json +++ b/docs/backlog.json @@ -7,7 +7,7 @@ "type:decision", "area:ci" ], - "body": "**RESOLVED 2026-08-02:** retain `TheDancingDeveloper-org/NGMS` as the canonical source repository and retain the existing `ghcr.io/thedancingdeveloper-org/ngms` image path.\n\nThe former source organization is not the canonical route. This avoids breaking source links and image consumers while the compatibility work is pre-alpha.\n\n**Acceptance:** decision recorded in this issue; \u00a710.3/\u00a710.4 of `docs/UNIFIED-ARR-PLAN.md` updated." + "body": "**RESOLVED 2026-08-02:** retain `TheDancingDeveloper-org/NGMS` as the canonical source repository and retain the existing `ghcr.io/thedancingdeveloper-org/ngms` image path.\n\nThe former source organization is not the canonical route. This avoids breaking source links and image consumers while the compatibility work is pre-alpha.\n\n**Acceptance:** decision recorded in the issue and \u00a73 of `docs/UNIFIED-ARR-PLAN.md`." }, { "id": "D2", @@ -39,17 +39,17 @@ "type:decision", "risk:high" ], - "body": "**RESOLVED 2026-08-02: freeze list ratified as written in \u00a74.3 of the plan.**\n\nFrozen: `stackarr-stream`, Stremio routes, `stackarr-bootstrap`. Deferred: discover/trending/requests/watchlist. Core: engines, Cardigann, migrate.\n\n'Freeze' = keeps compiling and stays tested, accepts no new features, revisited after P5. A PR adding functionality to a frozen subsystem should be rejected on those grounds alone.\n\n**Remaining acceptance:** enforced at PR review." + "body": "**RESOLVED 2026-08-02:** the scope boundary in \u00a72 of the canonical plan is ratified.\n\nFrozen: `stackarr-stream`, Stremio routes, `stackarr-bootstrap`. Deferred: discovery, trending, requests, and watchlist expansion. Core: engines, Cardigann, migration, unified media, compatibility, profiles, parser, and decisions.\n\n'Freeze' means keeps compiling and stays tested, accepts no new features through P5.\n\n**Remaining acceptance:** enforced at PR review." }, { "id": "D5", - "title": "D5: How is MariaDB delivered to end users", + "title": "D5: MariaDB delivery [RESOLVED: standard and standalone images]", "milestone": "P1", "labels": [ "type:decision", "area:db" ], - "body": "Sonarr/Radarr need no external database. Requiring one is an adoption tax on the NAS audience our memory-footprint story targets.\n\nOptions: bundle MariaDB in-container via s6-overlay (recommended), external only, or both.\n\n**Acceptance:** decision recorded; reflected in `docker/` and Appendix A's one-command install criterion." + "body": "**RESOLVED 2026-08-02:** publish both modes. The standard image uses an explicit external MariaDB 11.4 service. The standalone image supervises a private MariaDB 11.4 s6 service and persists it under `/config`. The application does not download database binaries.\n\n**Acceptance:** implementation is reflected in `docker/` and the v1 completion contract in `docs/UNIFIED-ARR-PLAN.md`." }, { "id": "D6", @@ -73,23 +73,23 @@ }, { "id": "D8", - "title": "D8: Does Indexarr merge in or stay separate", + "title": "D8: Indexarr boundary [RESOLVED: separate service, internal adapter]", "milestone": "P3", "labels": [ "type:decision", "area:indexer" ], - "body": "`stackarr-indexer` integrates Indexarr today. It overlaps heavily with the Prowlarr-replacement story.\n\n**Acceptance:** decision recorded; affects P3 indexer work and the Prowlarr fa\u00e7ade scope." + "body": "**RESOLVED 2026-08-05:** Indexarr remains an independently deployable service. Its client and translation adapter remain in `stackarr-indexer`; StackArr does not copy Indexarr internals. The Prowlarr fa\u00e7ade presents Indexarr through the shared indexer domain.\n\n**Acceptance:** decision is recorded in \u00a73 of `docs/UNIFIED-ARR-PLAN.md` and governs P3 indexer work." }, { "id": "D9", - "title": "D9: Multi-instance semantics and reported version string", + "title": "D9: Logical instances and reported versions [RESOLVED]", "milestone": "P3", "labels": [ "type:decision", "area:compat-core" ], - "body": "Arr users commonly run two instances (1080p / 4K). Does one StackArr present as two Sonarrs, or does the unified model make that obsolete?\n\nAlso: clients gate features on `/api/v3/system/status` version. Pick it deliberately.\n\n**Acceptance:** instance-identity design and version string recorded before T39-T41." + "body": "**RESOLVED 2026-08-05:** one StackArr process supports multiple persisted logical fa\u00e7ade instances scoped over shared data. Each has a stable identity, API-key hash, path prefix, optional listener, and root-folder/tag/profile scope. System status reports the frozen contract versions: Sonarr 4.0.13.2931, Radarr 6.2.0.10390, and Prowlarr 2.1.4.5212 (October 2025 reference snapshot, commit `574721bfb5e5c929b1e585bd5d4d144665dd7a05`).\n\n**Acceptance:** the model is recorded in \u00a73 of `docs/UNIFIED-ARR-PLAN.md` and implemented before T39-T41." }, { "id": "E0", @@ -99,7 +99,7 @@ "type:epic", "phase:p0" ], - "body": "Phase P0 epic. See \u00a77 of `docs/UNIFIED-ARR-PLAN.md`.\n\nExit criteria for this phase are defined in the plan and must be met before the next phase begins." + "body": "Phase P0 epic. See \u00a76 of `docs/UNIFIED-ARR-PLAN.md`.\n\nExit criteria for this phase are defined in the plan and must be met before the next phase begins." }, { "id": "E1", @@ -109,7 +109,7 @@ "type:epic", "phase:p1" ], - "body": "Phase P1 epic. See \u00a77 of `docs/UNIFIED-ARR-PLAN.md`.\n\nExit criteria for this phase are defined in the plan and must be met before the next phase begins." + "body": "Phase P1 epic. See \u00a76 of `docs/UNIFIED-ARR-PLAN.md`.\n\nExit criteria for this phase are defined in the plan and must be met before the next phase begins." }, { "id": "E2", @@ -119,7 +119,7 @@ "type:epic", "phase:p2" ], - "body": "Phase P2 epic. See \u00a77 of `docs/UNIFIED-ARR-PLAN.md`.\n\nExit criteria for this phase are defined in the plan and must be met before the next phase begins." + "body": "Phase P2 epic. See \u00a76 of `docs/UNIFIED-ARR-PLAN.md`.\n\nExit criteria for this phase are defined in the plan and must be met before the next phase begins." }, { "id": "E3", @@ -129,7 +129,7 @@ "type:epic", "phase:p3" ], - "body": "Phase P3 epic. See \u00a77 of `docs/UNIFIED-ARR-PLAN.md`.\n\nExit criteria for this phase are defined in the plan and must be met before the next phase begins." + "body": "Phase P3 epic. See \u00a76 of `docs/UNIFIED-ARR-PLAN.md`.\n\nExit criteria for this phase are defined in the plan and must be met before the next phase begins." }, { "id": "E4", @@ -139,7 +139,7 @@ "type:epic", "phase:p4" ], - "body": "Phase P4 epic. See \u00a77 of `docs/UNIFIED-ARR-PLAN.md`.\n\nExit criteria for this phase are defined in the plan and must be met before the next phase begins." + "body": "Phase P4 epic. See \u00a76 of `docs/UNIFIED-ARR-PLAN.md`.\n\nExit criteria for this phase are defined in the plan and must be met before the next phase begins." }, { "id": "E5", @@ -149,7 +149,7 @@ "type:epic", "phase:p5" ], - "body": "Phase P5 epic. See \u00a77 of `docs/UNIFIED-ARR-PLAN.md`.\n\nExit criteria for this phase are defined in the plan and must be met before the next phase begins." + "body": "Phase P5 epic. See \u00a76 of `docs/UNIFIED-ARR-PLAN.md`.\n\nExit criteria for this phase are defined in the plan and must be met before the next phase begins." }, { "id": "E6", @@ -159,7 +159,7 @@ "type:epic", "phase:p6" ], - "body": "Phase P6 epic. See \u00a77 of `docs/UNIFIED-ARR-PLAN.md`.\n\nExit criteria for this phase are defined in the plan and must be met before the next phase begins." + "body": "Phase P6 epic. See \u00a76 of `docs/UNIFIED-ARR-PLAN.md`.\n\nExit criteria for this phase are defined in the plan and must be met before the next phase begins." }, { "id": "E7", @@ -169,7 +169,7 @@ "type:epic", "phase:p7" ], - "body": "Phase P7 epic. See \u00a77 of `docs/UNIFIED-ARR-PLAN.md`.\n\nExit criteria for this phase are defined in the plan and must be met before the next phase begins." + "body": "Phase P7 epic. See \u00a76 of `docs/UNIFIED-ARR-PLAN.md`.\n\nExit criteria for this phase are defined in the plan and must be met before the next phase begins." }, { "id": "T1", @@ -191,7 +191,7 @@ "phase:p0", "area:docs" ], - "body": "**Acceptance:** README states pre-alpha/not-yet-usable plainly, describes the arr compatibility goal, and embeds Appendix A as a public checklist." + "body": "**Acceptance:** README states pre-alpha/not-yet-usable plainly, describes the arr compatibility goal, and embeds the canonical v1 completion contract as a public checklist." }, { "id": "T3", @@ -393,7 +393,7 @@ "area:db", "risk:high" ], - "body": "**Not a translation.** Because the project deploys fresh, the schema can be restructured for free \u2014 bake in the media-type-generic core (\u00a75), the P5 profile-provenance tables, and the P6 explainable-decision records now. Doing this later costs a migration chain and a backfill.\n\n**Acceptance:** schema reviewed and agreed before T21 lands." + "body": "**Not a translation.** Because the project deploys fresh, the schema can be restructured before the first release. Implement the mandatory generic media identity, logical compatibility instances, P5 profile/provenance tables, and replayable P6 decision records specified in \u00a74 of `docs/UNIFIED-ARR-PLAN.md`.\n\n**Acceptance:** the target schema contract and live MariaDB tests are reviewed and agreed before T21 lands." }, { "id": "T21", @@ -483,7 +483,7 @@ "phase:p1", "area:ci" ], - "body": "`coverage-watchdog` already exists in the workspace and is unused here.\n\n**Acceptance:** per-crate coverage recorded; CI fails if the number drops." + "body": "Use the pinned `cargo-llvm-cov` tool; `coverage-watchdog` is not a Rust coverage-ratchet command. Commit per-crate and workspace line-coverage baselines.\n\n**Acceptance:** CI records coverage and fails if any crate drops below its reviewed baseline." }, { "id": "T29", @@ -549,7 +549,7 @@ "phase:p2", "area:compat-core" ], - "body": "**The most valuable output of P2.** Intersect recorded real-client traffic with the \u00a72.3 resource gap; emit one issue per unimplemented endpoint, ranked by actual usage.\n\nThis is why Appendix B is a placeholder \u2014 priority is measured, not guessed.\n\n**Acceptance:** issues created and milestoned; Appendix B filled in." + "body": "**The most valuable output of P2.** Intersect recorded real-client traffic with the pinned OpenAPI operations and intentional-deviation list; emit one issue per unimplemented endpoint, ranked by actual usage.\n\n**Acceptance:** generated issues are created and milestoned, and the ranked machine-readable report is checked in." }, { "id": "T35", diff --git a/docs/clientapp.md b/docs/clientapp.md index 4adbc50b..747ac249 100644 --- a/docs/clientapp.md +++ b/docs/clientapp.md @@ -1,5 +1,11 @@ # Plan: User Accounts + Client Web App +> [!WARNING] +> Historical implementation plan, not an active roadmap. Existing client code is +> preserved, but streaming, discovery, requests, watchlist, ratings, and PWA +> expansion are frozen or deferred by the +> [canonical product plan](UNIFIED-ARR-PLAN.md#2-scope-and-source-hierarchy). + ## Context StackArr currently has no user system — just a single admin API key and anonymous device tokens via claim codes. The user wants to: diff --git a/docs/phase1-user-system.md b/docs/phase1-user-system.md index e45a281f..8588555a 100644 --- a/docs/phase1-user-system.md +++ b/docs/phase1-user-system.md @@ -1,5 +1,9 @@ # Phase 1: User System + Web Login +> [!WARNING] +> Historical client-phase specification, not the current P1 roadmap. The active +> phases are defined only in [UNIFIED-ARR-PLAN.md](UNIFIED-ARR-PLAN.md). + ## Goal Add server-local user accounts with invite-only registration, session-based auth, and a login/register flow in the client web app. Migrate existing `remote_clients` to the new `user_devices` model. Mount the client app at `/app`. diff --git a/docs/phase2-watch-progress.md b/docs/phase2-watch-progress.md index 6f6704e6..d37b8341 100644 --- a/docs/phase2-watch-progress.md +++ b/docs/phase2-watch-progress.md @@ -1,5 +1,9 @@ # Phase 2: Watch Progress + Continue Watching +> [!WARNING] +> Historical client-phase specification, not the current P2 roadmap. The active +> phases are defined only in [UNIFIED-ARR-PLAN.md](UNIFIED-ARR-PLAN.md). + ## Goal Track per-user watch progress for all media files. Display a "Continue Watching" row on the client home page. Report progress from the video player automatically. diff --git a/docs/phase3-media-requests.md b/docs/phase3-media-requests.md index e69948c8..5ed4f61e 100644 --- a/docs/phase3-media-requests.md +++ b/docs/phase3-media-requests.md @@ -1,5 +1,10 @@ # Phase 3: Media Requests +> [!WARNING] +> Historical client-phase specification. Media requests are deferred beyond v1; +> current P3 is arr read compatibility. See +> [UNIFIED-ARR-PLAN.md](UNIFIED-ARR-PLAN.md#2-scope-and-source-hierarchy). + ## Goal Allow users to request TV series and movies that aren't in the library yet. Admins can approve/decline requests. Approved requests auto-add media to the library via the existing TMDB + series/movie creation flow. diff --git a/docs/phase4-watchlist-ratings.md b/docs/phase4-watchlist-ratings.md index 90735ffc..310526ef 100644 --- a/docs/phase4-watchlist-ratings.md +++ b/docs/phase4-watchlist-ratings.md @@ -1,5 +1,10 @@ # Phase 4: Watchlist + Ratings +> [!WARNING] +> Historical client-phase specification. Watchlist and ratings expansion are +> deferred beyond v1; current P4 is arr writes, migration, and download-client +> compatibility. See [UNIFIED-ARR-PLAN.md](UNIFIED-ARR-PLAN.md). + ## Goal Per-user watchlist (bookmark media to watch later) and ratings (1-10 score). These are personal to each user and visible on media detail pages and a dedicated watchlist page. diff --git a/docs/phase5-notifications-pwa.md b/docs/phase5-notifications-pwa.md index 0f9b09bc..35437553 100644 --- a/docs/phase5-notifications-pwa.md +++ b/docs/phase5-notifications-pwa.md @@ -1,5 +1,10 @@ # Phase 5: Notifications + PWA +> [!WARNING] +> Historical client-phase specification. PWA expansion is deferred beyond v1; +> current P5 is native TRaSH/Profilarr profile management. See +> [UNIFIED-ARR-PLAN.md](UNIFIED-ARR-PLAN.md). + ## Goal In-app notification system that alerts users when new content arrives, request status changes, or system events occur. Make the client web app installable as a PWA with optional push notifications. diff --git a/docs/streaming.md b/docs/streaming.md index e765ef70..2a6c255d 100644 --- a/docs/streaming.md +++ b/docs/streaming.md @@ -1,5 +1,11 @@ # StackArr Streaming Server +> [!WARNING] +> Current frozen subsystem reference, not a future-state commitment. +> `stackarr-stream` receives maintenance and tests but no new behavior through +> P5; StackArr does not aim to replace Jellyfin or Plex in v1. See +> [UNIFIED-ARR-PLAN.md](UNIFIED-ARR-PLAN.md#2-scope-and-source-hierarchy). + ## Overview A Plex-like streaming server built into StackArr with two components: