diff --git a/docs/instructions.md b/docs/instructions.md index f35b2e6..45b36e2 100644 --- a/docs/instructions.md +++ b/docs/instructions.md @@ -110,124 +110,32 @@ For each task completed: ## TODO List: -### ๐ŸŽฏ Priority: NFO FSK Rating Implementation โœ… COMPLETED +All tasks completed! The NFO database query issue has been resolved. -**Task: Implement German FSK Rating Support in NFO Files** +## Recently Fixed Issues: -**Status: COMPLETED** โœ… +### โœ… Fixed: NFO Database Query Error (2026-01-18) -**Implementation Summary:** +**Issue:** WARNING: Could not fetch NFO data from database: '\_AsyncGeneratorContextManager' object has no attribute 'query' -All requirements have been successfully implemented and tested: +**Root Cause:** -1. โœ… **TMDB API Integration**: Added `get_tv_show_content_ratings()` method to TMDBClient -2. โœ… **Data Model**: Added optional `fsk` field to `TVShowNFO` model -3. โœ… **FSK Extraction**: Implemented `_extract_fsk_rating()` method in NFOService with comprehensive mapping: - - Maps TMDB German ratings (0, 6, 12, 16, 18) to FSK format - - Handles already formatted FSK strings - - Supports partial matches (e.g., "Ab 16 Jahren" โ†’ "FSK 16") - - Fallback to None when German rating unavailable -4. โœ… **XML Generation**: Updated `generate_tvshow_nfo()` to prefer FSK over MPAA when available -5. โœ… **Configuration**: Added `nfo_prefer_fsk_rating` setting (default: True) -6. โœ… **Comprehensive Testing**: Added 31 new tests across test_nfo_service.py and test_nfo_generator.py - - All 112 NFO-related tests passing - - Test coverage includes FSK extraction, XML generation, edge cases, and integration +1. Synchronous code in `anime.py` was calling `get_db_session()` which returns an async context manager, but wasn't using `async with`. +2. Async methods in `anime_service.py` were calling `get_db_session()` without using `async with`. +3. Incorrect attribute name: used `folder_name` instead of `folder`. + +**Solution:** + +1. Changed `anime.py` line ~323 to use `get_sync_session()` instead of `get_db_session()` for synchronous database access. +2. Updated three async methods in `anime_service.py` to properly use `async with get_db_session()`: + - `update_nfo_status()` + - `get_series_without_nfo()` + - `get_nfo_statistics()` +3. Fixed attribute name from `db_series.folder_name` to `db_series.folder` in `anime.py`. **Files Modified:** -- `src/core/entities/nfo_models.py` - Added `fsk` field -- `src/core/services/nfo_service.py` - Added FSK extraction and TMDB API call -- `src/core/services/tmdb_client.py` - Added content ratings endpoint -- `src/core/utils/nfo_generator.py` - Updated XML generation to prefer FSK -- `src/config/settings.py` - Added `nfo_prefer_fsk_rating` setting +- [src/server/api/anime.py](../src/server/api/anime.py) +- [src/server/services/anime_service.py](../src/server/services/anime_service.py) -**Files Created:** - -- `tests/unit/test_nfo_service.py` - 23 comprehensive unit tests - -**Files Updated:** - -- `tests/unit/test_nfo_generator.py` - Added 5 FSK-specific tests - -**Acceptance Criteria Met:** - -- โœ… NFO files contain FSK rating when available from TMDB -- โœ… Fallback to MPAA rating if FSK not available -- โœ… Configuration setting to prefer FSK over MPAA -- โœ… Unit tests cover all FSK values and fallback scenarios -- โœ… Existing NFO functionality remains unchanged -- โœ… Documentation updated with FSK support details - ---- - -### ๐Ÿงช Priority: Comprehensive NFO and Image Download Tests โœ… COMPLETED - -**Task: Add Comprehensive Tests for NFO Creation and Media Downloads** - -**Status: COMPLETED** โœ… - -**Implementation Summary:** - -Significantly expanded test coverage for NFO functionality with comprehensive unit and integration tests: - -1. โœ… **Unit Tests Expanded** ([test_nfo_service.py](tests/unit/test_nfo_service.py)): - - Added 13 tests for media file downloads (poster, logo, fanart) - - Tests for various image sizes and configurations - - Tests for download failures and edge cases - - Configuration testing for NFO service settings - - **Total: 44 tests** in test_nfo_service.py - -2. โœ… **Integration Tests Created** ([test_nfo_integration.py](tests/integration/test_nfo_integration.py)): - - Complete NFO creation workflow with all media files - - NFO creation without media downloads - - Correct folder structure verification - - NFO update workflow with media re-download - - Error handling and recovery - - Concurrent NFO operations (batch creation) - - Data integrity validation - - **Total: 10 comprehensive integration tests** - -3. โœ… **API Endpoint Tests**: Existing test_nfo_endpoints.py already covers all NFO API endpoints - -**Files Modified:** - -- `tests/unit/test_nfo_service.py` - Added 23 new tests for media downloads and configuration - -**Files Created:** - -- `tests/integration/test_nfo_integration.py` - 10 comprehensive integration tests - -**Test Results:** - -- โœ… **44 tests passing** in test_nfo_service.py (unit) -- โœ… **10 tests passing** in test_nfo_integration.py (integration) -- โœ… **112 total NFO-related unit tests passing** -- โœ… **All tests verify**: - - FSK rating extraction and mapping - - Media file download scenarios - - NFO creation and update workflows - - Error handling and edge cases - - Concurrent operations - - Data integrity - -**Acceptance Criteria Met:** - -- โœ… Comprehensive unit tests for all media download scenarios -- โœ… Integration tests verify complete workflows -- โœ… Tests validate file system state after operations -- โœ… Edge cases and error scenarios covered -- โœ… Concurrent operations tested -- โœ… All tests use proper mocking for TMDB API -- โœ… Test fixtures provide realistic test data - -**Test Coverage Highlights:** - -- Media download with all combinations (poster/logo/fanart) -- Different image sizes (original, w500, w780, w342) -- Missing media scenarios -- Concurrent NFO creation -- NFO update workflows -- FSK rating preservation -- Complete metadata integrity - ---- +**Verification:** Server now starts without warnings, and NFO metadata can be fetched from the database correctly. diff --git a/docs/key b/docs/key new file mode 100644 index 0000000..326087c --- /dev/null +++ b/docs/key @@ -0,0 +1 @@ +API key : 299ae8f630a31bda814263c551361448 \ No newline at end of file diff --git a/src/server/api/anime.py b/src/server/api/anime.py index 84dc9e6..1ae6e62 100644 --- a/src/server/api/anime.py +++ b/src/server/api/anime.py @@ -318,27 +318,30 @@ async def list_anime( nfo_map = {} try: # Get all series from database to fetch NFO metadata - from src.server.database.connection import get_db_session - session = get_db_session() + from src.server.database.connection import get_sync_session from src.server.database.models import AnimeSeries as DBAnimeSeries - db_series_list = session.query(DBAnimeSeries).all() - for db_series in db_series_list: - nfo_created = ( - db_series.nfo_created_at.isoformat() - if db_series.nfo_created_at else None - ) - nfo_updated = ( - db_series.nfo_updated_at.isoformat() - if db_series.nfo_updated_at else None - ) - nfo_map[db_series.folder_name] = { - "has_nfo": db_series.has_nfo or False, - "nfo_created_at": nfo_created, - "nfo_updated_at": nfo_updated, - "tmdb_id": db_series.tmdb_id, - "tvdb_id": db_series.tvdb_id, - } + session = get_sync_session() + try: + db_series_list = session.query(DBAnimeSeries).all() + for db_series in db_series_list: + nfo_created = ( + db_series.nfo_created_at.isoformat() + if db_series.nfo_created_at else None + ) + nfo_updated = ( + db_series.nfo_updated_at.isoformat() + if db_series.nfo_updated_at else None + ) + nfo_map[db_series.folder] = { + "has_nfo": db_series.has_nfo or False, + "nfo_created_at": nfo_created, + "nfo_updated_at": nfo_updated, + "tmdb_id": db_series.tmdb_id, + "tvdb_id": db_series.tvdb_id, + } + finally: + session.close() except Exception as e: logger.warning(f"Could not fetch NFO data from database: {e}") # Continue without NFO data if database query fails diff --git a/src/server/services/anime_service.py b/src/server/services/anime_service.py index 7944583..aef6ad5 100644 --- a/src/server/services/anime_service.py +++ b/src/server/services/anime_service.py @@ -885,21 +885,57 @@ class AnimeService: """ from datetime import datetime, timezone + from sqlalchemy import select + from src.server.database.connection import get_db_session from src.server.database.models import AnimeSeries try: # Get or create database session - should_close = False if db is None: - db = get_db_session() - should_close = True - - try: - # Find series by key - series = db.query(AnimeSeries).filter( - AnimeSeries.key == key - ).first() + async with get_db_session() as db: + # Find series by key + result = await db.execute( + select(AnimeSeries).filter(AnimeSeries.key == key) + ) + series = result.scalars().first() + + if not series: + logger.warning( + "Series not found in database for NFO update", + key=key + ) + return + + # Update NFO fields + now = datetime.now(timezone.utc) + series.has_nfo = has_nfo + + if has_nfo: + if series.nfo_created_at is None: + series.nfo_created_at = now + series.nfo_updated_at = now + + if tmdb_id is not None: + series.tmdb_id = tmdb_id + + if tvdb_id is not None: + series.tvdb_id = tvdb_id + + await db.commit() + logger.info( + "Updated NFO status in database", + key=key, + has_nfo=has_nfo, + tmdb_id=tmdb_id, + tvdb_id=tvdb_id + ) + else: + # Use provided session + result = await db.execute( + select(AnimeSeries).filter(AnimeSeries.key == key) + ) + series = result.scalars().first() if not series: logger.warning( @@ -923,7 +959,7 @@ class AnimeService: if tvdb_id is not None: series.tvdb_id = tvdb_id - db.commit() + await db.commit() logger.info( "Updated NFO status in database", key=key, @@ -931,10 +967,6 @@ class AnimeService: tmdb_id=tmdb_id, tvdb_id=tvdb_id ) - - finally: - if should_close: - db.close() except Exception as exc: logger.exception( @@ -962,21 +994,45 @@ class AnimeService: Raises: AnimeServiceError: If query fails """ + from sqlalchemy import select + from src.server.database.connection import get_db_session from src.server.database.models import AnimeSeries try: # Get or create database session - should_close = False if db is None: - db = get_db_session() - should_close = True - - try: - # Query series without NFO - series_list = db.query(AnimeSeries).filter( - AnimeSeries.has_nfo == False # noqa: E712 - ).all() + async with get_db_session() as db: + # Query series without NFO + result_obj = await db.execute( + select(AnimeSeries).filter(AnimeSeries.has_nfo == False) # noqa: E712 + ) + series_list = result_obj.scalars().all() + + result = [] + for series in series_list: + result.append({ + "key": series.key, + "name": series.name, + "folder": series.folder, + "has_nfo": False, + "tmdb_id": series.tmdb_id, + "tvdb_id": series.tvdb_id, + "nfo_created_at": None, + "nfo_updated_at": None + }) + + logger.info( + "Retrieved series without NFO", + count=len(result) + ) + return result + else: + # Use provided session + result_obj = await db.execute( + select(AnimeSeries).filter(AnimeSeries.has_nfo == False) # noqa: E712 + ) + series_list = result_obj.scalars().all() result = [] for series in series_list: @@ -996,10 +1052,6 @@ class AnimeService: count=len(result) ) return result - - finally: - if should_close: - db.close() except Exception as exc: logger.exception("Failed to query series without NFO") @@ -1024,34 +1076,70 @@ class AnimeService: Raises: AnimeServiceError: If query fails """ + from sqlalchemy import func, select + from src.server.database.connection import get_db_session from src.server.database.models import AnimeSeries try: # Get or create database session - should_close = False if db is None: - db = get_db_session() - should_close = True - - try: + async with get_db_session() as db: + # Count total series + total_result = await db.execute(select(func.count()).select_from(AnimeSeries)) + total = total_result.scalar() + + # Count series with NFO + with_nfo_result = await db.execute( + select(func.count()).select_from(AnimeSeries).filter(AnimeSeries.has_nfo == True) # noqa: E712 + ) + with_nfo = with_nfo_result.scalar() + + # Count series with TMDB ID + with_tmdb_result = await db.execute( + select(func.count()).select_from(AnimeSeries).filter(AnimeSeries.tmdb_id.isnot(None)) + ) + with_tmdb = with_tmdb_result.scalar() + + # Count series with TVDB ID + with_tvdb_result = await db.execute( + select(func.count()).select_from(AnimeSeries).filter(AnimeSeries.tvdb_id.isnot(None)) + ) + with_tvdb = with_tvdb_result.scalar() + + stats = { + "total": total, + "with_nfo": with_nfo, + "without_nfo": total - with_nfo, + "with_tmdb_id": with_tmdb, + "with_tvdb_id": with_tvdb + } + + logger.info("Retrieved NFO statistics", **stats) + return stats + else: + # Use provided session # Count total series - total = db.query(AnimeSeries).count() + total_result = await db.execute(select(func.count()).select_from(AnimeSeries)) + total = total_result.scalar() # Count series with NFO - with_nfo = db.query(AnimeSeries).filter( - AnimeSeries.has_nfo == True # noqa: E712 - ).count() + with_nfo_result = await db.execute( + select(func.count()).select_from(AnimeSeries).filter(AnimeSeries.has_nfo == True) # noqa: E712 + ) + with_nfo = with_nfo_result.scalar() # Count series with TMDB ID - with_tmdb = db.query(AnimeSeries).filter( - AnimeSeries.tmdb_id.isnot(None) - ).count() + with_tmdb_result = await db.execute( + select(func.count()).select_from(AnimeSeries).filter(AnimeSeries.tmdb_id.isnot(None)) + ) + with_tmdb = with_tmdb_result.scalar() # Count series with TVDB ID - with_tvdb = db.query(AnimeSeries).filter( - AnimeSeries.tvdb_id.isnot(None) - ).count() + with_tvdb_result = await db.execute( + select(func.count()).select_from(AnimeSeries).filter(AnimeSeries.tvdb_id.isnot(None)) + ) + with_tvdb = with_tvdb_result.scalar() stats = { "total": total, @@ -1063,10 +1151,6 @@ class AnimeService: logger.info("Retrieved NFO statistics", **stats) return stats - - finally: - if should_close: - db.close() except Exception as exc: logger.exception("Failed to get NFO statistics")