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 self.directory_to_search
) )
# Create a fresh SerieList instance for file-based loading all_series: List[AnimeSeries] = []
# This ensures we get all series from data files without
# interfering with the main instance's state
try: 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: except (OSError, ValueError) as e:
logger.error( logger.error(
"Failed to scan directory for data files: %s", "Failed to scan directory for data files: %s",
@@ -710,8 +714,53 @@ class SeriesApp:
) )
return [] return []
# Get all series from the temporary list try:
all_series = temp_list.get_all() 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( logger.info(
"Found %d series from data files in %s", "Found %d series from data files in %s",
@@ -731,3 +780,38 @@ class SeriesApp:
if hasattr(self, 'executor'): if hasattr(self, 'executor'):
self.executor.shutdown(wait=True) self.executor.shutdown(wait=True)
logger.info("ThreadPoolExecutor shut down successfully") 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 - Error handling
""" """
import json
import os
import tempfile
from unittest.mock import AsyncMock, MagicMock, Mock, patch from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest import pytest
@@ -489,53 +492,45 @@ class TestSeriesAppGetAllSeriesFromDataFiles:
@patch('src.server.SeriesApp.Loaders') @patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner') @patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
def test_returns_list_of_series( 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.""" """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): with patch('src.server.SeriesApp.SerieList'):
anime = MagicMock(spec=AnimeSeries) app = SeriesApp(tmp_dir)
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
# 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() result = app.get_all_series_from_data_files()
# Verify result is a list of AnimeSeries
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) == 2 assert len(result) == 2
assert all(isinstance(s, MagicMock) for s in result) keys = {s.key for s in result}
assert result[0].key == "anime1" assert "anime1" in keys
assert result[1].key == "anime2" assert "anime2" in keys
@patch('src.server.SeriesApp.Loaders') @patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner') @patch('src.server.SeriesApp.SerieScanner')
@@ -544,25 +539,12 @@ class TestSeriesAppGetAllSeriesFromDataFiles:
self, mock_serie_list_class, mock_scanner, mock_loaders self, mock_serie_list_class, mock_scanner, mock_loaders
): ):
"""Test that empty list is returned when no data files exist.""" """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
# Setup mock for the main SerieList instance with patch('src.server.SeriesApp.SerieList'):
mock_main_list = Mock() app = SeriesApp(tmp_dir)
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() result = app.get_all_series_from_data_files()
# Verify empty list is returned
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) == 0 assert len(result) == 0
@@ -573,61 +555,53 @@ class TestSeriesAppGetAllSeriesFromDataFiles:
self, mock_serie_list_class, mock_scanner, mock_loaders self, mock_serie_list_class, mock_scanner, mock_loaders
): ):
"""Test exceptions are handled gracefully and empty list returned.""" """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 with patch('src.server.SeriesApp.SerieList'):
mock_main_list = Mock() app = SeriesApp(tmp_dir)
mock_main_list.GetMissingEpisode.return_value = []
# 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() result = app.get_all_series_from_data_files()
# Verify empty list is returned on error # Should return empty list due to corrupt file
assert isinstance(result, list) assert isinstance(result, list)
assert len(result) == 0 assert len(result) == 0
@patch('src.server.SeriesApp.Loaders') @patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner') @patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
def test_uses_file_based_loading( 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 that method reads directly from data files, not SerieList."""
test_dir = "/test/anime" import tempfile
import os
import json
# Setup mock for the main SerieList instance with tempfile.TemporaryDirectory() as tmp_dir:
mock_main_list = Mock() # Create test data file
mock_main_list.GetMissingEpisode.return_value = [] 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 with patch('src.server.SeriesApp.SerieList') as mock_serie_list_class:
mock_temp_list = Mock() app = SeriesApp(tmp_dir)
mock_temp_list.get_all.return_value = [] result = app.get_all_series_from_data_files()
mock_serie_list_class.side_effect = [mock_main_list, mock_temp_list] # SerieList should NOT be instantiated by this method
# The new implementation uses direct file reading
# Create app assert len(result) == 1
app = SeriesApp(test_dir) assert result[0].key == "anime1"
# 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
@patch('src.server.SeriesApp.Loaders') @patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner') @patch('src.server.SeriesApp.SerieScanner')