# Changelog ## [1.9.1] — 2026-05-16 ### 1.9.1 — Missed v1.9.0 items: health endpoint, silent failures, error UX ### Added: `GET /api/health` Lightweight liveness probe for Docker `HEALTHCHECK` and uptime monitors. Returns `{"status":"ok","version":"..."}` without any DB access. The Dockerfile `HEALTHCHECK` now uses `/api/health` instead of `/api/stats` — no full DB + AllDebrid query every 30 seconds just to confirm the process is alive. ### Fixed: Silent failures → logged Five `except ... pass` blocks in `manager_v2` now log at `DEBUG` level: - Hash extraction from torrent file - Webhook `on_added` task creation - Webhook `on_complete` task creation - Rule-engine magnet delete after block - Plex/Jellyfin library scan task creation ### Fixed: Jackett timeout error message Previously showed the raw exception. Now shows: > _"Search timed out. Try fewer indexers or increase the timeout in Settings → Search / Indexers."_ ### Fixed: JS syntax error in search error handler (unreachable code corrected) **292/292 tests passing** ## [1.9.0] - 2026-05-16 ### v1.9.0 — Production-ready release This release focuses entirely on stability, performance, correctness, and polish. No major new features — every change makes the existing system more reliable. #### Highlights **Search** - Jackett search no longer times out: removed per-result `check_before_add()` (up to 240 sequential DB connections per search), reduced to a single bulk lookup - `get_learning_stats()` cached for 60 s — no repeated 4-query DB round-trips on every search **Downloads** - aria2 defaults corrected: `split` and `max-connection-per-server` were `4` (a memory optimisation that capped speed to ~10% of link capacity) — now `16` - `file-allocation` corrected: `none` → `falloc` (better write performance on ext4/XFS) - Startup config migration: old low-performance values (4 or 8) are auto-upgraded to 16 on first start - Cached AllDebrid torrents (statusCode=4 at upload) now start downloading immediately without waiting for the next poll cycle - Jackett add: parallel batches of 3 instead of serial; 90 s timeout for torrent-URL path **Status display** - `TorrentStatus.PROCESSING` was written to the DB as the Enum repr (`"TorrentStatus.PROCESSING"`) instead of the plain value `"processing"` — fixed at source (`normalize_provider_state()`) and repaired at startup for existing rows **Orphan cleanup** - New `cleanup_alldebrid_orphans()`: deletes no-peer/error magnets from AllDebrid that have no local DB row; runs automatically every poll cycle; manual trigger via 🧹 Clean AD Orphans button **Search** - AllDebrid ID (`alldebrid_id`) now searchable in torrents list - AD ID displayed below torrent name in the queue table - Search placeholder updated accordingly #### Fixes in v1.8.x (now consolidated) - `v1.8.21` — Extract password list Add/Remove, Help tabs layout, Support nav card - `v1.8.22` — Extraction password list root cause: `filter(Boolean)` removed empty entries before render - `v1.8.23` — Password card was in `tab-advanced` not `tab-extract`; Help panels wrap for sticky tabs - `v1.8.24` — Escaped backtick in Extract tab template; `htab-automation` outside `view-help` - `v1.8.25` — AllDebrid orphan cleanup, AD-ID search, AD-ID display in table - `v1.8.26` — Archive Passwords in wrong Settings tab (premature ``), button order - `v1.8.27` — Jackett add fast-path + parallel batches + 90 s timeout - `v1.8.28` — Jackett search timeout: N×DB sequential calls replaced with bulk lookup - `v1.8.29` — `TorrentStatus.PROCESSING` stored as Enum repr; startup DB repair migration - `v1.8.30` — aria2 `split=4`/`max-connection=4` defaults throttled downloads to ~10% speed ## [1.8.20] - 2026-05-13 ### Changed — DB optimizations, Settings restructure, Help improvements, Nav cleanup, Expired fix #### 1. Database Optimizations **SQLite — additional pragmas:** | Pragma | Value | Effect | |--------|-------|--------| | `synchronous` | `NORMAL` | Fewer fsync calls; safe with WAL mode | | `busy_timeout` | `10000` | Wait up to 10 s before OperationalError (was 5 s) | | `temp_store` | `MEMORY` | Temp tables in RAM — avoids disk I/O for small ops | | `cache_size` | `-65536` | 64 MiB page cache — keeps hot queue pages in RAM | | `mmap_size` | `268435456` | 256 MiB memory-mapped I/O for sequential scans | | `foreign_keys` | `ON` | Enforce FK constraints; catches orphaned download_files | **New indexes (SQLite + PostgreSQL, idempotent):** | Index | Column(s) | Benefit | |-------|-----------|---------| | `idx_torrents_priority` | `(priority DESC, id ASC)` | Dispatch ORDER BY without full scan | | `idx_torrents_hash` | `(hash)` | Duplicate detection and magnet lookup | | `idx_torrents_created_at` | `(created_at)` | Analytics/learning 90-day window queries | | `idx_dlfiles_local_path` | `(local_path)` | MediaInfo and file-existence checks | #### 2. Settings — restructured tabs **New `🔎 Search / Indexers` tab** — Jackett and Prowlarr configuration moved here from Services. Services tab now only contains Sonarr, Radarr, and Media Servers. **Reporting merged into Notifications** — Webhook Actions, Statistics Reporting, and About Reporting are now part of `🔔 Notifications`. The tab-list no longer shows a separate Reporting entry. **Archive passwords → Add/Remove list** — the single-line password input in the Extract tab is replaced with a per-line list with `+` Add and `✕` Remove buttons. The hidden `s-extraction_password` field stores values as newline-separated strings. **Rule Engine enable toggle** was already present; confirmed visible in `🤖 Automation`. #### 3. Help — Automation tab New `🧠 Automation` help tab with: - **Rule Engine** — six practical examples (anime paths, REMUX low priority, large files pause, release group boost, sample block, auto label), condition/action reference table - **Download Profiles** — five example profiles (Streaming, Archive, Anime, Low Bandwidth, High Quality) as JSON with explanation of how active profiles interact with rules - **Saved Searches** — brief explanation of the scheduling and auto-add behaviour #### 4. Navigation - `Changelog` moved under **Project** nav-group (was floating above it) - **GitHub / Discord / Coffee** grouped under `❤️ Support` nav-section #### 5. AllDebrid — "Expired — Files removed" auto-reimport AllDebrid `statusCode 3` (Expired) was previously falling through to the `else: processing` branch and never resolved. Fix: - New constant `EXPIRED_CODE = 3` - Status mapping: `code == EXPIRED_CODE` → `provider_status = "expired"` - `sync_alldebrid_status()`: when `provider_status == "expired"`: - **Magnet stored** → clears `alldebrid_id`, sets `status = 'pending'`, fires `asyncio.create_task(_handle_expired_reimport(row, magnet_link))` Log: `WARN | alldebrid.manager | Magnet expired on AllDebrid (torrent N 'name') — reimporting` - **No magnet** → sets `status = 'error'` with message "Add magnet again to retry" - `_handle_expired_reimport()`: - Guards against double-reimport (checks status == 'pending' and alldebrid_id is NULL) - Re-uploads magnet via `upload_magnet`, updates row with new `alldebrid_id` - On failure: marks `status = 'error'` with error message ## [1.8.19] - 2026-05-13 ### Fixed - UI stability, Saved Searches, and PostgreSQL Learning - Fixed a broken Dashboard debug/status fragment that rendered raw CSS text in the UI. - Reset main content scroll position when changing pages so Changelog and other views open at the top. - Preserved settings scroll position when switching settings tabs to prevent the UI from jumping on Extract, Notifications, and related tabs. - Restored the standalone Saved Searches view loader so it no longer stays on "Loading..." and can create, run, and delete saved searches. - Fixed Historical Learning PostgreSQL queries by replacing SQLite-only date expressions with datetime parameters and PostgreSQL-safe string functions. - Synchronized the Docker image version label with the repository version. ## [1.8.18] - 2026-05-13 ### Added — Plex/Jellyfin, Extraction Password, Learning Score in Search #### Plex / Jellyfin Post-Import Trigger — `services/media_server.py` After every successful torrent completion, a library refresh is automatically triggered on configured media servers. Both execute as fire-and-forget tasks and never block the finalisation path. **Plex:** `GET /library/sections//refresh` (or `/sections/all/refresh` when no library ID is set). Auth via `X-Plex-Token` header. **Jellyfin:** `POST /Library/Refresh`. Auth via `X-MediaBrowser-Token` header. **New config fields:** | Field | Default | Description | |-------|---------|-------------| | `plex_url` | `""` | e.g. `http://192.168.1.10:32400` | | `plex_token` | `""` | Plex X-Plex-Token | | `plex_library_id` | `""` | Library section ID (empty = all) | | `jellyfin_url` | `""` | e.g. `http://192.168.1.10:8096` | | `jellyfin_api_key` | `""` | Jellyfin API key | **Settings → Services → 🎬 Media Servers** — new configuration panel. #### Archive Extraction Password — `extraction_password` config field A single password applied to all 7z and RAR extractions (`-p` flag). Useful for torrent packs distributed with a known password. Leave empty for unencrypted archives (default). **New config field:** `extraction_password: str = ""` **Settings → Advanced → 🔐 Extraction** — new configuration panel. #### Learning Score in Jackett Search Jackett search results are now annotated with a `_score` field (0.0–1.0) derived from the `GET /api/stats/learning` endpoint: - Score = 0.4 × indexer_trust + 0.4 × seeder_score (log-scale) + 0.2 × size_score - Results are sorted by score descending, then seeder count as tie-breaker - A colour-coded score badge (green ≥ 70, amber ≥ 50, grey < 50) appears in the new **Score** column in the results table header ## [1.8.16] - 2026-05-13 ### Added — Webhook Actions, Historical Learning, Advanced Extraction, Drag & Drop, Health Dashboard #### Generic Webhook Action System — `services/webhook_actions.py` Fire HTTP POST webhooks on torrent lifecycle events, independent of Discord notifications. **New config fields:** | Field | Description | |-------|-------------| | `webhook_on_added` | URL called when a torrent is accepted | | `webhook_on_complete` | URL called on successful download | | `webhook_on_error` | URL called when a torrent errors | | `webhook_secret` | Optional `Authorization: Bearer ` header | **Payload** (JSON): ```json {"event":"torrent.completed","torrent_id":42,"name":"…","status":"completed", "source":"jackett","label":"","size_bytes":4294967296,"hash":"…", "alldebrid_id":"…","error_message":null,"local_path":"/download/…"} ``` - Fire-and-forget via `asyncio.create_task` — never blocks the add/finalize path - **`POST /api/webhooks/test`** — sends a test payload to any URL without touching DB data - **Settings → Reporting → Webhook Actions** — configure URLs + test button #### Historical Learning — `services/learning.py` Derives success metrics from the last 90 days of torrent history. **`GET /api/stats/learning`** returns: - `indexers` — `[{indexer, total, completed, errors, no_peer, success_rate, score}]` Score is a weighted sum of success rate + low error rate + volume bonus (0.0–1.0) - `release_groups` — top release groups by success rate - `no_peer_rate` — overall fraction of uploads that received no-peer errors - `top_labels` — most-used labels **New `🧠 Learning` navigation view** — table of indexer performance scores, colour-coded (green ≥ 80 %, amber ≥ 50 %, red < 50 %), and a tag cloud of reliable release groups. #### Advanced Extraction Pipeline - **Nested archive support** — after extracting an archive, sub-directories of the destination are scanned for further `.rar`/`.zip`/`.7z` files and extracted automatically (up to 10 nested archives per parent, capped to avoid unbounded recursion). Only sub-directory archives are considered; sibling archives in the same folder are intentionally excluded. - **Retry on transient failures** — each archive is attempted twice before reporting an error, with a WARNING log on the first failure. #### Drag & Drop Priority Reordering Torrent table rows are now `draggable`. Dropping a row above another torrent adjusts its `priority` via `PATCH /api/torrents/{id}/priority`, moving it to the front or back of the dispatch queue. Visual `drag-over` highlight added via CSS (`.drag-over` class with dashed accent border). #### System Health Bar (Dashboard) A collapsible banner below the KPI strip shows live auto-recovery results: orphaned files reset, missed completions recovered, deadlock clears. The bar is hidden when the system is healthy and appears only when `recovery_loop` finds something to fix. #### `🧠 Learning` navigation entry New top-level nav item pointing to the Historical Learning view. ## [1.8.15] - 2026-05-13 ### Added — Analytics Chart, Smart Scheduler, Smart File Selection, MediaInfo #### Analytics — Hourly Completions Chart `loadAnalytics()` now renders an inline SVG bar chart of completions per hour when a window ≤ 48 h is selected. Each bar is labelled with the hour; hovering shows the exact count. No external chart library needed. #### Smart Scheduler — Night-Mode Enforcement `_enforce_smart_scheduler()` runs every poll cycle (inside `sync_status_loop`). When `bandwidth_day_window` is configured (e.g. `08:00-23:00`) and the current local time is **outside** that window, the function pushes reduced limits to aria2 via `change_global_options`: - `max-concurrent-downloads` → `bandwidth_night_max_dl` (default 1) - `max-overall-download-limit` → derived from `bandwidth_night_speed_mbps` Inside the day window the normal aria2 settings are restored automatically. No manual intervention required; works alongside the existing Settings UI. #### Smart File Selection — Sample & Extras Blocking Two new opt-in toggles in Settings → Advanced → File Filters: | Toggle | Config field | What it blocks | |--------|-------------|----------------| | Block sample / trailer files | `block_samples` | Files matching "sample", "trailer", "teaser" in name | | Block extras / featurettes | `block_extras` | Files inside /Extras/, /Featurettes/, /Behind the Scenes/ paths | Both default to `false` (opt-in) and are integrated into the existing `is_blocked()` gate — no schema changes needed. #### MediaInfo Integration — `services/mediainfo.py` (new service) New read-only service using **ffprobe** (preferred, already present in the Docker image) with **pymediainfo** as fallback. `GET /api/mediainfo?path=` returns: ```json { "format": "matroska", "duration_s": 5987.4, "size_bytes": 12884901888, "codec_video": "hevc", "codec_audio": "eac3", "resolution": "3840x2160", "hdr": true, "dolby_vision": false, "video": [...], "audio": [...], "subtitles": [...] } ``` Security: only paths inside the configured download folder are accepted (HTTP 403 otherwise). Results are cached in-process (up to 500 entries). #### CHANGELOG format fix (v1.8.14) v1.8.14 was logged with a non-standard header (`## v1.8.14 —` instead of `## [1.8.14]`). Fixed to conform to the Keep a Changelog format used by all other entries. ## [1.8.14] - 2026-05-13 ### Added — ETA, Starvation Prevention, Auto-Recovery, Download Profiles, Saved Searches View #### ETA Calculation `aria2_download_to_dict()` now returns an `eta_seconds` field computed from remaining bytes divided by the current download speed. A new `fmtEta()` helper in the frontend converts seconds to a human-readable format (`42s`, `3m 12s`, `1h 4m`) and shows the ETA below the live speed in the Downloads panel. #### Starvation Prevention — `priority_aging_loop` A new scheduler loop prevents low-priority torrents from waiting indefinitely behind a continuous stream of higher-priority additions. Every `priority_aging_interval_minutes` (default 15 min), torrents that have been in `ready`/`uploading`/`processing` status for longer than `priority_aging_threshold_minutes` (default 60 min) get their priority bumped by `priority_aging_step` (default 1), up to a maximum of 900. New config fields (all with safe defaults, no migration required): | Field | Default | Description | |-------|---------|-------------| | `priority_aging_interval_minutes` | `15` | How often the aging loop runs | | `priority_aging_threshold_minutes` | `60` | Minimum wait before a torrent is eligible | | `priority_aging_step` | `1` | Priority increment per cycle | #### Auto-Recovery — `services/recovery.py` (new service) Runs automatically every 5 minutes via `recovery_loop()` in the scheduler (with a 120 s startup delay to let the system settle). Also exposed as `POST /api/recovery/run` for manual triggering from Settings → Automation. Three checks: 1. **Orphaned queued files** — `download_files` rows with `status='queued'` whose GID no longer exists in aria2 (e.g. after a container restart) are reset to `status='pending'` so the next dispatch cycle re-submits them. 2. **Missed completions** — torrents stuck in `downloading`/`queued` whose *all* non-blocked `download_files` are `completed` locally (missed completion event, e.g. crash during finalisation) are marked `completed` and an event row is inserted. 3. **Queue deadlock** — if there are 0 active/queued downloads but ≥1 `ready` torrents, `manager.reset_services()` is called to clear the stuck Semaphore and trigger a fresh dispatch pass. #### Download Now Button `PATCH /api/torrents/{id}/priority` sets `priority=100`, moving a torrent to the front of the dispatch queue. A **⬇ Now** button appears in the torrents table for torrents in `ready` or `pending` status. #### Saved Searches — Dedicated Navigation View The Saved Searches feature now has its own `🔍 Saved Searches` navigation entry in addition to the Settings → Automation section. The dedicated view shows a table with query, filter, interval, auto-add flag, last-run timestamp, and per-row Run / Delete buttons. #### Download Profiles (Settings → Automation) A JSON editor for named download profiles (`name`, `download_path`, `priority`, `label`). Saved profiles can be activated/deactivated from the same panel; `GET /api/download-profiles` and `POST /api/download-profiles/activate` are the backing endpoints. #### Smart Scheduler Config Three new config fields for future night-mode bandwidth throttling: | Field | Default | Description | |-------|---------|-------------| | `bandwidth_day_window` | `""` | Active-hours window, e.g. `08:00-23:00` | | `bandwidth_night_max_dl` | `1` | Max concurrent downloads outside window | | `bandwidth_night_speed_mbps` | `0.0` | Speed cap at night (0 = unlimited) | #### Tests 6 new unit tests in `tests/test_recovery.py` covering `run_recovery_checks`, `_fix_queue_deadlock` (deadlock detected, reset called; no deadlock when active downloads present; no deadlock when nothing ready), and error-capture behaviour. **Total: 291/291 tests passing.** ## [1.8.13] - 2026-05-13 ### Added - Duplicate checks against completed files Duplicate Intelligence now compares incoming candidates against completed file names and local paths from `download_files`, not only torrent names. This lets the central duplicate gate catch files that were downloaded inside large packs, including same-episode matches with similar size or quality, before another AllDebrid upload is attempted. ## [1.8.12] - 2026-05-13 ### Fixed - Live download slot changes Changing the Downloads view slot selector now applies to the running process immediately. The quick aria2 global-options endpoint now updates the in-memory settings object after persisting `max_concurrent_downloads` and `aria2_max_active_downloads`, resets the manager semaphore, and triggers a pending aria2 dispatch pass so increased slots can fill without requiring an extra Settings save. ## [1.8.11] - 2026-05-13 ### Added - Stronger semantic duplicate decisions Duplicate Intelligence now treats same-episode or same-movie-year matches with the same quality or a very similar size as confident duplicates. These matches return `skip` before any AllDebrid upload, preventing duplicate downloads even when the magnet link or torrent hash differs across indexers. Semantic duplicate reasons now distinguish `same_episode`, `same_movie_year`, `same_normalized_title`, and `similar_title`, so API/UI consumers can explain why a result was flagged. ## [1.8.10] - 2026-05-13 ### Fixed - Auto-extract resource usage Auto-extract now uses the completed file paths already stored in `download_files` instead of recursively scanning the full torrent download folder after every download. This prevents large media packs from keeping disks and CPU busy just to discover whether archives exist. Archive extraction now runs in a dedicated bounded executor instead of the shared default executor, and `7z` extraction is limited to one worker thread per archive. The default concurrent extraction count is now `1` for safer Docker, NAS, and Unraid behavior while still allowing users to raise it explicitly. ## [1.8.9] - 2026-05-13 ### Fixed - Settings layout, reporting export, and aria2 log rotation Fixed the topbar labels for Search and Downloads, corrected the Analytics window default to show 24h as the selected view, and split Settings into clearer FlexGet, Reporting, and Database tabs. Settings inputs now use a more readable max width across tabs, FlexGet scheduled task rows are more compact, remove buttons are easier to see, and enabled switches update visually immediately after being toggled. Statistics export now serializes date values safely for JSON responses and snapshot storage handles date-like values as well. Built-in aria2 now supports configurable log rotation size and backup count to prevent the aria2 log file from growing without bound. The scheduler checks the log periodically and restarts built-in aria2 only when rotation requires a fresh log handle. Added a Jackett webhook test endpoint and Settings button, and made the Sonarr and Radarr test buttons easier to find. ## [1.8.8] - 2026-05-12 ### Added - Read-only duplicate preview and safer saved-search adding Added `POST /api/torrents/check-duplicate`, a read-only duplicate preview endpoint that uses the central Duplicate Intelligence service without uploading or importing anything to AllDebrid. Jackett search results now include a `duplicate` decision payload alongside the existing `already_added` fields, so the UI can surface the same duplicate state that add flows use. Saved Search auto-add now normalizes Jackett/Prowlarr result payloads correctly and prefers Jackett `.torrent` downloads before falling back to magnet links. This keeps automated searches aligned with the manual Jackett add flow and keeps the duplicate gate in front of every real AllDebrid upload. ## [1.8.7] - 2026-05-12 ### Fixed - Docker-safe logging and startup output Container startup now emits the compact AllDebrid Client summary through the standard logger instead of raw `print()` calls, avoiding broken box-drawing characters in Docker and Unraid logs. The startup block includes the GitHub project link and Buy Me a Coffee support link once per boot. Logging now has safe defaults for `log_level`, `log_pretty`, and `log_format`. Sensitive values are redacted before they reach logs or API error responses, including magnet links, Discord webhook URLs, PostgreSQL passwords, API keys, tokens, and long Debrid download URLs. FlexGet webhook logging no longer prints webhook URL fragments, Discord notification failures sanitize response details, PostgreSQL connection/migration errors are returned without leaking DSNs, and aria2 queue logs no longer include full private download URLs. ## [1.8.6] — 2026-05-12 ### Fixed — Duplicate guard hardening and non-blocking Jackett search aborts Duplicate Intelligence now also checks existing AllDebrid IDs and `_add_magnet()` has its own final duplicate gate immediately before `upload_magnet()`. This prevents lower-level or legacy add paths from bypassing the central duplicate decision. `.torrent` uploads now always run through the duplicate service before contacting AllDebrid, including watch-folder torrent files. Duplicate warnings are preserved in the returned row, and duplicate watch-folder imports no longer send a false "added" notification. The qBittorrent-compatible torrent upload endpoint now passes the uploaded filename to `add_torrent_file_direct()`, fixing `.torrent` uploads through the qBit API emulation. Jackett search now wires its `AbortController` into the shared API helper, so a new search really aborts the previous request instead of only ignoring stale responses. ## [1.8.5] — 2026-05-12 ### Fixed — PostgreSQL analytics duration query Queue Analytics now uses PostgreSQL `EXTRACT(EPOCH FROM (completed_at - created_at))` for average download duration instead of SQLite-only `JULIANDAY(...)`. This fixes PostgreSQL errors like: `function julianday(timestamp with time zone) does not exist` ## [1.8.4] — 2026-05-12 ### Fixed — PostgreSQL analytics and transient AllDebrid file exposure Queue Analytics now passes real `datetime` values to PostgreSQL instead of ISO strings, fixing asyncpg errors like: `expected a datetime.date or datetime.datetime instance, got 'str'` The hourly analytics query now uses PostgreSQL `DATE_TRUNC` when running on PostgreSQL and keeps SQLite `STRFTIME` for SQLite. The download starter no longer marks a torrent as failed when AllDebrid returns no downloadable file list and the magnet status cannot be confirmed. That state is treated as transient and the torrent is reset to `ready` for a later retry. ## [1.8.3] — 2026-05-12 ### Fixed — PostgreSQL saved searches schema PostgreSQL initialisation now creates the `saved_searches` table that was added for recurring Jackett/Prowlarr searches in v1.8.2. Previously only SQLite created the table, so PostgreSQL users hit: `asyncpg.exceptions.UndefinedTableError: relation "saved_searches" does not exist` when opening or polling the saved searches endpoint. The table is also included in bidirectional SQLite/PostgreSQL migrations so saved search definitions are preserved during database moves. The dependency lock now keeps `uvloop` behind its Linux/macOS platform marker so Windows-based verification can install the locked requirements without trying to build a package that does not support Windows. ## [1.8.2] — 2026-05-12 ### Added — Priority Queue, Rule Engine, Saved Searches, Queue Analytics --- #### Priority Queue The dispatch loop now processes torrents in **priority order** instead of pure FIFO. - `ORDER BY priority DESC, id ASC` in `_dispatch_pending_aria2_queue()` and `full_alldebrid_sync()` — higher `priority` value = processed sooner - New endpoint: `PATCH /api/torrents/{id}/priority` — body `{"priority": }` - Default priority is 0; negative values defer a torrent behind others - Starvation prevention: lower-priority torrents are still eventually dispatched (no strict pre-emption — active downloads are not cancelled) --- #### Rule Engine — `services/rules.py` Evaluate configurable rules against every newly added torrent. **Enable:** Settings → Automation → Enable Rule Engine **Rule format** (JSON in `rules_list` config field): ```json [ {"if": {"title_contains": "REMUX"}, "then": {"priority": -10}}, {"if": {"size_gb_gt": 80}, "then": {"pause": true}}, {"if": {"label_contains": "anime"}, "then": {"download_path": "/anime"}}, {"if": {"title_matches": ".*4K.*REMUX"}, "then": {"priority": 10}}, {"if": {"source_is": "jackett"}, "then": {"block": false}} ] ``` **Supported conditions:** - `title_contains` — case-insensitive substring - `title_matches` — regex (re.search) - `size_gb_gt` / `size_gb_lt` — size threshold in GB - `label_contains` — label/category substring - `source_is` — exact source (manual, jackett, watch, qbit, saved_search…) **Supported actions:** - `priority` — added to current priority (accumulated across all matching rules) - `pause` — torrent starts in paused state - `download_path` — override download folder - `label` — set or override label - `block` — skip upload to AllDebrid entirely (logged as INFO) **Test endpoint:** `POST /api/rules/test` — dry-run without affecting any torrent. **UI:** Settings → Automation → test button + JSON formatter. --- #### Saved Searches — DB + Scheduler + API + UI Recurring searches via Jackett or Prowlarr that can optionally auto-add results. **New DB table:** `saved_searches` (idempotent `CREATE TABLE IF NOT EXISTS`) **Scheduler loop:** `saved_searches_loop()` runs enabled searches according to their `interval_minutes` setting; respects `last_run_at` to avoid redundant runs. **New API endpoints:** | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/saved-searches` | List all saved searches | | `POST` | `/api/saved-searches` | Create a saved search | | `PUT` | `/api/saved-searches/{id}` | Update | | `DELETE` | `/api/saved-searches/{id}` | Delete | | `POST` | `/api/saved-searches/{id}/run` | Manual trigger | **Filter options per search:** min seeders, max/min size GB, regex filter, auto-add. **UI:** Settings → Automation → Saved Searches section. --- #### Queue Analytics — `services/analytics.py` New read-only analytics endpoint and dedicated view. **Endpoint:** `GET /api/analytics?window_hours=<1-720>` **Metrics returned:** | Field | Description | |-------|-------------| | `completed_count` | Torrents completed in window | | `error_count` | Torrents that failed in window | | `no_peer_count` | No-peer errors in window | | `success_rate` | completed / (completed + errors) | | `avg_duration_seconds` | Mean time from added to completed | | `throughput_gb` | Total GB downloaded in window | | `total_active` | Currently active (all non-terminal) | | `total_error` | Currently in error state | | `top_error_reasons` | [{reason, count}] top 5 | | `hourly_completed` | [{hour, count}] per-hour breakdown (≤48h windows) | **UI:** New **Analytics** nav item with 1h / 24h / 7d / 30d time-window selector, KPI strip, and top-error-reasons breakdown. --- #### Settings — Automation Tab New **🤖 Automation** tab in Settings between Services and Advanced: - Rule Engine: enable toggle + JSON editor + Test button + Format JSON button - Saved Searches: interval setting + add/run/delete UI + last-run timestamp --- #### Config — new fields (all with safe defaults) | Field | Default | Description | |-------|---------|-------------| | `rules_enabled` | `false` | Rule Engine opt-in | | `rules_list` | `"[]"` | JSON rules definition | | `saved_searches_interval_minutes` | `60` | Saved search scheduler interval | Existing `config.json` files load without changes. --- ## [1.8.0] — 2026-05-12 ### Smart Queue & Automation — Phase 1: Duplicate Intelligence, Logging, Search UX --- #### 1. Duplicate Intelligence — `services/duplicates.py` New central duplicate-detection service. Every add-flow now calls `check_before_add()` **before any AllDebrid contact**. **Service functions:** | Function | Purpose | |----------|---------| | `check_before_add(candidate)` | Main gate — returns `allow` / `warn` / `skip` | | `find_hash_duplicate(infohash)` | Stage 1 — exact BTIH match against `torrents.hash` | | `find_alldebrid_id_duplicate(ad_id)` | Stage 2 — AllDebrid ID check | | `find_semantic_duplicates(candidate)` | Stages 4–6 — fuzzy title + episode + size | | `normalize_title(title)` | Removes quality/codec/group tags, unifies separators | | `extract_release_tokens(title)` | Parses S01E02, year, quality, release group | | `extract_btih(magnet)` | Extracts BTIH hash from magnet URI (tracker-param agnostic) | **Decision logic:** - Exact hash match + active/completed status → `skip` (confidence 1.0) - Exact hash match + error/pending status → `warn` (allow retry) - Semantic match ≥ 85 % confidence + active → `warn` - No match → `allow` **Conservative defaults:** semantic matching is advisory only; only hard-blocks on confidence 1.0 (exact hash). Deleted torrents do not block re-adds. **Add-flows secured:** - `add_magnet_direct()` — duplicate check before `_add_magnet()` - `add_torrent_file_direct()` — local hash extracted from `.torrent` bytes via `extract_hash_from_torrent()` before any AllDebrid upload **API responses** include `_duplicate` field when action is `skip` or `warn`. --- #### 2. `extract_hash_from_torrent()` — `services/alldebrid.py` New utility function that extracts the SHA-1 info-hash from raw `.torrent` file bytes locally (no AllDebrid contact needed). Uses `bencodepy` when available, falls back to a manual bencoded-data scanner. **Key invariant enforced:** > Erst lokal prüfen. Dann entscheiden. Erst danach AllDebrid kontaktieren. --- #### 3. Logging overhaul **New log format:** ``` 2026-05-12 18:42:10 | INFO | alldebrid.manager | Torrent ready: Example.Name.2024 ``` - Aligned columns: timestamp | level | module (20 chars) | message - Noisy library loggers (`uvicorn.access`, `httpx`, `asyncpg`) downgraded to WARNING - All service loggers renamed to consistent `alldebrid.*` namespace: `alldebrid.manager`, `alldebrid.routes`, `alldebrid.jackett`, `alldebrid.prowlarr`, `alldebrid.aria2`, `alldebrid.db`, `alldebrid.duplicates`, `alldebrid.scheduler`, … - Logger name shortened: `alldebrid.aria2.runtime` → `alldebrid.aria2` **Startup banner** (printed once at container start): ``` ────────────────────────────────────────────────── AllDebrid Client vX.Y.Z ────────────────────────────────────────────────── Database: SQLite Download client: aria2 builtin Auth: disabled Web UI: http://0.0.0.0:8080 GitHub: https://github.com/kroeberd/alldebrid-client Support: https://ko-fi.com/kroeberd ────────────────────────────────────────────────── ``` --- #### 4. Jackett Search UX — non-blocking, AbortController, race-condition-safe **Problems fixed:** 1. **Global UI block** — while a Jackett search was in flight, navigating away or starting a new search could leave the UI in a broken state. 2. **Race condition** — if search B completed before search A, search A's stale results could overwrite B's results. 3. **Old results erased on new search start** — table was cleared before new results arrived, leaving a blank panel during long searches. **Solution:** ```javascript var _jackettSearchInFlight = false; var _jackettSearchController = null; // AbortController var _jackettSearchReqId = 0; // monotonic request ID ``` - Every new search aborts the previous via `AbortController.abort()` - `myReqId` guard: stale responses are silently dropped - Old results **stay visible** until new results are ready - `finally` block always resets `_jackettSearchInFlight` and re-enables the button - Search remains isolated — navigation, Dashboard, Torrents, aria2 queue all continue to work during a long Jackett search --- #### 5. Tests 34 new unit tests in `tests/test_duplicates.py`: - `TestExtractBtih` — BTIH extraction from magnets (6 tests) - `TestNormalizeTitle` — title normalisation (7 tests) - `TestExtractReleaseTokens` — S01E02, year, quality, group (6 tests) - `TestSizeSimilar` — size comparison logic (5 tests) - `TestFindHashDuplicate` — DB lookup with mocked DB (3 tests) - `TestCheckBeforeAdd` — allow/warn/skip decisions (3 tests) - `TestSearchReadOnly` — regression: search never calls `upload_magnet` (3 tests) Total: **262 tests passing** (was 228). --- #### Backward compatibility - No DB schema changes required — `torrents.hash` already exists - All new config fields have defaults — existing `config.json` loads unchanged - `_duplicate` field in API responses is optional — old clients ignore it - Duplicate detection defaults to `allow` on any internal error — never fails hard ## [1.7.0] — 2026-05-12 ### Release — Security hardening, repo cleanup, Dashboard redesign This is the v1.7.0 release, finalising all changes since v1.6.0. #### Security (CodeQL — 30 alerts → resolved) **Errors fixed:** | Alert | File | Fix | |-------|------|-----| | Log Injection (×3) | `api/qbit.py` | `hash_val` sanitised in `logger.warning` calls — newlines stripped, truncated to 64 chars | | Stack trace exposure | `api/routes.py` | Prowlarr `test_connection` error message sanitised and truncated to 200 chars before returning HTTP 502 | **Warnings fixed:** | Alert | File | Fix | |-------|------|-----| | Implicit string concatenation (×14) | `db/database.py` | All `CREATE INDEX` SQL strings merged into single-line strings via regex replacement | **Notes fixed / dismissed:** | Alert | File | Action | |-------|------|--------| | Empty `except:` in qbit.py | `api/qbit.py` | Added `except Exception as exc: logger.debug(...)` | | Empty `except:` in routes.py | `api/routes.py` | Added `except Exception as _e: logger.debug(...)` | | Empty `except:` in main.py | `main.py` | Added `# noqa: BLE001` with explanation (malformed auth header) | | Unused import `time`, `Path` | `api/qbit.py` | Removed | | Unused import `DB_PATH` | `services/manager_v2.py` | Removed from import | | Unused global `_ad_rate_lock` | `services/manager_v2.py` | Removed | | Unused local `source` | `services/manager_v2.py` | Added to `logger.error` call | | Unused imports (×4) | `tests/test_extractor.py` | Removed `shutil`, `tempfile`, inline `asyncio`, `timings` | | Cyclic import ×2 | `manager_v2`, `alldebrid` | Dismissed — intentional lazy-import pattern | #### Repo cleanup 19 unused files removed: - `.github/workflows/build-windows-exe.yml` (Windows EXE) - `backend/windows_main.py` - `packaging/` (PyInstaller spec + hooks + windows requirements) - `docs/.nojekyll`, `docs/_config.yml` (Jekyll) - `docs/fenrus.md`, `docs/windows-exe-build.md` - `docs/screenshots/*.svg` + `generate_screenshots.py` - `unraid-template.xml` (now in `kroeberd/unraid-templates`) - `deploy-unraid.sh` - `backend/tests/_tmp_watch_scan/` (added to `.gitignore`) #### Dashboard redesign New layout with three visual levels: 1. **Hero stat row** — 6 large cards (Total, Completed, Active, Processing, Errors, Downloaded) with color-coded top accent, icon, bold number and label. Error card is clickable and dims when error count is 0. 2. **KPI strip** — horizontal bar with 7 key metrics: Queue Health (color-coded), Last 24h, Last 7d, Success Rate, Avg Duration, Avg Size, Database. 3. **Add Magnet + Recent Activity** — Import/Recover All moved to card header; Recent Activity shows entry count and a mini progress bar for active downloads. ## [1.6.10] — 2026-05-12 ### Fixed — Bidirectional max-downloads sync, Design improvements, Mobile #### 1. Bidirectional max-downloads synchronization (main bug) **Root cause:** `max_concurrent_downloads` (Manager Semaphore) and `aria2_max_active_downloads` (aria2 RPC option) were two separate config fields that could diverge silently. **Symptom reported:** Changing *max downloads* in **Settings** → **Download Client** overwrote the value set in the **Downloads panel** quick-setter. The reverse direction (Downloads → Settings) did not propagate back. **Fix — four coordinated changes:** 1. **`PUT /settings` (backend)** now runs `clean.model_copy(update={"aria2_max_active_downloads": clean.max_concurrent_downloads})` before saving. Every Save always writes both fields to the same value, so `aria2_global_options()` (which reads `aria2_max_active_downloads`) sends the correct limit to aria2. 2. **`loadAria2SpeedLimit()` (frontend)** now also: - writes `settingsData.max_concurrent_downloads` and `settingsData.aria2_max_active_downloads` from the live aria2 value - sets the `s-max_concurrent_downloads` and `s-aria2_max_active_downloads` form inputs so the Settings tab always shows the live value 3. **`saveSettings()` (frontend)** calls `loadAria2SpeedLimit()` after a successful PUT, so the Downloads panel immediately reflects any change made in Settings without requiring a manual refresh. 4. **`syncMaxDlFields(val)` (new frontend function)** — attached as `onchange` to both `s-max_concurrent_downloads` and `s-aria2_max_active_downloads`. Typing in either field instantly mirrors the value to the other, preventing the fields from diverging before Save. **max speed**: `aria2_max_download_limit` (bytes/s, persisted by `POST /aria2/global-options`) and `max_speed_mbps` (legacy field, always 0) are handled separately. `loadAria2SpeedLimit` now also writes `settingsData.aria2_max_download_limit` from the live aria2 value, keeping the speed preset consistent after reload. #### 2. CSS improvements | Item | Change | |------|--------| | `form-hint` | `body.light .form-hint` uses `var(--text2)` for better readability | | `.btn:focus-visible` | Added 2px accent outline for keyboard accessibility | | `.aria2-err-banner` | CSS class added (was only inline style in HTML) | | `settings-grid` | `minmax(min(340px,100%),1fr)` — fills full width on narrow viewports | | `.t-name` max-width | `clamp(140px,30vw,380px)` — responsive, not hard-coded 240px | #### 3. Mobile improvements - **Settings grid** — collapses to single column at 700px (was two-column regardless) - **Downloads view header** — stacks vertically on narrow screens - **Filter tabs** — `flex-wrap: wrap` allows tabs to wrap on small screens - **Event search row** — stacks vertically on mobile #### 4. Parallel downloads — confirmed working The Manager Semaphore now always matches aria2's `max-concurrent-downloads` because both fields are kept in sync at save time and at aria2 startup (`aria2_global_options()` reads `aria2_max_active_downloads`, which equals `max_concurrent_downloads` after this fix). ## [1.6.9] — 2026-05-12 ### Fixed & Improved — aria2 parallel downloads, Settings race conditions, Dashboard, UI #### 1. Code cleanup - **`stats_report_window_hours` duplicate removed** — `getFormSettings()` had an early `n('stats_report_window_hours')` entry that was immediately overwritten by the correct `reportWindowHours` value later in the same object literal. The dead first entry has been removed. - **Config form-hints added** for `aria2 Split Connections` (parallel connections per file, capped by max-connections-per-server), `aria2 Disk Cache` (write buffer with format examples), and `AllDebrid Poll Interval` (min/default/effect). - `loadAria2SpeedLimit` timeout raised from 8 s (default) to 10 s for consistency with other aria2 calls. #### 2. aria2 parallel downloads — three-layer fix **Root cause:** changing max concurrent downloads via the Downloads panel quick-setter (`↕ Max DL`) updated aria2 via RPC and persisted `aria2_max_active_downloads` to settings.json, but **not** `max_concurrent_downloads`. The Manager Semaphore (which gates how many torrents are dispatched to aria2 at once) reads `max_concurrent_downloads` and was never reset, so it kept the old limit until the next container restart. **Fix — three coordinated changes:** 1. **Backend `POST /aria2/global-options`** now persists **both** `aria2_max_active_downloads` and `max_concurrent_downloads` when `max_concurrent_downloads` is included in the body. Previously only the aria2 field was written. 2. **Backend** calls `manager.reset_services()` after persisting the new `max_concurrent_downloads` so the lazy-initialised Semaphore is discarded immediately. The next `_start_download` call recreates it with the new limit. 3. **Frontend `applyAria2MaxDlPreset`** now updates `settingsData.max_concurrent_downloads` (in addition to `aria2_max_active_downloads`) so a subsequent **Save Settings** PUT does not clobber the live-applied value. #### 3. Dashboard & Torrents — Button layout The **⬇ Import from AllDebrid** and **⟳ Recover All** buttons were cramped into the same `input-row` as the magnet-link input and primary Add button, causing horizontal overflow on narrow screens and making Recover All look like a standard input action. Both views (Dashboard and Torrents) now place Import and Recover All in a separate `flex-wrap` row below the Add input, using `btn-sm` sizing. This matches the pattern used in the Jackett Search view and improves clarity on all screen widths. #### 4. CSS — `btn-warn` added `btn-warn` was referenced throughout the HTML (Dashboard, Torrents view) but had no CSS rule — the button rendered as an unstyled browser default. Added: ```css .btn-warn { background: rgba(234,179,8,.12); color: var(--yellow); border: 1px solid rgba(234,179,8,.25); } .btn-warn:hover { background: rgba(234,179,8,.2); border-color: rgba(234,179,8,.5); } ``` #### 5. Light Mode improvements - **`insight-card`** (Dashboard KPI grid): `rgba(255,255,255,.04)` was near-invisible on a white background. Light mode now uses `var(--surface2)` for a visible card. - **`input-row`**: added `flex-wrap: wrap` so buttons don't overflow on narrow viewports. ## [1.6.8] — 2026-05-12 ### Fixed & Improved — aria2 panel timeout, settings race condition, UI contrast #### 1 — aria2 Download panel: timeout + auto-recovery **Root cause:** `api()` has an 8-second default timeout. The `GET /aria2/downloads` call can exceed that when aria2 has many completed items to enumerate. On timeout the catch block replaced the entire table body with an error message and — crucially — did **not** reschedule the polling timer, so the panel went permanently blank. **Fixes:** - `api('GET', '/aria2/downloads', null, 20000)` — explicit 20 s timeout. - **Non-destructive error banner** (`#aria2q-err-banner`, added to HTML) — shown on failure without clearing existing table rows, so previously fetched downloads remain visible during a temporary outage. - **Always reschedule**, even after errors — `setTimeout(loadAria2QueueView, delay)` runs in every code path (success and failure alike). - **Exponential back-off** — 2 s on success, 3 s after 1–3 consecutive errors, 10 s after 4+ errors (aria2 restarting). - Error counter `_aria2qErrCount` resets to 0 on the next successful response. #### 2 — Settings: hardcoded values clobbering saved configuration Two fields in `getFormSettings()` were hardcoded regardless of what the user had saved, causing silent data loss on every **Save Settings** click: | Field | Old value | Fix | |-------|-----------|-----| | `max_speed_mbps` | always `0` | reads from `settingsData.max_speed_mbps` | | `download_client` | always `'aria2'` | reads from the `s-download_client` form field (preserves `'symlink'`) | Additionally, `applyAria2MaxDlPreset()` wrote the new max-concurrent-downloads value into `settingsData.aria2_max_active_downloads` but did **not** sync the `s-max_concurrent_downloads` form input. A subsequent **Save Settings** would then read the stale input value and overwrite the change. Fixed by syncing both the `s-max_concurrent_downloads` and `s-aria2_max_active_downloads` inputs when the quick-setter is used. #### 3 — Config page: missing descriptions Added `form-hint` descriptions for: - **Max Concurrent Downloads** — explains the difference from aria2's own max-active-downloads, recommends default of 3, mentions RAM/bandwidth trade-off. - **aria2 Poll Interval** — clarifies unit (seconds), typical range, and effect on UI responsiveness. #### 5 — Dark / Light mode contrast | Item | Change | |------|--------| | `badge-queued` | `#93c5fd` → `var(--blue)` — `#93c5fd` is near-white in light mode | | `badge-completed` | `#4ade80` → `var(--green)` — consistent with other badges | | `:disabled` opacity | `.5` → `.45` (dark) / `.55` (light) — better visibility in both modes | | `form-hint` text | light mode now uses `var(--text2)` for improved readability | | `::placeholder` | light mode boosts opacity slightly for better contrast | #### 6 — Quality assurance - 228/228 backend tests passing - JS syntax verified (`node --check`) - All existing functions preserved ## [1.6.7] — 2026-05-11 ### Fixed — No-peer (code 8) errors not being cleaned up Two related bugs caused torrents stuck in `error` state with "No peer after 30 minutes" (AllDebrid statusCode 8) to persist indefinitely instead of being automatically removed. #### Bug 1 — `cleanup_no_peer_errors` missed NULL error_message The cleanup SQL used `LOWER(error_message) LIKE '%no peer%'` as the primary match condition. In SQLite, `LOWER(NULL) LIKE '%..%'` evaluates to NULL (not TRUE), so any torrent whose `error_message` was NULL — e.g. freshly imported via `import_existing_magnets` before `_apply_provider_update` ran — was silently skipped even though `provider_status_code = 8` was set. **Fix:** Replaced `LOWER(error_message)` with `LOWER(COALESCE(error_message, ''))` so NULL is treated as an empty string. Also reordered the conditions to check `provider_status_code` first (cheaper index scan) before the LIKE patterns. #### Bug 2 — Already-error torrents with code 8 were never re-processed `_apply_provider_update` handled no-peer only inside: ```python elif provider_status == "error" and current_status != "error": ``` If a torrent was already in `error` state (from a previous cycle or from `import_existing_magnets`), this branch was skipped entirely. The only fallback was `cleanup_no_peer_errors`, but that required `error_message` to be set (Bug 1) or `provider_status_code = 8` to be in the DB. **Fix:** Added a new branch: ```python elif provider_status == "error" and current_status == "error" and status_code in (7, 8): ``` This branch writes the AllDebrid error message to `error_message` via `COALESCE(NULLIF(error_message, ''), ?)` — only filling in the field if it was previously empty. This ensures the cleanup SQL's LIKE condition matches on the next cycle even if the torrent was imported without an error message. The actual deletion still happens in `cleanup_no_peer_errors` (called immediately after `sync_alldebrid_status` in `sync_status_loop`), which removes the magnet from AllDebrid and sets `status='deleted'`. ## [1.6.6] — 2026-05-11 ### Fixed — Torrent details, bulk actions, error filter; Added — Unraid auto-update #### Fix — Torrent detail modal not opening `GET /api/torrents/{id}` did not exist as a registered FastAPI endpoint. The implementation code had been accidentally placed inside `block_file()` after its `return` statement — making it unreachable dead code. **Fix:** Extracted the torrent detail logic into a proper `@router.get("/torrents/{torrent_id}")` endpoint that returns the torrent row, its `download_files` and the 50 most recent `events`. #### Fix — Bulk-action toolbar not visible The `.bulk-bar` CSS class controls `display:none/flex` via `.bulk-bar.visible`. The Torrents view had no `bulk-bar` element at all — `onCheckboxChange()` was calling `getElementById('bulk-bar')` on a non-existent element, causing a silent JS error that also broke other interactions in the view. **Fix:** Added the `
` element to the Torrents view HTML, containing: - Selected-count label - **✕ Delete** — bulk delete from AllDebrid and DB - **↺ Reset** — set status back to `ready` (re-queues for next AllDebrid sync) - **⏸ Pause** / **▶ Resume** - **✕ Clear** — deselect all Also added the **⟳ Recover All** button to the Torrents input bar. #### Fix — Bulk reset / pause / resume not implemented in backend `POST /api/torrents/bulk` handled `delete` and `retry` but not `reset`, `pause`, or `resume`. Added all three: - `reset` — sets `status='ready'`, clears `error_message` and `polling_failures` for torrents that have an `alldebrid_id` (safe to re-queue) - `pause` — calls `manager.pause_torrent()` - `resume` — calls `manager.resume_torrent()` #### Fix — Error filter showing no results The Error filter tab (`setFilter(this,'error')`) was correct, and the backend `list_torrents` query (`WHERE t.status = 'error'`) was correct. The filter appeared empty because the JS runtime crashed silently on every `showDetail()` call attempt (due to the missing `GET /api/torrents/{id}` endpoint returning 404), leaving the torrent list in a broken state. Fixed as a side-effect of the endpoint fix above. #### Added — Automatic Unraid template update on release New GitHub Actions workflow: `.github/workflows/update-unraid-template.yml` Triggers on every published GitHub Release (and manually via `workflow_dispatch`). Steps: 1. Checks out `kroeberd/unraid-templates` using `UNRAID_TEMPLATES_PAT` secret 2. Replaces `[b]AllDebrid-Client vX.Y.Z[/b]` with the new version 3. Updates the `` field to the release date 4. Commits and pushes to `unraid-templates` The `UNRAID_TEMPLATES_PAT` secret has been set in the repository. It uses the same PAT that has write access to `kroeberd/unraid-templates`. ## [1.6.5] — 2026-05-11 ### Maintenance — German text cleanup, Dependabot PRs, Unraid template update #### German text → English All remaining German-language strings in source files have been translated: | File | String | |------|--------| | `backend/core/config.py` | `# falls leer → discord_webhook_url` | | `backend/db/migration.py` | 2 German docstrings in migration functions | | `backend/tests/test_manager_v2.py` | 2 German inline comments | | `docs/discord-webhooks.md` | Heading and table cell | | `docs/migration.md` | H1 heading | | `frontend/static/app.js` | 6 German debug and comment strings | #### Dependabot PRs merged (#20–#25) All open Dependabot pull requests merged: | PR | Change | |----|--------| | #20 | `peter-evans/dockerhub-description` 4.0.2 → 5.0.0 | | #21 | `actions/upload-artifact` v4 → v7 | | #22 | `softprops/action-gh-release` 2.6.2 → 3.0.0 | | #23 | `docker/login-action` 3.7.0 → 4.1.0 | | #24 | `docker/setup-buildx-action` 3.12.0 → 4.0.0 | | #25 | `pydantic` 2.13.3 → 2.13.4, `pydantic-settings` 2.14.0 → 2.14.1, `python-multipart` 0.0.27 → 0.0.28, `prometheus-client` 0.21.1 → 0.25.0 | #### Unraid Community App template updated `kroeberd/unraid-templates` — `templates/alldebrid-client.xml`: - Version reference updated: v1.5.2 → v1.6.4 - Features list rewritten to include qBit API, Prowlarr, SSE, Auth, Disk guard, Post-processing, Symlink downloader, File selection, Prometheus metrics - Quick Setup updated: added Sonarr/Radarr qBit configuration step - Description updated - Date: 2026-05-11 #### README - Added Prowlarr row to Features table - Added File Selection row to Features table ## [1.6.4] — 2026-05-11 ### Added — P1.6 Prowlarr, P2.1 File Selection, P3.4 Request-IDs, P3.5 Backoff-Jitter All remaining open items from the architecture analysis are now complete (19/19). #### P1.6 — Prowlarr integration Prowlarr is the modern successor to Jackett with native *arr integration. It can be used alongside or instead of Jackett. **New service:** `services/prowlarr.py` - `search(query, indexer_ids, categories, limit)` — searches via `GET /api/v1/search` - `get_indexers()` — lists configured indexers via `GET /api/v1/indexer` - `test_connection()` — checks `/api/v1/health` - Returns the same normalised result format as the Jackett integration **New API endpoints:** - `GET /api/prowlarr/indexers` — list Prowlarr indexers - `GET /api/prowlarr/search?q=...` — search (optional `indexerIds`, `categories`, `limit`) - `POST /api/prowlarr/test` — connectivity check **New settings:** `prowlarr_enabled`, `prowlarr_url` (default `http://localhost:9696`), `prowlarr_api_key` — all exposed in **Settings → Services → Prowlarr**. #### P2.1 — Per-file selection before download Two new API endpoints allow inspecting and blocking individual files within a torrent before or after download starts. **`GET /api/torrents/{id}/files-preview`** Returns the list of downloadable files for a torrent: - For `ready`/`processing` torrents: fetches the file list live from AllDebrid via `get_magnet_files()` — no state change, no download started. - For `queued`/`downloading`/`completed` torrents: returns the local `download_files` rows. Response: `{"source": "alldebrid"|"local", "files": [...]}` **`POST /api/torrents/{id}/files/{file_id}/block?blocked=true|false`** Toggles the `blocked` flag on a `download_files` row. Blocked files are skipped by `_dispatch_pending_aria2_queue` and not counted toward torrent completion. Use `blocked=false` to unblock. Works on already-queued files (the next dispatch cycle will skip them). #### P3.4 — Request-IDs for API correlation Every HTTP response now includes an `X-Request-ID` header. If the client sends `X-Request-ID: `, that value is echoed back. Otherwise a UUID4 is generated server-side. Implemented as a lightweight `@app.middleware("http")` in `main.py` — zero overhead, no dependencies. #### P3.5 — Startup jitter for scheduler loops All scheduler loops previously started simultaneously at container boot, causing a burst of AllDebrid API calls in the first few seconds. **New helper:** `_jitter_sleep(base_seconds, jitter_fraction=0.25)` — sleeps for `base ± 25%` of the configured interval, minimum 1 second. Applied to: `watch_folder_loop`, `sync_status_loop`, `full_sync_loop` (replaces the hard-coded `asyncio.sleep(10)`), `sync_download_clients_loop`. With `poll_interval_seconds=30` (default), the four critical loops now start spread across a ±7.5-second window instead of firing simultaneously. --- ### Analysis completion — 19/19 items | # | Item | Version | |---|------|---------| | P0.1 | HTTP Basic Auth | v1.5.49 | | P0.2 | qBittorrent API emulation | v1.5.51 | | P1.1 | SSE live updates | v1.5.50 | | P1.2 | DB indexes | v1.5.48 | | P1.3 | Token-bucket rate limiter | v1.5.48 | | P1.4 | Events TTL | v1.5.48 | | P1.5 | Disk space guard | v1.5.49 | | P1.6 | Prowlarr integration | **v1.6.4** | | P2.1 | File selection (preview + block) | **v1.6.4** | | P2.2 | Symlink downloader | v1.6.1 | | P2.3 | Prometheus metrics | v1.5.50 | | P2.4 | OpenAPI / Swagger UI | v1.6.1 | | P2.5 | State machine | v1.5.51 | | P2.6 | Post-processing scripts | v1.5.49 | | P3.1 | Formal state machine | v1.5.51 | | P3.2 | Frontend CSS/JS split | v1.6.1 | | P3.3 | Requirements hash-pinning | v1.6.1 / v1.6.3 | | P3.4 | Request-IDs for correlation | **v1.6.4** | | P3.5 | Startup jitter for scheduler | **v1.6.4** | ## [1.6.3] — 2026-05-10 ### Fixed — Docker release tags on VERSION pushes Docker builds triggered from `main` now read the repository `VERSION` file and publish matching image tags (`` and `.`) in addition to `latest` and `sha-*`. This closes the release gap where the GitHub Release workflow created `v*` tags from `VERSION`, but the Docker workflow running on the same `main` push only published `latest` and `sha-*` because semver tags are only resolved on tag refs. ## [1.6.2] — 2026-05-10 ### Fixed — Docker build dependency lock The Docker image build failed because `requirements.txt` mixed hashed and unhashed requirements. Pip automatically enabled hash-checking mode and then rejected the un-hashed test dependency `pytest==9.0.3`. This release fixes the install path by separating runtime and development dependencies: - `backend/requirements.in` lists direct runtime dependencies. - `backend/requirements.txt` is the generated runtime lock installed by Docker. - `backend/requirements-dev.in` lists test-only dependencies on top of runtime. - `backend/requirements-dev.txt` is used by CI tests. The redundant unpinned Docker install of `asyncpg>=0.29.0` was removed so the container now installs only the locked dependency set. The Docker image version label was also synchronized with the repository version. ## [1.6.1] — 2026-05-10 ### Added — Symlink downloader (P2.2), OpenAPI/Swagger (P2.4), Frontend split (P3.2), Hash-pinning (P3.3) #### P2.2 — Symlink / .url downloader A second delivery mode alongside aria2. Instead of downloading files, the client unlocks each AllDebrid CDN link and writes a `.url` file containing the unlocked HTTPS URL. Useful for setups with an rclone AllDebrid mount where the cloud storage is directly accessible (e.g. Plex/Jellyfin streaming without local disk usage). **Config:** `download_client: "symlink"` and optionally `symlink_path` (defaults to `download_folder`). Both settings are exposed in **Settings → Download Client → Delivery Mode**. **Behaviour:** - Fetches the ready-file list from AllDebrid (same as aria2 mode) - Unlocks all links in parallel via `ad().unlock_link()` - Writes `//.url` text files - Marks the torrent `completed` immediately — no aria2 involvement - On failure: `_fail_torrent` with a clear message; torrent row kept for retry **Implementation:** `manager_v2._download_symlink()` is called from `_download()` when `download_client_name() == "symlink"`. `download_client_name()` now reads `cfg.download_client` instead of hardcoding `"aria2"`. #### P2.4 — OpenAPI / Swagger UI FastAPI's built-in interactive documentation is now enabled: | Path | Description | |------|-------------| | `/docs` | Swagger UI — try endpoints interactively | | `/redoc` | ReDoc — clean reference documentation | | `/openapi.json` | Machine-readable OpenAPI 3.1 schema | The `FastAPI()` constructor now passes explicit `docs_url`, `redoc_url`, and `openapi_url` parameters, and the app description includes a Markdown table describing the two API prefixes (`/api/` and `/api/v2/`). #### P3.2 — Frontend split into separate CSS and JS files The 5 248-line `index.html` has been split into three files: | File | Lines | Contents | |------|-------|---------| | `index.html` | 989 | HTML skeleton, `` and `