feat(setup): detect filesystem properties during initial scan

SetupService.run() now checks each anime folder for tvshow.nfo,
logo.png, and poster/fanart images instead of using hardcoded
defaults. Provider key resolution via search is unchanged.
This commit is contained in:
2026-06-05 20:05:04 +02:00
parent 5c2be3f7c4
commit 18d10b44b5
3 changed files with 262 additions and 4 deletions

View File

@@ -70,6 +70,10 @@ class AnimeSeriesService:
logo_loaded: bool = False,
images_loaded: bool = False,
loading_started_at: datetime | None = None,
has_nfo: bool = False,
nfo_path: str | None = None,
nfo_created_at: datetime | None = None,
nfo_updated_at: datetime | None = None,
) -> AnimeSeries:
"""Create a new anime series.
@@ -85,6 +89,10 @@ class AnimeSeriesService:
logo_loaded: Whether logo is loaded (default: False)
images_loaded: Whether images are loaded (default: False)
loading_started_at: When loading started (optional)
has_nfo: Whether tvshow.nfo exists (default: False)
nfo_path: Path to tvshow.nfo file (optional)
nfo_created_at: When NFO file was created (optional)
nfo_updated_at: When NFO file was last updated (optional)
Returns:
Created AnimeSeries instance
@@ -103,6 +111,10 @@ class AnimeSeriesService:
logo_loaded=logo_loaded,
images_loaded=images_loaded,
loading_started_at=loading_started_at,
has_nfo=has_nfo,
nfo_path=nfo_path,
nfo_created_at=nfo_created_at,
nfo_updated_at=nfo_updated_at,
)
db.add(series)
await db.flush()

View File

@@ -11,6 +11,8 @@ via _check_initial_scan_status, not by this service itself.
"""
import os
import re
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
@@ -24,6 +26,17 @@ from src.server.utils.dependencies import get_series_app
logger = structlog.get_logger(__name__)
@dataclass
class SeriesProperties:
"""Filesystem-derived properties for an AnimeSeries."""
has_nfo: bool = False
nfo_path: Optional[str] = None
nfo_created_at: Optional[datetime] = None
nfo_updated_at: Optional[datetime] = None
logo_loaded: bool = False
images_loaded: bool = False
class SetupService:
"""Service for setup operations during application initialization."""
@@ -92,6 +105,77 @@ class SetupService:
return ""
@staticmethod
def _check_nfo_file(folder_path: Path) -> tuple[bool, Optional[str], Optional[datetime], Optional[datetime]]:
"""Check if tvshow.nfo exists and return its metadata.
Args:
folder_path: Path to the series folder
Returns:
Tuple of (has_nfo, nfo_path, nfo_created_at, nfo_updated_at)
"""
nfo_path = folder_path / "tvshow.nfo"
if nfo_path.is_file():
stat = nfo_path.stat()
created = datetime.fromtimestamp(stat.st_ctime, tz=timezone.utc)
updated = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc)
return True, str(nfo_path), created, updated
return False, None, None, None
@staticmethod
def _check_logo_file(folder_path: Path) -> bool:
"""Check if logo.png exists.
Args:
folder_path: Path to the series folder
Returns:
True if logo.png exists, False otherwise
"""
return (folder_path / "logo.png").is_file()
@staticmethod
def _check_image_files(folder_path: Path) -> bool:
"""Check if any image files (poster, fanart) exist.
Args:
folder_path: Path to the series folder
Returns:
True if any poster.jpg/jpeg/png or fanart.jpg/jpeg/png exists
"""
image_extensions = {'.jpg', '.jpeg', '.png'}
for child in folder_path.iterdir():
if child.is_file():
name_lower = child.name.lower()
if name_lower.startswith(('poster', 'fanart')) and child.suffix.lower() in image_extensions:
return True
return False
@classmethod
def _get_series_properties(cls, folder_path: Path) -> SeriesProperties:
"""Get all filesystem-derived properties for a series folder.
Args:
folder_path: Path to the series folder
Returns:
SeriesProperties with all detected values
"""
has_nfo, nfo_path, nfo_created_at, nfo_updated_at = cls._check_nfo_file(folder_path)
logo_loaded = cls._check_logo_file(folder_path)
images_loaded = cls._check_image_files(folder_path)
return SeriesProperties(
has_nfo=has_nfo,
nfo_path=nfo_path,
nfo_created_at=nfo_created_at,
nfo_updated_at=nfo_updated_at,
logo_loaded=logo_loaded,
images_loaded=images_loaded,
)
@classmethod
async def run(cls) -> int:
"""Run the setup service.
@@ -152,8 +236,11 @@ class SetupService:
# Resolve key via provider search
resolved_key = await cls._resolve_key_via_search(title)
# Check filesystem properties
props = cls._get_series_properties(folder)
# Create AnimeSeries record
await AnimeSeriesService.create(
series = await AnimeSeriesService.create(
db=db,
key=resolved_key,
name=title,
@@ -162,8 +249,12 @@ class SetupService:
year=year,
loading_status="completed",
episodes_loaded=True,
logo_loaded=False,
images_loaded=False,
logo_loaded=props.logo_loaded,
images_loaded=props.images_loaded,
has_nfo=props.has_nfo,
nfo_path=props.nfo_path,
nfo_created_at=props.nfo_created_at,
nfo_updated_at=props.nfo_updated_at,
)
created_count += 1
@@ -172,7 +263,10 @@ class SetupService:
folder=folder_name,
title=title,
year=year,
key=resolved_key or "(unresolved)"
key=resolved_key or "(unresolved)",
has_nfo=props.has_nfo,
logo_loaded=props.logo_loaded,
images_loaded=props.images_loaded,
)
logger.info(