feat: Complete Task 3.2 - Update AnimeService to use key as primary identifier

- Enhanced class and method docstrings to clarify 'key' as primary identifier
- Documented that 'folder' is metadata only (display and filesystem operations)
- Updated event handler documentation to show both key and folder are received
- Modernized type hints to Python 3.9+ style (list[dict] vs List[dict])
- Fixed PEP 8 line length violations
- All 18 anime service tests passing

Implementation follows identifier standardization initiative:
- key: Primary series identifier (provider-assigned, URL-safe)
- folder: Metadata for display and filesystem paths only

Task 3.2 completed November 23, 2025
Documented in infrastructure.md and instructions.md
This commit is contained in:
2025-11-23 20:19:04 +01:00
parent e1c8b616a8
commit e8129f847c
3 changed files with 122 additions and 42 deletions

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import asyncio
from functools import lru_cache
from typing import List, Optional
from typing import Optional
import structlog
@@ -23,9 +23,13 @@ class AnimeServiceError(Exception):
class AnimeService:
"""Wraps SeriesApp for use in the FastAPI web layer.
This service provides a clean interface to anime operations, using 'key'
as the primary series identifier (provider-assigned, URL-safe) and 'folder'
as metadata only (filesystem folder name for display purposes).
- SeriesApp methods are now async, no need for threadpool
- Subscribes to SeriesApp events for progress tracking
- Exposes async methods
- Exposes async methods using 'key' for all series identification
- Adds simple in-memory caching for read operations
"""
@@ -51,8 +55,12 @@ class AnimeService:
def _on_download_status(self, args) -> None:
"""Handle download status events from SeriesApp.
Events include both 'key' (primary identifier) and 'serie_folder'
(metadata for display and filesystem operations).
Args:
args: DownloadStatusEventArgs from SeriesApp
args: DownloadStatusEventArgs from SeriesApp containing key,
serie_folder, season, episode, status, and progress info
"""
try:
# Get event loop - try running loop first, then stored loop
@@ -74,7 +82,10 @@ class AnimeService:
progress_id = (
args.item_id
if args.item_id
else f"download_{args.serie_folder}_{args.season}_{args.episode}"
else (
f"download_{args.serie_folder}_"
f"{args.season}_{args.episode}"
)
)
# Map SeriesApp download events to progress service
@@ -85,7 +96,11 @@ class AnimeService:
progress_type=ProgressType.DOWNLOAD,
title=f"Downloading {args.serie_folder}",
message=f"S{args.season:02d}E{args.episode:02d}",
metadata={"item_id": args.item_id} if args.item_id else None,
metadata=(
{"item_id": args.item_id}
if args.item_id
else None
),
),
loop
)
@@ -136,8 +151,12 @@ class AnimeService:
def _on_scan_status(self, args) -> None:
"""Handle scan status events from SeriesApp.
Events include both 'key' (primary identifier) and 'folder'
(metadata for display purposes).
Args:
args: ScanStatusEventArgs from SeriesApp
args: ScanStatusEventArgs from SeriesApp containing key,
folder, current, total, status, and progress info
"""
try:
scan_id = "library_scan"
@@ -206,22 +225,33 @@ class AnimeService:
logger.error("Error handling scan status event", error=str(exc))
@lru_cache(maxsize=128)
def _cached_list_missing(self) -> List[dict]:
def _cached_list_missing(self) -> list[dict]:
# Synchronous cached call - SeriesApp.series_list is populated
# during initialization
try:
series = self._app.series_list
# normalize to simple dicts
return [
s.to_dict() if hasattr(s, "to_dict") else s
for s in series
]
result: list[dict] = []
for s in series:
if hasattr(s, "to_dict"):
result.append(s.to_dict())
else:
result.append(s) # type: ignore
return result
except Exception:
logger.exception("Failed to get missing episodes list")
raise
async def list_missing(self) -> List[dict]:
"""Return list of series with missing episodes."""
async def list_missing(self) -> list[dict]:
"""Return list of series with missing episodes.
Each series dictionary includes 'key' as the primary identifier
and 'folder' as metadata for display purposes.
Returns:
List of series dictionaries with 'key', 'name', 'site',
'folder', and 'episodeDict' fields
"""
try:
# series_list is already populated, just access it
return self._cached_list_missing()
@@ -231,14 +261,15 @@ class AnimeService:
logger.exception("list_missing failed")
raise AnimeServiceError("Failed to list missing series") from exc
async def search(self, query: str) -> List[dict]:
"""Search for series using underlying loader.
async def search(self, query: str) -> list[dict]:
"""Search for series using underlying provider.
Args:
query: Search query string
Returns:
List of search results as dictionaries
List of search results as dictionaries, each containing 'key'
as the primary identifier and other metadata fields
"""
if not query:
return []
@@ -251,10 +282,14 @@ class AnimeService:
raise AnimeServiceError("Search failed") from exc
async def rescan(self) -> None:
"""Trigger a re-scan.
"""Trigger a re-scan of the anime library directory.
The SeriesApp now handles progress tracking via events which are
Scans the filesystem for anime series and updates the series list.
The SeriesApp handles progress tracking via events which are
forwarded to the ProgressService through event handlers.
All series are identified by their 'key' (provider identifier),
with 'folder' stored as metadata.
"""
try:
# Store event loop for event handlers
@@ -281,19 +316,30 @@ class AnimeService:
key: str,
item_id: Optional[str] = None,
) -> bool:
"""Start a download.
"""Start a download for a specific episode.
The SeriesApp now handles progress tracking via events which are
The SeriesApp handles progress tracking via events which are
forwarded to the ProgressService through event handlers.
Args:
serie_folder: Serie folder name
serie_folder: Serie folder name (metadata only, used for
filesystem operations and display)
season: Season number
episode: Episode number
key: Serie key
key: Serie unique identifier (primary identifier for series
lookup, provider-assigned)
item_id: Optional download queue item ID for tracking
Returns True on success or raises AnimeServiceError on failure.
Returns:
True on success
Raises:
AnimeServiceError: If download fails
Note:
The 'key' parameter is the primary identifier used for all
series lookups. The 'serie_folder' is only used for filesystem
path construction and display purposes.
"""
try:
# Store event loop for event handlers