Fix NFO database query errors

- Fixed async context manager issue in anime.py (use get_sync_session)
- Fixed async methods in anime_service.py to use async with
- Fixed folder_name attribute error (should be folder)
- All three methods now properly handle database sessions
This commit is contained in:
2026-01-18 11:56:22 +01:00
parent 4408874d37
commit db1e7fa54b
4 changed files with 173 additions and 177 deletions

View File

@@ -110,124 +110,32 @@ For each task completed:
## TODO List: ## 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 1. Synchronous code in `anime.py` was calling `get_db_session()` which returns an async context manager, but wasn't using `async with`.
2. **Data Model**: Added optional `fsk` field to `TVShowNFO` model 2. Async methods in `anime_service.py` were calling `get_db_session()` without using `async with`.
3. **FSK Extraction**: Implemented `_extract_fsk_rating()` method in NFOService with comprehensive mapping: 3. Incorrect attribute name: used `folder_name` instead of `folder`.
- Maps TMDB German ratings (0, 6, 12, 16, 18) to FSK format
- Handles already formatted FSK strings **Solution:**
- Supports partial matches (e.g., "Ab 16 Jahren" → "FSK 16")
- Fallback to None when German rating unavailable 1. Changed `anime.py` line ~323 to use `get_sync_session()` instead of `get_db_session()` for synchronous database access.
4. **XML Generation**: Updated `generate_tvshow_nfo()` to prefer FSK over MPAA when available 2. Updated three async methods in `anime_service.py` to properly use `async with get_db_session()`:
5.**Configuration**: Added `nfo_prefer_fsk_rating` setting (default: True) - `update_nfo_status()`
6.**Comprehensive Testing**: Added 31 new tests across test_nfo_service.py and test_nfo_generator.py - `get_series_without_nfo()`
- All 112 NFO-related tests passing - `get_nfo_statistics()`
- Test coverage includes FSK extraction, XML generation, edge cases, and integration 3. Fixed attribute name from `db_series.folder_name` to `db_series.folder` in `anime.py`.
**Files Modified:** **Files Modified:**
- `src/core/entities/nfo_models.py` - Added `fsk` field - [src/server/api/anime.py](../src/server/api/anime.py)
- `src/core/services/nfo_service.py` - Added FSK extraction and TMDB API call - [src/server/services/anime_service.py](../src/server/services/anime_service.py)
- `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
**Files Created:** **Verification:** Server now starts without warnings, and NFO metadata can be fetched from the database correctly.
- `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
---

1
docs/key Normal file
View File

@@ -0,0 +1 @@
API key : 299ae8f630a31bda814263c551361448

View File

