Complete Task 8: Database Support for NFO Status

- Added 5 NFO tracking fields to AnimeSeries model
- Fields: has_nfo, nfo_created_at, nfo_updated_at, tmdb_id, tvdb_id
- Added 3 service methods to AnimeService for NFO operations
- Methods: update_nfo_status, get_series_without_nfo, get_nfo_statistics
- SQLAlchemy auto-migration (no manual migration needed)
- Backward compatible with existing data
- 15 new tests added (19/19 passing)
- Tests: database models, service methods, integration queries
This commit is contained in:
2026-01-16 18:50:04 +01:00
parent 56b4975d10
commit d642234814
9 changed files with 1014 additions and 50 deletions

View File

@@ -78,6 +78,28 @@ class AnimeSeries(Base, TimestampMixin):
doc="Release year of the series"
)
# NFO metadata tracking
has_nfo: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=False, server_default="0",
doc="Whether tvshow.nfo file exists for this series"
)
nfo_created_at: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True), nullable=True,
doc="Timestamp when NFO was first created"
)
nfo_updated_at: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True), nullable=True,
doc="Timestamp when NFO was last updated"
)
tmdb_id: Mapped[Optional[int]] = mapped_column(
Integer, nullable=True, index=True,
doc="TMDB (The Movie Database) ID for series metadata"
)
tvdb_id: Mapped[Optional[int]] = mapped_column(
Integer, nullable=True, index=True,
doc="TVDB (TheTVDB) ID for series metadata"
)
# Relationships
episodes: Mapped[List["Episode"]] = relationship(
"Episode",
@@ -127,7 +149,10 @@ class AnimeSeries(Base, TimestampMixin):
return value.strip()
def __repr__(self) -> str:
return f"<AnimeSeries(id={self.id}, key='{self.key}', name='{self.name}')>"
return (
f"<AnimeSeries(id={self.id}, key='{self.key}', "
f"name='{self.name}')>"
)
class Episode(Base, TimestampMixin):

View File

@@ -863,6 +863,215 @@ class AnimeService:
logger.exception("download failed")
raise AnimeServiceError("Download failed") from exc
async def update_nfo_status(
self,
key: str,
has_nfo: bool,
tmdb_id: Optional[int] = None,
tvdb_id: Optional[int] = None,
db=None
) -> None:
"""Update NFO status for a series in the database.
Args:
key: Serie unique identifier
has_nfo: Whether tvshow.nfo exists
tmdb_id: Optional TMDB ID
tvdb_id: Optional TVDB ID
db: Optional database session (will create if not provided)
Raises:
AnimeServiceError: If update fails
"""
from datetime import datetime, timezone
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()
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
db.commit()
logger.info(
"Updated NFO status in database",
key=key,
has_nfo=has_nfo,
tmdb_id=tmdb_id,
tvdb_id=tvdb_id
)
finally:
if should_close:
db.close()
except Exception as exc:
logger.exception(
"Failed to update NFO status",
key=key,
has_nfo=has_nfo
)
raise AnimeServiceError("NFO status update failed") from exc
async def get_series_without_nfo(self, db=None) -> list[dict]:
"""Get list of series that don't have NFO files.
Args:
db: Optional database session
Returns:
List of series dictionaries with keys:
- key: Series unique identifier
- name: Series name
- folder: Series folder name
- has_nfo: Always False
- tmdb_id: TMDB ID if available
- tvdb_id: TVDB ID if available
Raises:
AnimeServiceError: If query fails
"""
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()
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
finally:
if should_close:
db.close()
except Exception as exc:
logger.exception("Failed to query series without NFO")
raise AnimeServiceError(
"Query for series without NFO failed"
) from exc
async def get_nfo_statistics(self, db=None) -> dict:
"""Get NFO statistics for all series.
Args:
db: Optional database session
Returns:
Dictionary with statistics:
- total: Total series count
- with_nfo: Series with NFO files
- without_nfo: Series without NFO files
- with_tmdb_id: Series with TMDB ID
- with_tvdb_id: Series with TVDB ID
Raises:
AnimeServiceError: If query fails
"""
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:
# Count total series
total = db.query(AnimeSeries).count()
# Count series with NFO
with_nfo = db.query(AnimeSeries).filter(
AnimeSeries.has_nfo == True # noqa: E712
).count()
# Count series with TMDB ID
with_tmdb = db.query(AnimeSeries).filter(
AnimeSeries.tmdb_id.isnot(None)
).count()
# Count series with TVDB ID
with_tvdb = db.query(AnimeSeries).filter(
AnimeSeries.tvdb_id.isnot(None)
).count()
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
finally:
if should_close:
db.close()
except Exception as exc:
logger.exception("Failed to get NFO statistics")
raise AnimeServiceError("NFO statistics query failed") from exc
def get_anime_service(series_app: SeriesApp) -> AnimeService:
"""Factory used for creating AnimeService with a SeriesApp instance."""