This commit is contained in:
Lukas 2025-12-16 19:22:16 +01:00
parent 700f491ef9
commit 32dc893434
7 changed files with 2 additions and 477 deletions

View File

@ -16,9 +16,6 @@
"path": "data/backups",
"keep_days": 30
},
"other": {
"master_password_hash": "$pbkdf2-sha256$29000$tRZCyFnr/d87x/i/19p7Lw$BoD8EF67N97SRs7kIX8SREbotRwvFntS.WCH9ZwTxHY",
"anime_directory": "/home/lukas/Volume/serien/"
},
"other": {},
"version": "1.0.0"
}

View File

@ -1,23 +0,0 @@
{
"name": "Aniworld",
"data_dir": "data",
"scheduler": {
"enabled": true,
"interval_minutes": 60
},
"logging": {
"level": "INFO",
"file": null,
"max_bytes": null,
"backup_count": 3
},
"backup": {
"enabled": false,
"path": "data/backups",
"keep_days": 30
},
"other": {
"master_password_hash": "$pbkdf2-sha256$29000$JWTsXWstZYyxNiYEQAihFA$K9QPNr2J9biZEX/7SFKU94dnynvyCICrGjKtZcEu6t8"
},
"version": "1.0.0"
}

View File

@ -1,23 +0,0 @@
{
"name": "Aniworld",
"data_dir": "data",
"scheduler": {
"enabled": true,
"interval_minutes": 60
},
"logging": {
"level": "INFO",
"file": null,
"max_bytes": null,
"backup_count": 3
},
"backup": {
"enabled": false,
"path": "data/backups",
"keep_days": 30
},
"other": {
"master_password_hash": "$pbkdf2-sha256$29000$1fo/x1gLYax1bs15L.X8/w$T2GKqjDG7LT9tTZIwX/P2T/uKKuM9IhOD9jmhFUw4A0"
},
"version": "1.0.0"
}

View File

@ -1,24 +0,0 @@
{
"name": "Aniworld",
"data_dir": "data",
"scheduler": {
"enabled": true,
"interval_minutes": 60
},
"logging": {
"level": "INFO",
"file": null,
"max_bytes": null,
"backup_count": 3
},
"backup": {
"enabled": false,
"path": "data/backups",
"keep_days": 30
},
"other": {
"master_password_hash": "$pbkdf2-sha256$29000$nbNWSkkJIeTce48xxrh3bg$QXT6A63JqmSLimtTeI04HzC4eKfQS26xFW7UL9Ry5co",
"anime_directory": "/home/lukas/Volume/serien/"
},
"version": "1.0.0"
}

View File

@ -1,24 +0,0 @@
{
"name": "Aniworld",
"data_dir": "data",
"scheduler": {
"enabled": true,
"interval_minutes": 60
},
"logging": {
"level": "INFO",
"file": null,
"max_bytes": null,
"backup_count": 3
},
"backup": {
"enabled": false,
"path": "data/backups",
"keep_days": 30
},
"other": {
"master_password_hash": "$pbkdf2-sha256$29000$j5HSWuu9V.rdm9Pa2zunNA$gjQqL753WLBMZtHVOhziVn.vW3Bkq8mGtCzSkbBjSHo",
"anime_directory": "/home/lukas/Volume/serien/"
},
"version": "1.0.0"
}

View File

