Add Step 4 fallback: generate key from folder name

- SerieScanner: generate key from folder when no key/data files exist
- Handle edge cases: non-Latin characters, special symbols in folder names
- anime_service: expose loading_status and loading_error fields
- Update tests to match new fallback behavior
This commit is contained in:
2026-05-28 18:48:43 +02:00
parent 7abba0dae2
commit 14b8ef7f06
3 changed files with 42 additions and 7 deletions

View File

@@ -27,6 +27,7 @@ from src.core.providers.base_provider import Loader
from src.server.database.connection import get_sync_session from src.server.database.connection import get_sync_session
from src.server.database.service import AnimeSeriesService, EpisodeService from src.server.database.service import AnimeSeriesService, EpisodeService
from src.core.utils.key_utils import generate_key_from_folder
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
error_logger = logging.getLogger("error") error_logger = logging.getLogger("error")
@@ -708,6 +709,31 @@ class SerieScanner:
) )
return Serie.load_from_file(serie_file) return Serie.load_from_file(serie_file)
# Step 4: Generate key from folder name as last resort
# This handles edge cases like non-Latin characters or special symbols
try:
generated_key = generate_key_from_folder(folder_name)
year_from_folder = self._extract_year_from_folder_name(folder_name)
logger.info(
"Generated key for folder '%s' -> key='%s'",
folder_name,
generated_key
)
return Serie(
key=generated_key,
name="", # Name will be fetched from provider if needed
site="aniworld.to",
folder=folder_name,
episodeDict=dict(),
year=year_from_folder
)
except Exception as exc:
logger.warning(
"Failed to generate key for folder '%s': %s",
folder_name,
exc
)
return None return None
def __get_episode_and_season(self, filename: str) -> tuple[int, int]: def __get_episode_and_season(self, filename: str) -> tuple[int, int]:

View File

@@ -528,6 +528,8 @@ class AnimeService:
"tmdb_id": db_series.tmdb_id, "tmdb_id": db_series.tmdb_id,
"tvdb_id": db_series.tvdb_id, "tvdb_id": db_series.tvdb_id,
"series_id": db_series.id, "series_id": db_series.id,
"loading_status": db_series.loading_status,
"loading_error": db_series.loading_error,
} }
# Build episodeDict from DB, skipping is_downloaded=True # Build episodeDict from DB, skipping is_downloaded=True
@@ -596,6 +598,8 @@ class AnimeService:
"tmdb_id": nfo_data.get("tmdb_id"), "tmdb_id": nfo_data.get("tmdb_id"),
"tvdb_id": nfo_data.get("tvdb_id"), "tvdb_id": nfo_data.get("tvdb_id"),
"series_id": nfo_data.get("series_id"), "series_id": nfo_data.get("series_id"),
"loading_status": nfo_data.get("loading_status"),
"loading_error": nfo_data.get("loading_error"),
} }
result_list.append(series_dict) result_list.append(series_dict)

View File

@@ -552,8 +552,8 @@ class TestReadDataFromFile:
assert result is not None assert result is not None
assert result.key == "test-key" assert result.key == "test-key"
def test_no_files_returns_none(self, mock_loader): def test_no_files_returns_serie_with_generated_key(self, mock_loader):
"""Should return None when no key or data file exists.""" """Should return Serie with generated key when no key or data file exists."""
import tempfile import tempfile
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
@@ -562,7 +562,10 @@ class TestReadDataFromFile:
scanner = SerieScanner(tmpdir, mock_loader) scanner = SerieScanner(tmpdir, mock_loader)
result = scanner._SerieScanner__read_data_from_file("Empty") result = scanner._SerieScanner__read_data_from_file("Empty")
assert result is None # Step 4 generates key from folder name when no files exist
assert result is not None
assert isinstance(result, Serie)
assert result.key == "empty"
class TestReinit: class TestReinit:
@@ -763,7 +766,7 @@ class TestDbLookupFallback:
assert scanner.keyDict["rooster-fighter"].episodeDict == {1: [1, 2, 3]} assert scanner.keyDict["rooster-fighter"].episodeDict == {1: [1, 2, 3]}
def test_db_lookup_returns_none_folder_skipped(self, mock_loader): def test_db_lookup_returns_none_folder_skipped(self, mock_loader):
"""When db_lookup returns None, the folder is skipped with a warning.""" """When db_lookup returns None, Step 4 fallback generates key from folder name."""
import tempfile import tempfile
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
@@ -773,10 +776,11 @@ class TestDbLookupFallback:
with patch.object(scanner, 'get_total_to_scan', return_value=1): with patch.object(scanner, 'get_total_to_scan', return_value=1):
scanner.scan() scanner.scan()
assert len(scanner.keyDict) == 0 # Step 4 generates key from folder name, so keyDict is not empty
assert len(scanner.keyDict) == 1
def test_db_lookup_exception_skips_folder(self, mock_loader): def test_db_lookup_exception_skips_folder(self, mock_loader):
"""When db_lookup raises, the folder is skipped gracefully.""" """When db_lookup raises, Step 4 fallback generates key from folder name."""
import tempfile import tempfile
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
@@ -786,7 +790,8 @@ class TestDbLookupFallback:
with patch.object(scanner, 'get_total_to_scan', return_value=1): with patch.object(scanner, 'get_total_to_scan', return_value=1):
scanner.scan() # should not raise scanner.scan() # should not raise
assert len(scanner.keyDict) == 0 # Step 4 generates key from folder name, so keyDict is not empty
assert len(scanner.keyDict) == 1
def test_db_lookup_warning_logged_when_no_files( def test_db_lookup_warning_logged_when_no_files(
self, mock_loader, caplog self, mock_loader, caplog