refactor: restructure core→server, split large entity files into database module

- Move src/core/ → src/server/
- Split SerieList.py (531 lines) and series.py (414 lines) into src/server/database/
- Add database/models.py for SQLAlchemy models
- Update all test imports to reflect new structure
- Remove deprecated test files (test_serie_class.py, test_serie_folder_with_year.py)
This commit is contained in:
2026-06-04 21:11:53 +02:00
parent 09d454d4c0
commit 5526ab884a
76 changed files with 1186 additions and 3574 deletions

View File

@@ -6,13 +6,33 @@ special characters, Unicode names, and malformed folder structures.
import os
import tempfile
from pathlib import Path
from unittest.mock import Mock
from unittest.mock import MagicMock, Mock
import pytest
from src.core.entities.series import Serie
from src.core.providers.base_provider import Loader
from src.core.SerieScanner import SerieScanner
from src.server.database.models import AnimeSeries
from src.server.providers.base_provider import Loader
from src.server.SerieScanner import SerieScanner
from src.server.utils.filesystem import sanitize_folder_name
def make_anime(key, name, folder=None, episode_dict=None, year=None, site="aniworld.to"):
"""Create a mock AnimeSeries with needed properties."""
anime = MagicMock(spec=AnimeSeries)
anime.key = key
anime.name = name
anime.folder = folder or name
anime.site = site
anime.year = year
anime.episodeDict = episode_dict or {}
# Compute name_with_year
if year:
anime.name_with_year = f"{name} ({year})"
else:
anime.name_with_year = name
# Compute sanitized_folder
anime.sanitized_folder = sanitize_folder_name(anime.name_with_year)
return anime
@pytest.fixture
@@ -133,112 +153,112 @@ class TestSpecialCharacters:
def test_colon_in_name(self, temp_anime_dir, mock_loader):
"""Test series name with colon."""
serie = Serie(
anime = make_anime(
key="re-zero",
name="Re:Zero - Starting Life in Another World",
site="aniworld.to",
folder="Re Zero",
episodeDict={}
episode_dict={}
)
# Sanitized folder should remove colon
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
assert ":" not in sanitized
assert "Re" in sanitized
assert "Zero" in sanitized
def test_slash_in_name(self, temp_anime_dir, mock_loader):
"""Test series name with slash."""
serie = Serie(
anime = make_anime(
key="fate-stay-night",
name="Fate/Stay Night: Unlimited Blade Works",
site="aniworld.to",
folder="Fate Stay Night",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
assert "/" not in sanitized
assert "\\" not in sanitized
def test_question_mark_in_name(self, temp_anime_dir, mock_loader):
"""Test series name with question mark."""
serie = Serie(
anime = make_anime(
key="is-it-wrong",
name="Is It Wrong to Try to Pick Up Girls in a Dungeon?",
site="aniworld.to",
folder="Is It Wrong",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
assert "?" not in sanitized
def test_asterisk_in_name(self, temp_anime_dir, mock_loader):
"""Test series name with asterisk."""
serie = Serie(
anime = make_anime(
key="series",
name="Series * Special",
site="aniworld.to",
folder="Series Special",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
assert "*" not in sanitized
def test_pipe_in_name(self, temp_anime_dir, mock_loader):
"""Test series name with pipe character."""
serie = Serie(
anime = make_anime(
key="series",
name="Series | Part 2",
site="aniworld.to",
folder="Series Part 2",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
assert "|" not in sanitized
def test_quotes_in_name(self, temp_anime_dir, mock_loader):
"""Test series name with quotes."""
serie = Serie(
anime = make_anime(
key="series",
name='Series "Subtitle" Edition',
site="aniworld.to",
folder="Series Subtitle Edition",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
# Quotes should be removed or replaced
assert '"' not in sanitized or sanitized.count('"') == 0
def test_less_greater_than_in_name(self, temp_anime_dir, mock_loader):
"""Test series name with < and >."""
serie = Serie(
anime = make_anime(
key="series",
name="Series <Special> Edition",
site="aniworld.to",
folder="Series Special Edition",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
assert "<" not in sanitized
assert ">" not in sanitized
def test_multiple_special_chars(self, temp_anime_dir, mock_loader):
"""Test series name with multiple special characters."""
serie = Serie(
anime = make_anime(
key="complex",
name="Re:Zero / Fate * Special? <Edition>",
site="aniworld.to",
folder="Re Zero Fate Special Edition",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
# Should remove all special chars
invalid_chars = [':', '/', '*', '?', '<', '>']
for char in invalid_chars:
@@ -250,45 +270,45 @@ class TestMultipleSpaces:
def test_double_spaces(self, temp_anime_dir, mock_loader):
"""Test series name with double spaces."""
serie = Serie(
anime = make_anime(
key="series",
name="Attack on Titan",
site="aniworld.to",
folder="Attack on Titan",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
# Multiple spaces should be preserved or normalized to single space
assert "Attack" in sanitized
assert "Titan" in sanitized
def test_leading_trailing_spaces(self, temp_anime_dir, mock_loader):
"""Test series name with leading/trailing spaces."""
serie = Serie(
anime = make_anime(
key="series",
name=" Attack on Titan ",
site="aniworld.to",
folder="Attack on Titan",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
# Leading/trailing spaces should be stripped
assert not sanitized.startswith(" ")
assert not sanitized.endswith(" ")
def test_tabs_in_name(self, temp_anime_dir, mock_loader):
"""Test series name with tab characters."""
serie = Serie(
anime = make_anime(
key="series",
name="Attack\ton\tTitan",
site="aniworld.to",
folder="Attack on Titan",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
# Tabs should be handled (removed or replaced)
assert "\t" not in sanitized or sanitized.replace("\t", " ")
@@ -298,95 +318,95 @@ class TestUnicodeNames:
def test_japanese_name(self, temp_anime_dir, mock_loader):
"""Test series name in Japanese."""
serie = Serie(
anime = make_anime(
key="shingeki",
name="進撃の巨人",
site="aniworld.to",
folder="進撃の巨人",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
# Unicode should be preserved
assert "進撃の巨人" in sanitized
def test_chinese_name(self, temp_anime_dir, mock_loader):
"""Test series name in Chinese."""
serie = Serie(
anime = make_anime(
key="series",
name="进击的巨人",
site="aniworld.to",
folder="进击的巨人",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
assert "进击的巨人" in sanitized
def test_korean_name(self, temp_anime_dir, mock_loader):
"""Test series name in Korean."""
serie = Serie(
anime = make_anime(
key="series",
name="진격의 거인",
site="aniworld.to",
folder="진격의 거인",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
assert "진격의" in sanitized
def test_arabic_name(self, temp_anime_dir, mock_loader):
"""Test series name in Arabic."""
serie = Serie(
anime = make_anime(
key="series",
name="هجوم العمالقة",
site="aniworld.to",
folder="هجوم العمالقة",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
assert "هجوم" in sanitized
def test_cyrillic_name(self, temp_anime_dir, mock_loader):
"""Test series name in Cyrillic."""
serie = Serie(
anime = make_anime(
key="series",
name="Атака Титанов",
site="aniworld.to",
folder="Атака Титанов",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
assert "Атака" in sanitized
def test_mixed_languages(self, temp_anime_dir, mock_loader):
"""Test series name with mixed languages."""
serie = Serie(
anime = make_anime(
key="series",
name="Attack on Titan - 進撃の巨人",
site="aniworld.to",
folder="Attack on Titan",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
assert "Attack" in sanitized
assert "進撃の巨人" in sanitized
def test_emoji_in_name(self, temp_anime_dir, mock_loader):
"""Test series name with emoji."""
serie = Serie(
anime = make_anime(
key="series",
name="Series ⚔️ Special",
site="aniworld.to",
folder="Series Special",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
# Emoji should be handled gracefully
assert "Series" in sanitized
@@ -418,16 +438,16 @@ class TestMalformedFolderStructures:
def test_very_long_folder_name(self, temp_anime_dir, mock_loader):
"""Test handling of very long folder names."""
long_name = "A" * 300 # Very long name
serie = Serie(
anime = make_anime(
key="long",
name=long_name,
site="aniworld.to",
folder=long_name,
episodeDict={}
episode_dict={}
)
# Should handle long names without error
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
assert len(sanitized) > 0
def test_folder_name_with_dots(self, temp_anime_dir, mock_loader):
@@ -439,127 +459,80 @@ class TestMalformedFolderStructures:
def test_folder_name_with_underscores(self, temp_anime_dir, mock_loader):
"""Test folder name with underscores."""
serie = Serie(
anime = make_anime(
key="series",
name="Attack_on_Titan",
site="aniworld.to",
folder="Attack_on_Titan",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
# Underscores are valid filesystem chars
assert "Attack" in sanitized
class TestNameWithYearProperty:
"""Test Serie.name_with_year property."""
"""Test AnimeSeries.name_with_year property."""
def test_name_with_year_adds_year(self):
"""Test that name_with_year adds year in parentheses."""
serie = Serie(
anime = make_anime(
key="dororo",
name="Dororo",
site="aniworld.to",
folder="Dororo",
episodeDict={},
episode_dict={},
year=2025
)
assert serie.name_with_year == "Dororo (2025)"
assert anime.name_with_year == "Dororo (2025)"
def test_name_with_year_no_year(self):
"""Test name_with_year without year returns just name."""
serie = Serie(
anime = make_anime(
key="dororo",
name="Dororo",
site="aniworld.to",
folder="Dororo",
episodeDict={}
episode_dict={}
)
assert serie.name_with_year == "Dororo"
assert anime.name_with_year == "Dororo"
def test_name_with_year_used_in_sanitized_folder(self):
"""Test that sanitized_folder uses name_with_year."""
serie = Serie(
anime = make_anime(
key="attack",
name="Attack on Titan",
site="aniworld.to",
folder="Attack on Titan",
episodeDict={},
episode_dict={},
year=2013
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
assert "(2013)" in sanitized
assert "Attack on Titan" in sanitized
def test_name_with_year_does_not_duplicate(self):
"""Test that name_with_year doesn't duplicate year."""
serie = Serie(
key="eighty-six",
name="86 Eighty Six (2021)",
site="aniworld.to",
folder="86 Eighty Six (2021)",
episodeDict={},
year=2021
)
assert serie.name_with_year == "86 Eighty Six (2021)"
assert serie.name_with_year.count("(2021)") == 1
class TestSanitizedFolder:
"""Test AnimeSeries.sanitized_folder property."""
class TestEnsureFolderWithYear:
"""Test Serie.ensure_folder_with_year method."""
def test_ensure_folder_adds_year_when_missing(self):
"""Test that ensure_folder_with_year adds year to folder."""
serie = Serie(
def test_sanitized_folder_uses_name_with_year(self):
"""Test that sanitized_folder uses name_with_year."""
anime = make_anime(
key="attack",
name="Attack on Titan",
site="aniworld.to",
folder="Attack on Titan",
episodeDict={},
episode_dict={},
year=2013
)
result = serie.ensure_folder_with_year()
assert "(2013)" in result
assert serie.folder == result
def test_ensure_folder_doesnt_duplicate_year(self):
"""Test that year isn't added if already present."""
serie = Serie(
key="attack",
name="Attack on Titan",
site="aniworld.to",
folder="Attack on Titan (2013)",
episodeDict={},
year=2013
)
original_folder = serie.folder
result = serie.ensure_folder_with_year()
# Should not change
assert result.count("(2013)") == 1
def test_ensure_folder_no_year_unchanged(self):
"""Test that folder unchanged when no year available."""
serie = Serie(
key="attack",
name="Attack on Titan",
site="aniworld.to",
folder="Attack on Titan",
episodeDict={}
)
original_folder = serie.folder
result = serie.ensure_folder_with_year()
assert result == original_folder
sanitized = anime.sanitized_folder
assert "(2013)" in sanitized
assert "Attack on Titan" in sanitized
class TestRealWorldScenarios:
@@ -576,15 +549,15 @@ class TestRealWorldScenarios:
]
for key, name, expected_part in test_cases:
serie = Serie(
anime = make_anime(
key=key,
name=name,
site="aniworld.to",
folder="old-folder",
episodeDict={}
episode_dict={}
)
sanitized = serie.sanitized_folder
sanitized = anime.sanitized_folder
# Check that expected part is in sanitized name
assert any(word in sanitized for word in expected_part.split())
# Check invalid chars removed (< > : " / \ | ? *)