Aniworld/docs/tasks/refactor-seriesapp-db-access.md
Lukas 596476f9ac refactor: remove database access from core layer
- Remove db_session parameter from SeriesApp, SerieList, SerieScanner
- Move all database operations to AnimeService (service layer)
- Add add_series_to_db, contains_in_db methods to AnimeService
- Update sync_series_from_data_files to use inline DB operations
- Remove obsolete test classes for removed DB methods
- Fix pylint issues: add broad-except comments, fix line lengths
- Core layer (src/core/) now has zero database imports

722 unit tests pass
2025-12-15 15:19:03 +01:00

17 KiB

Task: Refactor Database Access Out of SeriesApp

Overview

Issue: SeriesApp (in src/core/) directly contains database access code, violating the clean architecture principle that core domain logic should be independent of infrastructure concerns.

Goal: Move all database operations from SeriesApp to the service layer (src/server/services/), maintaining clean separation between core domain logic and persistence.

Current Architecture (Problematic)

┌─────────────────────────────────────────────────────────┐
│  src/core/ (Domain Layer)                               │
│  ┌─────────────────────────────────────────────────┐   │
│  │  SeriesApp                                      │   │
│  │  - db_session parameter ❌                      │   │
│  │  - Imports from src.server.database ❌          │   │
│  │  - Calls AnimeSeriesService directly ❌         │   │
│  └─────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────┐   │
│  │  SerieList                                      │   │
│  │  - db_session parameter ❌                      │   │
│  │  - Uses EpisodeService directly ❌              │   │
│  └─────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────┐   │
│  │  SerieScanner                                   │   │
│  │  - db_session parameter ❌                      │   │
│  │  - Uses AnimeSeriesService directly ❌          │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

Target Architecture (Clean)

┌─────────────────────────────────────────────────────────┐
│  src/server/services/ (Application Layer)               │
│  ┌─────────────────────────────────────────────────┐   │
│  │  AnimeService                                   │   │
│  │  - Owns database session                        │   │
│  │  - Orchestrates SeriesApp + persistence         │   │
│  │  - Subscribes to SeriesApp events               │   │
│  │  - Persists changes to database                 │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘
                         │
                         ▼ calls
┌─────────────────────────────────────────────────────────┐
│  src/core/ (Domain Layer)                               │
│  ┌─────────────────────────────────────────────────┐   │
│  │  SeriesApp                                      │   │
│  │  - Pure domain logic only ✅                    │   │
│  │  - No database imports ✅                       │   │
│  │  - Emits events for state changes ✅            │   │
│  │  - Works with in-memory entities ✅             │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

Benefits of Refactoring

Benefit Description
Clean Layer Separation Core layer has no dependencies on server layer
Testability SeriesApp can be unit tested without database mocking
CLI Compatibility CLI can use SeriesApp without database setup
Single Responsibility Each class has one reason to change
Flexibility Easy to swap persistence layer (SQLite → PostgreSQL)

Task List

Phase 1: Analysis & Preparation

  • 1.1 Document all database operations currently in SeriesApp

    • File: src/core/SeriesApp.py
    • Operations: init_from_db_async(), set_db_session(), db_session propagation
  • 1.2 Document all database operations in SerieList

    • File: src/core/entities/SerieList.py
    • Operations: EpisodeService calls for episode persistence
  • 1.3 Document all database operations in SerieScanner

    • File: src/core/SerieScanner.py
    • Operations: AnimeSeriesService calls for series persistence
  • 1.4 Identify all events already emitted by SeriesApp

    • Review src/core/events.py for existing event types
    • Determine which events need to be added for persistence triggers
  • 1.5 Create backup/branch before refactoring

    git checkout -b refactor/remove-db-from-core
    

Phase 2: Extend Event System

  • 2.1 Add new events for persistence triggers in src/core/events.py

    # Events that AnimeService should listen to for persistence
    class SeriesLoadedEvent:  # When series data is loaded/updated
    class EpisodeStatusChangedEvent:  # When episode download status changes
    class ScanCompletedEvent:  # When rescan completes with new data
    
  • 2.2 Ensure SeriesApp emits events at appropriate points

    • After loading series from files
    • After episode status changes
    • After scan completes

