feat!: migrate to OpenAI native tool calls and fix circular deps (#fuck-gemini)
- Fix circular dependencies in agent/tools - Migrate from custom JSON to OpenAI tool calls format - Add async streaming (step_stream, complete_stream) - Simplify prompt system and remove token counting - Add 5 new API endpoints (/health, /v1/models, /api/memory/*) - Add 3 new tools (get_torrent_by_index, add_torrent_by_index, set_language) - Fix all 500 tests and add coverage config (80% threshold) - Add comprehensive docs (README, pytest guide) BREAKING: LLM interface changed, memory injection via get_memory()
This commit is contained in:
+58
-54
@@ -1,13 +1,13 @@
|
||||
"""Movie domain services - Business logic."""
|
||||
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
import re
|
||||
|
||||
from ..shared.value_objects import ImdbId, FilePath
|
||||
from ..shared.value_objects import FilePath, ImdbId
|
||||
from .entities import Movie
|
||||
from .value_objects import Quality
|
||||
from .exceptions import MovieAlreadyExists, MovieNotFound
|
||||
from .repositories import MovieRepository
|
||||
from .exceptions import MovieNotFound, MovieAlreadyExists
|
||||
from .value_objects import Quality
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -15,46 +15,48 @@ 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")
|
||||
|
||||
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
|
||||
"""
|
||||
@@ -62,89 +64,89 @@ class MovieService:
|
||||
if not movie:
|
||||
raise MovieNotFound(f"Movie with IMDb ID {imdb_id} not found")
|
||||
return movie
|
||||
|
||||
def get_all_movies(self) -> List[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:
|
||||
if "2160p" in filename_lower or "4k" in filename_lower:
|
||||
return Quality.UHD_4K
|
||||
elif '1080p' in filename_lower:
|
||||
elif "1080p" in filename_lower:
|
||||
return Quality.FULL_HD
|
||||
elif '720p' in filename_lower:
|
||||
elif "720p" in filename_lower:
|
||||
return Quality.HD
|
||||
elif '480p' in filename_lower:
|
||||
elif "480p" in filename_lower:
|
||||
return Quality.SD
|
||||
|
||||
|
||||
return Quality.UNKNOWN
|
||||
|
||||
def extract_year_from_filename(self, filename: str) -> Optional[int]:
|
||||
|
||||
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
|
||||
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:
|
||||
@@ -152,37 +154,39 @@ class MovieService:
|
||||
# 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'}
|
||||
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")
|
||||
logger.warning(
|
||||
f"File too small to be a movie: {file_path.value.stat().st_size} bytes"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user