--- name: houndarr-architecture description: Houndarr's source layout and architectural patterns at file granularity. Loads when reading or editing src/houndarr/. Covers the per-file purpose for auth/, clients/, engine/, routes/, services/; the wire-models vs domain-models split; the *arr API spec snapshots under docs/api/; and pointers to the more specific database / engine skills for narrower scopes. paths: - "src/houndarr/**" --- # Houndarr architecture reference For narrower scopes: - Database schema and migrations: see `houndarr-database` skill (loads on `src/houndarr/database.py`). - Algorithmic verification: see `verify-algorithms` skill (loads on `src/houndarr/engine/**`). ## Source layout ``` src/houndarr/ __main__.py # CLI entry point (Click), logging setup, uvicorn.run app.py # create_app(), lifespan, middleware registration auth/ # AuthMiddleware, bcrypt, CSRF, rate limiter (seam package) password.py # bcrypt verify / hash helpers rate_limit.py # in-memory login rate limiter session.py # signed session cookie encode / decode setup.py # first-run admin setup + password policy csrf.py # CSRF double-submit token rotation proxy_auth.py # reverse-proxy trust gate and header extraction identity.py # current-user resolution from session or proxy header middleware.py # AuthMiddleware dispatch (builtin vs proxy path) config.py # AppSettings dataclass, get_settings() singleton crypto.py # Fernet encrypt/decrypt, master key management database.py # get_db() context manager, schema migrations enums.py # StrEnum consolidation (SearchKind, SearchAction, CycleTrigger, ItemType) errors.py # HoundarrError hierarchy (Client/Engine/Service/Route) value_objects.py # Frozen value objects shared across layers (ItemRef) clients/ # httpx-based *arr API clients base.py # ArrClient ABC with _get()/_post() + raise_for_status() + get_queue_status() sonarr.py # SonarrClient (episode/season search, v3 API) radarr.py # RadarrClient (movie search, v3 API) lidarr.py # LidarrClient (album/artist search, v1 API) readarr.py # ReadarrClient (book/author search, v1 API) whisparr_v2.py # WhisparrV2Client (Sonarr-based, episode/season search) whisparr_v3.py # WhisparrV3Client (v3, Radarr-based, movie/scene search) engine/ candidates.py # SearchCandidate dataclass, ItemType re-export, date helpers search_loop.py # run_instance_search(): unified search pipeline (missing/cutoff/upgrade passes, queue-backpressure gate) supervisor.py # Supervisor: one asyncio.Task per enabled instance adapters/ __init__.py # AppAdapter dataclass, ADAPTERS registry, get_adapter() protocols.py # AppAdapterProto: runtime_checkable Protocol matching the AppAdapter shape sonarr.py # Sonarr adapter: candidate conversion + dispatch radarr.py # Radarr adapter: candidate conversion + dispatch lidarr.py # Lidarr adapter: candidate conversion + dispatch readarr.py # Readarr adapter: candidate conversion + dispatch whisparr_v2.py # Whisparr v2 adapter: candidate conversion + dispatch whisparr_v3.py # Whisparr v3 adapter: movie/scene candidate conversion + dispatch routes/ _htmx.py # is_hx_request() shared helper for partial vs full renders pages.py # Setup, Login, Dashboard, Logs, Settings page routes health.py # GET /api/health (Docker HEALTHCHECK) settings/ # Settings surface split by concern __init__.py # composes the sub-routers into a single settings_router _helpers.py # template render, client build, connection check, validators page.py # GET /settings account.py # POST /settings/account/password instances.py # /settings/instances/* (CRUD, test-connection, toggle) api/ logs.py # GET /api/logs (JSON, with cursor-based pagination) status.py # GET /api/status (JSON, dashboard polling) services/ instances.py # Instance CRUD, InstanceType StrEnum cooldown.py # Per-item search cooldown tracking url_validation.py # SSRF guard for instance URLs ``` ## Wire models vs domain models - **Wire models** (`clients/_wire_models/`): every *arr HTTP response is validated with a Pydantic model from this package before it reaches a parser. `PaginatedResponse[T]` (generic, PEP 695 syntax) covers the shared `/wanted/*` envelope; `SystemStatus` and `QueueStatus` back `ArrClient.ping()` and `ArrClient.get_queue_status()`; per-app `*WantedEpisode` / `*WantedMovie` / `*WantedAlbum` / `*WantedBook` and `*LibraryEpisode` / `*LibraryMovie` / `*LibraryAlbum` / `*LibraryBook` models name the record shapes. `ArrSeries` / `ArrArtist` / `ArrAuthor` type the parent-aggregate fetches. All wire models extend an internal `_ArrModel` that sets `populate_by_name=True` + `extra="ignore"` so unknown fields from new *arr versions never raise. Field names are snake_case in Python and alias to the camelCase the APIs serialise. - **Domain models** (parsed result types): `MissingEpisode`, `LibraryMovie`, etc. are frozen dataclasses, one per client file next to the client that builds them. Every frozen dataclass uses `slots=True`. `Instance` composes seven frozen sub-structs (`core`, `missing`, `cutoff`, `upgrade`, `schedule`, `snapshot`, `timestamps`) and is itself frozen and slotted; callers evolve it through `dataclasses.replace`. `AppSettings` is the only deliberately-mutable dataclass (env overrides applied in-place on the lazy singleton). ## Auth composition Global `AuthMiddleware` (Starlette `BaseHTTPMiddleware`) routes every request through one of three path buckets; no per-route auth decorators: - `_API_KEY_PATHS` (currently `/api/v1/widget`): the top-level `dispatch()` sends the request straight to `_dispatch_api_key`, which verifies `X-Api-Key` against the `widget_api_key` table (constant-time compare on the SHA-256 digest) and applies a per-IP attempt rate limit. Bypasses session and CSRF. - `_PUBLIC_PATHS` (`/setup`, `/login`, `/api/health`, `/static`): no auth. Each of `_dispatch_builtin` and `_dispatch_proxy` short-circuits these before any session or proxy-header check. - Everything else: mode-dependent. `_dispatch_builtin` enforces the session cookie + CSRF (default); `_dispatch_proxy` enforces the proxy-trust gate + CSRF when `HOUNDARR_AUTH_MODE=proxy`. Proxy-auth trust and header reads flow through two primitives in `auth.py`: `_is_trusted_proxy(request)` (IP gate) and `_extract_proxy_username(request)` (header read, assumes trust already verified). The middleware's `_dispatch_proxy` and the standalone `_validate_proxy_auth` both compose these so the gate logic lives in one place. ## Encryption Master key in `request.app.state.master_key`; passed explicitly to service functions as `master_key=` kwarg; never imported globally. ## HTMX SPA-like shell navigation; nav links use `hx-target="#app-content"` with `hx-swap="innerHTML"` and `hx-push-url="true"`. Routes check `is_hx_request(request)` from `routes/_htmx.py` and return either partial or full template. Templates are lazily initialised via a module-level singleton. ## Supervisor One `asyncio.Task` per enabled instance; 10s shutdown timeout. ## search_log Every search attempt writes a row with action `searched` / `skipped` / `error` / `info`. ## *arr API reference (local) Full upstream OpenAPI specs vendored under `docs/api/` (one per app: sonarr, radarr, whisparr_v2, whisparr_v3, lidarr, readarr). **Source of truth** when touching `clients/` code; see `docs/api/README.md`. Refreshed weekly (Mon 10:00 UTC) by `api-snapshot-refresh.yml`, so specs are never more than a week stale.