@ -1,379 +0,0 @@
# 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 ✅
- [x] **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
- [x] **1.2** Document all database operations in `SerieList`
- File: `src/core/entities/SerieList.py`
- Operations: `EpisodeService` calls for episode persistence
- [x] **1.3** Document all database operations in `SerieScanner`
- File: `src/core/SerieScanner.py`
- Operations: `AnimeSeriesService` calls for series persistence
- [x] **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
- [x] **1.5** Create backup/branch before refactoring
```bash
git checkout -b refactor/remove-db-from-core
```
### Phase 2: Extend Event System ✅
- [x] **2.1** Add new events for persistence triggers in `src/core/events.py`
```python
# 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
```
- [x] **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 ✅
- [x] **3.1** Remove `db_session` parameter from `SeriesApp.__init__()`
- File: `src/core/SeriesApp.py`
- Remove lines ~147-149 (db_session parameter and storage)
- [x] **3.2** Remove `set_db_session()` method from `SeriesApp`
- File: `src/core/SeriesApp.py`
- Remove entire method (~lines 191-204)
- [x] **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`
- [x] **3.4** Remove database imports from `SeriesApp`
- Remove: `from src.server.database.services.anime_series_service import AnimeSeriesService`
- [x] **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 ✅
- [x] **4.1** Remove `db_session` parameter from `SerieList.__init__()`
- File: `src/core/entities/SerieList.py`
- [x] **4.2** Remove `set_db_session()` method from `SerieList`
- File: `src/core/entities/SerieList.py`
- [x] **4.3** Remove database imports from `SerieList`
- Remove: `from src.server.database.services.episode_service import EpisodeService`
- [x] **4.4** Update episode status methods to emit events
- When download status changes, emit `EpisodeStatusChangedEvent`
### Phase 5: Refactor SerieScanner ✅
- [x] **5.1** Remove `db_session` parameter from `SerieScanner.__init__()`
- File: `src/core/SerieScanner.py`
- [x] **5.2** Remove database imports from `SerieScanner`
- Remove: `from src.server.database.services.anime_series_service import AnimeSeriesService`
- [x] **5.3** Update scanner to return results instead of persisting
- Return scan results as domain objects
- Let `AnimeService` handle persistence
### Phase 6: Update AnimeService ✅
- [x] **6.1** Add event subscription in `AnimeService.__init__()`
- File: `src/server/services/anime_service.py`
- Subscribe to `SeriesLoadedEvent`, `EpisodeStatusChangedEvent`, `ScanCompletedEvent`
- [x] **6.2** Implement `_on_series_loaded()` handler
- Persist series data to database via `AnimeSeriesService`
- [x] **6.3** Implement `_on_episode_status_changed()` handler
- Update episode status in database via `EpisodeService`
- [x] **6.4** Implement `_on_scan_completed()` handler
- Persist new/updated series to database
- [x] **6.5** Move `init_from_db_async()` logic to `AnimeService`
- New method: `load_series_from_database()`
- Loads from DB and populates `SeriesApp` in-memory
- [x] **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 ✅
- [x] **7.1** Update `src/server/dependencies.py`
- Remove `db_session` from `SeriesApp` initialization
- Ensure `AnimeService` handles DB session lifecycle
- [x] **7.2** Update API routes if they directly access `SeriesApp` with DB
- File: `src/server/routes/*.py`
- Routes should call `AnimeService`, not `SeriesApp` directly
- [x] **7.3** Update CLI if it uses `SeriesApp`
- Ensure CLI works without database (pure file-based mode)
### Phase 8: Testing ✅
- [x] **8.1** Create unit tests for `SeriesApp` without database
- File: `tests/core/test_series_app.py`
- Test pure domain logic in isolation
- [x] **8.2** Create unit tests for `AnimeService` with mocked DB
- File: `tests/server/services/test_anime_service.py`
- Test persistence logic
- [x] **8.3** Create integration tests for full flow
- Test `AnimeService``SeriesApp` → Events → Persistence
- [x] **8.4** Run existing tests and fix failures
```bash
pytest tests/ -v
```
- **Result**: 146 unit tests pass for refactored components
### Phase 9: Documentation ✅
- [x] **9.1** Update `docs/instructions.md` architecture section
- Document new clean separation
- [x] **9.2** Update inline code documentation
- Add docstrings explaining the architecture
- [x] **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)
```python
# 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)
```python
# 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
- [x] `src/core/` has **zero imports** from `src/server/database/`
- [x] `SeriesApp` can be instantiated **without any database session**
- [x] All unit tests pass (146/146)
- [x] CLI works without database (file-based mode)
- [x] API endpoints continue to work with full persistence
- [x] 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
- [docs/instructions.md](../instructions.md) - Project architecture guidelines
- [PEP 8](https://peps.python.org/pep-0008/) - Python style guide
- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) - Architecture principles

View File

@ -159,6 +159,7 @@ class SeriesApp:
directory_to_search, self.loader
)
self.list = SerieList(self.directory_to_search)
self.series_list: List[Any] = []
# Synchronous init used during constructor to avoid awaiting
# in __init__
self._init_list_sync()