feat: scan anime folders to populate AnimeSeries DB

- Add _scan_folders_to_database() - iterates anime_directory subdirs
- Extract title/year from folder names via (YYYY) pattern
- Resolve provider key via search when single match found
- Create AnimeSeries records for new folders only
- Add corresponding unit tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-06-04 21:34:10 +02:00
parent 830f6b4c93
commit 2b5c969a83
2 changed files with 396 additions and 0 deletions

View File

@@ -1,17 +1,22 @@
"""Centralized initialization service for application startup and setup."""
import asyncio
import os
import re
from pathlib import Path
from typing import Callable, Optional
import structlog
from src.config.settings import settings
from src.server.database.service import AnimeSeriesService
from src.server.services.anime_service import sync_legacy_series_to_db
from src.server.services.legacy_file_migration import migrate_series_from_files_to_db
logger = structlog.get_logger(__name__)
# Provider site URL constant
ANIMEWORLD_URL = "https://aniworld.to"
async def _check_scan_status(
check_method: Callable,
@@ -299,6 +304,110 @@ async def _load_series_into_memory(progress_service=None) -> None:
)
async def _scan_folders_to_database(progress_service=None) -> int:
"""Scan anime folders and create AnimeSeries DB records.
This function runs during initial setup only. It:
1. Iterates subdirectories of anime_directory
2. Extracts title/year from folder names (year via (YYYY) pattern)
3. Uses provider search to resolve key field when single match found
4. Creates AnimeSeries records for new folders
Args:
progress_service: Optional ProgressService for progress updates
Returns:
int: Number of new series created
"""
from src.server.database.connection import get_db_session
from src.server.utils.dependencies import get_series_app
logger.info("Scanning anime folders for new series...")
if not settings.anime_directory or not os.path.isdir(settings.anime_directory):
logger.info(
"Anime directory not configured or does not exist, skipping folder scan"
)
return 0
created_count = 0
skipped_existing = 0
try:
series_app = get_series_app()
async with get_db_session() as db:
for folder in settings.anime_directory.iterdir():
if not folder.is_dir():
continue
folder_name = folder.name
# Skip if series already exists in DB
existing = await AnimeSeriesService.get_by_folder(db, folder_name)
if existing:
skipped_existing += 1
continue
# Extract year from folder name using (YYYY) pattern
year = None
match = re.search(r'\((\d{4})\)', folder_name)
if match:
year = int(match.group(1))
# Extract title by removing year suffix
title = re.sub(r'\s*\(\d{4}\)\s*$', '', folder_name).strip()
# Try to resolve key via provider search
resolved_key = ""
if title:
try:
results = await series_app.search(title)
if len(results) == 1:
result_name = results[0].get('name', '').lower()
if result_name == title.lower():
resolved_key = results[0].get('key', '')
except Exception as exc:
logger.warning(
"Provider search failed for folder",
folder=folder_name,
error=str(exc)
)
# Create AnimeSeries record
await AnimeSeriesService.create(
db=db,
key=resolved_key,
name=title,
site=ANIMEWORLD_URL,
folder=folder_name,
year=year,
)
created_count += 1
logger.debug(
"Created series from folder",
folder=folder_name,
title=title,
year=year,
key=resolved_key or "(unresolved)"
)
except Exception as exc:
logger.error(
"Folder scan failed",
error=str(exc),
exc_info=True
)
return created_count
logger.info(
"Folder scan complete",
created=created_count,
skipped_existing=skipped_existing
)
return created_count
async def _validate_anime_directory(progress_service=None) -> bool:
"""Validate that anime directory is configured.
@@ -373,6 +482,11 @@ async def perform_initial_setup(progress_service=None):
# Perform the actual initialization
try:
# Scan folders and create AnimeSeries records first
folder_scan_count = await _scan_folders_to_database(progress_service)
if folder_scan_count > 0:
logger.info("Created %d series from anime folders", folder_scan_count)
# First, run legacy file migration if needed (independent of initial scan)
is_legacy_migration_done = await _check_legacy_migration_status()
if not is_legacy_migration_done: