205 lines
6.8 KiB
Python
205 lines
6.8 KiB
Python
"""TV Show domain entities."""
|
|
|
|
import re
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
|
|
from ..shared.value_objects import FilePath, FileSize, ImdbId
|
|
from .value_objects import EpisodeNumber, SeasonNumber, ShowStatus
|
|
|
|
|
|
@dataclass
|
|
class TVShow:
|
|
"""
|
|
TV Show entity representing a TV show in the media library.
|
|
|
|
This is the main aggregate root for the TV shows domain.
|
|
Migrated from agent/models/tv_show.py
|
|
"""
|
|
|
|
imdb_id: ImdbId
|
|
title: str
|
|
seasons_count: int
|
|
status: ShowStatus
|
|
tmdb_id: int | None = None
|
|
first_air_date: str | None = None
|
|
added_at: datetime = field(default_factory=datetime.now)
|
|
|
|
def __post_init__(self):
|
|
"""Validate TV show entity."""
|
|
# Ensure ImdbId is actually an ImdbId instance
|
|
if not isinstance(self.imdb_id, ImdbId):
|
|
if isinstance(self.imdb_id, str):
|
|
object.__setattr__(self, "imdb_id", ImdbId(self.imdb_id))
|
|
else:
|
|
raise ValueError(
|
|
f"imdb_id must be ImdbId or str, got {type(self.imdb_id)}"
|
|
)
|
|
|
|
# Ensure ShowStatus is actually a ShowStatus instance
|
|
if not isinstance(self.status, ShowStatus):
|
|
if isinstance(self.status, str):
|
|
object.__setattr__(self, "status", ShowStatus.from_string(self.status))
|
|
else:
|
|
raise ValueError(
|
|
f"status must be ShowStatus or str, got {type(self.status)}"
|
|
)
|
|
|
|
# Validate seasons_count
|
|
if not isinstance(self.seasons_count, int) or self.seasons_count < 0:
|
|
raise ValueError(
|
|
f"seasons_count must be a non-negative integer, got {self.seasons_count}"
|
|
)
|
|
|
|
def is_ongoing(self) -> bool:
|
|
"""Check if the show is still ongoing."""
|
|
return self.status == ShowStatus.ONGOING
|
|
|
|
def is_ended(self) -> bool:
|
|
"""Check if the show has ended."""
|
|
return self.status == ShowStatus.ENDED
|
|
|
|
def get_folder_name(self) -> str:
|
|
"""
|
|
Get the folder name for this TV show.
|
|
|
|
Format: "Title"
|
|
Example: "Breaking.Bad"
|
|
"""
|
|
# Remove special characters and replace spaces with dots
|
|
cleaned = re.sub(r"[^\w\s\.\-]", "", self.title)
|
|
return cleaned.replace(" ", ".")
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.title} ({self.status.value}, {self.seasons_count} seasons)"
|
|
|
|
def __repr__(self) -> str:
|
|
return f"TVShow(imdb_id={self.imdb_id}, title='{self.title}')"
|
|
|
|
|
|
@dataclass
|
|
class Season:
|
|
"""
|
|
Season entity representing a season of a TV show.
|
|
"""
|
|
|
|
show_imdb_id: ImdbId
|
|
season_number: SeasonNumber
|
|
episode_count: int
|
|
name: str | None = None
|
|
overview: str | None = None
|
|
air_date: str | None = None
|
|
poster_path: str | None = None
|
|
|
|
def __post_init__(self):
|
|
"""Validate season entity."""
|
|
# Ensure ImdbId is actually an ImdbId instance
|
|
if not isinstance(self.show_imdb_id, ImdbId):
|
|
if isinstance(self.show_imdb_id, str):
|
|
object.__setattr__(self, "show_imdb_id", ImdbId(self.show_imdb_id))
|
|
|
|
# Ensure SeasonNumber is actually a SeasonNumber instance
|
|
if not isinstance(self.season_number, SeasonNumber):
|
|
if isinstance(self.season_number, int):
|
|
object.__setattr__(
|
|
self, "season_number", SeasonNumber(self.season_number)
|
|
)
|
|
|
|
# Validate episode_count
|
|
if not isinstance(self.episode_count, int) or self.episode_count < 0:
|
|
raise ValueError(
|
|
f"episode_count must be a non-negative integer, got {self.episode_count}"
|
|
)
|
|
|
|
def is_special(self) -> bool:
|
|
"""Check if this is the specials season."""
|
|
return self.season_number.is_special()
|
|
|
|
def get_folder_name(self) -> str:
|
|
"""
|
|
Get the folder name for this season.
|
|
|
|
Format: "Season 01" or "Specials" for season 0
|
|
"""
|
|
if self.is_special():
|
|
return "Specials"
|
|
return f"Season {self.season_number.value:02d}"
|
|
|
|
def __str__(self) -> str:
|
|
if self.name:
|
|
return f"Season {self.season_number.value}: {self.name}"
|
|
return f"Season {self.season_number.value}"
|
|
|
|
def __repr__(self) -> str:
|
|
return f"Season(show={self.show_imdb_id}, number={self.season_number.value})"
|
|
|
|
|
|
@dataclass
|
|
class Episode:
|
|
"""
|
|
Episode entity representing an episode of a TV show.
|
|
"""
|
|
|
|
show_imdb_id: ImdbId
|
|
season_number: SeasonNumber
|
|
episode_number: EpisodeNumber
|
|
title: str
|
|
file_path: FilePath | None = None
|
|
file_size: FileSize | None = None
|
|
overview: str | None = None
|
|
air_date: str | None = None
|
|
still_path: str | None = None
|
|
vote_average: float | None = None
|
|
runtime: int | None = None # in minutes
|
|
|
|
def __post_init__(self):
|
|
"""Validate episode entity."""
|
|
# Ensure ImdbId is actually an ImdbId instance
|
|
if not isinstance(self.show_imdb_id, ImdbId):
|
|
if isinstance(self.show_imdb_id, str):
|
|
object.__setattr__(self, "show_imdb_id", ImdbId(self.show_imdb_id))
|
|
|
|
# Ensure SeasonNumber is actually a SeasonNumber instance
|
|
if not isinstance(self.season_number, SeasonNumber):
|
|
if isinstance(self.season_number, int):
|
|
object.__setattr__(
|
|
self, "season_number", SeasonNumber(self.season_number)
|
|
)
|
|
|
|
# Ensure EpisodeNumber is actually an EpisodeNumber instance
|
|
if not isinstance(self.episode_number, EpisodeNumber):
|
|
if isinstance(self.episode_number, int):
|
|
object.__setattr__(
|
|
self, "episode_number", EpisodeNumber(self.episode_number)
|
|
)
|
|
|
|
def has_file(self) -> bool:
|
|
"""Check if the episode has an associated file."""
|
|
return self.file_path is not None and self.file_path.exists()
|
|
|
|
def is_downloaded(self) -> bool:
|
|
"""Check if the episode is downloaded."""
|
|
return self.has_file()
|
|
|
|
def get_filename(self) -> str:
|
|
"""
|
|
Get the suggested filename for this episode.
|
|
|
|
Format: "S01E01 - Episode Title.ext"
|
|
Example: "S01E05 - Pilot.mkv"
|
|
"""
|
|
season_str = f"S{self.season_number.value:02d}"
|
|
episode_str = f"E{self.episode_number.value:02d}"
|
|
|
|
# Clean title for filename
|
|
clean_title = re.sub(r"[^\w\s\-]", "", self.title)
|
|
clean_title = clean_title.replace(" ", ".")
|
|
|
|
return f"{season_str}{episode_str}.{clean_title}"
|
|
|
|
def __str__(self) -> str:
|
|
return f"S{self.season_number.value:02d}E{self.episode_number.value:02d} - {self.title}"
|
|
|
|
def __repr__(self) -> str:
|
|
return f"Episode(show={self.show_imdb_id}, S{self.season_number.value:02d}E{self.episode_number.value:02d})"
|