Lukas c3aca9217d fix(episodes): prevent duplicate-row accumulation at schema + write sites
Followup to commit c84f968 (read-boundary dedup) and commit f75d591
(cleanup CLI). The read boundary filters duplicates out of the
in-memory episodeDict and the CLI cleans up historical duplicates
in the DB, but the underlying pathology — duplicate rows being
created in the first place — was still active on every rescan.

Two layered prevention fixes:

1. Schema-level guard: add UNIQUE(series_id, season, episode_number)
   to the episodes table. SQLite's CREATE UNIQUE INDEX requires
   no existing duplicates, but the cleanup CLI from f75d591 has
   already been run (or is a one-shot prerequisite for users on
   older DBs). Future duplicate rows are rejected at the DB layer.

2. Write-site guard: SerieScanner.scan_single_series used to
   `extend` the in-memory episodeDict on every rescan of a
   series already in keyDict — across N rescans, the same missing
   list was appended N times, growing the dict with duplicates that
   then flowed through _update_series_in_db into the episodes
   table. The fix replaces the cache with the latest scan result
   instead of extending, and dedupes within a single call as
   defense in depth against a buggy upstream loader.

Defensive dedup is layered three deep:
  - schema constraint (this commit, primary)
  - scan_single_series replace-not-extend (this commit, secondary)
  - episodeDict property read-boundary dedup (commit c84f968,
    tertiary — covers legacy DBs that predate the constraint)

Tests:
  - Updated test_serie_scanner.test_scan_single_series_existing_entry
    to assert the new replace-not-merge behavior (the old assertion
    encoded the buggy extend behavior).
  - New test_serie_scanner_scan_dedup.py covers the regression
    directly: two rescans of the same series with the same missing
    list must yield a canonical dict, not an accumulated one.
  - test_database_models and test_clean_duplicate_episodes_cli now
    use a legacy_engine fixture that drops the UNIQUE constraint,
    so the duplicate-row scenarios they exercise (the read-boundary
    dedup and the cleanup tool, both meant to defend against
    pre-migration state) can still be tested under the new schema.

Verified manually: clean_duplicate_episodes --apply on the user's
backup DB still removes all 633 duplicate rows under the new
schema (the CLI doesn't depend on the UNIQUE constraint — it
operates on whatever rows already exist).
2026-09-15 20:58:30 +02:00
2025-10-12 22:39:51 +02:00
2026-08-16 19:53:38 +02:00
2026-09-05 19:26:07 +02:00
2026-06-02 20:59:13 +02:00
2025-10-12 18:05:31 +02:00
2026-06-21 12:32:44 +02:00
2026-09-05 19:26:07 +02:00
2026-07-31 07:33:12 +02:00

Aniworld Download Manager

A web-based anime download manager with REST API, WebSocket real-time updates, and a modern web interface.

Features

  • Web interface for managing anime library
  • REST API for programmatic access
  • WebSocket real-time progress updates
  • Download queue with priority management
  • Automatic library scanning for missing episodes
  • NFO metadata management with TMDB integration
  • Automatic poster/fanart/logo downloads
  • JWT-based authentication
  • SQLite database for persistence
  • Comprehensive test coverage (1,070+ tests, 91.3% coverage)

Quick Start

Prerequisites

  • Python 3.10+
  • Conda (recommended) or virtualenv

Installation

  1. Clone the repository:
git clone https://github.com/your-repo/aniworld.git
cd aniworld
  1. Create and activate conda environment:
conda create -n AniWorld python=3.10
conda activate AniWorld
  1. Install dependencies:
pip install -r requirements.txt
  1. Start the server:
python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000
  1. Open http://127.0.0.1:8000 in your browser

First-Time Setup

  1. Navigate to http://127.0.0.1:8000/setup
  2. Set a master password (minimum 8 characters, mixed case, number, special character)
  3. Configure your anime directory path
  4. (Optional) Configure NFO settings with your TMDB API key
  5. Login with your master password

NFO Metadata Setup (Optional)

For automatic NFO file generation with metadata and images:

  1. Get a free TMDB API key from https://www.themoviedb.org/settings/api
  2. Go to Configuration → NFO Settings in the web interface
  3. Enter your TMDB API key and click "Test Connection"
  4. Enable auto-creation and select which images to download
  5. NFO files will be created automatically during downloads

Documentation

