feat(scanner): add DB fallback for series key resolution

When SerieScanner encounters a folder without a local key or data file,
it now optionally falls back to a database lookup by folder name. This
prevents newly-added series from being silently skipped on rescan when
their metadata only lives in the DB.

Changes:
- SerieScanner accepts an optional db_lookup callable
- SeriesApp forwards db_lookup to SerieScanner
- AnimeSeriesService adds get_by_folder_sync() helper
- dependencies.py wires a sync DB lookup into get_series_app()
- Unit tests cover fallback hit, miss, and exception paths
This commit is contained in:
2026-05-14 19:28:43 +02:00
parent 69c2fd01f9
commit e3509f5c8f
5 changed files with 288 additions and 8 deletions

View File

@@ -148,7 +148,27 @@ class AnimeSeriesService:
select(AnimeSeries).where(AnimeSeries.key == key)
)
return result.scalar_one_or_none()
@staticmethod
def get_by_folder_sync(db: Session, folder: str) -> Optional[AnimeSeries]:
"""Look up an anime series by its filesystem folder name (sync).
Intended as a fallback for ``SerieScanner`` when neither a ``key``
file nor a ``data`` file exists on disk for a given folder.
Args:
db: Synchronous database session (from ``get_sync_session``).
folder: Filesystem folder name to match (e.g.
``"Rooster Fighter (2026)"``).
Returns:
``AnimeSeries`` instance or ``None`` if not found.
"""
result = db.execute(
select(AnimeSeries).where(AnimeSeries.folder == folder)
)
return result.scalar_one_or_none()
@staticmethod
async def get_all(
db: AsyncSession,

View File

@@ -57,6 +57,44 @@ _rate_limit_lock = Lock()
_RATE_LIMIT_WINDOW_SECONDS = 60.0
def _make_db_lookup():
"""Build a synchronous ``(folder) -> Serie | None`` callable for SerieScanner.
The returned function opens a short-lived sync DB session, queries for a
series whose ``folder`` column matches the given name, and converts the
ORM row to a ``Serie`` domain object. Returns ``None`` when the DB is not
yet initialised or no matching row is found.
"""
from src.core.entities.series import Serie
def _lookup(folder: str) -> Optional["Serie"]:
try:
from src.server.database.connection import get_sync_session
from src.server.database.service import AnimeSeriesService
db = get_sync_session()
try:
row = AnimeSeriesService.get_by_folder_sync(db, folder)
finally:
db.close()
if row is None:
return None
return Serie(
key=row.key,
name=row.name or "",
site=row.site,
folder=row.folder,
episodeDict={},
year=row.year,
)
except RuntimeError:
# DB not initialised yet (e.g. first boot before init_db())
return None
return _lookup
def get_series_app() -> SeriesApp:
"""
Dependency to get SeriesApp instance.
@@ -134,7 +172,7 @@ def get_series_app() -> SeriesApp:
),
)
_series_app = SeriesApp(anime_dir)
_series_app = SeriesApp(anime_dir, db_lookup=_make_db_lookup())
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,