import json import warnings from src.server.utils.filesystem import sanitize_folder_name class Serie: """ Represents an anime series with metadata and episode information. The `key` property is the unique identifier for the series (provider-assigned, URL-safe). The `folder` property is the filesystem folder name (metadata only, not used for lookups). Args: key: Unique series identifier from provider (e.g., "attack-on-titan"). Cannot be empty. name: Display name of the series site: Provider site URL folder: Filesystem folder name (metadata only, e.g., "Attack on Titan (2013)") episodeDict: Dictionary mapping season numbers to lists of episode numbers Raises: ValueError: If key is None or empty string """ def __init__( self, key: str, name: str, site: str, folder: str, episodeDict: dict[int, list[int]] ): if not key or not key.strip(): raise ValueError("Serie key cannot be None or empty") self._key = key.strip() self._name = name self._site = site self._folder = folder self._episodeDict = episodeDict def __str__(self): """String representation of Serie object""" return ( f"Serie(key='{self.key}', name='{self.name}', " f"site='{self.site}', folder='{self.folder}', " f"episodeDict={self.episodeDict})" ) @property def key(self) -> str: """ Unique series identifier (primary identifier for all lookups). This is the provider-assigned, URL-safe identifier used throughout the application for series identification, lookups, and operations. Returns: str: The unique series key """ return self._key @key.setter def key(self, value: str): """ Set the unique series identifier. Args: value: New key value Raises: ValueError: If value is None or empty string """ if not value or not value.strip(): raise ValueError("Serie key cannot be None or empty") self._key = value.strip() @property def name(self) -> str: return self._name @name.setter def name(self, value: str): self._name = value @property def site(self) -> str: return self._site @site.setter def site(self, value: str): self._site = value @property def folder(self) -> str: """ Filesystem folder name (metadata only, not used for lookups). This property contains the local directory name where the series files are stored. It should NOT be used as an identifier for series lookups - use `key` instead. Returns: str: The filesystem folder name """ return self._folder @folder.setter def folder(self, value: str): """ Set the filesystem folder name. Args: value: Folder name for the series """ self._folder = value @property def episodeDict(self) -> dict[int, list[int]]: return self._episodeDict @episodeDict.setter def episodeDict(self, value: dict[int, list[int]]): self._episodeDict = value @property def sanitized_folder(self) -> str: """ Get a filesystem-safe folder name derived from the display name. This property returns a sanitized version of the series name suitable for use as a filesystem folder name. It removes/replaces characters that are invalid for filesystems while preserving Unicode characters. Use this property when creating folders for the series on disk. The `folder` property stores the actual folder name used. Returns: str: Filesystem-safe folder name based on display name Example: >>> serie = Serie("attack-on-titan", "Attack on Titan: Final", ...) >>> serie.sanitized_folder 'Attack on Titan Final' """ # Use name if available, fall back to folder, then key name_to_sanitize = self._name or self._folder or self._key try: return sanitize_folder_name(name_to_sanitize) except ValueError: # Fallback to key if name cannot be sanitized return sanitize_folder_name(self._key) def to_dict(self): """Convert Serie object to dictionary for JSON serialization.""" return { "key": self.key, "name": self.name, "site": self.site, "folder": self.folder, "episodeDict": { str(k): list(v) for k, v in self.episodeDict.items() } } @staticmethod def from_dict(data: dict): """Create a Serie object from dictionary.""" # Convert keys to int episode_dict = { int(k): v for k, v in data["episodeDict"].items() } return Serie( data["key"], data["name"], data["site"], data["folder"], episode_dict ) def save_to_file(self, filename: str): """Save Serie object to JSON file. .. deprecated:: File-based storage is deprecated. Use database storage via `AnimeSeriesService.create()` instead. This method will be removed in v3.0.0. Args: filename: Path to save the JSON file """ warnings.warn( "save_to_file() is deprecated and will be removed in v3.0.0. " "Use database storage via AnimeSeriesService.create() instead.", DeprecationWarning, stacklevel=2 ) with open(filename, "w", encoding="utf-8") as file: json.dump(self.to_dict(), file, indent=4) @classmethod def load_from_file(cls, filename: str) -> "Serie": """Load Serie object from JSON file. .. deprecated:: File-based storage is deprecated. Use database storage via `AnimeSeriesService.get_by_key()` instead. This method will be removed in v3.0.0. Args: filename: Path to load the JSON file from Returns: Serie: The loaded Serie object """ warnings.warn( "load_from_file() is deprecated and will be removed in v3.0.0. " "Use database storage via AnimeSeriesService instead.", DeprecationWarning, stacklevel=2 ) with open(filename, "r", encoding="utf-8") as file: data = json.load(file) return cls.from_dict(data)