Document Description
docs/API.md REST API and WebSocket reference
docs/ARCHITECTURE.md System architecture and design
docs/CONFIGURATION.md Configuration options
docs/DATABASE.md Database schema
docs/DEVELOPMENT.md Developer setup guide
docs/TESTING.md Testing guidelines

Project Structure

src/
+-- cli/                # CLI interface (legacy)
+-- config/             # Application settings
+-- core/               # Domain logic
|   +-- SeriesApp.py    # Main application facade
|   +-- SerieScanner.py # Directory scanning
|   +-- entities/       # Domain entities
|   +-- providers/      # External provider adapters
+-- server/             # FastAPI web server
    +-- api/            # REST API endpoints
    +-- services/       # Business logic
    +-- models/         # Pydantic models
    +-- database/       # SQLAlchemy ORM
    +-- middleware/     # Auth, rate limiting

API Endpoints

Endpoint Description
POST /api/auth/login Authenticate and get JWT token
GET /api/anime List anime with missing episodes
GET /api/anime/search?query= Search for anime
POST /api/queue/add Add episodes to download queue
POST /api/queue/start Start queue processing
GET /api/queue/status Get queue status
GET /api/nfo/check Check NFO status for anime
POST /api/nfo/create Create NFO files
WS /ws/connect WebSocket for real-time updates

See docs/API.md for complete API reference.

Configuration

Environment variables (via .env file):

Variable Default Description
JWT_SECRET_KEY (random) Secret for JWT signing
DATABASE_URL sqlite:///./data/aniworld.db Database connection
ANIME_DIRECTORY (empty) Path to anime library
TMDB_API_KEY (empty) TMDB API key for metadata
LOG_LEVEL INFO Logging level

See docs/CONFIGURATION.md for all options.

Running Tests

The project includes a comprehensive test suite with 1,070+ tests and 91.3% coverage across all critical systems:

# Run all Python tests
conda run -n AniWorld python -m pytest tests/ -v

# Run unit tests only
conda run -n AniWorld python -m pytest tests/unit/ -v

# Run integration tests
conda run -n AniWorld python -m pytest tests/integration/ -v

# Run with coverage report
conda run -n AniWorld python -m pytest tests/ --cov --cov-report=html

# Run JavaScript/E2E tests (requires Node.js)
npm test                    # Unit tests (Vitest)
npm run test:e2e           # E2E tests (Playwright)

Test Coverage:

  • 1,070+ tests across 4 priority tiers (644 Python tests passing, 426 JavaScript/E2E tests)
  • 91.3% code coverage
  • TIER 1 Critical: 159/159 tests - Scheduler, NFO batch, download queue, persistence
  • TIER 2 High Priority: 390/390 tests - Frontend UI, WebSocket, dark mode, settings
  • TIER 3 Medium Priority: 95/156 tests - Performance, edge cases (core scenarios complete)
  • TIER 4 Polish: 426 tests - Internationalization, accessibility, media server compatibility
  • Security: Complete coverage (authentication, authorization, CSRF, XSS, SQL injection)
  • Performance: Validated (200+ concurrent WebSocket clients, batch operations)

See docs/TESTING_COMPLETE.md for comprehensive testing documentation.

Technology Stack

  • Web Framework: FastAPI 0.104.1
  • Database: SQLite + SQLAlchemy 2.0
  • Auth: JWT (python-jose) + passlib
  • Validation: Pydantic 2.5
  • Logging: structlog
  • Testing: pytest + pytest-asyncio

Application Lifecycle

Initialization

On first startup, the application performs a one-time sync of series from data files to the database:

  1. FastAPI lifespan starts
  2. Database is initialized
  3. sync_series_from_data_files() reads all data files from the anime directory (creates temporary SeriesApp)
  4. Series metadata is synced to the database
  5. DownloadService initializes (triggers main SeriesApp creation)
  6. SeriesApp loads series from database via service layer (not from files)

On subsequent startups, the same flow applies but the sync finds no new series. SeriesApp always initializes with an empty series list (skip_load=True) and loads data from the database on demand, avoiding redundant file system scans.

Adding New Series

When adding a new series:

  1. Series is added to the database via AnimeService
  2. Data file is created in the anime directory
  3. In-memory SerieList is updated via load_series_from_list()

License

MIT License

Description
No description provided
Readme 186 MiB
Languages
Python 77.2%
JavaScript 13.8%
HTML 4.2%
RobotFramework 2.2%
CSS 1.7%
Other 0.8%