# Aniworld Web Application Development Instructions This document provides detailed tasks for AI agents to implement a modern web application for the Aniworld anime download manager. All tasks should follow the coding guidelines specified in the project's copilot instructions. ## Project Overview The goal is to create a FastAPI-based web application that provides a modern interface for the existing Aniworld anime download functionality. The core anime logic should remain in `SeriesApp.py` while the web layer provides REST API endpoints and a responsive UI. ## Architecture Principles - **Single Responsibility**: Each file/class has one clear purpose - **Dependency Injection**: Use FastAPI's dependency system - **Clean Separation**: Web layer calls core logic, never the reverse - **File Size Limit**: Maximum 500 lines per file - **Type Hints**: Use comprehensive type annotations - **Error Handling**: Proper exception handling and logging ## Additional Implementation Guidelines ### Code Style and Standards - **Type Hints**: Use comprehensive type annotations throughout all modules - **Docstrings**: Follow PEP 257 for function and class documentation - **Error Handling**: Implement custom exception classes with meaningful messages - **Logging**: Use structured logging with appropriate log levels - **Security**: Validate all inputs and sanitize outputs - **Performance**: Use async/await patterns for I/O operations ## πŸ“ž Escalation If you encounter: - Architecture issues requiring design decisions - Tests that conflict with documented requirements - Breaking changes needed - Unclear requirements or expectations **Document the issue and escalate rather than guessing.** --- ## οΏ½ Credentials **Admin Login:** - Username: `admin` - Password: `Hallo123!` --- ## οΏ½πŸ“š Helpful Commands ```bash # Run all tests conda run -n AniWorld python -m pytest tests/ -v --tb=short # Run specific test file conda run -n AniWorld python -m pytest tests/unit/test_websocket_service.py -v # Run specific test class conda run -n AniWorld python -m pytest tests/unit/test_websocket_service.py::TestWebSocketService -v # Run specific test conda run -n AniWorld python -m pytest tests/unit/test_websocket_service.py::TestWebSocketService::test_broadcast_download_progress -v # Run with extra verbosity conda run -n AniWorld python -m pytest tests/ -vv # Run with full traceback conda run -n AniWorld python -m pytest tests/ -v --tb=long # Run and stop at first failure conda run -n AniWorld python -m pytest tests/ -v -x # Run tests matching pattern conda run -n AniWorld python -m pytest tests/ -v -k "auth" # Show all print statements conda run -n AniWorld python -m pytest tests/ -v -s #Run app conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000 --reload ``` --- ## Implementation Notes 1. **Incremental Development**: Implement features incrementally, testing each component thoroughly before moving to the next 2. **Code Review**: Review all generated code for adherence to project standards 3. **Documentation**: Document all public APIs and complex logic 4. **Testing**: Maintain test coverage above 80% for all new code 5. **Performance**: Profile and optimize critical paths, especially download and streaming operations 6. **Security**: Regular security audits and dependency updates 7. **Monitoring**: Implement comprehensive monitoring and alerting 8. **Maintenance**: Plan for regular maintenance and updates --- ## Task Completion Checklist For each task completed: - [ ] Implementation follows coding standards - [ ] Unit tests written and passing - [ ] Integration tests passing - [ ] Documentation updated - [ ] Error handling implemented - [ ] Logging added - [ ] Security considerations addressed - [ ] Performance validated - [ ] Code reviewed - [ ] Task marked as complete in instructions.md - [ ] Infrastructure.md updated and other docs - [ ] Changes committed to git; keep your messages in git short and clear - [ ] Take the next task --- ## TODO List: --- ### Task 0: Populate all required NFO tags during normal creation **Goal:** Ensure `create_tvshow_nfo()` writes every required tag from the start, so newly created NFOs are complete and never require a repair pass. All missing fields must be mapped inside `_tmdb_to_nfo_model()` in `src/core/services/nfo_service.py`. **Required tags currently missing from creation path:** | Field | Current state | Fix | | --------------- | ---------------------- | --------------------------------------------------- | | `originaltitle` | Never set | `tmdb_data.get("original_name")` | | `year` | Never set | Extract 4-digit year from `first_air_date` | | `plot` | May be empty | `tmdb_data.get("overview")` | | `runtime` | Never set | `tmdb_data.get("episode_run_time", [None])[0]` | | `premiered` | Never set | `tmdb_data.get("first_air_date")` | | `status` | Never set | `tmdb_data.get("status")` | | `imdbid` | Only `imdb_id` written | Add explicit `imdbid` field alongside `imdb_id` | | `genre` | Not checked | Map each `tmdb_data["genres"]` entry | | `studio` | Not checked | Map each `tmdb_data["networks"]` entry | | `country` | Never set | `tmdb_data.get("origin_country", [])` | | `actor` | Not checked | Map each `credits["cast"]` entry | | `watched` | Pydantic default | Set explicitly to `False` | | `tagline` | Never set | `tmdb_data.get("tagline")` | | `outline` | Never set | Set equal to `plot` (same `overview` source) | | `sorttitle` | Never set | Set equal to `title` | | `dateadded` | Never set | `datetime.now().strftime("%Y-%m-%d %H:%M:%S")` | | `mpaa` | Never set | Extract US rating from `content_ratings` via helper | **Implementation steps:** 1. Open `src/core/services/nfo_service.py` β€” read the full file before making any changes 2. In `_tmdb_to_nfo_model()`, add assignments for all fields listed above and pass them to `TVShowNFO(...)` 3. Add `_extract_mpaa_rating(content_ratings)` helper β€” mirrors existing `_extract_fsk_rating` but looks for country code `"US"`; refactor both into `_extract_rating_by_country(content_ratings, country_code: str)` to eliminate duplication 4. Add `from datetime import datetime` import if not already present 5. No changes needed to `update_tvshow_nfo()` β€” it re-calls `_tmdb_to_nfo_model()` already 6. If `TVShowNFO` Pydantic model is missing fields (`originaltitle`, `outline`, `sorttitle`, `dateadded`, `mpaa`, `country`, `watched`), add them with `Optional[...]` defaults of `None` / `False` 7. Open `src/core/utils/nfo_generator.py` β€” read the full file before making changes 8. Confirm `_add_element` calls exist (ungated) for: `tagline`, `outline`, `sorttitle`, `dateadded`, `originaltitle`, `year`, `runtime`, `premiered`, `status`, `country`, `imdbid` 9. Verify `mpaa` fallback: prefer `fsk` when `nfo_prefer_fsk_rating=True` AND `fsk is not None`; otherwise write `mpaa` 10. Verify `_add_element` does NOT suppress `"false"` strings β€” `watched=False` must appear as `false` 11. Keep both files under 500 lines; if `nfo_service.py` exceeds limit, extract `_tmdb_to_nfo_model` into `src/core/utils/nfo_mapper.py` **Validation steps:** - Read the updated `_tmdb_to_nfo_model()` and confirm all 17 fields are explicitly assigned - Read the updated `nfo_generator.py` and confirm every field has a corresponding `_add_element` call - Delete a real series' `tvshow.nfo`, trigger creation, open result and cross-check every tag against `tvshow.nfo.good` - Confirm no wildcard imports added (`from module import *`) - Confirm both files stay under 500 lines (`wc -l src/core/services/nfo_service.py src/core/utils/nfo_generator.py`) **Test steps** β€” create `tests/unit/test_nfo_creation_tags.py`: - `test_tmdb_to_nfo_model_sets_originaltitle` - `test_tmdb_to_nfo_model_sets_year_from_first_air_date` - `test_tmdb_to_nfo_model_sets_plot_from_overview` - `test_tmdb_to_nfo_model_sets_runtime` - `test_tmdb_to_nfo_model_sets_premiered` - `test_tmdb_to_nfo_model_sets_status` - `test_tmdb_to_nfo_model_sets_imdbid` - `test_tmdb_to_nfo_model_sets_genres` - `test_tmdb_to_nfo_model_sets_studios_from_networks` - `test_tmdb_to_nfo_model_sets_country` - `test_tmdb_to_nfo_model_sets_actors` - `test_tmdb_to_nfo_model_sets_watched_false` - `test_tmdb_to_nfo_model_sets_tagline` - `test_tmdb_to_nfo_model_sets_outline_from_overview` - `test_tmdb_to_nfo_model_sets_sorttitle_from_name` - `test_tmdb_to_nfo_model_sets_dateadded` - `test_tmdb_to_nfo_model_sets_mpaa_from_content_ratings` - `test_extract_rating_by_country_returns_us_rating` - `test_extract_rating_by_country_returns_none_when_no_match` - `test_extract_rating_by_country_handles_empty_results` - `test_generate_nfo_includes_all_required_tags` β€” generate XML from a fully-populated `TVShowNFO`; parse with `lxml`; assert every required tag present - `test_generate_nfo_writes_watched_false` β€” assert `false` not omitted - `test_generate_nfo_minimal_model_does_not_crash` - `test_generate_nfo_writes_fsk_over_mpaa_when_prefer_fsk` β€” both set, prefer_fsk=True β†’ FSK value written - `test_generate_nfo_writes_mpaa_when_no_fsk` β€” `fsk=None`, `mpaa="TV-14"`, prefer_fsk=True β†’ `TV-14` ```bash conda run -n AniWorld python -m pytest tests/unit/test_nfo_creation_tags.py -v --tb=short conda run -n AniWorld python -m pytest tests/ -v --tb=short ``` **Documentation steps:** - Update `docs/NFO_GUIDE.md` β€” add "Tag Reference" table: tag β†’ TMDB source field β†’ optional/required - Add entry in `docs/CHANGELOG.md` under `[Unreleased]` - Update `docs/ARCHITECTURE.md` under the NFO services section to reflect new fields and the `_extract_rating_by_country` helper **Commit step:** ```bash git add src/core/services/nfo_service.py src/core/utils/nfo_generator.py tests/unit/test_nfo_creation_tags.py docs/ git commit -m "feat: write all required NFO tags on creation" ``` ### Task 1: Create `NfoRepairService` β€” detect missing/empty NFO tags **Goal:** Implement a service that parses an existing `tvshow.nfo` file and determines whether required tags are missing or empty, compared to the Kodi/Jellyfin standard defined in `tvshow.nfo.good`. **Required tags to check** (must be present and non-empty): - `title`, `originaltitle`, `year`, `plot`, `runtime`, `premiered`, `status` - `imdbid` (not just `imdb_id`) - At least one `genre`, `studio`, `country`, `actor/name` - `watched` **Implementation steps:** 1. Create `src/core/services/nfo_repair_service.py` (max 500 lines, type hints, docstrings) 2. Define `REQUIRED_TAGS: dict[str, str]` β€” XPath expression β†’ human-readable label for each required tag 3. Implement `def parse_nfo_tags(nfo_path: Path) -> dict` β€” uses `lxml.etree.parse()`, returns tagβ†’value(s), silent on parse errors 4. Implement `def find_missing_tags(nfo_path: Path) -> list[str]` β€” calls `parse_nfo_tags()`, returns list of tag names that are absent or have empty text 5. Implement `def nfo_needs_repair(nfo_path: Path) -> bool` β€” returns `bool(find_missing_tags(nfo_path))` 6. Implement class `NfoRepairService`: - `__init__(self, nfo_service: NFOService)` β€” inject existing `NFOService` - `async def repair_series(self, series_path: Path, series_name: str) -> bool` β€” calls `nfo_needs_repair()`, logs which tags are missing, calls `nfo_service.update_tvshow_nfo(series_path, series_name)`, returns `True` if repair was triggered 7. Add structured logging at INFO level for: repair triggered (with list of missing tags), repair skipped, repair completed **Validation steps:** - Manually verify `find_missing_tags()` returns correct results when given `tvshow.nfo.bad` as input - Manually verify `find_missing_tags()` returns an empty list when given `tvshow.nfo.good` as input - Check that no imports are wildcard (`from module import *`) - Check file stays under 500 lines **Test steps** β€” create `tests/unit/test_nfo_repair_service.py`: - `test_find_missing_tags_with_bad_nfo` β€” use fixture copying `tvshow.nfo.bad`; assert all 13 expected missing tags are returned - `test_find_missing_tags_with_good_nfo` β€” use fixture copying `tvshow.nfo.good`; assert empty list returned - `test_nfo_needs_repair_returns_true_for_bad_nfo` - `test_nfo_needs_repair_returns_false_for_good_nfo` - `test_repair_series_calls_update_when_nfo_needs_repair` β€” mock `NFOService.update_tvshow_nfo`; assert it is called exactly once - `test_repair_series_skips_when_nfo_is_complete` β€” mock `NFOService.update_tvshow_nfo`; assert it is NOT called - `test_parse_nfo_tags_handles_missing_file_gracefully` β€” pass nonexistent path; expect empty dict or graceful return - `test_parse_nfo_tags_handles_malformed_xml_gracefully` β€” pass file with invalid XML; expect graceful return **Run tests:** ```bash conda run -n AniWorld python -m pytest tests/unit/test_nfo_repair_service.py -v --tb=short ``` **Documentation steps:** - Add entry for `NfoRepairService` in `docs/ARCHITECTURE.md` under the NFO services section - Add entry in `docs/CHANGELOG.md` under a new `[Unreleased]` section **Commit step:** ``` git add src/core/services/nfo_repair_service.py tests/unit/test_nfo_repair_service.py docs/ git commit -m "feat: add NfoRepairService for missing NFO tag detection" ``` --- ### Task 2: Add `perform_nfo_repair_scan()` startup hook in `initialization_service.py` **Goal:** Add a new async function that runs every startup (not run-once), scans all series folders for NFOs with missing tags, and queues repair tasks via `BackgroundLoaderService`. **Implementation steps:** 1. Open `src/server/services/initialization_service.py` 2. Add import for `NfoRepairService` from `src.core.services.nfo_repair_service` 3. Implement `async def perform_nfo_repair_scan(background_loader=None) -> None`: - Guard: if `not settings.tmdb_api_key` β†’ log warning and return early - Guard: if `not settings.anime_directory` β†’ log warning and return early - Create `NfoRepairService` instance using `NFOServiceFactory` (follow same pattern as existing `perform_nfo_scan_if_needed()`) - Iterate all subdirectories in `settings.anime_directory` - For each series folder that contains a `tvshow.nfo`: call `nfo_needs_repair(nfo_path)` - If repair needed: queue via `background_loader.add_series_loading_task()` (non-blocking), or call `repair_service.repair_series()` directly if no background_loader provided - Log summary at INFO level: `"NFO repair scan complete: X of Y series queued for repair"` 4. Keep function under 60 lines; extract helpers if needed **Validation steps:** - Read the function and confirm it does NOT use a run-once DB flag (unlike `perform_nfo_scan_if_needed`) - Confirm both guards (tmdb_api_key, anime_directory) are present - Confirm the function is async and uses `await` where needed - Confirm `initialization_service.py` stays under 500 lines after the addition **Test steps** β€” add to `tests/unit/test_initialization_service.py` (create if not exists): - `test_perform_nfo_repair_scan_skips_without_tmdb_api_key` β€” patch `settings.tmdb_api_key = ""`; assert no repair is attempted - `test_perform_nfo_repair_scan_skips_without_anime_directory` β€” patch `settings.anime_directory = ""`; assert no repair is attempted - `test_perform_nfo_repair_scan_queues_deficient_series` β€” create temp dir with one bad NFO; mock `NfoRepairService.nfo_needs_repair` returning True; assert background_loader is called - `test_perform_nfo_repair_scan_skips_complete_series` β€” create temp dir with one good NFO; mock `nfo_needs_repair` returning False; assert background_loader NOT called **Run tests:** ```bash conda run -n AniWorld python -m pytest tests/unit/test_initialization_service.py -v --tb=short ``` **Documentation steps:** - Update `docs/ARCHITECTURE.md` startup sequence diagram/description to include the new `perform_nfo_repair_scan` step - Add note in `docs/CONFIGURATION.md` that `tmdb_api_key` is required for the repair scan **Commit step:** ``` git add src/server/services/initialization_service.py tests/unit/test_initialization_service.py docs/ git commit -m "feat: add perform_nfo_repair_scan startup hook" ``` --- ### Task 3: Wire `perform_nfo_repair_scan` into `fastapi_app.py` lifespan **Goal:** Call the new repair scan on every application startup, after the existing NFO creation scan. **Implementation steps:** 1. Open `src/server/fastapi_app.py` 2. Add `perform_nfo_repair_scan` to the existing import from `src.server.services.initialization_service` 3. In the `lifespan()` async context manager, after the line `await perform_nfo_scan_if_needed()`, add: ``` await perform_nfo_repair_scan(background_loader) ``` (pass the `background_loader` instance so repairs are queued non-blocking) 4. Confirm the call is inside the startup block (before `yield`), not in the shutdown block **Validation steps:** - Read `fastapi_app.py` and confirm ordering: `perform_nfo_scan_if_needed` runs before `perform_nfo_repair_scan` - Confirm `background_loader` is instantiated before the repair scan call - Start the server locally and check logs for `"NFO repair scan"` log lines - Confirm app boot completes without errors even if TMDB key is missing **Test steps** β€” add to `tests/integration/test_nfo_repair_startup.py` (new file): - `test_startup_triggers_nfo_repair_scan` β€” use TestClient with lifespan; mock `perform_nfo_repair_scan`; assert it was called once during startup - `test_startup_repair_scan_receives_background_loader` β€” assert `perform_nfo_repair_scan` is called with a non-None `background_loader` argument **Run tests:** ```bash conda run -n AniWorld python -m pytest tests/integration/test_nfo_repair_startup.py -v --tb=short ``` **Full regression test β€” run all tests and confirm nothing broken:** ```bash conda run -n AniWorld python -m pytest tests/ -v --tb=short ``` **Documentation steps:** - Update startup sequence in `docs/ARCHITECTURE.md` to add the new step after `perform_nfo_scan_if_needed` - Update `docs/CHANGELOG.md` unreleased section with the wiring change **Commit step:** ``` git add src/server/fastapi_app.py tests/integration/test_nfo_repair_startup.py docs/ git commit -m "feat: wire NFO repair scan into app startup lifespan" ``` --- ### Task 4: End-to-end validation of NFO repair **Goal:** Prove the full flow works with a real (or realistic) deficient NFO file. **Validation steps:** 1. Copy `tvshow.nfo.bad` into a test series folder inside `settings.anime_directory` 2. Ensure `tmdb_api_key` is configured in `data/config.json` 3. Start the server: `conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000 --reload` 4. Wait for background tasks to complete (check WebSocket or logs) 5. Open the repaired `tvshow.nfo` and confirm all previously missing tags are now present and non-empty 6. Restart the server and confirm the repair scan runs again but does NOT re-queue series whose NFO is already complete (verify via logs: `"0 of Y series queued for repair"`) 7. Place `tvshow.nfo.good` in the same folder; restart; confirm it is NOT queued for repair **Run full test suite one final time:** ```bash conda run -n AniWorld python -m pytest tests/ -v --tb=short ``` **Final documentation steps:** - Review `docs/NFO_GUIDE.md` and add a section "Automatic NFO Repair" explaining the startup scan, which tags are checked, and how to trigger a manual repair via the API (`/api/nfo/update/{series}`) - Update `docs/CHANGELOG.md` to move all unreleased entries under a dated release heading `[1.x.x] - 2026-02-22` **Final commit step:** ``` git add docs/ git commit -m "docs: document NFO repair feature" ``` ---