# 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_<verb>_<noun>` 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
