- 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
7.3 KiB
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
# 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_casefor functions, variables, constantsPascalCasefor classes and enumsUPPER_SNAKE_CASEfor 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
HTTPExceptionfor API errors with proper status codesraise ValueError()for business logic validationtry/exceptwith logging — never bareexcept:(known tech debt exists)response.raise_for_status()for HTTP errors- Never return
Nonefor 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.pycreates engine on import;main.pycreatesdownload_manageron import - Async everywhere: Always
async/awaitfor I/O — usehttpx.AsyncClient, neverrequests - 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.pyuses lazy init (set_download_manager())
ANTI-PATTERNS (DO NOT)
- Use sync
requests— alwayshttpx.AsyncClient - Return
Nonefor 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_managerfrommain.pyin app/ modules — causes circular imports - Store secrets in
config/*.json— use.env - Use
as any,@ts-ignoreto suppress type errors (if adding TS)
TEST CONVENTIONS
tests/directory withconftest.pyfor 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>inTest*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— usesrequirements.txtwith exact version pinning GEMINI.mdandCLAUDE.mdexist 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_KEYmin 32 chars, default rejected at startup; generate viaSettings.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