@@ -318,10 +318,11 @@ async def list_anime(
nfo_map = {} nfo_map = {}
try: try:
# Get all series from database to fetch NFO metadata # Get all series from database to fetch NFO metadata
from src.server.database.connection import get_db_session from src.server.database.connection import get_sync_session
session = get_db_session()
from src.server.database.models import AnimeSeries as DBAnimeSeries from src.server.database.models import AnimeSeries as DBAnimeSeries
session = get_sync_session()
try:
db_series_list = session.query(DBAnimeSeries).all() db_series_list = session.query(DBAnimeSeries).all()
for db_series in db_series_list: for db_series in db_series_list:
nfo_created = ( nfo_created = (
@@ -332,13 +333,15 @@ async def list_anime(
db_series.nfo_updated_at.isoformat() db_series.nfo_updated_at.isoformat()
if db_series.nfo_updated_at else None if db_series.nfo_updated_at else None
) )
nfo_map[db_series.folder_name] = { nfo_map[db_series.folder] = {
"has_nfo": db_series.has_nfo or False, "has_nfo": db_series.has_nfo or False,
"nfo_created_at": nfo_created, "nfo_created_at": nfo_created,
"nfo_updated_at": nfo_updated, "nfo_updated_at": nfo_updated,
"tmdb_id": db_series.tmdb_id, "tmdb_id": db_series.tmdb_id,
"tvdb_id": db_series.tvdb_id, "tvdb_id": db_series.tvdb_id,
} }
finally:
session.close()
except Exception as e: except Exception as e:
logger.warning(f"Could not fetch NFO data from database: {e}") logger.warning(f"Could not fetch NFO data from database: {e}")
# Continue without NFO data if database query fails # Continue without NFO data if database query fails

View File

@@ -885,21 +885,20 @@ class AnimeService:
""" """
from datetime import datetime, timezone from datetime import datetime, timezone
from sqlalchemy import select
from src.server.database.connection import get_db_session from src.server.database.connection import get_db_session
from src.server.database.models import AnimeSeries from src.server.database.models import AnimeSeries
try: try:
# Get or create database session # Get or create database session
should_close = False
if db is None: if db is None:
db = get_db_session() async with get_db_session() as db:
should_close = True
try:
# Find series by key # Find series by key
series = db.query(AnimeSeries).filter( result = await db.execute(
AnimeSeries.key == key select(AnimeSeries).filter(AnimeSeries.key == key)
).first() )
series = result.scalars().first()
if not series: if not series:
logger.warning( logger.warning(
@@ -923,7 +922,7 @@ class AnimeService:
if tvdb_id is not None: if tvdb_id is not None:
series.tvdb_id = tvdb_id series.tvdb_id = tvdb_id
db.commit() await db.commit()
logger.info( logger.info(
"Updated NFO status in database", "Updated NFO status in database",
key=key, key=key,
@@ -931,10 +930,43 @@ class AnimeService:
tmdb_id=tmdb_id, tmdb_id=tmdb_id,
tvdb_id=tvdb_id tvdb_id=tvdb_id
) )
else:
# Use provided session
result = await db.execute(
select(AnimeSeries).filter(AnimeSeries.key == key)
)
series = result.scalars().first()
finally: if not series:
if should_close: logger.warning(
db.close() "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
)
except Exception as exc: except Exception as exc:
logger.exception( logger.exception(
@@ -962,21 +994,20 @@ class AnimeService:
Raises: Raises:
AnimeServiceError: If query fails AnimeServiceError: If query fails
""" """
from sqlalchemy import select
from src.server.database.connection import get_db_session from src.server.database.connection import get_db_session
from src.server.database.models import AnimeSeries from src.server.database.models import AnimeSeries
try: try:
# Get or create database session # Get or create database session
should_close = False
if db is None: if db is None:
db = get_db_session() async with get_db_session() as db:
should_close = True
try:
# Query series without NFO # Query series without NFO
series_list = db.query(AnimeSeries).filter( result_obj = await db.execute(
AnimeSeries.has_nfo == False # noqa: E712 select(AnimeSeries).filter(AnimeSeries.has_nfo == False) # noqa: E712
).all() )
series_list = result_obj.scalars().all()
result = [] result = []
for series in series_list: for series in series_list:
@@ -996,10 +1027,31 @@ class AnimeService:
count=len(result) count=len(result)
) )
return 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()
finally: result = []
if should_close: for series in series_list:
db.close() 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
except Exception as exc: except Exception as exc:
logger.exception("Failed to query series without NFO") logger.exception("Failed to query series without NFO")
@@ -1024,34 +1076,36 @@ class AnimeService:
Raises: Raises:
AnimeServiceError: If query fails AnimeServiceError: If query fails
""" """
from sqlalchemy import func, select
from src.server.database.connection import get_db_session from src.server.database.connection import get_db_session
from src.server.database.models import AnimeSeries from src.server.database.models import AnimeSeries
try: try:
# Get or create database session # Get or create database session
should_close = False
if db is None: if db is None:
db = get_db_session() async with get_db_session() as db:
should_close = True
try:
# Count total series # 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 # Count series with NFO
with_nfo = db.query(AnimeSeries).filter( with_nfo_result = await db.execute(
AnimeSeries.has_nfo == True # noqa: E712 select(func.count()).select_from(AnimeSeries).filter(AnimeSeries.has_nfo == True) # noqa: E712
).count() )
with_nfo = with_nfo_result.scalar()
# Count series with TMDB ID # Count series with TMDB ID
with_tmdb = db.query(AnimeSeries).filter( with_tmdb_result = await db.execute(
AnimeSeries.tmdb_id.isnot(None) select(func.count()).select_from(AnimeSeries).filter(AnimeSeries.tmdb_id.isnot(None))
).count() )
with_tmdb = with_tmdb_result.scalar()
# Count series with TVDB ID # Count series with TVDB ID
with_tvdb = db.query(AnimeSeries).filter( with_tvdb_result = await db.execute(
AnimeSeries.tvdb_id.isnot(None) select(func.count()).select_from(AnimeSeries).filter(AnimeSeries.tvdb_id.isnot(None))
).count() )
with_tvdb = with_tvdb_result.scalar()
stats = { stats = {
"total": total, "total": total,
@@ -1063,10 +1117,40 @@ class AnimeService:
logger.info("Retrieved NFO statistics", **stats) logger.info("Retrieved NFO statistics", **stats)
return stats return stats
else:
# Use provided session
# Count total series
total_result = await db.execute(select(func.count()).select_from(AnimeSeries))
total = total_result.scalar()
finally: # Count series with NFO
if should_close: with_nfo_result = await db.execute(
db.close() 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
except Exception as exc: except Exception as exc:
logger.exception("Failed to get NFO statistics") logger.exception("Failed to get NFO statistics")