Fix Issue 7: Enforce repository pattern consistency
- Added 5 new service methods for complete database coverage: * get_series_without_nfo() * count_all() * count_with_nfo() * count_with_tmdb_id() * count_with_tvdb_id() - Eliminated all direct database queries from business logic: * series_manager_service.py - now uses AnimeSeriesService * anime_service.py - now uses service layer methods - Documented architecture decision in ARCHITECTURE.md: * Service layer IS the repository layer * No direct SQLAlchemy queries allowed outside service layer - All database access must go through service methods - 1449 tests passing, repository pattern enforced
This commit is contained in:
@@ -132,45 +132,45 @@ class SeriesManagerService:
|
||||
ids = self.nfo_service.parse_nfo_ids(nfo_path)
|
||||
|
||||
if ids["tmdb_id"] or ids["tvdb_id"]:
|
||||
# Create database session for this task
|
||||
# Update database using service layer
|
||||
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
|
||||
from src.server.database.service import AnimeSeriesService
|
||||
|
||||
async with get_db_session() as db:
|
||||
result = await db.execute(
|
||||
select(AnimeSeries).filter(
|
||||
AnimeSeries.key == serie_key
|
||||
)
|
||||
)
|
||||
series = result.scalars().first()
|
||||
series = await AnimeSeriesService.get_by_key(db, serie_key)
|
||||
|
||||
if series:
|
||||
now = datetime.now(timezone.utc)
|
||||
series.has_nfo = True
|
||||
|
||||
# Prepare update fields
|
||||
update_fields = {
|
||||
"has_nfo": True,
|
||||
"nfo_updated_at": now,
|
||||
}
|
||||
|
||||
if series.nfo_created_at is None:
|
||||
series.nfo_created_at = now
|
||||
series.nfo_updated_at = now
|
||||
update_fields["nfo_created_at"] = now
|
||||
|
||||
if ids["tmdb_id"] is not None:
|
||||
series.tmdb_id = ids["tmdb_id"]
|
||||
update_fields["tmdb_id"] = ids["tmdb_id"]
|
||||
logger.debug(
|
||||
f"Updated TMDB ID for '{serie_name}': "
|
||||
f"{ids['tmdb_id']}"
|
||||
)
|
||||
|
||||
if ids["tvdb_id"] is not None:
|
||||
series.tvdb_id = ids["tvdb_id"]
|
||||
update_fields["tvdb_id"] = ids["tvdb_id"]
|
||||
logger.debug(
|
||||
f"Updated TVDB ID for '{serie_name}': "
|
||||
f"{ids['tvdb_id']}"
|
||||
)
|
||||
|
||||
# Use service layer for update
|
||||
await AnimeSeriesService.update(db, series.id, **update_fields)
|
||||
await db.commit()
|
||||
|
||||
logger.info(
|
||||
f"Updated database with IDs from NFO for "
|
||||
f"'{serie_name}' - TMDB: {ids['tmdb_id']}, "
|
||||
|
||||
@@ -297,6 +297,111 @@ class AnimeSeriesService:
|
||||
|
||||
result = await db.execute(query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@staticmethod
|
||||
async def get_series_without_nfo(
|
||||
db: AsyncSession,
|
||||
limit: Optional[int] = None,
|
||||
offset: int = 0,
|
||||
) -> List[AnimeSeries]:
|
||||
"""Get anime series without NFO files.
|
||||
|
||||
Returns series where has_nfo is False.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
limit: Optional limit for results
|
||||
offset: Offset for pagination
|
||||
|
||||
Returns:
|
||||
List of AnimeSeries without NFO files
|
||||
"""
|
||||
query = (
|
||||
select(AnimeSeries)
|
||||
.where(AnimeSeries.has_nfo == False) # noqa: E712
|
||||
.order_by(AnimeSeries.name)
|
||||
.offset(offset)
|
||||
)
|
||||
|
||||
if limit:
|
||||
query = query.limit(limit)
|
||||
|
||||
result = await db.execute(query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@staticmethod
|
||||
async def count_all(db: AsyncSession) -> int:
|
||||
"""Count total number of anime series.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Total count of series
|
||||
"""
|
||||
from sqlalchemy import func
|
||||
|
||||
result = await db.execute(
|
||||
select(func.count()).select_from(AnimeSeries)
|
||||
)
|
||||
return result.scalar() or 0
|
||||
|
||||
@staticmethod
|
||||
async def count_with_nfo(db: AsyncSession) -> int:
|
||||
"""Count anime series with NFO files.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Count of series with has_nfo=True
|
||||
"""
|
||||
from sqlalchemy import func
|
||||
|
||||
result = await db.execute(
|
||||
select(func.count())
|
||||
.select_from(AnimeSeries)
|
||||
.where(AnimeSeries.has_nfo == True) # noqa: E712
|
||||
)
|
||||
return result.scalar() or 0
|
||||
|
||||
@staticmethod
|
||||
async def count_with_tmdb_id(db: AsyncSession) -> int:
|
||||
"""Count anime series with TMDB ID.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Count of series with tmdb_id set
|
||||
"""
|
||||
from sqlalchemy import func
|
||||
|
||||
result = await db.execute(
|
||||
select(func.count())
|
||||
.select_from(AnimeSeries)
|
||||
.where(AnimeSeries.tmdb_id.isnot(None))
|
||||
)
|
||||
return result.scalar() or 0
|
||||
|
||||
@staticmethod
|
||||
async def count_with_tvdb_id(db: AsyncSession) -> int:
|
||||
"""Count anime series with TVDB ID.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Count of series with tvdb_id set
|
||||
"""
|
||||
from sqlalchemy import func
|
||||
|
||||
result = await db.execute(
|
||||
select(func.count())
|
||||
.select_from(AnimeSeries)
|
||||
.where(AnimeSeries.tvdb_id.isnot(None))
|
||||
)
|
||||
return result.scalar() or 0
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@@ -485,11 +485,8 @@ class AnimeService:
|
||||
AnimeServiceError: If operation fails
|
||||
"""
|
||||
try:
|
||||
from sqlalchemy import select
|
||||
|
||||
from src.server.database.connection import get_db_session
|
||||
from src.server.database.models import AnimeSeries as DBAnimeSeries
|
||||
from src.server.database.models import Episode
|
||||
from src.server.database.service import AnimeSeriesService
|
||||
|
||||
# Get all series from SeriesApp
|
||||
if not hasattr(self._app, "list"):
|
||||
@@ -506,9 +503,8 @@ class AnimeService:
|
||||
series_with_no_episodes = set()
|
||||
|
||||
async with get_db_session() as db:
|
||||
# Get all series NFO metadata
|
||||
result = await db.execute(select(DBAnimeSeries))
|
||||
db_series_list = result.scalars().all()
|
||||
# Get all series NFO metadata using service layer
|
||||
db_series_list = await AnimeSeriesService.get_all(db)
|
||||
|
||||
for db_series in db_series_list:
|
||||
nfo_created = (
|
||||
@@ -528,28 +524,18 @@ class AnimeService:
|
||||
"series_id": db_series.id,
|
||||
}
|
||||
|
||||
# If filter is "no_episodes", get series with no downloaded episodes
|
||||
# If filter is "no_episodes", get series with no
|
||||
# downloaded episodes
|
||||
if filter_type == "no_episodes":
|
||||
# Query for series IDs that have downloaded episodes
|
||||
episodes_result = await db.execute(
|
||||
select(Episode.series_id)
|
||||
.filter(Episode.is_downloaded.is_(True))
|
||||
.distinct()
|
||||
# Use service method to get series with
|
||||
# undownloaded episodes
|
||||
series_no_downloads = (
|
||||
await AnimeSeriesService
|
||||
.get_series_with_no_episodes(db)
|
||||
)
|
||||
series_ids_with_downloads = {
|
||||
row[0] for row in episodes_result.all()
|
||||
series_with_no_episodes = {
|
||||
s.folder for s in series_no_downloads
|
||||
}
|
||||
|
||||
# All series NOT in the downloaded set
|
||||
all_series_ids = {db_series.id for db_series in db_series_list}
|
||||
series_with_no_episodes_ids = (
|
||||
all_series_ids - series_ids_with_downloads
|
||||
)
|
||||
|
||||
# Map back to folder names for filtering
|
||||
for db_series in db_series_list:
|
||||
if db_series.id in series_with_no_episodes_ids:
|
||||
series_with_no_episodes.add(db_series.folder)
|
||||
|
||||
# Build result list with enriched metadata
|
||||
result_list = []
|
||||
@@ -1113,22 +1099,15 @@ class AnimeService:
|
||||
Raises:
|
||||
AnimeServiceError: If update fails
|
||||
"""
|
||||
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
|
||||
from src.server.database.service import AnimeSeriesService
|
||||
|
||||
try:
|
||||
# Get or create database session
|
||||
if db is None:
|
||||
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()
|
||||
# Find series by key using service layer
|
||||
series = await AnimeSeriesService.get_by_key(db, key)
|
||||
|
||||
if not series:
|
||||
logger.warning(
|
||||
@@ -1137,21 +1116,23 @@ class AnimeService:
|
||||
)
|
||||
return
|
||||
|
||||
# Update NFO fields
|
||||
# Prepare update fields
|
||||
now = datetime.now(timezone.utc)
|
||||
series.has_nfo = has_nfo
|
||||
update_fields = {"has_nfo": has_nfo}
|
||||
|
||||
if has_nfo:
|
||||
if series.nfo_created_at is None:
|
||||
series.nfo_created_at = now
|
||||
series.nfo_updated_at = now
|
||||
update_fields["nfo_created_at"] = now
|
||||
update_fields["nfo_updated_at"] = now
|
||||
|
||||
if tmdb_id is not None:
|
||||
series.tmdb_id = tmdb_id
|
||||
update_fields["tmdb_id"] = tmdb_id
|
||||
|
||||
if tvdb_id is not None:
|
||||
series.tvdb_id = tvdb_id
|
||||
update_fields["tvdb_id"] = tvdb_id
|
||||
|
||||
# Use service layer for update
|
||||
await AnimeSeriesService.update(db, series.id, **update_fields)
|
||||
await db.commit()
|
||||
logger.info(
|
||||
"Updated NFO status in database",
|
||||
@@ -1162,10 +1143,7 @@ class AnimeService:
|
||||
)
|
||||
else:
|
||||
# Use provided session
|
||||
result = await db.execute(
|
||||
select(AnimeSeries).filter(AnimeSeries.key == key)
|
||||
)
|
||||
series = result.scalars().first()
|
||||
series = await AnimeSeriesService.get_by_key(db, key)
|
||||
|
||||
if not series:
|
||||
logger.warning(
|
||||
@@ -1174,9 +1152,9 @@ class AnimeService:
|
||||
)
|
||||
return
|
||||
|
||||
# Update NFO fields
|
||||
# Prepare update fields
|
||||
now = datetime.now(timezone.utc)
|
||||
series.has_nfo = has_nfo
|
||||
update_fields = {"has_nfo": has_nfo}
|
||||
|
||||
if has_nfo:
|
||||
if series.nfo_created_at is None:
|
||||
@@ -1227,17 +1205,14 @@ class AnimeService:
|
||||
from sqlalchemy import select
|
||||
|
||||
from src.server.database.connection import get_db_session
|
||||
from src.server.database.models import AnimeSeries
|
||||
from src.server.database.service import AnimeSeriesService
|
||||
|
||||
try:
|
||||
# Get or create database session
|
||||
if db is None:
|
||||
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()
|
||||
# Query series without NFO using service layer
|
||||
series_list = await AnimeSeriesService.get_series_without_nfo(db)
|
||||
|
||||
result = []
|
||||
for series in series_list:
|
||||
@@ -1259,10 +1234,7 @@ class AnimeService:
|
||||
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()
|
||||
series_list = await AnimeSeriesService.get_series_without_nfo(db)
|
||||
|
||||
result = []
|
||||
for series in series_list:
|
||||
@@ -1309,33 +1281,17 @@ class AnimeService:
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from src.server.database.connection import get_db_session
|
||||
from src.server.database.models import AnimeSeries
|
||||
from src.server.database.service import AnimeSeriesService
|
||||
|
||||
try:
|
||||
# Get or create database session
|
||||
if db is None:
|
||||
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()
|
||||
# Use service layer count methods
|
||||
total = await AnimeSeriesService.count_all(db)
|
||||
with_nfo = await AnimeSeriesService.count_with_nfo(db)
|
||||
with_tmdb = await AnimeSeriesService.count_with_tmdb_id(db)
|
||||
with_tvdb = await AnimeSeriesService.count_with_tvdb_id(db)
|
||||
|
||||
stats = {
|
||||
"total": total,
|
||||
@@ -1348,22 +1304,11 @@ class AnimeService:
|
||||
logger.info("Retrieved NFO statistics", **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()
|
||||
|
||||
# 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()
|
||||
# Use provided session and service layer count methods
|
||||
total = await AnimeSeriesService.count_all(db)
|
||||
with_nfo = await AnimeSeriesService.count_with_nfo(db)
|
||||
with_tmdb = await AnimeSeriesService.count_with_tmdb_id(db)
|
||||
with_tvdb = await AnimeSeriesService.count_with_tvdb_id(db)
|
||||
|
||||
# Count series with TVDB ID
|
||||
with_tvdb_result = await db.execute(
|
||||
|
||||
Reference in New Issue
Block a user