Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@
pr_check_report.md
pr_number.txt
AGENT_HANDOFF.md

# Trail indexes and machine-local state (knowledge stays versioned)
/.trail/local/
/.trail/cache/
6 changes: 6 additions & 0 deletions .trail/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Only curated decisions are shared. Generated/user state stays local.
*
!.gitignore
!learnings/
!learnings/*.md
cache/
19 changes: 19 additions & 0 deletions .trail/learnings/01-no-source-hostnames.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: Never name or commit real source hostnames
date: 2026-10-19
author: Quasarr
touches:
- AGENTS.md
- check_trail.py
- quasarr/storage/config.py
---

## Decided

Never name, hardcode, or commit real source hostnames. Never put them in any file that is not gitignored, including code, tests, comments, learnings, captures, temporary scripts, and generated assets. Refer to sources only by their two-letter uppercase identifiers. Hostnames belong in encrypted runtime configuration outside the versioned repository.

Why: Source identity is runtime configuration, not public repository content. Architectural knowledge and reproducible tests must preserve behavior without publishing configured sources or user information.

## Watch out

Use synthetic titles and reserved `.invalid` domains in examples and tests. Keep real traffic evidence gitignored and extract only sanitized conclusions. Never force-add ignored captures or configuration. A pre-commit edit does not override this prohibition.
21 changes: 21 additions & 0 deletions .trail/learnings/02-run-and-accept-pre-commit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
title: Run pre-commit and accept all of its edits
date: 2026-10-19
author: Quasarr
touches:
- AGENTS.md
- pre-commit.py
- pyproject.toml
- uv.lock
- quasarr/providers/version.py
---

## Decided

Before finalizing changes or pushing, run `uv run python -X utf8 pre-commit.py`. Accept and keep every edit it makes, including formatting, dependency upgrades, lockfile changes, and version bumps. Do not revert its edits as unrelated cleanup.

Why: The script upgrades dependencies before formatting and testing, so its resulting files are the validated submission. Reverting them discards that validation and leaves CI to restore them in separate automatic commits.

## Watch out

Use the uv version required by the project. Re-run pre-commit after history rewrites and keep its resulting edits before pushing. A failed run must be fixed and completed successfully; it is not permission to skip the gate. The no-source-hostnames rule still takes precedence.
18 changes: 18 additions & 0 deletions .trail/learnings/03-startup-gates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
title: Gate application startup through ordered setup
date: 2026-10-19
author: Quasarr
touches:
- quasarr/__init__.py
- quasarr/storage/setup/__init__.py
---

## Decided

Keep prerequisite setup blocking and separate from the main API.

Why: Later checks depend on persisted configuration and verified services. Temporary setup servers collect prerequisites without exposing a partially initialized application. Required JDownloader setup cannot be skipped because Quasarr cannot fulfill grabs without it.

## Watch out

API serving starts only after required gates finish; skippable prerequisites record explicit skip state and required JDownloader setup cannot be skipped.
19 changes: 19 additions & 0 deletions .trail/learnings/04-source-identity-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: Use source keys as stable cross-layer identity
date: 2026-10-19
author: Quasarr
touches:
- quasarr/search/sources/__init__.py
- quasarr/downloads/sources/__init__.py
- quasarr/constants/__init__.py
---

## Decided

Represent each source with one two-letter key shared by its search module, hostname config entry, logs, and any source-specific download extractor.

Why: The key is the join between configuration, discovery, capability metadata, logs, and extraction. Keeping source-specific behavior in source modules prevents shared orchestration from accumulating hostname rules. Search-only sources may omit a download module and use generic crypter detection.

## Watch out

Source-specific hostname logic stays at the source boundary; shared code consumes source metadata and keys.
19 changes: 19 additions & 0 deletions .trail/learnings/05-category-semantics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: Separate category identity from search behavior
date: 2026-10-19
author: Quasarr
touches:
- quasarr/constants/__init__.py
- quasarr/search/__init__.py
- quasarr/storage/categories.py
---

## Decided

Keep category definitions, base-category semantics, behavior categories, cache-family ownership, and source whitelists as separate concepts.

Why: Radarr/Sonarr-facing categories can be stricter views of one content family. Search needs the base category for metadata and capability checks, the behavior category for query rules, and the cache owner for safe result reuse. Combining them makes subcategory filtering leak across requests.

## Watch out

Static category metadata comes from constants; mutable source lists and custom category settings come from storage.
18 changes: 18 additions & 0 deletions .trail/learnings/06-search-wall-clock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
title: Bound search fan-out by one absolute deadline
date: 2026-10-19
author: Quasarr
touches:
- quasarr/search/__init__.py
- quasarr/constants/__init__.py
---

## Decided

Anchor one absolute deadline before metadata warming and give each eligible source its own worker in the fan-out.

Why: *arr clients disable indexers that exceed their request budget. A relative timeout per source lets slow work accumulate, while a CPU-sized pool can queue sources that never get started. One worker per submitted source plus a shared wall-clock deadline preserves response bounds and source fairness.

## Watch out

No source gets a fresh full timeout after another source consumes the budget; pending work is abandoned when the shared deadline expires.
19 changes: 19 additions & 0 deletions .trail/learnings/07-source-extraction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: Keep download extraction source-specific
date: 2026-10-19
author: Quasarr
touches:
- quasarr/downloads/sources/helpers/abstract_source.py
- quasarr/downloads/sources/helpers/download_release.py
- quasarr/downloads/sources/__init__.py
---

## Decided

Let each download source implement its own release-link extraction behind the shared source helper contract.

Why: Protected pages, redirects, mirrors, and release layouts differ by source. The orchestrator needs a common result shape, while extraction rules must stay close to the source that owns them so a change in one parser cannot widen or corrupt another source's links.

## Watch out

Shared download flow classifies and schedules extracted links; it does not infer source payloads or HTML structure.
19 changes: 19 additions & 0 deletions .trail/learnings/08-browser-captcha-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: Solve protected crypters in the user browser
date: 2026-10-19
author: Quasarr
touches:
- quasarr/downloads/linkcrypters/__init__.py
- quasarr/api/captcha/__init__.py
- quasarr/providers/web_server.py
---

## Decided

Keep CAPTCHA-protected crypter solving in the browser userscript flow and have Quasarr coordinate links, status, and callbacks.

Why: CAPTCHA challenges bind to browser context and user interaction. Server-side solving would duplicate browser state and turn source-specific challenge handling into a fragile backend service. The web API can expose the standardized flow while the browser performs the challenge.

## Watch out

Protected-queue callbacks preserve browser ownership; do not turn the browser crypter flow into a backend CAPTCHA service.
17 changes: 17 additions & 0 deletions .trail/learnings/09-flaresolverr-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
title: Clean uncertain solver sessions asynchronously
date: 2026-10-19
author: Quasarr
touches:
- quasarr/providers/cloudflare.py
---

## Decided

When solver session creation times out after a session may exist, queue bounded asynchronous cleanup instead of blocking the request indefinitely.

Why: A create timeout leaves ownership uncertain. Listing sessions before destruction handles delayed visibility, while exponential retry, a hard cleanup deadline, and endpoint round-robin prevent one backlog from starving another endpoint.

## Watch out

Cleanup is best-effort but bounded; pending session IDs cannot consume unbounded threads, retries, or request time.
19 changes: 19 additions & 0 deletions .trail/learnings/10-config-db-lock-order.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: Serialize config and database writes with fixed lock order
date: 2026-10-19
author: Quasarr
touches:
- quasarr/storage/config.py
- quasarr/storage/sqlite_database.py
- quasarr/storage/lock.py
---

## Decided

Protect INI and SQLite writes with the shared multiprocess lock, and allow lock acquisition only in config-then-database order.

Why: Setup and API workers can write concurrently, while forked or spawned processes may otherwise interleave file replacement and database updates. A fixed order prevents the two storage layers from deadlocking each other.

## Watch out

Database methods never call into config storage while holding the database lock; storage mutations remain serialized across processes.
19 changes: 19 additions & 0 deletions .trail/learnings/11-atomic-encrypted-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: Treat config persistence as encrypted atomic storage
date: 2026-10-19
author: Quasarr
touches:
- quasarr/storage/config.py
- tests/test_config_atomic_writes.py
- tests/test_config_secret_decryption.py
---

## Decided

Encrypt sensitive config values at rest and write config through an atomic replacement path.

Why: Credentials must not appear as plain persistent values, and a process interruption during a direct write could destroy the only usable config. Read-time decryption keeps callers working with normal values while atomic replacement protects the file boundary. If the INI is restored without its matching database, undecryptable ciphertext is reported as unset but preserved so restoring the matching database can recover it. Startup treats storage maintenance as tri-state: healthy, repaired, or failed, so a transient lock or maintenance error cannot be mistaken for a clean database or silently kill startup.

## Watch out

Callers see decrypted settings in memory; disk format remains encrypted for secrets and complete-file replacement is the persistence operation. A failed decrypt never overwrites ciphertext, and only an explicit integrity failure blocks startup.
19 changes: 19 additions & 0 deletions .trail/learnings/12-package-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: Use package UUIDs to preserve download ownership
date: 2026-10-19
author: Quasarr
touches:
- quasarr/downloads/packages/__init__.py
- quasarr/storage/categories.py
- tests/test_package_identity.py
---

## Decided

Encode Quasarr package identity in the package ID and use JDownloader's package UUID to join package and link state.

Why: Names and comments can be changed by JDownloader or collide across jobs. The package UUID gives status, comments, cleanup, and category recovery one stable join key, while the strict ID pattern prevents unrelated packages from entering Quasarr lifecycle logic.

## Watch out

Package operations filter links by package UUID and recognize only IDs matching the project pattern.
18 changes: 18 additions & 0 deletions .trail/learnings/13-mirror-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
title: Evaluate mirrors as one package decision
date: 2026-10-19
author: Quasarr
touches:
- quasarr/downloads/packages/__init__.py
- quasarr/downloads/mirror_filters.py
---

## Decided

Group links by mirror domain before deciding package status and cleanup.

Why: A failed or offline mirror is recoverable when a sibling mirror remains usable. Per-link decisions would delete useful alternatives or report a package failed while a valid mirror exists.

## Watch out

Cleanup removes unusable sibling links only when package-level evidence shows an online alternative; extraction activity can override a naive archive-only status.
19 changes: 19 additions & 0 deletions .trail/learnings/14-notification-isolation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: Isolate notification providers behind one dispatcher
date: 2026-10-19
author: Quasarr
touches:
- quasarr/providers/notifications/__init__.py
- quasarr/providers/notifications/helpers/message_builder.py
- quasarr/storage/setup/notifications.py
---

## Decided

Separate normalized notification meaning from independent provider delivery, case preferences, and silence settings.

Why: Notification delivery is optional and providers fail independently. Shared message construction keeps event meaning consistent, while isolated sends prevent one provider outage from suppressing the others. Tracked sends preserve provider references for later updates.

## Watch out

A notification succeeds when at least one enabled provider delivers; one provider exception does not abort the dispatch.
18 changes: 18 additions & 0 deletions .trail/learnings/15-container-restart-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
title: Reserve container exit codes for restart policy
date: 2026-10-19
author: Quasarr
touches:
- docker/Dockerfile
- quasarr/__init__.py
---

## Decided

Let the Docker entrypoint restart Quasarr after exit code zero and stop the container on any nonzero exit.

Why: A clean process exit can follow a recoverable restart condition, while integrity or configuration failures must remain visible and stop retry churn. The application therefore uses its exit status as the boundary between recovery and failure.

## Watch out

Changes to startup failure handling must preserve the zero-restart/nonzero-stop contract unless the container policy changes with it.
20 changes: 20 additions & 0 deletions .trail/learnings/16-episode-selection-safety.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: Preserve native episode selection and raw season evidence
date: 2026-10-19
author: Quasarr
touches:
- quasarr/downloads/sources/al.py
- quasarr/downloads/sources/wx.py
- quasarr/downloads/linkcrypters/hide.py
- quasarr/downloads/__init__.py
---

## Decided

Carry requested season and episode identity to the source or crypter's native selector, preserve raw season signals until matching is complete, and fail closed when native confirmation is unavailable.

Why: Source rows can represent merged episodes, and container filenames can be randomized. Row position, guessed season text, or filename filtering can select the wrong content. Native selection plus explicit confirmation protects AL and Hide grabs; raw signals and precise fallbacks keep WX episode requests from widening into season packs.

## Watch out

An episode-specific grab never downloads an unconfirmed whole container, and fallback candidates retain enough source identity to be checked against the original request.
19 changes: 19 additions & 0 deletions .trail/learnings/17-source-match-specificity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: Match releases by authoritative identity and pinned location
date: 2026-10-19
author: Quasarr
touches:
- quasarr/search/sources/mx.py
- quasarr/search/sources/wx.py
- quasarr/downloads/sources/dl.py
---

## Decided

Require the strongest identity available at each source boundary: explicit IMDb equality for MX, episode-specific links before season packs for WX, and fragment-pinned posts for DL.

Why: Search order is not identity. The first API result may be a different title, a season pack can satisfy a broad query while missing the requested episode, and a thread can contain several unrelated posts. Matching the carried identifier or pinned fragment prevents false positives and prevents a fallback from widening scope.

## Watch out

Candidate order may optimize selection, but it cannot replace explicit identity or a requested post/episode constraint.
19 changes: 19 additions & 0 deletions .trail/learnings/18-localized-title-authority.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: Use authoritative localized titles once at the metadata boundary
date: 2026-10-19
author: Quasarr
touches:
- quasarr/providers/imdb_metadata.py
- quasarr/search/sources/mx.py
- quasarr/search/sources/wx.py
---

## Decided

Resolve a source-language title only from evidence that proves the requested locale, then apply shared transliteration once before source queries.

Why: Original titles and instance-localized display names can look plausible while naming a different spelling. Unproven fallbacks create silent misses or wrong matches. Central normalization gives every source the same German transliteration behavior and avoids source-specific spelling retries.

## Watch out

A source receives one canonical localized form or no title; individual source modules must not invent locale fallbacks or alternate transliteration rules.
19 changes: 19 additions & 0 deletions .trail/learnings/19-payload-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: Preserve source identity through the emulated download payload
date: 2026-10-19
author: Quasarr
touches:
- quasarr/providers/utils.py
- quasarr/api/arr/__init__.py
- quasarr/downloads/__init__.py
---

## Decided

Carry source identity in the generated download payload, preserving the shared encoding contract between search and grab.

Why: The *arr round-trip separates discovery from download submission. Source key, media metadata, and URL must survive encoding without ambiguity so dispatch can try the intended source first. Filename or hostname guesses cannot replace the original identity.

## Watch out

Payload generation and parsing change together; fallback dispatch does not erase the source key carried by search.
Loading