Files
alfred/alfred/domain/movies/entities.py
T
2026-05-26 05:45:07 +02:00

92 lines
3.1 KiB
Python

"""Movie domain entities."""
from dataclasses import dataclass
from ..shared.value_objects import ImdbId, TmdbId
from .value_objects import MovieTitle, ReleaseYear
@dataclass(frozen=True, eq=False)
class Movie:
"""
Movie aggregate root for the movies domain.
TMDB-only aggregate: carries identity (``tmdb_id`` + optional
``imdb_id``) plus the catalog facts that come from TMDB (``title``,
``release_year``). Filesystem-side concerns (file path, quality,
tracks, ``added_at``) live on :class:`alfred.domain.releases.entities.
MovieRelease`, the per-movie release aggregate persisted alongside.
Frozen: rebuild via ``dataclasses.replace`` to project metadata
updates (e.g. a TMDB refresh) onto a new instance.
Equality is identity-based on ``tmdb_id``: two ``Movie`` instances
are equal iff they share the same primary key. ``imdb_id`` is a
secondary anchor and not part of the identity.
"""
tmdb_id: TmdbId
title: MovieTitle
imdb_id: ImdbId | None = None
release_year: ReleaseYear | None = None
def __post_init__(self) -> None:
if not isinstance(self.tmdb_id, TmdbId):
raise ValueError(
f"tmdb_id must be TmdbId, got {type(self.tmdb_id)}"
)
if not isinstance(self.title, MovieTitle):
if isinstance(self.title, str):
object.__setattr__(self, "title", MovieTitle(self.title))
else:
raise ValueError(
f"title must be MovieTitle or str, got {type(self.title)}"
)
if self.imdb_id is not None and not isinstance(self.imdb_id, ImdbId):
raise ValueError(
f"imdb_id must be ImdbId or None, got {type(self.imdb_id)}"
)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Movie):
return NotImplemented
return self.tmdb_id == other.tmdb_id
def __hash__(self) -> int:
return hash(self.tmdb_id)
# WRONG
def get_folder_name(self) -> str:
"""
Get the folder name for this movie.
Format: "Title (Year)"
Example: "Inception (2010)"
"""
if self.release_year:
return f"{self.title.value} ({self.release_year.value})"
return self.title.value
# WRONG
def get_filename(self) -> str:
"""
Get the suggested base filename (without extension) for this movie.
Format: ``Title.Year`` (quality lives on
:class:`alfred.domain.releases.entities.MovieRelease` now and is
appended by the release-aware caller — typically the rescan /
organize flow, after Phase 4).
Example: ``Inception.2010``.
"""
parts = [self.title.normalized()]
if self.release_year:
parts.append(str(self.release_year.value))
return ".".join(parts)
def __str__(self) -> str:
return f"{self.title.value} ({self.release_year.value if self.release_year else 'Unknown'})"
def __repr__(self) -> str:
return f"Movie(tmdb_id={self.tmdb_id}, title='{self.title.value}')"