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:
@@ -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
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|||||||
@@ -318,27 +318,30 @@ 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
|
||||||
|
|
||||||
db_series_list = session.query(DBAnimeSeries).all()
|
session = get_sync_session()
|
||||||
for db_series in db_series_list:
|
try:
|
||||||
nfo_created = (
|
db_series_list = session.query(DBAnimeSeries).all()
|
||||||
db_series.nfo_created_at.isoformat()
|
for db_series in db_series_list:
|
||||||
if db_series.nfo_created_at else None
|
nfo_created = (
|
||||||
)
|
db_series.nfo_created_at.isoformat()
|
||||||
nfo_updated = (
|
if db_series.nfo_created_at else None
|
||||||
db_series.nfo_updated_at.isoformat()
|
)
|
||||||
if db_series.nfo_updated_at else None
|
nfo_updated = (
|
||||||
)
|
db_series.nfo_updated_at.isoformat()
|
||||||
nfo_map[db_series.folder_name] = {
|
if db_series.nfo_updated_at else None
|
||||||
"has_nfo": db_series.has_nfo or False,
|
)
|
||||||
"nfo_created_at": nfo_created,
|
nfo_map[db_series.folder] = {
|
||||||
"nfo_updated_at": nfo_updated,
|
"has_nfo": db_series.has_nfo or False,
|
||||||
"tmdb_id": db_series.tmdb_id,
|
"nfo_created_at": nfo_created,
|
||||||
"tvdb_id": db_series.tvdb_id,
|
"nfo_updated_at": nfo_updated,
|
||||||
}
|
"tmdb_id": db_series.tmdb_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
|
||||||
|
|||||||
@@ -885,21 +885,57 @@ 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
|
# Find series by key
|
||||||
|
result = await db.execute(
|
||||||
|
select(AnimeSeries).filter(AnimeSeries.key == key)
|
||||||
|
)
|
||||||
|
series = result.scalars().first()
|
||||||
|
|
||||||
try:
|
if not series:
|
||||||
# Find series by key
|
logger.warning(
|
||||||
series = db.query(AnimeSeries).filter(
|
"Series not found in database for NFO update",
|
||||||
AnimeSeries.key == key
|
key=key
|
||||||
).first()
|
)
|
||||||
|
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:
|
if not series:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -923,7 +959,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,
|
||||||
@@ -932,10 +968,6 @@ class AnimeService:
|
|||||||
tvdb_id=tvdb_id
|
tvdb_id=tvdb_id
|
||||||
)
|
)
|
||||||
|
|
||||||
finally:
|
|
||||||
if should_close:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Failed to update NFO status",
|
"Failed to update NFO status",
|
||||||
@@ -962,21 +994,45 @@ 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
|
# Query series without NFO
|
||||||
|
result_obj = await db.execute(
|
||||||
|
select(AnimeSeries).filter(AnimeSeries.has_nfo == False) # noqa: E712
|
||||||
|
)
|
||||||
|
series_list = result_obj.scalars().all()
|
||||||
|
|
||||||
try:
|
result = []
|
||||||
# Query series without NFO
|
for series in series_list:
|
||||||
series_list = db.query(AnimeSeries).filter(
|
result.append({
|
||||||
AnimeSeries.has_nfo == False # noqa: E712
|
"key": series.key,
|
||||||
).all()
|
"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 = []
|
result = []
|
||||||
for series in series_list:
|
for series in series_list:
|
||||||
@@ -997,10 +1053,6 @@ class AnimeService:
|
|||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
finally:
|
|
||||||
if should_close:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception("Failed to query series without NFO")
|
logger.exception("Failed to query series without NFO")
|
||||||
raise AnimeServiceError(
|
raise AnimeServiceError(
|
||||||
@@ -1024,34 +1076,70 @@ 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
|
# Count total series
|
||||||
|
total_result = await db.execute(select(func.count()).select_from(AnimeSeries))
|
||||||
|
total = total_result.scalar()
|
||||||
|
|
||||||
try:
|
# 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
|
# 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,
|
||||||
@@ -1064,10 +1152,6 @@ class AnimeService:
|
|||||||
logger.info("Retrieved NFO statistics", **stats)
|
logger.info("Retrieved NFO statistics", **stats)
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
finally:
|
|
||||||
if should_close:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception("Failed to get NFO statistics")
|
logger.exception("Failed to get NFO statistics")
|
||||||
raise AnimeServiceError("NFO statistics query failed") from exc
|
raise AnimeServiceError("NFO statistics query failed") from exc
|
||||||
|
|||||||
Reference in New Issue
Block a user