Add NFO file support to Serie and SerieList entities

- Add nfo_path property to Serie class
- Add has_nfo(), has_poster(), has_logo(), has_fanart() methods
- Update to_dict()/from_dict() to include nfo metadata
- Modify SerieList.load_series() to detect NFO and media files
- Add logging for missing NFO and media files with statistics
- Comprehensive unit tests with 100% coverage
- All 67 tests passing
This commit is contained in:
2026-01-11 20:12:23 +01:00
parent 9a1c9b39ee
commit 65b116c39f
5 changed files with 745 additions and 57 deletions

View File

@@ -413,3 +413,337 @@ class TestSerieSanitizedFolder:
# Note: semicolon is valid on Linux but we test common invalid chars
assert ":" not in result
assert "/" not in result
class TestSerieNFOFeatures:
"""Test Serie class NFO-related features."""
def test_serie_creation_with_nfo_path(self):
"""Test creating Serie with NFO path."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1]},
nfo_path="/path/to/tvshow.nfo"
)
assert serie.nfo_path == "/path/to/tvshow.nfo"
def test_serie_creation_without_nfo_path(self):
"""Test creating Serie without NFO path defaults to None."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1]}
)
assert serie.nfo_path is None
def test_serie_nfo_path_setter(self):
"""Test setting NFO path property."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1]}
)
serie.nfo_path = "/new/path/tvshow.nfo"
assert serie.nfo_path == "/new/path/tvshow.nfo"
def test_has_nfo_with_existing_file(self, tmp_path):
"""Test has_nfo returns True when NFO file exists."""
# Create a test directory structure
base_dir = tmp_path / "anime"
series_dir = base_dir / "Test Series"
series_dir.mkdir(parents=True)
nfo_file = series_dir / "tvshow.nfo"
nfo_file.write_text("test nfo content")
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_nfo(str(base_dir)) is True
def test_has_nfo_with_missing_file(self, tmp_path):
"""Test has_nfo returns False when NFO file doesn't exist."""
base_dir = tmp_path / "anime"
base_dir.mkdir(parents=True)
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_nfo(str(base_dir)) is False
def test_has_nfo_with_nfo_path_set(self, tmp_path):
"""Test has_nfo using nfo_path when base_directory not provided."""
nfo_file = tmp_path / "tvshow.nfo"
nfo_file.write_text("test nfo content")
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]},
nfo_path=str(nfo_file)
)
assert serie.has_nfo() is True
def test_has_nfo_without_base_directory_or_path(self):
"""Test has_nfo returns False when no base_directory or nfo_path."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_nfo() is False
def test_has_poster_with_existing_file(self, tmp_path):
"""Test has_poster returns True when poster.jpg exists."""
base_dir = tmp_path / "anime"
series_dir = base_dir / "Test Series"
series_dir.mkdir(parents=True)
poster_file = series_dir / "poster.jpg"
poster_file.write_text("test image data")
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_poster(str(base_dir)) is True
def test_has_poster_with_missing_file(self, tmp_path):
"""Test has_poster returns False when poster.jpg doesn't exist."""
base_dir = tmp_path / "anime"
base_dir.mkdir(parents=True)
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_poster(str(base_dir)) is False
def test_has_poster_without_base_directory(self):
"""Test has_poster returns False when no base_directory provided."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_poster() is False
def test_has_logo_with_existing_file(self, tmp_path):
"""Test has_logo returns True when logo.png exists."""
base_dir = tmp_path / "anime"
series_dir = base_dir / "Test Series"
series_dir.mkdir(parents=True)
logo_file = series_dir / "logo.png"
logo_file.write_text("test logo data")
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_logo(str(base_dir)) is True
def test_has_logo_with_missing_file(self, tmp_path):
"""Test has_logo returns False when logo.png doesn't exist."""
base_dir = tmp_path / "anime"
base_dir.mkdir(parents=True)
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_logo(str(base_dir)) is False
def test_has_logo_without_base_directory(self):
"""Test has_logo returns False when no base_directory provided."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_logo() is False
def test_has_fanart_with_existing_file(self, tmp_path):
"""Test has_fanart returns True when fanart.jpg exists."""
base_dir = tmp_path / "anime"
series_dir = base_dir / "Test Series"
series_dir.mkdir(parents=True)
fanart_file = series_dir / "fanart.jpg"
fanart_file.write_text("test fanart data")
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_fanart(str(base_dir)) is True
def test_has_fanart_with_missing_file(self, tmp_path):
"""Test has_fanart returns False when fanart.jpg doesn't exist."""
base_dir = tmp_path / "anime"
base_dir.mkdir(parents=True)
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_fanart(str(base_dir)) is False
def test_has_fanart_without_base_directory(self):
"""Test has_fanart returns False when no base_directory provided."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_fanart() is False
def test_to_dict_includes_nfo_path(self):
"""Test that to_dict includes nfo_path field."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1, 2], 2: [1]},
year=2024,
nfo_path="/path/to/tvshow.nfo"
)
result = serie.to_dict()
assert result["nfo_path"] == "/path/to/tvshow.nfo"
assert result["key"] == "test-series"
assert result["name"] == "Test Series"
assert result["year"] == 2024
def test_to_dict_with_none_nfo_path(self):
"""Test that to_dict handles None nfo_path."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1]}
)
result = serie.to_dict()
assert result["nfo_path"] is None
def test_from_dict_with_nfo_path(self):
"""Test that from_dict correctly loads nfo_path."""
data = {
"key": "test-series",
"name": "Test Series",
"site": "https://example.com",
"folder": "Test Folder",
"episodeDict": {"1": [1, 2]},
"year": 2024,
"nfo_path": "/path/to/tvshow.nfo"
}
serie = Serie.from_dict(data)
assert serie.nfo_path == "/path/to/tvshow.nfo"
assert serie.key == "test-series"
assert serie.year == 2024
def test_from_dict_without_nfo_path(self):
"""Test that from_dict handles missing nfo_path (backward compatibility)."""
data = {
"key": "test-series",
"name": "Test Series",
"site": "https://example.com",
"folder": "Test Folder",
"episodeDict": {"1": [1, 2]}
}
serie = Serie.from_dict(data)
assert serie.nfo_path is None
assert serie.key == "test-series"
def test_save_and_load_file_with_nfo_path(self, tmp_path):
"""Test that save_to_file and load_from_file preserve nfo_path."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1, 2], 2: [1]},
year=2024,
nfo_path="/path/to/tvshow.nfo"
)
file_path = tmp_path / "data"
with pytest.warns(DeprecationWarning):
serie.save_to_file(str(file_path))
with pytest.warns(DeprecationWarning):
loaded_serie = Serie.load_from_file(str(file_path))
assert loaded_serie.nfo_path == "/path/to/tvshow.nfo"
assert loaded_serie.key == "test-series"
assert loaded_serie.year == 2024

