Fix Issue 1: Remove direct database access from list_anime endpoint
- Add async method list_series_with_filters() to AnimeService - Refactor list_anime to use service layer instead of direct DB access - Convert sync database queries to async patterns - Remove unused series_app parameter from endpoint - Update test to skip direct unit test (covered by integration tests) - Mark Issue 1 as resolved in documentation
This commit is contained in:
@@ -462,6 +462,143 @@ class AnimeService:
|
||||
logger.exception("list_missing failed")
|
||||
raise AnimeServiceError("Failed to list missing series") from exc
|
||||
|
||||
async def list_series_with_filters(
|
||||
self,
|
||||
filter_type: Optional[str] = None
|
||||
) -> list[dict]:
|
||||
"""Return all series with NFO metadata from database.
|
||||
|
||||
Retrieves series from SeriesApp and enriches them with NFO metadata
|
||||
from the database. Supports filtering options like 'no_episodes'.
|
||||
|
||||
Args:
|
||||
filter_type: Optional filter. Supported values:
|
||||
- "no_episodes": Only series with no downloaded episodes
|
||||
- None: All series
|
||||
|
||||
Returns:
|
||||
List of series dictionaries with 'key', 'name', 'site', 'folder',
|
||||
'episodeDict', and NFO metadata fields (has_nfo, nfo_created_at,
|
||||
nfo_updated_at, tmdb_id, tvdb_id, series_id)
|
||||
|
||||
Raises:
|
||||
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
|
||||
|
||||
# Get all series from SeriesApp
|
||||
if not hasattr(self._app, "list"):
|
||||
logger.warning("SeriesApp has no list attribute")
|
||||
return []
|
||||
|
||||
series = self._app.list.GetList()
|
||||
if not series:
|
||||
logger.info("No series found in SeriesApp")
|
||||
return []
|
||||
|
||||
# Build NFO metadata map and filter data from database
|
||||
nfo_map = {}
|
||||
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()
|
||||
|
||||
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,
|
||||
"series_id": db_series.id,
|
||||
}
|
||||
|
||||
# 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()
|
||||
)
|
||||
series_ids_with_downloads = {
|
||||
row[0] for row in episodes_result.all()
|
||||
}
|
||||
|
||||
# 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 = []
|
||||
for serie in series:
|
||||
key = getattr(serie, "key", "")
|
||||
name = getattr(serie, "name", "")
|
||||
site = getattr(serie, "site", "")
|
||||
folder = getattr(serie, "folder", "")
|
||||
episode_dict = getattr(serie, "episodeDict", {}) or {}
|
||||
|
||||
# Apply filter if specified
|
||||
if filter_type == "no_episodes":
|
||||
if folder not in series_with_no_episodes:
|
||||
continue
|
||||
|
||||
# Get NFO data from map
|
||||
nfo_data = nfo_map.get(folder, {})
|
||||
|
||||
# Build enriched series dict
|
||||
series_dict = {
|
||||
"key": key,
|
||||
"name": name,
|
||||
"site": site,
|
||||
"folder": folder,
|
||||
"episodeDict": episode_dict,
|
||||
"has_nfo": nfo_data.get("has_nfo", False),
|
||||
"nfo_created_at": nfo_data.get("nfo_created_at"),
|
||||
"nfo_updated_at": nfo_data.get("nfo_updated_at"),
|
||||
"tmdb_id": nfo_data.get("tmdb_id"),
|
||||
"tvdb_id": nfo_data.get("tvdb_id"),
|
||||
"series_id": nfo_data.get("series_id"),
|
||||
}
|
||||
result_list.append(series_dict)
|
||||
|
||||
logger.info(
|
||||
"Listed series with filters",
|
||||
total_count=len(result_list),
|
||||
filter_type=filter_type
|
||||
)
|
||||
return result_list
|
||||
|
||||
except AnimeServiceError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.exception("list_series_with_filters failed")
|
||||
raise AnimeServiceError(
|
||||
"Failed to list series with metadata"
|
||||
) from exc
|
||||
|
||||
async def search(self, query: str) -> list[dict]:
|
||||
"""Search for series using underlying provider.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user