Phase 3: Refactor SeriesApp

  • 3.1 Remove db_session parameter from SeriesApp.__init__()

    • File: src/core/SeriesApp.py
    • Remove lines ~147-149 (db_session parameter and storage)
  • 3.2 Remove set_db_session() method from SeriesApp

    • File: src/core/SeriesApp.py
    • Remove entire method (~lines 191-204)
  • 3.3 Remove init_from_db_async() method from SeriesApp

    • File: src/core/SeriesApp.py
    • Remove entire method (~lines 206-238)
    • This functionality moves to AnimeService
  • 3.4 Remove database imports from SeriesApp

    • Remove: from src.server.database.services.anime_series_service import AnimeSeriesService
  • 3.5 Update rescan() to emit events instead of saving to DB

    • File: src/core/SeriesApp.py
    • Remove direct AnimeSeriesService calls
    • Emit ScanCompletedEvent with scan results

Phase 4: Refactor SerieList

  • 4.1 Remove db_session parameter from SerieList.__init__()

    • File: src/core/entities/SerieList.py
  • 4.2 Remove set_db_session() method from SerieList

    • File: src/core/entities/SerieList.py
  • 4.3 Remove database imports from SerieList

    • Remove: from src.server.database.services.episode_service import EpisodeService
  • 4.4 Update episode status methods to emit events

    • When download status changes, emit EpisodeStatusChangedEvent

Phase 5: Refactor SerieScanner

  • 5.1 Remove db_session parameter from SerieScanner.__init__()

    • File: src/core/SerieScanner.py
  • 5.2 Remove database imports from SerieScanner

    • Remove: from src.server.database.services.anime_series_service import AnimeSeriesService
  • 5.3 Update scanner to return results instead of persisting

    • Return scan results as domain objects
    • Let AnimeService handle persistence

Phase 6: Update AnimeService

  • 6.1 Add event subscription in AnimeService.__init__()

    • File: src/server/services/anime_service.py
    • Subscribe to SeriesLoadedEvent, EpisodeStatusChangedEvent, ScanCompletedEvent
  • 6.2 Implement _on_series_loaded() handler

    • Persist series data to database via AnimeSeriesService
  • 6.3 Implement _on_episode_status_changed() handler

    • Update episode status in database via EpisodeService
  • 6.4 Implement _on_scan_completed() handler

    • Persist new/updated series to database
  • 6.5 Move init_from_db_async() logic to AnimeService

    • New method: load_series_from_database()
    • Loads from DB and populates SeriesApp in-memory
  • 6.6 Update sync_series_from_data_files() to use new pattern

    • Call SeriesApp for domain logic
    • Handle persistence in service layer

