diff --git a/docs/instructions.md b/docs/instructions.md index 5f3d3a0..6f0f25f 100644 --- a/docs/instructions.md +++ b/docs/instructions.md @@ -112,58 +112,6 @@ For each task completed: ### 🎬 NFO Metadata Integration -#### Task 1: Add NFO File Support to Core Entities - -**Priority:** High -**Estimated Time:** 2-3 hours - -Create support for tvshow.nfo metadata files in the core domain layer. - -**Implementation Steps:** - -1. **Add NFO Path Property to Serie Class** ([src/core/entities/series.py](../src/core/entities/series.py)) - - - Add `nfo_path` optional property to track tvshow.nfo file location - - Add `has_nfo` method to check if tvshow.nfo exists - - Add properties to check for media files: `has_poster()`, `has_logo()`, `has_fanart()` - - Update `to_dict()` and `from_dict()` to include nfo metadata - -2. **Update SerieList Class** ([src/core/entities/SerieList.py](../src/core/entities/SerieList.py)) - - Modify `load_series()` to check for tvshow.nfo files - - Check for associated media files (poster.jpg, logo.png, fanart.jpg) - - Add logging for series without tvshow.nfo or media files - - Track NFO and media file status during series loading - -**Acceptance Criteria:** - -- [ ] Serie class has nfo_path property -- [ ] Serie class can check if tvshow.nfo exists -- [ ] SerieList logs missing NFO files -- [ ] Existing functionality remains unchanged -- [ ] Unit tests pass -- [ ] Test coverage > 90% for new code - -**Testing Requirements:** - -- Test `nfo_path` property getter/setter -- Test `has_nfo()` method with existing and missing files -- Test `has_poster()`, `has_logo()`, `has_fanart()` methods -- Test `to_dict()` includes NFO metadata -- Test `from_dict()` correctly loads NFO metadata -- Test SerieList NFO detection during load -- Test SerieList media file detection during load -- Test logging output for missing NFO and media files -- Mock filesystem operations in tests - -**Files to Modify:** - -- [src/core/entities/series.py](../src/core/entities/series.py) -- [src/core/entities/SerieList.py](../src/core/entities/SerieList.py) -- Add tests to `tests/unit/test_serie_class.py` -- Add tests to `tests/unit/test_serie_list.py` - ---- - #### Task 2: Create NFO Models and Schemas **Priority:** High diff --git a/src/core/entities/SerieList.py b/src/core/entities/SerieList.py index ef232cc..d995716 100644 --- a/src/core/entities/SerieList.py +++ b/src/core/entities/SerieList.py @@ -132,25 +132,110 @@ class SerieList: ) return + nfo_stats = {"total": 0, "with_nfo": 0, "without_nfo": 0} + media_stats = { + "with_poster": 0, + "without_poster": 0, + "with_logo": 0, + "without_logo": 0, + "with_fanart": 0, + "without_fanart": 0 + } + for anime_folder in entries: anime_path = os.path.join(self.directory, anime_folder, "data") if os.path.isfile(anime_path): logging.debug("Found data file for folder %s", anime_folder) - self._load_data(anime_folder, anime_path) + serie = self._load_data(anime_folder, anime_path) + + if serie: + nfo_stats["total"] += 1 + # Check for NFO file + nfo_file_path = os.path.join( + self.directory, anime_folder, "tvshow.nfo" + ) + if os.path.isfile(nfo_file_path): + serie.nfo_path = nfo_file_path + nfo_stats["with_nfo"] += 1 + else: + nfo_stats["without_nfo"] += 1 + logging.debug( + "Series '%s' (key: %s) is missing tvshow.nfo", + serie.name, + serie.key + ) + + # Check for media files + folder_path = os.path.join(self.directory, anime_folder) + + poster_path = os.path.join(folder_path, "poster.jpg") + if os.path.isfile(poster_path): + media_stats["with_poster"] += 1 + else: + media_stats["without_poster"] += 1 + logging.debug( + "Series '%s' (key: %s) is missing poster.jpg", + serie.name, + serie.key + ) + + logo_path = os.path.join(folder_path, "logo.png") + if os.path.isfile(logo_path): + media_stats["with_logo"] += 1 + else: + media_stats["without_logo"] += 1 + logging.debug( + "Series '%s' (key: %s) is missing logo.png", + serie.name, + serie.key + ) + + fanart_path = os.path.join(folder_path, "fanart.jpg") + if os.path.isfile(fanart_path): + media_stats["with_fanart"] += 1 + else: + media_stats["without_fanart"] += 1 + logging.debug( + "Series '%s' (key: %s) is missing fanart.jpg", + serie.name, + serie.key + ) + continue logging.warning( "Skipping folder %s because no metadata file was found", anime_folder, ) + + # Log summary statistics + if nfo_stats["total"] > 0: + logging.info( + "NFO scan complete: %d series total, %d with NFO, %d without NFO", + nfo_stats["total"], + nfo_stats["with_nfo"], + nfo_stats["without_nfo"] + ) + logging.info( + "Media scan complete: Poster (%d/%d), Logo (%d/%d), Fanart (%d/%d)", + media_stats["with_poster"], + nfo_stats["total"], + media_stats["with_logo"], + nfo_stats["total"], + media_stats["with_fanart"], + nfo_stats["total"] + ) - def _load_data(self, anime_folder: str, data_path: str) -> None: + def _load_data(self, anime_folder: str, data_path: str) -> Optional[Serie]: """ Load a single series metadata file into the in-memory collection. Args: anime_folder: The folder name (for logging only) data_path: Path to the metadata file + + Returns: + Serie: The loaded Serie object, or None if loading failed """ try: serie = Serie.load_from_file(data_path) @@ -161,6 +246,7 @@ class SerieList: anime_folder, serie.key ) + return serie except (OSError, JSONDecodeError, KeyError, ValueError) as error: logging.error( "Failed to load metadata for folder %s from %s: %s", @@ -168,6 +254,7 @@ class SerieList: data_path, error, ) + return None def GetMissingEpisode(self) -> List[Serie]: """Return all series that still contain missing episodes.""" diff --git a/src/core/entities/series.py b/src/core/entities/series.py index c5ab7b4..068dc11 100644 --- a/src/core/entities/series.py +++ b/src/core/entities/series.py @@ -1,5 +1,8 @@ import json +import os import warnings +from pathlib import Path +from typing import Optional from src.server.utils.filesystem import sanitize_folder_name @@ -35,7 +38,8 @@ class Serie: site: str, folder: str, episodeDict: dict[int, list[int]], - year: int | None = None + year: int | None = None, + nfo_path: Optional[str] = None ): if not key or not key.strip(): raise ValueError("Serie key cannot be None or empty") @@ -46,6 +50,7 @@ class Serie: self._folder = folder self._episodeDict = episodeDict self._year = year + self._nfo_path = nfo_path def __str__(self): """String representation of Serie object""" @@ -148,6 +153,93 @@ class Serie: """Set the release year of the series.""" self._year = value + @property + def nfo_path(self) -> Optional[str]: + """ + Path to the tvshow.nfo metadata file. + + Returns: + str or None: Path to the NFO file, or None if not set + """ + return self._nfo_path + + @nfo_path.setter + def nfo_path(self, value: Optional[str]): + """Set the path to the NFO file.""" + self._nfo_path = value + + def has_nfo(self, base_directory: Optional[str] = None) -> bool: + """ + Check if tvshow.nfo file exists for this series. + + Args: + base_directory: Base anime directory path. If provided, checks + relative to base_directory/folder/tvshow.nfo. If not provided, + uses nfo_path directly. + + Returns: + bool: True if tvshow.nfo exists, False otherwise + """ + if base_directory: + nfo_file = Path(base_directory) / self.folder / "tvshow.nfo" + elif self._nfo_path: + nfo_file = Path(self._nfo_path) + else: + return False + + return nfo_file.exists() and nfo_file.is_file() + + def has_poster(self, base_directory: Optional[str] = None) -> bool: + """ + Check if poster.jpg file exists for this series. + + Args: + base_directory: Base anime directory path. If provided, checks + relative to base_directory/folder/poster.jpg. + + Returns: + bool: True if poster.jpg exists, False otherwise + """ + if not base_directory: + return False + + poster_file = Path(base_directory) / self.folder / "poster.jpg" + return poster_file.exists() and poster_file.is_file() + + def has_logo(self, base_directory: Optional[str] = None) -> bool: + """ + Check if logo.png file exists for this series. + + Args: + base_directory: Base anime directory path. If provided, checks + relative to base_directory/folder/logo.png. + + Returns: + bool: True if logo.png exists, False otherwise + """ + if not base_directory: + return False + + logo_file = Path(base_directory) / self.folder / "logo.png" + return logo_file.exists() and logo_file.is_file() + + def has_fanart(self, base_directory: Optional[str] = None) -> bool: + """ + Check if fanart.jpg file exists for this series. + + Args: + base_directory: Base anime directory path. If provided, checks + relative to base_directory/folder/fanart.jpg. + + Returns: + bool: True if fanart.jpg exists, False otherwise + """ + if not base_directory: + return False + + fanart_file = Path(base_directory) / self.folder / "fanart.jpg" + return fanart_file.exists() and fanart_file.is_file() + @property def name_with_year(self) -> str: """ @@ -208,7 +300,8 @@ class Serie: "episodeDict": { str(k): list(v) for k, v in self.episodeDict.items() }, - "year": self.year + "year": self.year, + "nfo_path": self.nfo_path } @staticmethod @@ -224,7 +317,8 @@ class Serie: data["site"], data["folder"], episode_dict, - data.get("year") # Optional year field for backward compatibility + data.get("year"), # Optional year field for backward compatibility + data.get("nfo_path") # Optional nfo_path field ) def save_to_file(self, filename: str): diff --git a/tests/unit/test_serie_class.py b/tests/unit/test_serie_class.py index 38a0ae0..41bf42e 100644 --- a/tests/unit/test_serie_class.py +++ b/tests/unit/test_serie_class.py @@ -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 + diff --git a/tests/unit/test_serie_list.py b/tests/unit/test_serie_list.py index 99d858a..b262138 100644 --- a/tests/unit/test_serie_list.py +++ b/tests/unit/test_serie_list.py @@ -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("") + + # 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("") + 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("") + 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 +