# AGENTS.md — Ohm Stream Downloader FastAPI anime/series downloader with HTMX+Alpine.js frontend, SQLModel/SQLite DB, 3-tier scraper architecture, JWT auth, Sonarr webhooks, and auto-download scheduler. ## COMMANDS ```bash # Dev server uvicorn main:app --reload --host 0.0.0.0 --port 3000 # --- Tests (pytest, configured in pytest.ini, asyncio_mode=auto) --- pytest # All tests (coverage + verbose by default) pytest -m "unit" # Fast unit tests only pytest -m "integration" # API integration tests pytest -m "not slow" # CI default — excludes slow tests pytest -m "network" # Tests requiring network access # Single file / class / test pytest tests/test_sonarr.py -v pytest tests/test_sonarr.py::TestSonarrHandler -v pytest tests/test_sonarr.py::TestSonarrHandler::test_add_mapping -v # Debug pytest -s # Show print() output pytest --cov=app --cov-report=html # HTML coverage report in htmlcov/ # --- Lint & Format (ruff) --- ruff check app/ # Lint ruff format --check app/ # Format check (CI enforces this) ruff format app/ # Auto-format # --- Type Check --- mypy app/ --ignore-missing-imports # Type check (CI enforces) # --- DB Migrations --- alembic revision --autogenerate -m "description" alembic upgrade head # --- Frontend (optional) --- npm test # Vitest JS tests npx playwright test # E2E browser tests ``` ## CODE STYLE ### Imports Three groups separated by blank lines: stdlib → third-party → local (`app.*`). Always absolute imports (no relative). No wildcard imports (`from X import *` enforced). ### Formatting PEP 8, 120 chars max line length. Single quotes for strings, double quotes for docstrings. Ruff handles linting and formatting (no local config — CI-only). ### Types Explicit type hints on all function signatures and return types. Use `Optional[X]` from `typing`. Use `list[str]` / `dict[str, X]` (lowercase generics). Pydantic models for all API schemas. Return type annotations required on public methods. ### Naming - `snake_case` for functions, variables, constants - `PascalCase` for classes and enums - `UPPER_SNAKE_CASE` for enum values (`DownloadStatus.PENDING`) - `logger = logging.getLogger(__name__)` at module level - `_` prefix for private methods (`_fetch_page`, `_sanitize`) - `get_*` for factory functions (`get_downloader`, `get_anime_site`) ### Error Handling - `HTTPException` for API errors with proper status codes - `raise ValueError()` for business logic validation - `try/except` with logging — never bare `except:` (known tech debt exists) - `response.raise_for_status()` for HTTP errors - Never return `None` for missing URLs from downloaders — raise an exception ### Docstrings Triple double quotes `"""`. Module docstrings describe purpose. Class docstrings describe responsibility. Complex methods use Args/Returns sections. Not all methods require docstrings. ## ARCHITECTURE ``` main.py # App entry, middleware, startup, router registration app/ ├── routers/ # 11 APIRouter modules (one per feature domain) ├── downloaders/ # 3-tier: anime_sites/ → series_sites/ → video_players/ ├── models/ # Pydantic/SQLModel (Base → Table → Schema pattern) ├── config.py # Pydantic Settings from .env ├── database.py # SQLModel engine (created at import time) ├── download_manager.py # Async queue, semaphore-based parallelism ├── auth.py # JWT + bcrypt, SQLModel user storage ├── providers.py # ANIME_PROVIDERS, SERIES_PROVIDERS, FILE_HOSTS registries └── utils.py # sanitize_filename(), is_safe_filename() templates/ # Jinja2 + HTMX + Alpine.js static/js/ # Vanilla ES modules (no build step) tests/ # pytest suite (conftest.py has shared fixtures) config/ # Runtime JSON files (users, watchlist, sonarr) alembic/ # DB migrations ``` ## KEY CONVENTIONS - **URL pipe format**: `video_url|anime_page_url|episode_title` — metadata preserved through download pipeline - **Module-level init**: `database.py` creates engine on import; `main.py` creates `download_manager` on import - **Async everywhere**: Always `async/await` for I/O — use `httpx.AsyncClient`, never `requests` - **Factory pattern**: `get_downloader(url)` routes anime → series → video player → generic - **Router deps**: `Depends(lambda: download_manager)`, `Depends(get_current_user_from_token)`, `Depends(lambda: templates)` - **Dual storage**: Some features use JSON files (legacy) + SQLModel tables (newer) - **Frontend**: No JS build step. HTMX for server interactions, Alpine.js for client state, Plyr.io for video - **Circular import avoidance**: `episode_checker.py` uses lazy init (`set_download_manager()`) ## ANTI-PATTERNS (DO NOT) - Use sync `requests` — always `httpx.AsyncClient` - Return `None` for missing URLs from downloaders — raise an exception - Skip `sanitize_filename()` on extracted filenames — path traversal risk - Forget `await self.close()` in downloaders — resource leak - Hardcode User-Agent in individual players — use base class headers - Use `from X import *` — always explicit imports - Import `download_manager` from `main.py` in app/ modules — causes circular imports - Store secrets in `config/*.json` — use `.env` - Use `as any`, `@ts-ignore` to suppress type errors (if adding TS) ## TEST CONVENTIONS - `tests/` directory with `conftest.py` for shared fixtures - Markers: `@pytest.mark.unit`, `@pytest.mark.integration`, `@pytest.mark.network`, `@pytest.mark.slow` - `asyncio_mode = auto` — async test functions run without explicit marker - Test naming: `test__` in `Test*` classes - 300s timeout configured in pytest.ini; `testpaths = tests` - Legacy test files at project root: `test_watchlist_simple.py`, `test_watchlist_e2e.py` ## ADDING NEW PROVIDERS **Video player**: Create in `app/downloaders/video_players/`, inherit `BaseVideoPlayer`, implement `can_handle()` + `get_download_link(url, target_filename=None)`, register in `__init__.py`, add to `FILE_HOSTS` in `providers.py`. **Anime/series site**: Create in `app/downloaders/anime_sites/` or `series_sites/`, inherit base class, implement `search_anime()` + `get_episodes()` + `get_anime_metadata()` + `get_download_link()`, register in `__init__.py`, add to `providers.py`. ## NOTES - Python 3.11+, CI tests on 3.11 and 3.12 - No `pyproject.toml` — uses `requirements.txt` with exact version pinning - `GEMINI.md` and `CLAUDE.md` exist with tool-specific instructions (merge if conflicting) - French-language project (animes, séries, VOSTFR) but all code and comments in English - ~20 empty `except:` blocks in downloaders/tests — known tech debt - `JWT_SECRET_KEY` min 32 chars, default rejected at startup; generate via `Settings.generate_secret()` - Sub-AGENTS.md files exist in `app/`, `app/routers/`, `app/downloaders/`, `app/models/`, `app/downloaders/anime_sites/`, `app/downloaders/video_players/` for deeper context