mirror of
https://github.com/maxdorninger/MediaManager.git
synced 2026-04-17 15:13:24 +02:00
This PR replaces the APScheduler lib with the Taskiq task queuing lib. # why APScheduler doesn't support FastAPI's DI in tasks, this makes them quite cumbersome to read and write since DB, Repositories and Services all need to be instanciated manually. Moreover, Taskiq makes it easier to start background tasks from FastAPI requests. This enables MM to move to a more event-based architecture. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * App now uses an orchestrated async startup/shutdown and runs background scheduling via a database-backed task queue; startup enqueues pre-load/import/update tasks. * **Bug Fixes** * Improved torrent client handling with clearer conflict messages and guidance for manual resolution. * Enhanced logging around season, episode and metadata update operations; minor regex/behaviour formatting preserved. * **Chores** * Updated dependencies to support the new task queue and connection pooling. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
706 lines
26 KiB
Python
706 lines
26 KiB
Python
import re
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import overload
|
|
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
from media_manager.config import MediaManagerConfig
|
|
from media_manager.exceptions import InvalidConfigError, NotFoundError, RenameError
|
|
from media_manager.indexer.schemas import IndexerQueryResult, IndexerQueryResultId
|
|
from media_manager.indexer.service import IndexerService
|
|
from media_manager.indexer.utils import evaluate_indexer_query_results
|
|
from media_manager.metadataProvider.abstract_metadata_provider import (
|
|
AbstractMetadataProvider,
|
|
)
|
|
from media_manager.metadataProvider.schemas import MetaDataProviderSearchResult
|
|
from media_manager.metadataProvider.tmdb import TmdbMetadataProvider
|
|
from media_manager.metadataProvider.tvdb import TvdbMetadataProvider
|
|
from media_manager.movies import log
|
|
from media_manager.movies.repository import MovieRepository
|
|
from media_manager.movies.schemas import (
|
|
Movie,
|
|
MovieFile,
|
|
MovieId,
|
|
PublicMovie,
|
|
PublicMovieFile,
|
|
RichMovieTorrent,
|
|
)
|
|
from media_manager.notification.service import NotificationService
|
|
from media_manager.schemas import MediaImportSuggestion
|
|
from media_manager.torrent.schemas import (
|
|
Quality,
|
|
Torrent,
|
|
TorrentStatus,
|
|
)
|
|
from media_manager.torrent.service import TorrentService
|
|
from media_manager.torrent.utils import (
|
|
extract_external_id_from_string,
|
|
get_files_for_import,
|
|
get_importable_media_directories,
|
|
import_file,
|
|
remove_special_characters,
|
|
remove_special_chars_and_parentheses,
|
|
)
|
|
|
|
|
|
class MovieService:
|
|
def __init__(
|
|
self,
|
|
movie_repository: MovieRepository,
|
|
torrent_service: TorrentService,
|
|
indexer_service: IndexerService,
|
|
notification_service: NotificationService,
|
|
) -> None:
|
|
self.movie_repository = movie_repository
|
|
self.torrent_service = torrent_service
|
|
self.indexer_service = indexer_service
|
|
self.notification_service = notification_service
|
|
|
|
def add_movie(
|
|
self,
|
|
external_id: int,
|
|
metadata_provider: AbstractMetadataProvider,
|
|
language: str | None = None,
|
|
) -> Movie:
|
|
"""
|
|
Add a new movie to the database.
|
|
|
|
:param external_id: The ID of the movie in the metadata provider's system.
|
|
:param metadata_provider: The name of the metadata provider.
|
|
:param language: Optional language code (ISO 639-1) to fetch metadata in.
|
|
"""
|
|
movie_with_metadata = metadata_provider.get_movie_metadata(
|
|
movie_id=external_id, language=language
|
|
)
|
|
if not movie_with_metadata:
|
|
raise NotFoundError
|
|
|
|
saved_movie = self.movie_repository.save_movie(movie=movie_with_metadata)
|
|
metadata_provider.download_movie_poster_image(movie=saved_movie)
|
|
return saved_movie
|
|
|
|
def delete_movie(
|
|
self,
|
|
movie: Movie,
|
|
delete_files_on_disk: bool = False,
|
|
delete_torrents: bool = False,
|
|
) -> None:
|
|
"""
|
|
Delete a movie from the database, optionally deleting files and torrents.
|
|
|
|
:param movie: The movie to delete.
|
|
:param delete_files_on_disk: Whether to delete the movie's files from disk.
|
|
:param delete_torrents: Whether to delete associated torrents from the torrent client.
|
|
"""
|
|
if delete_files_on_disk or delete_torrents:
|
|
if delete_files_on_disk:
|
|
# Get the movie's directory path
|
|
movie_dir = self.get_movie_root_path(movie=movie)
|
|
|
|
if movie_dir.exists() and movie_dir.is_dir():
|
|
try:
|
|
shutil.rmtree(movie_dir)
|
|
log.info(f"Deleted movie directory: {movie_dir}")
|
|
except OSError:
|
|
log.exception(f"Deleting movie directory: {movie_dir}")
|
|
|
|
if delete_torrents:
|
|
# Get all torrents associated with this movie
|
|
movie_torrents = self.movie_repository.get_torrents_by_movie_id(
|
|
movie_id=movie.id
|
|
)
|
|
|
|
for movie_torrent in movie_torrents:
|
|
torrent = self.torrent_service.get_torrent_by_id(
|
|
torrent_id=movie_torrent.torrent_id
|
|
)
|
|
try:
|
|
self.torrent_service.cancel_download(
|
|
torrent=torrent, delete_files=True
|
|
)
|
|
log.info(f"Deleted torrent: {torrent.torrent_title}")
|
|
except Exception:
|
|
log.warning(
|
|
f"Failed to delete torrent {torrent.hash}", exc_info=True
|
|
)
|
|
|
|
# Delete from database
|
|
self.movie_repository.delete_movie(movie_id=movie.id)
|
|
|
|
def get_public_movie_files(self, movie: Movie) -> list[PublicMovieFile]:
|
|
"""
|
|
Get all public movie files for a given movie.
|
|
|
|
:param movie: The movie object.
|
|
:return: A list of public movie files.
|
|
"""
|
|
movie_files = self.movie_repository.get_movie_files_by_movie_id(
|
|
movie_id=movie.id
|
|
)
|
|
public_movie_files = [PublicMovieFile.model_validate(x) for x in movie_files]
|
|
result = []
|
|
for movie_file in public_movie_files:
|
|
movie_file.imported = self.movie_file_exists_on_file(movie_file=movie_file)
|
|
result.append(movie_file)
|
|
return result
|
|
|
|
@overload
|
|
def check_if_movie_exists(
|
|
self, *, external_id: int, metadata_provider: str
|
|
) -> bool:
|
|
"""
|
|
Check if a movie exists in the database.
|
|
|
|
:param external_id: The external ID of the movie.
|
|
:param metadata_provider: The metadata provider.
|
|
:return: True if the movie exists, False otherwise.
|
|
"""
|
|
|
|
@overload
|
|
def check_if_movie_exists(self, *, movie_id: MovieId) -> bool:
|
|
"""
|
|
Check if a movie exists in the database.
|
|
|
|
:param movie_id: The ID of the movie.
|
|
:return: True if the movie exists, False otherwise.
|
|
"""
|
|
|
|
def check_if_movie_exists(
|
|
self,
|
|
*,
|
|
external_id=None,
|
|
metadata_provider=None,
|
|
movie_id=None,
|
|
) -> bool:
|
|
"""
|
|
Check if a movie exists in the database.
|
|
|
|
:param external_id: The external ID of the movie.
|
|
:param metadata_provider: The metadata provider.
|
|
:param movie_id: The ID of the movie.
|
|
:return: True if the movie exists, False otherwise.
|
|
:raises ValueError: If neither external ID and metadata provider nor movie ID are provided.
|
|
"""
|
|
|
|
if not (external_id is None or metadata_provider is None):
|
|
try:
|
|
self.movie_repository.get_movie_by_external_id(
|
|
external_id=external_id, metadata_provider=metadata_provider
|
|
)
|
|
except NotFoundError:
|
|
return False
|
|
elif movie_id is not None:
|
|
try:
|
|
self.movie_repository.get_movie_by_id(movie_id=movie_id)
|
|
except NotFoundError:
|
|
return False
|
|
else:
|
|
msg = "Use one of the provided overloads for this function!"
|
|
raise ValueError(msg)
|
|
|
|
return True
|
|
|
|
def get_all_available_torrents_for_movie(
|
|
self, movie: Movie, search_query_override: str | None = None
|
|
) -> list[IndexerQueryResult]:
|
|
"""
|
|
Get all available torrents for a given movie.
|
|
|
|
:param movie: The movie object.
|
|
:param search_query_override: Optional override for the search query.
|
|
:return: A list of indexer query results.
|
|
"""
|
|
if search_query_override:
|
|
return self.indexer_service.search(query=search_query_override, is_tv=False)
|
|
|
|
torrents = self.indexer_service.search_movie(movie=movie)
|
|
|
|
return evaluate_indexer_query_results(
|
|
is_tv=False, query_results=torrents, media=movie
|
|
)
|
|
|
|
def get_all_movies(self) -> list[Movie]:
|
|
"""
|
|
Get all movies.
|
|
|
|
:return: A list of all movies.
|
|
"""
|
|
return self.movie_repository.get_movies()
|
|
|
|
def search_for_movie(
|
|
self, query: str, metadata_provider: AbstractMetadataProvider
|
|
) -> list[MetaDataProviderSearchResult]:
|
|
"""
|
|
Search for movies using a given query.
|
|
|
|
:param query: The search query.
|
|
:param metadata_provider: The metadata provider to search.
|
|
:return: A list of metadata provider movie search results.
|
|
"""
|
|
results = metadata_provider.search_movie(query)
|
|
for result in results:
|
|
if self.check_if_movie_exists(
|
|
external_id=result.external_id, metadata_provider=metadata_provider.name
|
|
):
|
|
result.added = True
|
|
|
|
# Fetch the internal movie ID.
|
|
try:
|
|
movie = self.movie_repository.get_movie_by_external_id(
|
|
external_id=result.external_id,
|
|
metadata_provider=metadata_provider.name,
|
|
)
|
|
result.id = movie.id
|
|
except Exception:
|
|
log.error(
|
|
f"Unable to find internal movie ID for {result.external_id} on {metadata_provider.name}"
|
|
)
|
|
return results
|
|
|
|
def get_popular_movies(
|
|
self, metadata_provider: AbstractMetadataProvider
|
|
) -> list[MetaDataProviderSearchResult]:
|
|
"""
|
|
Get popular movies from a given metadata provider.
|
|
|
|
:param metadata_provider: The metadata provider to use.
|
|
:return: A list of metadata provider movie search results.
|
|
"""
|
|
results = metadata_provider.search_movie()
|
|
|
|
return [
|
|
result
|
|
for result in results
|
|
if not self.check_if_movie_exists(
|
|
external_id=result.external_id, metadata_provider=metadata_provider.name
|
|
)
|
|
]
|
|
|
|
def get_public_movie_by_id(self, movie: Movie) -> PublicMovie:
|
|
"""
|
|
Get a public movie from a Movie object.
|
|
|
|
:param movie: The movie object.
|
|
:return: A public movie.
|
|
"""
|
|
torrents = self.get_torrents_for_movie(movie=movie).torrents
|
|
public_movie = PublicMovie.model_validate(movie)
|
|
public_movie.downloaded = self.is_movie_downloaded(movie=movie)
|
|
public_movie.torrents = torrents
|
|
return public_movie
|
|
|
|
def get_movie_by_id(self, movie_id: MovieId) -> Movie:
|
|
"""
|
|
Get a movie by its ID.
|
|
|
|
:param movie_id: The ID of the movie.
|
|
:return: The movie.
|
|
"""
|
|
return self.movie_repository.get_movie_by_id(movie_id=movie_id)
|
|
|
|
def is_movie_downloaded(self, movie: Movie) -> bool:
|
|
"""
|
|
Check if a movie is downloaded.
|
|
|
|
:param movie: The movie object.
|
|
:return: True if the movie is downloaded, False otherwise.
|
|
"""
|
|
movie_files = self.movie_repository.get_movie_files_by_movie_id(
|
|
movie_id=movie.id
|
|
)
|
|
for movie_file in movie_files:
|
|
if self.movie_file_exists_on_file(movie_file=movie_file):
|
|
return True
|
|
return False
|
|
|
|
def movie_file_exists_on_file(self, movie_file: MovieFile) -> bool:
|
|
"""
|
|
Check if a movie file exists on the filesystem.
|
|
|
|
:param movie_file: The movie file to check.
|
|
:return: True if the file exists, False otherwise.
|
|
"""
|
|
if movie_file.torrent_id is None:
|
|
return True
|
|
torrent_file = self.torrent_service.get_torrent_by_id(
|
|
torrent_id=movie_file.torrent_id
|
|
)
|
|
if torrent_file.imported:
|
|
return True
|
|
return False
|
|
|
|
def get_movie_by_external_id(
|
|
self, external_id: int, metadata_provider: str
|
|
) -> Movie | None:
|
|
"""
|
|
Get a movie by its external ID and metadata provider.
|
|
|
|
:param external_id: The external ID of the movie.
|
|
:param metadata_provider: The metadata provider.
|
|
:return: The movie or None if not found.
|
|
"""
|
|
return self.movie_repository.get_movie_by_external_id(
|
|
external_id=external_id, metadata_provider=metadata_provider
|
|
)
|
|
|
|
def set_movie_library(self, movie: Movie, library: str) -> None:
|
|
self.movie_repository.set_movie_library(movie_id=movie.id, library=library)
|
|
|
|
def get_torrents_for_movie(self, movie: Movie) -> RichMovieTorrent:
|
|
"""
|
|
Get torrents for a given movie.
|
|
|
|
:param movie: The movie.
|
|
:return: A rich movie torrent.
|
|
"""
|
|
movie_torrents = self.movie_repository.get_torrents_by_movie_id(
|
|
movie_id=movie.id
|
|
)
|
|
return RichMovieTorrent(
|
|
movie_id=movie.id,
|
|
name=movie.name,
|
|
year=movie.year,
|
|
metadata_provider=movie.metadata_provider,
|
|
torrents=movie_torrents,
|
|
)
|
|
|
|
def get_all_movies_with_torrents(self) -> list[RichMovieTorrent]:
|
|
"""
|
|
Get all movies with torrents.
|
|
|
|
:return: A list of rich movie torrents.
|
|
"""
|
|
movies = self.movie_repository.get_all_movies_with_torrents()
|
|
return [self.get_torrents_for_movie(movie=movie) for movie in movies]
|
|
|
|
def download_torrent(
|
|
self,
|
|
public_indexer_result_id: IndexerQueryResultId,
|
|
movie: Movie,
|
|
override_movie_file_path_suffix: str = "",
|
|
) -> Torrent:
|
|
"""
|
|
Download a torrent for a given indexer result and movie.
|
|
|
|
:param public_indexer_result_id: The ID of the indexer result.
|
|
:param movie: The movie object.
|
|
:param override_movie_file_path_suffix: Optional override for the file path suffix.
|
|
:return: The downloaded torrent.
|
|
"""
|
|
indexer_result = self.indexer_service.get_result(
|
|
result_id=public_indexer_result_id
|
|
)
|
|
movie_torrent = self.torrent_service.download(indexer_result=indexer_result)
|
|
self.torrent_service.pause_download(torrent=movie_torrent)
|
|
movie_file = MovieFile(
|
|
movie_id=movie.id,
|
|
quality=indexer_result.quality,
|
|
torrent_id=movie_torrent.id,
|
|
file_path_suffix=override_movie_file_path_suffix,
|
|
)
|
|
try:
|
|
self.movie_repository.add_movie_file(movie_file=movie_file)
|
|
except IntegrityError:
|
|
log.warning(
|
|
f"Movie file for movie {movie.name} and torrent {movie_torrent.title} already exists"
|
|
)
|
|
self.torrent_service.cancel_download(
|
|
torrent=movie_torrent, delete_files=True
|
|
)
|
|
raise
|
|
else:
|
|
log.info(
|
|
f"Added movie file for movie {movie.name} and torrent {movie_torrent.title}"
|
|
)
|
|
self.torrent_service.resume_download(torrent=movie_torrent)
|
|
return movie_torrent
|
|
|
|
def get_movie_root_path(self, movie: Movie) -> Path:
|
|
misc_config = MediaManagerConfig().misc
|
|
movie_file_path = (
|
|
misc_config.movie_directory
|
|
/ f"{remove_special_characters(movie.name)} ({movie.year}) [{movie.metadata_provider}id-{movie.external_id}]"
|
|
)
|
|
log.debug(
|
|
f"Movie {movie.name} without special characters: {remove_special_characters(movie.name)}"
|
|
)
|
|
if movie.library != "Default":
|
|
for library in misc_config.movie_libraries:
|
|
if library.name == movie.library:
|
|
log.debug(f"Using library {library.name} for movie {movie.name}")
|
|
return (
|
|
Path(library.path)
|
|
/ f"{remove_special_characters(movie.name)} ({movie.year}) [{movie.metadata_provider}id-{movie.external_id}]"
|
|
)
|
|
else:
|
|
log.warning(
|
|
f"Library {movie.library} not found in config, using default library"
|
|
)
|
|
return movie_file_path
|
|
|
|
def import_movie(
|
|
self,
|
|
movie: Movie,
|
|
video_files: list[Path],
|
|
subtitle_files: list[Path],
|
|
file_path_suffix: str = "",
|
|
) -> bool:
|
|
movie_file_name = f"{remove_special_characters(movie.name)} ({movie.year})"
|
|
movie_root_path = self.get_movie_root_path(movie=movie)
|
|
success: bool = False
|
|
if file_path_suffix != "":
|
|
movie_file_name += f" - {file_path_suffix}"
|
|
|
|
try:
|
|
movie_root_path.mkdir(parents=True, exist_ok=True)
|
|
except Exception:
|
|
log.exception("Failed to create directory {movie_root_path}")
|
|
return False
|
|
|
|
# import movie video
|
|
if video_files:
|
|
target_video_file = (
|
|
movie_root_path / f"{movie_file_name}{video_files[0].suffix}"
|
|
)
|
|
import_file(target_file=target_video_file, source_file=video_files[0])
|
|
success = True
|
|
|
|
# import subtitles
|
|
for subtitle_file in subtitle_files:
|
|
language_code_match = re.search(
|
|
r"[. ]([a-z]{2})\.srt$", subtitle_file.name, re.IGNORECASE
|
|
)
|
|
if not language_code_match:
|
|
log.warning(
|
|
f"Subtitle file {subtitle_file.name} does not match expected format, can't extract language code, skipping."
|
|
)
|
|
continue
|
|
language_code = language_code_match.group(1)
|
|
target_subtitle_file = (
|
|
movie_root_path / f"{movie_file_name}.{language_code}.srt"
|
|
)
|
|
import_file(target_file=target_subtitle_file, source_file=subtitle_file)
|
|
|
|
return success
|
|
|
|
def import_torrent_files(self, torrent: Torrent, movie: Movie) -> None:
|
|
"""
|
|
Organizes files from a torrent into the movie directory structure.
|
|
:param torrent: The Torrent object
|
|
:param movie: The Movie object
|
|
"""
|
|
|
|
video_files, subtitle_files, _all_files = get_files_for_import(torrent=torrent)
|
|
|
|
if len(video_files) != 1:
|
|
# Send notification about multiple video files found
|
|
if self.notification_service:
|
|
self.notification_service.send_notification_to_all_providers(
|
|
title="Manual Import Required",
|
|
message=f"Multiple video files found for movie {movie.name}. Please import manually.",
|
|
)
|
|
log.error(
|
|
f"Found {len(video_files)} video files for movie {movie.name}, expected 1. Skipping auto import."
|
|
)
|
|
return
|
|
|
|
log.debug(
|
|
f"Importing these {len(video_files)} video files and {len(subtitle_files)} subtitle files"
|
|
)
|
|
|
|
movie_files: list[MovieFile] = self.torrent_service.get_movie_files_of_torrent(
|
|
torrent=torrent
|
|
)
|
|
log.info(
|
|
f"Found {len(movie_files)} movie files associated with torrent {torrent.title}"
|
|
)
|
|
|
|
success = [
|
|
self.import_movie(
|
|
movie, video_files, subtitle_files, movie_file.file_path_suffix
|
|
)
|
|
for movie_file in movie_files
|
|
]
|
|
|
|
if all(success):
|
|
torrent.imported = True
|
|
self.torrent_service.torrent_repository.save_torrent(torrent=torrent)
|
|
|
|
if self.notification_service:
|
|
self.notification_service.send_notification_to_all_providers(
|
|
title="Movie Downloaded",
|
|
message=f"Movie {movie.name} has been successfully downloaded and imported.",
|
|
)
|
|
else:
|
|
log.error(
|
|
f"Failed to import files for torrent {torrent.title}. Check logs for details."
|
|
)
|
|
|
|
if self.notification_service:
|
|
self.notification_service.send_notification_to_all_providers(
|
|
title="Import Failed",
|
|
message=f"Failed to import files for movie {movie.name}. Please check logs.",
|
|
)
|
|
|
|
log.info(f"Finished importing files for torrent {torrent.title}")
|
|
|
|
def get_import_candidates(
|
|
self, movie: Path, metadata_provider: AbstractMetadataProvider
|
|
) -> MediaImportSuggestion:
|
|
search_result = self.search_for_movie(
|
|
query=remove_special_chars_and_parentheses(movie.name),
|
|
metadata_provider=metadata_provider,
|
|
)
|
|
import_candidates = MediaImportSuggestion(
|
|
directory=movie,
|
|
candidates=search_result,
|
|
)
|
|
log.debug(
|
|
f"Found {len(search_result)} candidates for {movie.name} in {movie.parent}"
|
|
)
|
|
return import_candidates
|
|
|
|
def import_existing_movie(self, movie: Movie, source_directory: Path) -> bool:
|
|
new_source_path = source_directory.parent / ("." + source_directory.name)
|
|
try:
|
|
source_directory.rename(new_source_path)
|
|
except Exception as e:
|
|
log.exception(f"Failed to rename {source_directory} to {new_source_path}")
|
|
raise RenameError from e
|
|
|
|
video_files, subtitle_files, _all_files = get_files_for_import(
|
|
directory=new_source_path
|
|
)
|
|
|
|
success = self.import_movie(
|
|
movie=movie,
|
|
video_files=video_files,
|
|
subtitle_files=subtitle_files,
|
|
file_path_suffix="IMPORTED",
|
|
)
|
|
if success:
|
|
self.movie_repository.add_movie_file(
|
|
MovieFile(
|
|
movie_id=movie.id,
|
|
file_path_suffix="IMPORTED",
|
|
torrent_id=None,
|
|
quality=Quality.unknown,
|
|
)
|
|
)
|
|
|
|
return success
|
|
|
|
def update_movie_metadata(
|
|
self, db_movie: Movie, metadata_provider: AbstractMetadataProvider
|
|
) -> Movie | None:
|
|
"""
|
|
Updates the metadata of a movie.
|
|
|
|
:param metadata_provider: The metadata provider object to fetch fresh data from.
|
|
:param db_movie: The Movie to update
|
|
:return: The updated Movie object, or None if the movie is not found or an error occurs.
|
|
"""
|
|
log.debug(f"Found movie: {db_movie.name} for metadata update.")
|
|
|
|
# Use stored original_language preference for metadata fetching
|
|
fresh_movie_data = metadata_provider.get_movie_metadata(
|
|
movie_id=db_movie.external_id, language=db_movie.original_language
|
|
)
|
|
if not fresh_movie_data:
|
|
log.warning(
|
|
f"Could not fetch fresh metadata for movie: {db_movie.name} ({db_movie.year})"
|
|
)
|
|
return None
|
|
log.debug(f"Fetched fresh metadata for movie: {fresh_movie_data.name}")
|
|
|
|
self.movie_repository.update_movie_attributes(
|
|
movie_id=db_movie.id,
|
|
name=fresh_movie_data.name,
|
|
overview=fresh_movie_data.overview,
|
|
year=fresh_movie_data.year,
|
|
imdb_id=fresh_movie_data.imdb_id,
|
|
)
|
|
|
|
updated_movie = self.movie_repository.get_movie_by_id(movie_id=db_movie.id)
|
|
|
|
log.info(
|
|
f"Successfully updated metadata for movie: {db_movie.name} ({db_movie.year})"
|
|
)
|
|
metadata_provider.download_movie_poster_image(movie=updated_movie)
|
|
return updated_movie
|
|
|
|
def get_importable_movies(
|
|
self, metadata_provider: AbstractMetadataProvider
|
|
) -> list[MediaImportSuggestion]:
|
|
movie_root_path = MediaManagerConfig().misc.movie_directory
|
|
importable_movies: list[MediaImportSuggestion] = []
|
|
candidate_dirs = get_importable_media_directories(movie_root_path)
|
|
|
|
for movie_dir in candidate_dirs:
|
|
metadata, external_id = extract_external_id_from_string(movie_dir.name)
|
|
if metadata is not None and external_id is not None:
|
|
try:
|
|
self.movie_repository.get_movie_by_external_id(
|
|
external_id=external_id, metadata_provider=metadata
|
|
)
|
|
log.debug(
|
|
f"Movie {movie_dir.name} already exists in the database, skipping."
|
|
)
|
|
continue
|
|
except NotFoundError:
|
|
log.debug(
|
|
f"Movie {movie_dir.name} not found in database, checking for import candidates."
|
|
)
|
|
|
|
import_candidates = self.get_import_candidates(
|
|
movie=movie_dir, metadata_provider=metadata_provider
|
|
)
|
|
importable_movies.append(import_candidates)
|
|
|
|
log.debug(f"Found {len(importable_movies)} importable movies.")
|
|
return importable_movies
|
|
|
|
def import_all_torrents(self) -> None:
|
|
log.info("Importing all torrents")
|
|
torrents = self.torrent_service.get_all_torrents()
|
|
log.info("Found %d torrents to import", len(torrents))
|
|
for t in torrents:
|
|
try:
|
|
if not t.imported and t.status == TorrentStatus.finished:
|
|
movie = self.torrent_service.get_movie_of_torrent(torrent=t)
|
|
if movie is None:
|
|
log.warning(
|
|
f"torrent {t.title} is not a movie torrent, skipping import."
|
|
)
|
|
continue
|
|
self.import_torrent_files(torrent=t, movie=movie)
|
|
except RuntimeError:
|
|
log.exception(f"Failed to import torrent {t.title}")
|
|
log.info("Finished importing all torrents")
|
|
|
|
def update_all_metadata(self) -> None:
|
|
"""Updates the metadata of all movies."""
|
|
log.info("Updating metadata for all movies")
|
|
movies = self.movie_repository.get_movies()
|
|
log.info(f"Found {len(movies)} movies to update")
|
|
for movie in movies:
|
|
try:
|
|
if movie.metadata_provider == "tmdb":
|
|
metadata_provider = TmdbMetadataProvider()
|
|
elif movie.metadata_provider == "tvdb":
|
|
metadata_provider = TvdbMetadataProvider()
|
|
else:
|
|
log.error(
|
|
f"Unsupported metadata provider {movie.metadata_provider} for movie {movie.name}, skipping update."
|
|
)
|
|
continue
|
|
except InvalidConfigError:
|
|
log.exception(
|
|
f"Error initializing metadata provider {movie.metadata_provider} for movie {movie.name}",
|
|
)
|
|
continue
|
|
self.update_movie_metadata(
|
|
db_movie=movie, metadata_provider=metadata_provider
|
|
)
|