feat: Standardize SerieList to use key as primary identifier (Task 1.2)

- Renamed folderDict to keyDict for clarity
- Updated internal storage to use serie.key instead of serie.folder
- Optimized contains() from O(n) to O(1) with direct key lookup
- Added get_by_key() as primary lookup method
- Added get_by_folder() for backward compatibility
- Enhanced docstrings to clarify key vs folder usage
- Created comprehensive test suite (12 tests, all passing)
- Verified no breaking changes (16 SeriesApp tests pass)

This establishes key as the single source of truth for series
identification while maintaining folder as metadata for filesystem
operations only.
This commit is contained in:
2025-11-23 12:25:08 +01:00
parent 048434d49c
commit 8b5b06ca9a
3 changed files with 310 additions and 24 deletions

View File

@@ -3,22 +3,35 @@
import logging
import os
from json import JSONDecodeError
from typing import Dict, Iterable, List
from typing import Dict, Iterable, List, Optional
from src.core.entities.series import Serie
class SerieList:
"""Represents the collection of cached series stored on disk."""
"""
Represents the collection of cached series stored on disk.
Series are identified by their unique 'key' (provider identifier).
The 'folder' is metadata only and not used for lookups.
"""
def __init__(self, base_path: str) -> None:
self.directory: str = base_path
self.folderDict: Dict[str, Serie] = {}
# Internal storage using serie.key as the dictionary key
self.keyDict: Dict[str, Serie] = {}
self.load_series()
def add(self, serie: Serie) -> None:
"""Persist a new series if it is not already present."""
"""
Persist a new series if it is not already present.
Uses serie.key for identification. The serie.folder is used for
filesystem operations only.
Args:
serie: The Serie instance to add
"""
if self.contains(serie.key):
return
@@ -27,12 +40,20 @@ class SerieList:
os.makedirs(anime_path, exist_ok=True)
if not os.path.isfile(data_path):
serie.save_to_file(data_path)
self.folderDict[serie.folder] = serie
# Store by key, not folder
self.keyDict[serie.key] = serie
def contains(self, key: str) -> bool:
"""Return True when a series identified by ``key`` already exists."""
return any(value.key == key for value in self.folderDict.values())
"""
Return True when a series identified by ``key`` already exists.
Args:
key: The unique provider identifier for the series
Returns:
True if the series exists in the collection
"""
return key in self.keyDict
def load_series(self) -> None:
"""Populate the in-memory map with metadata discovered on disk."""
@@ -61,11 +82,22 @@ class SerieList:
)
def _load_data(self, anime_folder: str, data_path: str) -> None:
"""Load a single series metadata file into the in-memory collection."""
"""
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
"""
try:
self.folderDict[anime_folder] = Serie.load_from_file(data_path)
logging.debug("Successfully loaded metadata for %s", anime_folder)
serie = Serie.load_from_file(data_path)
# Store by key, not folder
self.keyDict[serie.key] = serie
logging.debug(
"Successfully loaded metadata for %s (key: %s)",
anime_folder,
serie.key
)
except (OSError, JSONDecodeError, KeyError, ValueError) as error:
logging.error(
"Failed to load metadata for folder %s from %s: %s",
@@ -76,24 +108,52 @@ class SerieList:
def GetMissingEpisode(self) -> List[Serie]:
"""Return all series that still contain missing episodes."""
return [
serie
for serie in self.folderDict.values()
for serie in self.keyDict.values()
if serie.episodeDict
]
def get_missing_episodes(self) -> List[Serie]:
"""PEP8-friendly alias for :meth:`GetMissingEpisode`."""
return self.GetMissingEpisode()
def GetList(self) -> List[Serie]:
"""Return all series instances stored in the list."""
return list(self.folderDict.values())
return list(self.keyDict.values())
def get_all(self) -> List[Serie]:
"""PEP8-friendly alias for :meth:`GetList`."""
return self.GetList()
def get_by_key(self, key: str) -> Optional[Serie]:
"""
Get a series by its unique provider key.
This is the primary method for series lookup.
Args:
key: The unique provider identifier (e.g., "attack-on-titan")
Returns:
The Serie instance if found, None otherwise
"""
return self.keyDict.get(key)
def get_by_folder(self, folder: str) -> Optional[Serie]:
"""
Get a series by its folder name.
This method is provided for backward compatibility only.
Prefer using get_by_key() for new code.
Args:
folder: The filesystem folder name (e.g., "Attack on Titan (2013)")
Returns:
The Serie instance if found, None otherwise
"""
for serie in self.keyDict.values():
if serie.folder == folder:
return serie
return None