Files
Aniworld/docs/DEVELOPMENT.md
Lukas 9a20541598 feat(NFO): add TMDB search fallback with alt_titles support
- New _search_with_fallback() method tries multiple strategies:
  1. Primary query with year filter (de-DE locale)
  2. Alternative titles with ja-JP / en-US locales
  3. English search (en-US)
  4. Search without year constraint
  5. Punctuation-normalized query
- create_nfo() accepts new alt_titles param for Japanese/title fallback
- Better match rate for anime with non-English titles

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-23 21:57:00 +02:00

6.1 KiB

Development Guide

Document Purpose

This document provides guidance for developers working on the Aniworld project.

What This Document Contains

  • Prerequisites: Required software and tools
  • Environment Setup: Step-by-step local development setup
  • Project Structure: Source code organization explanation
  • Development Workflow: Branch strategy, commit conventions
  • Coding Standards: Style guide, linting, formatting
  • Running the Application: Development server, CLI usage
  • Debugging Tips: Common debugging approaches
  • IDE Configuration: VS Code settings, recommended extensions
  • Contributing Guidelines: How to submit changes
  • Code Review Process: Review checklist and expectations

What This Document Does NOT Contain

Target Audience

  • New Developers joining the project
  • Contributors (internal and external)
  • Anyone setting up a development environment

Sections to Document

  1. Prerequisites
    • Python version
    • Conda environment
    • Node.js (if applicable)
    • Git
  2. Getting Started
    • Clone repository
    • Setup conda environment
    • Install dependencies
    • Configuration setup
  3. Project Structure Overview
  4. Development Server
    • Starting FastAPI server
    • Hot reload configuration
    • Debug mode
  5. CLI Development
  6. Code Style
    • PEP 8 compliance
    • Type hints requirements
    • Docstring format
    • Import organization
  7. Git Workflow
    • Branch naming
    • Commit message format
    • Pull request process
  8. Common Development Tasks

Adding Queue Deduplication

The download queue prevents duplicate entries at two levels:

In-Memory Deduplication (src/server/services/download_service.py):

  • _pending_by_episode dict tracks pending episodes: key = (serie_id, season, episode)
  • _add_to_pending_queue() updates the dict when adding items
  • add_to_queue() checks this dict before adding episodes (includes batch-local dedup)
  • _remove_from_pending_queue() cleans up the dict when items are removed

Database Constraint (src/server/models.py):

  • DownloadQueueItem has a unique index on episode_id via __table_args__
  • Prevents duplicate queue entries at the database level
  • Unique constraint: Index("ix_download_queue_episode_pending", "episode_id", unique=True)

Scheduler Cooldown (src/server/services/scheduler_service.py):

  • _last_auto_download_time tracks when auto-download last ran
  • 5-minute cooldown prevents rapid re-triggers
  • Checked at start of _auto_download_missing()

Mocking the Download Queue

When testing components that use the download queue:

# Mock repository for unit tests
class MockQueueRepository:
    def __init__(self):
        self._items: Dict[str, DownloadItem] = {}

    async def save_item(self, item: DownloadItem) -> DownloadItem:
        self._items[item.id] = item
        return item

    async def get_all_items(self) -> List[DownloadItem]:
        return list(self._items.values())

# Use in fixture
@pytest.fixture
def mock_queue_repository():
    return MockQueueRepository()

@pytest.fixture
def download_service(mock_anime_service, mock_queue_repository):
    return DownloadService(
        anime_service=mock_anime_service,
        queue_repository=mock_queue_repository,
        max_retries=3,
    )
  1. Troubleshooting Development Issues

Async Context Managers for aiohttp

All aiohttp.ClientSession usages must be wrapped in async with:

# Correct — session properly closed on exit
async with TMDBClient(api_key="key") as client:
    result = await client.search_tv_show("Show")

# Wrong — session may leak if exception occurs
client = TMDBClient(api_key="key")
result = await client.search_tv_show("Show")
await client.close()  # May not be called if exception raised earlier

Why:

  • aiohttp.ClientSession holds TCP connections that must be explicitly closed
  • If exception occurs before close(), session leaks
  • Context manager guarantees __aexit__ runs even on exceptions

Services that use aiohttp:

  • TMDBClient — has __aenter__/__aexit__, use async with
  • ImageDownloader — has __aenter__/__aexit__, use async with
  • NFOService — wraps both above, use async with

Verification:

  • Missing context manager usage triggers __del__ warning on garbage collection
  • Integration tests verify no "Unclosed client session" errors in logs

Scheduler Persistence and Recovery

APScheduler stores jobs in data/scheduler.db (SQLite) so they survive process restarts:

from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore

jobstores = {
    "default": SQLAlchemyJobStore(url="sqlite:///./data/scheduler.db"),
}
scheduler = AsyncIOScheduler(jobstores=jobstores)

Grace period: misfire_grace_time=3600 (1 hour). If server is down at scheduled time and restarts within 1 hour, missed job runs automatically via APScheduler coalesce behavior.

Startup recovery: On start(), scheduler loads persisted jobs from DB. APScheduler handles missed jobs internally when coalesce=True.

Health endpoint: GET /health returns scheduler_next_run and scheduler_last_run for external monitors (Uptime Kuma, Prometheus, etc.).

If server is down >1 hour: No automatic recovery. Manual trigger via POST /api/scheduler/trigger-rescan or wait for next scheduled run.

Troubleshooting Development Issues

Scheduler missed a run

  1. Server was down at scheduled time (03:00 UTC by default).
  2. Check data/scheduler.db exists — if not, jobs are not persisted.
  3. If server was down >1 hour, missed job is dropped (misfire window exceeded).
  4. Trigger manually: POST /api/scheduler/trigger-rescan
  5. Monitor next run: GET /healthscheduler_next_run
  6. If problem repeats, increase misfire_grace_time in scheduler_service.py.