View File

@@ -294,3 +294,228 @@ class TestSerieListBackwardCompatibility:
assert serie_list.contains(sample_serie.key)
loaded = serie_list.get_by_key(sample_serie.key)
assert loaded.name == sample_serie.name
class TestSerieListNFOFeatures:
"""Test SerieList NFO detection and logging."""
def test_load_series_detects_nfo_file(self, temp_directory, caplog):
"""Test load_series detects and sets nfo_path for series with NFO."""
import logging
caplog.set_level(logging.INFO)
# Create series folder with data file and NFO
folder_name = "Test Series"
folder_path = os.path.join(temp_directory, folder_name)
os.makedirs(folder_path)
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder=folder_name,
episodeDict={1: [1, 2]}
)
data_path = os.path.join(folder_path, "data")
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie.save_to_file(data_path)
# Create NFO file
nfo_path = os.path.join(folder_path, "tvshow.nfo")
with open(nfo_path, "w") as f:
f.write("<tvshow></tvshow>")
# Load series
serie_list = SerieList(temp_directory)
# Verify NFO was detected
loaded = serie_list.get_by_key("test-series")
assert loaded is not None
assert loaded.nfo_path == nfo_path
# Verify logging
assert "1 with NFO" in caplog.text
def test_load_series_detects_missing_nfo(self, temp_directory, caplog):
"""Test load_series logs when NFO is missing."""
import logging
caplog.set_level(logging.DEBUG)
# Create series folder with data file but NO NFO
folder_name = "Test Series"
folder_path = os.path.join(temp_directory, folder_name)
os.makedirs(folder_path)
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder=folder_name,
episodeDict={1: [1, 2]}
)
data_path = os.path.join(folder_path, "data")
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie.save_to_file(data_path)
# Load series
serie_list = SerieList(temp_directory)
# Verify NFO not set
loaded = serie_list.get_by_key("test-series")
assert loaded is not None
assert loaded.nfo_path is None
# Verify logging
assert "missing tvshow.nfo" in caplog.text
def test_load_series_detects_media_files(self, temp_directory, caplog):
"""Test load_series detects poster, logo, and fanart files."""
import logging
caplog.set_level(logging.INFO)
# Create series folder with all media files
folder_name = "Test Series"
folder_path = os.path.join(temp_directory, folder_name)
os.makedirs(folder_path)
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder=folder_name,
episodeDict={1: [1, 2]}
)
data_path = os.path.join(folder_path, "data")
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie.save_to_file(data_path)
# Create media files
with open(os.path.join(folder_path, "poster.jpg"), "w") as f:
f.write("poster data")
with open(os.path.join(folder_path, "logo.png"), "w") as f:
f.write("logo data")
with open(os.path.join(folder_path, "fanart.jpg"), "w") as f:
f.write("fanart data")
# Load series
serie_list = SerieList(temp_directory)
# Verify logging shows all media found
assert "Poster (1/1)" in caplog.text
assert "Logo (1/1)" in caplog.text
assert "Fanart (1/1)" in caplog.text
def test_load_series_detects_missing_media_files(
self, temp_directory, caplog
):
"""Test load_series logs when media files are missing."""
import logging
caplog.set_level(logging.DEBUG)
# Create series folder with NO media files
folder_name = "Test Series"
folder_path = os.path.join(temp_directory, folder_name)
os.makedirs(folder_path)
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder=folder_name,
episodeDict={1: [1, 2]}
)
data_path = os.path.join(folder_path, "data")
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie.save_to_file(data_path)
# Load series
serie_list = SerieList(temp_directory)
# Verify logging shows missing media
assert "missing poster.jpg" in caplog.text
assert "missing logo.png" in caplog.text
assert "missing fanart.jpg" in caplog.text
def test_load_series_summary_statistics(self, temp_directory, caplog):
"""Test load_series logs summary statistics for NFO and media."""
import logging
caplog.set_level(logging.INFO)
# Create multiple series with varying NFO/media status
for i in range(3):
folder_name = f"Series {i}"
folder_path = os.path.join(temp_directory, folder_name)
os.makedirs(folder_path)
serie = Serie(
key=f"series-{i}",
name=f"Series {i}",
site="https://example.com",
folder=folder_name,
episodeDict={1: [1]}
)
data_path = os.path.join(folder_path, "data")
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie.save_to_file(data_path)
# First series has everything
if i == 0:
with open(os.path.join(folder_path, "tvshow.nfo"), "w") as f:
f.write("<tvshow></tvshow>")
with open(os.path.join(folder_path, "poster.jpg"), "w") as f:
f.write("poster")
with open(os.path.join(folder_path, "logo.png"), "w") as f:
f.write("logo")
with open(os.path.join(folder_path, "fanart.jpg"), "w") as f:
f.write("fanart")
# Second series has NFO and poster only
elif i == 1:
with open(os.path.join(folder_path, "tvshow.nfo"), "w") as f:
f.write("<tvshow></tvshow>")
with open(os.path.join(folder_path, "poster.jpg"), "w") as f:
f.write("poster")
# Third series has nothing
# Load series
serie_list = SerieList(temp_directory)
# Verify summary statistics
assert "3 series total" in caplog.text
assert "2 with NFO, 1 without NFO" in caplog.text
assert "Poster (2/3)" in caplog.text
assert "Logo (1/3)" in caplog.text
assert "Fanart (1/3)" in caplog.text
def test_load_series_handles_load_failure(self, temp_directory, caplog):
"""Test load_series handles series that fail to load gracefully."""
import logging
caplog.set_level(logging.ERROR)
# Create folder with invalid data file
folder_name = "Invalid Series"
folder_path = os.path.join(temp_directory, folder_name)
os.makedirs(folder_path)
data_path = os.path.join(folder_path, "data")
with open(data_path, "w") as f:
f.write("invalid json {{{")
# Load series - should not crash
serie_list = SerieList(temp_directory)
# Verify error logged
assert "Failed to load metadata" in caplog.text
# Should not be in keyDict
assert len(serie_list.keyDict) == 0