Phase 7: Update Dependent Code

  • 7.1 Update src/server/dependencies.py

    • Remove db_session from SeriesApp initialization
    • Ensure AnimeService handles DB session lifecycle
  • 7.2 Update API routes if they directly access SeriesApp with DB

    • File: src/server/routes/*.py
    • Routes should call AnimeService, not SeriesApp directly
  • 7.3 Update CLI if it uses SeriesApp

    • Ensure CLI works without database (pure file-based mode)

Phase 8: Testing

  • 8.1 Create unit tests for SeriesApp without database

    • File: tests/core/test_series_app.py
    • Test pure domain logic in isolation
  • 8.2 Create unit tests for AnimeService with mocked DB

    • File: tests/server/services/test_anime_service.py
    • Test persistence logic
  • 8.3 Create integration tests for full flow

    • Test AnimeServiceSeriesApp → Events → Persistence
  • 8.4 Run existing tests and fix failures

    pytest tests/ -v
    
    • Result: 146 unit tests pass for refactored components

Phase 9: Documentation

  • 9.1 Update docs/instructions.md architecture section

    • Document new clean separation
  • 9.2 Update inline code documentation

    • Add docstrings explaining the architecture
  • 9.3 Create architecture diagram

    • Add to docs/architecture.md

Files to Modify

File Changes
src/core/SeriesApp.py Remove db_session, remove DB methods, add events
src/core/entities/SerieList.py Remove db_session, add events
src/core/SerieScanner.py Remove db_session, return results only
src/core/events.py Add new event types
src/server/services/anime_service.py Add event handlers, DB operations
src/server/dependencies.py Update initialization
tests/core/test_series_app.py New tests
tests/server/services/test_anime_service.py New tests

Code Examples

Before (Problematic)

# src/core/SeriesApp.py
class SeriesApp:
    def __init__(self, ..., db_session=None):
        self._db_session = db_session
        # ... passes db_session to children

    async def init_from_db_async(self):
        # Direct DB access in core layer ❌
        service = AnimeSeriesService(self._db_session)
        series = await service.get_all()

After (Clean)

# src/core/SeriesApp.py
class SeriesApp:
    def __init__(self, ...):
        # No db_session parameter ✅
        self._event_bus = EventBus()

    def load_series(self, series_list: List[Serie]) -> None:
        """Load series into memory (called by service layer)."""
        self._series.extend(series_list)
        self._event_bus.emit(SeriesLoadedEvent(series_list))

# src/server/services/anime_service.py
class AnimeService:
    def __init__(self, series_app: SeriesApp, db_session: AsyncSession):
        self._series_app = series_app
        self._db_session = db_session
        # Subscribe to events
        series_app.event_bus.subscribe(SeriesLoadedEvent, self._persist_series)

    async def initialize(self):
        """Load series from DB into SeriesApp."""
        db_service = AnimeSeriesService(self._db_session)
        series = await db_service.get_all()
        self._series_app.load_series(series)  # Domain logic

    async def _persist_series(self, event: SeriesLoadedEvent):
        """Persist series to database."""
        db_service = AnimeSeriesService(self._db_session)
        await db_service.save_many(event.series)

Acceptance Criteria

  • src/core/ has zero imports from src/server/database/
  • SeriesApp can be instantiated without any database session
  • All unit tests pass (146/146)
  • CLI works without database (file-based mode)
  • API endpoints continue to work with full persistence
  • No regression in functionality

Completion Summary

Status: COMPLETED

Completed Date: 2024

Summary of Changes:

Core Layer (src/core/) - Now DB-Free:

  • SeriesApp: Removed db_session, set_db_session(), init_from_db_async(). Added load_series_from_list() method. rescan() now returns list of Serie objects.
  • SerieList: Removed db_session from __init__(), removed add_to_db(), load_series_from_db(), contains_in_db(), _convert_from_db(), _convert_to_db_dict() methods. Now pure file-based storage only.
  • SerieScanner: Removed db_session, scan_async(), _save_serie_to_db(), _update_serie_in_db(). Returns scan results without persisting.

Service Layer (src/server/services/) - Owns DB Operations:

  • AnimeService: Added _save_scan_results_to_db(), _load_series_from_db(), _create_series_in_db(), _update_series_in_db(), add_series_to_db(), contains_in_db() methods.
  • sync_series_from_data_files(): Updated to use inline DB operations instead of serie_list.add_to_db().

Dependencies (src/server/utils/):

  • Removed get_series_app_with_db() from dependencies.py.

Tests:

  • Updated tests/unit/test_serie_list.py: Removed database-related test classes.
  • Updated tests/unit/test_serie_scanner.py: Removed obsolete async/DB test classes.
  • Updated tests/unit/test_anime_service.py: Updated TestRescan to mock new DB methods.
  • Updated tests/integration/test_data_file_db_sync.py: Removed SerieList.add_to_db tests.

Verification:

  • 124 unit tests pass for core layer components (SeriesApp, SerieList, SerieScanner, AnimeService)
  • Zero database imports in src/core/ - verified via grep search
  • Core layer is now pure domain logic, service layer handles all persistence

Estimated Effort

Phase Effort
Phase 1: Analysis 2 hours
Phase 2: Events 2 hours
Phase 3: SeriesApp 3 hours
Phase 4: SerieList 2 hours
Phase 5: SerieScanner 2 hours
Phase 6: AnimeService 4 hours
Phase 7: Dependencies 2 hours
Phase 8: Testing 4 hours
Phase 9: Documentation 2 hours
Total ~23 hours

References