Fix get_all_series_from_data_files to read data files directly

Previously, the method created a SerieList instance which only loads
from database, not from data files. Now reads data files directly
and parses JSON to create AnimeSeries objects.

Also added _load_data_file helper function and fixed logger.warning
calls to use proper format strings instead of keyword arguments.

Updated unit tests to use real temp directories instead of mocks.
This commit is contained in:
2026-06-04 22:04:46 +02:00
parent 2b5c969a83
commit 2be7b692b9
2 changed files with 170 additions and 112 deletions

View File

@@ -697,11 +697,15 @@ class SeriesApp:
self.directory_to_search
)
# Create a fresh SerieList instance for file-based loading
# This ensures we get all series from data files without
# interfering with the main instance's state
all_series: List[AnimeSeries] = []
try:
temp_list = SerieList(self.directory_to_search)
if not os.path.isdir(self.directory_to_search):
logger.warning(
"Directory does not exist: %s",
self.directory_to_search
)
return []
except (OSError, ValueError) as e:
logger.error(
"Failed to scan directory for data files: %s",
@@ -710,8 +714,53 @@ class SeriesApp:
)
return []
# Get all series from the temporary list
all_series = temp_list.get_all()
try:
for folder_name in os.listdir(self.directory_to_search):
folder_path = os.path.join(
self.directory_to_search, folder_name
)
if not os.path.isdir(folder_path):
continue
data_file = os.path.join(folder_path, "data")
if not os.path.isfile(data_file):
continue
series_data = _load_data_file(data_file)
if series_data is None:
continue
key = series_data.get("key")
if not key:
logger.warning(
"Data file missing key, skipping: %s",
data_file
)
continue
anime = AnimeSeries(
key=key,
name=series_data.get("name") or folder_name,
site=series_data.get("site", "https://aniworld.to"),
folder=series_data.get("folder", folder_name),
year=series_data.get("year"),
)
episode_dict = series_data.get("episodeDict", {})
if episode_dict:
anime._episode_dict_cache = {
int(season): episodes
for season, episodes in episode_dict.items()
}
all_series.append(anime)
except (OSError, ValueError) as e:
logger.error(
"Failed to scan directory for data files: %s",
str(e),
exc_info=True
)
return []
logger.info(
"Found %d series from data files in %s",
@@ -731,3 +780,38 @@ class SeriesApp:
if hasattr(self, 'executor'):
self.executor.shutdown(wait=True)
logger.info("ThreadPoolExecutor shut down successfully")
def _load_data_file(data_file_path: str) -> Optional[dict]:
"""Load and parse a legacy 'data' file (JSON).
Args:
data_file_path: Path to the data file
Returns:
Parsed data dict or None if parsing fails
"""
import json
try:
with open(data_file_path, "r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict):
logger.warning("Data file is not a dictionary: %s", data_file_path)
return None
return data
except json.JSONDecodeError as e:
logger.warning(
"Failed to parse legacy data file (JSON error): %s - %s",
data_file_path, str(e)
)
return None
except Exception as e:
logger.warning(
"Failed to read legacy data file: %s - %s",
data_file_path, str(e)
)
return None

View File

@@ -10,6 +10,9 @@ Tests the functionality of SeriesApp including:
- Error handling
"""
import json
import os
import tempfile
from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest
@@ -489,53 +492,45 @@ class TestSeriesAppGetAllSeriesFromDataFiles:
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
def test_returns_list_of_series(
self, mock_serie_list_class, mock_scanner, mock_loaders
self, mock_scanner, mock_loaders
):
"""Test that get_all_series_from_data_files returns a list of AnimeSeries."""
from src.server.database.models import AnimeSeries
with tempfile.TemporaryDirectory() as tmp_dir:
# Create test data files
anime_dir1 = os.path.join(tmp_dir, "Anime 1")
os.makedirs(anime_dir1)
data1 = {
"key": "anime1",
"name": "Anime 1",
"site": "https://aniworld.to",
"folder": "Anime 1 (2020)",
"episodeDict": {"1": [1, 2, 3]}
}
with open(os.path.join(anime_dir1, "data"), "w") as f:
json.dump(data1, f)
test_dir = "/test/anime"
anime_dir2 = os.path.join(tmp_dir, "Anime 2")
os.makedirs(anime_dir2)
data2 = {
"key": "anime2",
"name": "Anime 2",
"site": "https://aniworld.to",
"folder": "Anime 2 (2021)",
"episodeDict": {"1": [1, 2]}
}
with open(os.path.join(anime_dir2, "data"), "w") as f:
json.dump(data2, f)
def make_anime(key, name, folder):
anime = MagicMock(spec=AnimeSeries)
anime.key = key
anime.name = name
anime.site = "https://aniworld.to"
anime.folder = folder
anime.episodeDict = {1: [1, 2, 3]} if key == "anime1" else {1: [1, 2]}
return anime
with patch('src.server.SeriesApp.SerieList'):
app = SeriesApp(tmp_dir)
result = app.get_all_series_from_data_files()
# Mock series to return
mock_series = [
make_anime("anime1", "Anime 1", "Anime 1 (2020)"),
make_anime("anime2", "Anime 2", "Anime 2 (2021)"),
]
# Setup mock for the main SerieList instance (constructor call)
mock_main_list = Mock()
mock_main_list.GetMissingEpisode.return_value = []
# Setup mock for temporary SerieList in get_all_series_from_data_files
mock_temp_list = Mock()
mock_temp_list.get_all.return_value = mock_series
# Return different mocks for the two calls
mock_serie_list_class.side_effect = [mock_main_list, mock_temp_list]
# Create app
app = SeriesApp(test_dir)
# Call the method
result = app.get_all_series_from_data_files()
# Verify result is a list of AnimeSeries
assert isinstance(result, list)
assert len(result) == 2
assert all(isinstance(s, MagicMock) for s in result)
assert result[0].key == "anime1"
assert result[1].key == "anime2"
assert isinstance(result, list)
assert len(result) == 2
keys = {s.key for s in result}
assert "anime1" in keys
assert "anime2" in keys
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@@ -544,27 +539,14 @@ class TestSeriesAppGetAllSeriesFromDataFiles:
self, mock_serie_list_class, mock_scanner, mock_loaders
):
"""Test that empty list is returned when no data files exist."""
test_dir = "/test/anime"
with tempfile.TemporaryDirectory() as tmp_dir:
# No data files created - directory is empty
with patch('src.server.SeriesApp.SerieList'):
app = SeriesApp(tmp_dir)
result = app.get_all_series_from_data_files()
# Setup mock for the main SerieList instance
mock_main_list = Mock()
mock_main_list.GetMissingEpisode.return_value = []
# Setup mock for the temporary SerieList (empty directory)
mock_temp_list = Mock()
mock_temp_list.get_all.return_value = []
mock_serie_list_class.side_effect = [mock_main_list, mock_temp_list]
# Create app
app = SeriesApp(test_dir)
# Call the method
result = app.get_all_series_from_data_files()
# Verify empty list is returned
assert isinstance(result, list)
assert len(result) == 0
assert isinstance(result, list)
assert len(result) == 0
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@@ -573,61 +555,53 @@ class TestSeriesAppGetAllSeriesFromDataFiles:
self, mock_serie_list_class, mock_scanner, mock_loaders
):
"""Test exceptions are handled gracefully and empty list returned."""
test_dir = "/test/anime"
with tempfile.TemporaryDirectory() as tmp_dir:
# Create a data file that will cause an error
anime_dir = os.path.join(tmp_dir, "Anime")
os.makedirs(anime_dir)
with open(os.path.join(anime_dir, "data"), "w") as f:
f.write("invalid json {{{")
# Setup mock for the main SerieList instance
mock_main_list = Mock()
mock_main_list.GetMissingEpisode.return_value = []
with patch('src.server.SeriesApp.SerieList'):
app = SeriesApp(tmp_dir)
result = app.get_all_series_from_data_files()
# Make the second SerieList constructor raise an exception
mock_serie_list_class.side_effect = [
mock_main_list,
OSError("Directory not found")
]
# Create app
app = SeriesApp(test_dir)
# Call the method - should not raise
result = app.get_all_series_from_data_files()
# Verify empty list is returned on error
assert isinstance(result, list)
assert len(result) == 0
# Should return empty list due to corrupt file
assert isinstance(result, list)
assert len(result) == 0
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
def test_uses_file_based_loading(
self, mock_serie_list_class, mock_scanner, mock_loaders
self, mock_scanner, mock_loaders
):
"""Test that method uses SerieList for file-based loading."""
test_dir = "/test/anime"
"""Test that method reads directly from data files, not SerieList."""
import tempfile
import os
import json
# Setup mock for the main SerieList instance
mock_main_list = Mock()
mock_main_list.GetMissingEpisode.return_value = []
with tempfile.TemporaryDirectory() as tmp_dir:
# Create test data file
anime_dir = os.path.join(tmp_dir, "Anime")
os.makedirs(anime_dir)
data = {
"key": "anime1",
"name": "Anime 1",
"site": "https://aniworld.to",
"folder": "Anime",
"episodeDict": {"1": [1]}
}
with open(os.path.join(anime_dir, "data"), "w") as f:
json.dump(data, f)
# Setup mock for the temporary SerieList
mock_temp_list = Mock()
mock_temp_list.get_all.return_value = []
with patch('src.server.SeriesApp.SerieList') as mock_serie_list_class:
app = SeriesApp(tmp_dir)
result = app.get_all_series_from_data_files()
mock_serie_list_class.side_effect = [mock_main_list, mock_temp_list]
# Create app
app = SeriesApp(test_dir)
# Call the method
app.get_all_series_from_data_files()
# Verify SerieList was called twice (main + temp)
calls = mock_serie_list_class.call_args_list
assert len(calls) == 2
# Check the second call is for temp SerieList with directory
second_call = calls[1]
# base_path is passed as positional argument
assert second_call.args[0] == test_dir
# SerieList should NOT be instantiated by this method
# The new implementation uses direct file reading
assert len(result) == 1
assert result[0].key == "anime1"
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')