3dc5dd8fe9
- Fix register/login: dict-style access on UserTable ORM objects - Fix HTMX auth: inject JWT token in all HTMX request headers - Fix FS7 search: use DLE AJAX endpoint /engine/ajax/search.php - Fix ZT search: use ?p=series&search=QUERY (not DLE format) - Fix provider health: load hardcoded providers + domain manager - Add self.id to all anime/series providers - Redesign homepage: Netflix-style horizontal scroll cards (.hc) - Redesign search results: grouped by title, poster + synopsis + 3 buttons - Add Télécharger dropdown: season download + episode picker - Fix navbar CSS: restore .tabs flex layout, remove orphan rules - Fix HTMX spinner: remove inline display:none, use CSS indicator - Add AGENTS.md files across project for developer documentation
157 lines
7.3 KiB
Markdown
157 lines
7.3 KiB
Markdown
# 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
|