193 lines
5.3 KiB
Python
193 lines
5.3 KiB
Python
"""Movie domain services - Business logic."""
|
|
|
|
import logging
|
|
import re
|
|
|
|
from ..shared.value_objects import FilePath, ImdbId
|
|
from .entities import Movie
|
|
from .exceptions import MovieAlreadyExists, MovieNotFound
|
|
from .repositories import MovieRepository
|
|
from .value_objects import Quality
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MovieService:
|
|
"""
|
|
Domain service for movie-related business logic.
|
|
|
|
This service contains business rules that don't naturally fit
|
|
within a single entity.
|
|
"""
|
|
|
|
def __init__(self, repository: MovieRepository):
|
|
"""
|
|
Initialize movie service.
|
|
|
|
Args:
|
|
repository: Movie repository for persistence
|
|
"""
|
|
self.repository = repository
|
|
|
|
def add_movie(self, movie: Movie) -> None:
|
|
"""
|
|
Add a new movie to the library.
|
|
|
|
Args:
|
|
movie: Movie entity to add
|
|
|
|
Raises:
|
|
MovieAlreadyExists: If movie with same IMDb ID already exists
|
|
"""
|
|
if self.repository.exists(movie.imdb_id):
|
|
raise MovieAlreadyExists(
|
|
f"Movie with IMDb ID {movie.imdb_id} already exists"
|
|
)
|
|
|
|
self.repository.save(movie)
|
|
logger.info(f"Added movie: {movie.title.value} ({movie.imdb_id})")
|
|
|
|
def get_movie(self, imdb_id: ImdbId) -> Movie:
|
|
"""
|
|
Get a movie by IMDb ID.
|
|
|
|
Args:
|
|
imdb_id: IMDb ID of the movie
|
|
|
|
Returns:
|
|
Movie entity
|
|
|
|
Raises:
|
|
MovieNotFound: If movie not found
|
|
"""
|
|
movie = self.repository.find_by_imdb_id(imdb_id)
|
|
if not movie:
|
|
raise MovieNotFound(f"Movie with IMDb ID {imdb_id} not found")
|
|
return movie
|
|
|
|
def get_all_movies(self) -> list[Movie]:
|
|
"""
|
|
Get all movies in the library.
|
|
|
|
Returns:
|
|
List of all movies
|
|
"""
|
|
return self.repository.find_all()
|
|
|
|
def update_movie(self, movie: Movie) -> None:
|
|
"""
|
|
Update an existing movie.
|
|
|
|
Args:
|
|
movie: Movie entity with updated data
|
|
|
|
Raises:
|
|
MovieNotFound: If movie doesn't exist
|
|
"""
|
|
if not self.repository.exists(movie.imdb_id):
|
|
raise MovieNotFound(f"Movie with IMDb ID {movie.imdb_id} not found")
|
|
|
|
self.repository.save(movie)
|
|
logger.info(f"Updated movie: {movie.title.value} ({movie.imdb_id})")
|
|
|
|
def remove_movie(self, imdb_id: ImdbId) -> None:
|
|
"""
|
|
Remove a movie from the library.
|
|
|
|
Args:
|
|
imdb_id: IMDb ID of the movie to remove
|
|
|
|
Raises:
|
|
MovieNotFound: If movie not found
|
|
"""
|
|
if not self.repository.delete(imdb_id):
|
|
raise MovieNotFound(f"Movie with IMDb ID {imdb_id} not found")
|
|
|
|
logger.info(f"Removed movie with IMDb ID: {imdb_id}")
|
|
|
|
def detect_quality_from_filename(self, filename: str) -> Quality:
|
|
"""
|
|
Detect video quality from filename.
|
|
|
|
Args:
|
|
filename: Filename to analyze
|
|
|
|
Returns:
|
|
Detected quality or UNKNOWN
|
|
"""
|
|
filename_lower = filename.lower()
|
|
|
|
# Check for quality indicators
|
|
if "2160p" in filename_lower or "4k" in filename_lower:
|
|
return Quality.UHD_4K
|
|
elif "1080p" in filename_lower:
|
|
return Quality.FULL_HD
|
|
elif "720p" in filename_lower:
|
|
return Quality.HD
|
|
elif "480p" in filename_lower:
|
|
return Quality.SD
|
|
|
|
return Quality.UNKNOWN
|
|
|
|
def extract_year_from_filename(self, filename: str) -> int | None:
|
|
"""
|
|
Extract release year from filename.
|
|
|
|
Args:
|
|
filename: Filename to analyze
|
|
|
|
Returns:
|
|
Year if found, None otherwise
|
|
"""
|
|
# Look for 4-digit year in parentheses or standalone
|
|
# Examples: "Movie (2010)", "Movie.2010.1080p"
|
|
patterns = [
|
|
r"\((\d{4})\)", # (2010)
|
|
r"\.(\d{4})\.", # .2010.
|
|
r"\s(\d{4})\s", # 2010
|
|
]
|
|
|
|
for pattern in patterns:
|
|
match = re.search(pattern, filename)
|
|
if match:
|
|
year = int(match.group(1))
|
|
# Validate year is reasonable
|
|
if 1888 <= year <= 2100:
|
|
return year
|
|
|
|
return None
|
|
|
|
def validate_movie_file(self, file_path: FilePath) -> bool:
|
|
"""
|
|
Validate that a file is a valid movie file.
|
|
|
|
Args:
|
|
file_path: Path to the file
|
|
|
|
Returns:
|
|
True if valid movie file, False otherwise
|
|
"""
|
|
if not file_path.exists():
|
|
logger.warning(f"File does not exist: {file_path}")
|
|
return False
|
|
|
|
if not file_path.is_file():
|
|
logger.warning(f"Path is not a file: {file_path}")
|
|
return False
|
|
|
|
# Check file extension
|
|
valid_extensions = {".mkv", ".mp4", ".avi", ".mov", ".wmv", ".flv", ".webm"}
|
|
if file_path.value.suffix.lower() not in valid_extensions:
|
|
logger.warning(f"Invalid file extension: {file_path.value.suffix}")
|
|
return False
|
|
|
|
# Check file size (should be at least 100 MB for a movie)
|
|
min_size = 100 * 1024 * 1024 # 100 MB
|
|
if file_path.value.stat().st_size < min_size:
|
|
logger.warning(
|
|
f"File too small to be a movie: {file_path.value.stat().st_size} bytes"
|
|
)
|
|
return False
|
|
|
|
return True
|