# AGENTS.md - Agentic Coding Guidelines This file provides guidance for AI agents working in this repository. ## Quick Start ```bash # Setup python3 -m venv venv && source venv/bin/activate pip install -r requirements.txt # Run dev server uvicorn main:app --reload --host 0.0.0.0 --port 3000 ``` ## Build, Lint & Test Commands ### Running Tests ```bash # All tests pytest # With coverage pytest --cov=app --cov-report=html # Unit only (fast) pytest -m "unit" # Exclude slow tests pytest -m "not slow" # Verbose with print debugging pytest -v -s ``` ### Running Single Tests ```bash # Specific file pytest tests/test_sonarr.py -v # Specific class pytest tests/test_sonarr.py::TestSonarrHandler -v # Specific test pytest tests/test_sonarr.py::TestSonarrHandler::test_add_mapping -v # Pattern match pytest -k "test_download" -v ``` ## Code Style ### Imports (PEP 8 order) 1. Standard library (`os`, `json`, `asyncio`) 2. Third-party (`httpx`, `beautifulsoup4`, `fastapi`) 3. Local app (`app.config`, `app.utils`) ```python import os import asyncio from typing import Optional import httpx from fastapi import APIRouter, HTTPException from app.config import get_settings from app.models import DownloadTask, DownloadStatus ``` ### Formatting - **Line length**: 120 chars max - **Indentation**: 4 spaces - **Blank lines**: 2 between top-level, 1 between inline ### Type Annotations - Use explicit types - Use `Optional[X]` not `X | None` - Use `list[X]`, `dict[X, Y]` ```python # Good async def get_download_link(url: str, target_filename: Optional[str] = None) -> tuple[str, str]: results: list[dict[str, str]] = [] # Avoid async def get_download_link(url, target_filename=None): results = [] ``` ### Naming Conventions | Element | Convention | Example | |---------|------------|---------| | Modules | snake_case | `download_manager.py` | | Classes | PascalCase | `DownloadManager` | | Functions | snake_case | `get_download_link()` | | Constants | UPPER_SNAKE | `MAX_PARALLEL_DOWNLOADS` | | Variables | snake_case | `download_task` | | Enums | PascalCase | `DownloadStatus` | | Enum values | UPPER_SNAKE | `DownloadStatus.PENDING` | ### Async/Await - Always use for I/O operations - Close clients properly to avoid leaks ```python async def close(self): await self.client.aclose() ``` ### Error Handling - Use try/except for recoverable errors - Raise specific exceptions (`HTTPException`, `ValueError`) - Never use empty except blocks - Log errors appropriately ```python try: result = await client.get(url) except httpx.TimeoutException: logger.warning(f"Request timeout for {url}") raise HTTPException(status_code=504, detail="Request timeout") ``` ### File Operations - Always sanitize filenames: `app.utils.sanitize_filename()` - Validate paths: `app.utils.is_safe_filename()` ### Testing - Use pytest with pytest-asyncio - Mark tests: `@pytest.mark.unit`, `@pytest.mark.integration` - Use fixtures from `tests/conftest.py` ```python @pytest.mark.unit @pytest.mark.asyncio async def test_download_manager(): manager = DownloadManager(max_parallel=3) assert manager.max_parallel == 3 ``` ### Security - Never hardcode secrets - use environment variables - Validate all inputs (URLs, filenames) - Use HMAC for webhook verification when configured - Limit CORS origins - never use `*` in production ## Architecture Patterns **Three-Tier Downloader:** 1. `app/downloaders/anime_sites/` - Anime catalogs 2. `app/downloaders/series_sites/` - TV series catalogs 3. `app/downloaders/video_players/` - File hosting Each has base class and factory. When adding providers: 1. Inherit from appropriate base class 2. Implement required methods 3. Register in factory 4. Add to providers config in `app/providers.py` **URL Convention**: Pipe-separated format preserves metadata: ``` video_url|anime_page_url|episode_title ``` ## Key Files | File | Purpose | |------|---------| | `main.py` | FastAPI app, endpoints | | `app/config.py` | Pydantic Settings | | `app/download_manager.py` | Download queue | | `app/utils.py` | sanitize_filename | | `app/auth.py` | JWT auth | | `app/models/__init__.py` | Pydantic models | ## Configuration - Use `.env` from `.env.example` - JWT_SECRET_KEY must change in production