From bcd1ccfd2143d88f055208278039320e244dd16a Mon Sep 17 00:00:00 2001 From: maxid Date: Fri, 19 Dec 2025 18:41:51 +0100 Subject: [PATCH 01/15] enhance torrent deletion to remove associated media files if the torrent was not imported --- media_manager/torrent/repository.py | 17 +++++++++++++++-- media_manager/torrent/service.py | 5 ++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/media_manager/torrent/repository.py b/media_manager/torrent/repository.py index 1cb0133..a09451a 100644 --- a/media_manager/torrent/repository.py +++ b/media_manager/torrent/repository.py @@ -1,4 +1,4 @@ -from sqlalchemy import select +from sqlalchemy import select, delete from media_manager.database import DbSessionDependency from media_manager.torrent.models import Torrent @@ -55,7 +55,20 @@ class TorrentRepository: raise NotFoundError(f"Torrent with ID {torrent_id} not found.") return TorrentSchema.model_validate(result) - def delete_torrent(self, torrent_id: TorrentId): + def delete_torrent( + self, torrent_id: TorrentId, delete_associated_media_files: bool = False + ): + if delete_associated_media_files: + movie_files_stmt = delete(MovieFile).where( + MovieFile.torrent_id == torrent_id + ) + self.db.execute(movie_files_stmt) + + season_files_stmt = delete(SeasonFile).where( + SeasonFile.torrent_id == torrent_id + ) + self.db.execute(season_files_stmt) + self.db.delete(self.db.get(Torrent, torrent_id)) def get_movie_of_torrent(self, torrent_id: TorrentId): diff --git a/media_manager/torrent/service.py b/media_manager/torrent/service.py index 1322bca..80bb430 100644 --- a/media_manager/torrent/service.py +++ b/media_manager/torrent/service.py @@ -107,7 +107,10 @@ class TorrentService: def delete_torrent(self, torrent_id: TorrentId): t = self.torrent_repository.get_torrent_by_id(torrent_id=torrent_id) - self.torrent_repository.delete_torrent(torrent_id=t.id) + delete_media_files = not t.imported + self.torrent_repository.delete_torrent( + torrent_id=torrent_id, delete_associated_media_files=delete_media_files + ) def get_movie_files_of_torrent(self, torrent: Torrent): return self.torrent_repository.get_movie_files_of_torrent(torrent_id=torrent.id) From 1edb2cae9b0a2a1ae6acf99d77f8c04bac2b5edc Mon Sep 17 00:00:00 2001 From: maxid Date: Fri, 19 Dec 2025 18:42:46 +0100 Subject: [PATCH 02/15] format files --- media_manager/movies/router.py | 1 - media_manager/movies/service.py | 15 +++++++++------ media_manager/tv/service.py | 11 ++++++----- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/media_manager/movies/router.py b/media_manager/movies/router.py index b0dc4e5..7d89fe0 100644 --- a/media_manager/movies/router.py +++ b/media_manager/movies/router.py @@ -87,7 +87,6 @@ def delete_a_movie( ) - # -------------------------------- # GET MOVIES # -------------------------------- diff --git a/media_manager/movies/service.py b/media_manager/movies/service.py index 7d8db9c..c473df4 100644 --- a/media_manager/movies/service.py +++ b/media_manager/movies/service.py @@ -143,12 +143,12 @@ class MovieService: if delete_torrents: # Get all torrents associated with this movie - torrents = self.movie_repository.get_torrents_by_movie_id(movie_id=movie_id) + torrents = self.movie_repository.get_torrents_by_movie_id( + movie_id=movie_id + ) for torrent in torrents: try: - self.torrent_service.cancel_download( - torrent, delete_files=True - ) + self.torrent_service.cancel_download(torrent, delete_files=True) log.info(f"Deleted torrent: {torrent.hash}") except Exception as e: log.warning(f"Failed to delete torrent {torrent.hash}: {e}") @@ -275,11 +275,14 @@ class MovieService: # 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 + 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}") + log.error( + f"Unable to find internal movie ID for {result.external_id} on {metadata_provider.name}" + ) return results def get_popular_movies( diff --git a/media_manager/tv/service.py b/media_manager/tv/service.py index 8af7b5d..386bbaf 100644 --- a/media_manager/tv/service.py +++ b/media_manager/tv/service.py @@ -162,9 +162,7 @@ class TvService: torrents = self.tv_repository.get_torrents_by_show_id(show_id=show_id) for torrent in torrents: try: - self.torrent_service.cancel_download( - torrent, delete_files=True - ) + self.torrent_service.cancel_download(torrent, delete_files=True) log.info(f"Deleted torrent: {torrent.hash}") except Exception as e: log.warning(f"Failed to delete torrent {torrent.hash}: {e}") @@ -288,11 +286,14 @@ class TvService: # Fetch the internal show ID. try: show = self.tv_repository.get_show_by_external_id( - external_id=result.external_id, metadata_provider=metadata_provider.name + external_id=result.external_id, + metadata_provider=metadata_provider.name, ) result.id = show.id except Exception: - log.error(f"Unable to find internal show ID for {result.external_id} on {metadata_provider.name}") + log.error( + f"Unable to find internal show ID for {result.external_id} on {metadata_provider.name}" + ) return results def get_popular_shows( From 1f50b18b9f2344ea811e95fa2bd6bb80e3d143f5 Mon Sep 17 00:00:00 2001 From: maxid <97409287+maxdorninger@users.noreply.github.com> Date: Sat, 20 Dec 2025 19:37:34 +0100 Subject: [PATCH 03/15] refactor media detection functions to improve accuracy --- media_manager/movies/router.py | 14 +++---------- media_manager/movies/service.py | 33 ++++++++++++++++++++++++++++++ media_manager/torrent/utils.py | 36 +++++++++++++++++++++------------ media_manager/tv/router.py | 14 +++---------- media_manager/tv/service.py | 34 +++++++++++++++++++++++++++++++ 5 files changed, 96 insertions(+), 35 deletions(-) diff --git a/media_manager/movies/router.py b/media_manager/movies/router.py index 7d89fe0..4bc690f 100644 --- a/media_manager/movies/router.py +++ b/media_manager/movies/router.py @@ -12,7 +12,7 @@ from media_manager.indexer.schemas import ( ) from media_manager.metadataProvider.schemas import MetaDataProviderSearchResult from media_manager.schemas import MediaImportSuggestion -from media_manager.torrent.utils import detect_unknown_media +from media_manager.torrent.utils import get_importable_media_directories from media_manager.torrent.schemas import Torrent from media_manager.movies import log from media_manager.movies.schemas import ( @@ -104,15 +104,7 @@ def get_all_importable_movies( """ get a list of unknown movies that were detected in the movie directory and are importable """ - directories = detect_unknown_media(AllEncompassingConfig().misc.movie_directory) - movies = [] - for directory in directories: - movies.append( - movie_service.get_import_candidates( - movie=directory, metadata_provider=metadata_provider - ) - ) - return movies + return movie_service.get_importable_movies(metadata_provider=metadata_provider) @router.post( @@ -127,7 +119,7 @@ def import_detected_movie( get a list of unknown movies that were detected in the movie directory and are importable """ source_directory = Path(directory) - if source_directory not in detect_unknown_media( + if source_directory not in get_importable_media_directories( AllEncompassingConfig().misc.movie_directory ): raise HTTPException(status.HTTP_400_BAD_REQUEST, "No such directory") diff --git a/media_manager/movies/service.py b/media_manager/movies/service.py index c473df4..d9d36d3 100644 --- a/media_manager/movies/service.py +++ b/media_manager/movies/service.py @@ -38,6 +38,8 @@ from media_manager.torrent.utils import ( get_files_for_import, remove_special_characters, strip_trailing_year, + get_importable_media_directories, + extract_external_id_from_string, ) from media_manager.indexer.service import IndexerService from media_manager.metadataProvider.abstractMetaDataProvider import ( @@ -716,6 +718,37 @@ class MovieService: 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 = AllEncompassingConfig().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 auto_download_all_approved_movie_requests() -> None: """ diff --git a/media_manager/torrent/utils.py b/media_manager/torrent/utils.py index 25b37f9..bc55f47 100644 --- a/media_manager/torrent/utils.py +++ b/media_manager/torrent/utils.py @@ -197,24 +197,34 @@ def strip_trailing_year(title: str) -> str: return re.sub(r"\s*\(\d{4}\)\s*$", "", title).strip() -def detect_unknown_media(path: Path) -> list[Path]: +def get_importable_media_directories(path: Path) -> list[Path]: libraries = [] libraries.extend(AllEncompassingConfig().misc.movie_libraries) libraries.extend(AllEncompassingConfig().misc.tv_libraries) - show_dirs = path.glob("*") + unfiltered_dirs = path.glob("*") log.debug(f"Using Directory {path}") - unknown_tv_shows = [] - for media_dir in show_dirs: - # check if directory is one created by MediaManager (contains [tmdbd/tvdbid-0000) or if it is a library - if ( - re.search(r"\[(?:tmdbid|tvdbid)-\d+]", media_dir.name, re.IGNORECASE) - or media_dir.absolute() - in [Path(library.path).absolute() for library in libraries] - or media_dir.name.startswith(".") - ): + media_dirs = [] + for media_dir in unfiltered_dirs: + if media_dir.absolute() in [ + Path(library.path).absolute() for library in libraries + ] or media_dir.name.startswith("."): log.debug(f"MediaManager directory detected: {media_dir.name}") else: log.info(f"Detected unknown media directory: {media_dir.name}") - unknown_tv_shows.append(media_dir) - return unknown_tv_shows + media_dirs.append(media_dir) + return media_dirs + + +def extract_external_id_from_string(input_string: str) -> tuple[str | None, int | None]: + """ + Extracts an external ID (tmdb/tvdb ID) from the given string. + + :param input_string: The string to extract the ID from. + :return: The extracted Metadata Provider and ID or None if not found. + """ + match = re.search(r"(tmdb|tvdb)(?:id)?[-_]?([0-9]+)", input_string, re.IGNORECASE) + if match: + return match.group(1).lower(), int(match.group(2)) + + return None, None diff --git a/media_manager/tv/router.py b/media_manager/tv/router.py index 69d99b5..31338ec 100644 --- a/media_manager/tv/router.py +++ b/media_manager/tv/router.py @@ -12,7 +12,7 @@ from media_manager.indexer.schemas import ( IndexerQueryResult, ) from media_manager.metadataProvider.schemas import MetaDataProviderSearchResult -from media_manager.torrent.utils import detect_unknown_media +from media_manager.torrent.utils import get_importable_media_directories from media_manager.torrent.schemas import Torrent from media_manager.tv import log from media_manager.exceptions import MediaAlreadyExists @@ -125,15 +125,7 @@ def get_all_importable_shows( """ get a list of unknown shows that were detected in the tv directory and are importable """ - directories = detect_unknown_media(AllEncompassingConfig().misc.tv_directory) - shows = [] - for directory in directories: - shows.append( - tv_service.get_import_candidates( - tv_show=directory, metadata_provider=metadata_provider - ) - ) - return shows + return tv_service.get_importable_tv_shows(metadata_provider=metadata_provider) @router.post( @@ -146,7 +138,7 @@ def import_detected_show(tv_service: tv_service_dep, tv_show: show_dep, director Import a detected show from the specified directory into the library. """ source_directory = Path(directory) - if source_directory not in detect_unknown_media( + if source_directory not in get_importable_media_directories( AllEncompassingConfig().misc.tv_directory ): raise HTTPException(status.HTTP_400_BAD_REQUEST, "No such directory") diff --git a/media_manager/tv/service.py b/media_manager/tv/service.py index 386bbaf..16fa8b2 100644 --- a/media_manager/tv/service.py +++ b/media_manager/tv/service.py @@ -43,6 +43,8 @@ from media_manager.torrent.utils import ( get_files_for_import, remove_special_characters, strip_trailing_year, + get_importable_media_directories, + extract_external_id_from_string, ) from media_manager.indexer.service import IndexerService from media_manager.metadataProvider.abstractMetaDataProvider import ( @@ -912,6 +914,38 @@ class TvService: except Exception as e: log.error(f"Failed to rename {source_directory} to {new_source_path}: {e}") + def get_importable_tv_shows( + self, metadata_provider: AbstractMetadataProvider + ) -> list[MediaImportSuggestion]: + tv_directory = AllEncompassingConfig().misc.tv_directory + import_suggestions: list[MediaImportSuggestion] = [] + candidate_dirs = get_importable_media_directories(tv_directory) + + for item in candidate_dirs: + metadata, external_id = extract_external_id_from_string(item.name) + if metadata is not None and external_id is not None: + try: + self.tv_repository.get_show_by_external_id( + external_id=external_id, + metadata_provider=metadata, + ) + log.debug( + f"Show {item.name} already exists in the database, skipping import suggestion." + ) + continue + except NotFoundError: + log.debug( + f"Show {item.name} not found in database, checking for import candidates." + ) + + import_suggestion = self.get_import_candidates( + tv_show=item, metadata_provider=metadata_provider + ) + import_suggestions.append(import_suggestion) + + log.debug(f"Detected {len(import_suggestions)} importable TV shows.") + return import_suggestions + def auto_download_all_approved_season_requests() -> None: """ From cfe34358a0c7d84d85d83d629848450d8335d7f9 Mon Sep 17 00:00:00 2001 From: maxid <97409287+maxdorninger@users.noreply.github.com> Date: Sat, 20 Dec 2025 20:00:16 +0100 Subject: [PATCH 04/15] refactor title processing to remove special characters and brackets --- media_manager/movies/service.py | 4 ++-- media_manager/torrent/utils.py | 22 +++++++++++++++++++--- media_manager/tv/service.py | 4 ++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/media_manager/movies/service.py b/media_manager/movies/service.py index d9d36d3..646023b 100644 --- a/media_manager/movies/service.py +++ b/media_manager/movies/service.py @@ -37,9 +37,9 @@ from media_manager.torrent.utils import ( import_file, get_files_for_import, remove_special_characters, - strip_trailing_year, get_importable_media_directories, extract_external_id_from_string, + remove_special_chars_and_parentheses, ) from media_manager.indexer.service import IndexerService from media_manager.metadataProvider.abstractMetaDataProvider import ( @@ -643,7 +643,7 @@ class MovieService: self, movie: Path, metadata_provider: AbstractMetadataProvider ) -> MediaImportSuggestion: search_result = self.search_for_movie( - strip_trailing_year(movie.name), metadata_provider + remove_special_chars_and_parentheses(movie.name), metadata_provider ) import_candidates = MediaImportSuggestion( directory=movie, candidates=search_result diff --git a/media_manager/torrent/utils.py b/media_manager/torrent/utils.py index bc55f47..f2b5e0e 100644 --- a/media_manager/torrent/utils.py +++ b/media_manager/torrent/utils.py @@ -190,11 +190,27 @@ def remove_special_characters(filename: str) -> str: return sanitized -def strip_trailing_year(title: str) -> str: +def remove_special_chars_and_parentheses(title: str) -> str: """ - Removes a trailing space + (4-digit year) at end of string + Removes special characters and bracketed information from the title. + + :param title: The original title. + :return: A sanitized version of the title. """ - return re.sub(r"\s*\(\d{4}\)\s*$", "", title).strip() + + # Remove content within brackets + sanitized = re.sub(r"\[.*?\]", "", title) + + # Remove content within curly brackets + sanitized = re.sub(r"\{.*?\}", "", sanitized) + + # Remove year within parentheses + sanitized = re.sub(r"\(\d{4}\)", "", sanitized) + + # Remove special characters + sanitized = remove_special_characters(sanitized) + + return sanitized def get_importable_media_directories(path: Path) -> list[Path]: diff --git a/media_manager/tv/service.py b/media_manager/tv/service.py index 16fa8b2..cea477f 100644 --- a/media_manager/tv/service.py +++ b/media_manager/tv/service.py @@ -42,9 +42,9 @@ from media_manager.torrent.utils import ( import_file, get_files_for_import, remove_special_characters, - strip_trailing_year, get_importable_media_directories, extract_external_id_from_string, + remove_special_chars_and_parentheses, ) from media_manager.indexer.service import IndexerService from media_manager.metadataProvider.abstractMetaDataProvider import ( @@ -877,7 +877,7 @@ class TvService: self, tv_show: Path, metadata_provider: AbstractMetadataProvider ) -> MediaImportSuggestion: search_result = self.search_for_show( - strip_trailing_year(tv_show.name), metadata_provider + remove_special_chars_and_parentheses(tv_show.name), metadata_provider ) import_candidates = MediaImportSuggestion( directory=tv_show, candidates=search_result From d5c649d5bff65f60556eb776a6b7f1d387cccfb4 Mon Sep 17 00:00:00 2001 From: maxid <97409287+maxdorninger@users.noreply.github.com> Date: Sat, 20 Dec 2025 20:05:02 +0100 Subject: [PATCH 05/15] refactor search queries to remove special characters and parentheses --- media_manager/metadataProvider/tmdb.py | 13 +++++++++++-- media_manager/metadataProvider/tvdb.py | 11 +++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/media_manager/metadataProvider/tmdb.py b/media_manager/metadataProvider/tmdb.py index 45f27ea..e554aaa 100644 --- a/media_manager/metadataProvider/tmdb.py +++ b/media_manager/metadataProvider/tmdb.py @@ -8,6 +8,7 @@ from media_manager.metadataProvider.abstractMetaDataProvider import ( AbstractMetadataProvider, ) from media_manager.metadataProvider.schemas import MetaDataProviderSearchResult +from media_manager.torrent.utils import remove_special_chars_and_parentheses from media_manager.tv.schemas import Episode, Season, Show, SeasonNumber, EpisodeNumber from media_manager.movies.schemas import Movie from media_manager.notification.manager import notification_manager @@ -60,7 +61,11 @@ class TmdbMetadataProvider(AbstractMetadataProvider): def __search_tv(self, query: str, page: int) -> dict: try: response = requests.get( - url=f"{self.url}/tv/search", params={"query": query, "page": page} + url=f"{self.url}/tv/search", + params={ + "query": remove_special_chars_and_parentheses(query), + "page": page, + }, ) response.raise_for_status() return response.json() @@ -104,7 +109,11 @@ class TmdbMetadataProvider(AbstractMetadataProvider): def __search_movie(self, query: str, page: int) -> dict: try: response = requests.get( - url=f"{self.url}/movies/search", params={"query": query, "page": page} + url=f"{self.url}/movies/search", + params={ + "query": remove_special_chars_and_parentheses(query), + "page": page, + }, ) response.raise_for_status() return response.json() diff --git a/media_manager/metadataProvider/tvdb.py b/media_manager/metadataProvider/tvdb.py index 1c90de6..5e5b93d 100644 --- a/media_manager/metadataProvider/tvdb.py +++ b/media_manager/metadataProvider/tvdb.py @@ -8,6 +8,7 @@ from media_manager.metadataProvider.abstractMetaDataProvider import ( AbstractMetadataProvider, ) from media_manager.metadataProvider.schemas import MetaDataProviderSearchResult +from media_manager.torrent.utils import remove_special_chars_and_parentheses from media_manager.tv.schemas import Episode, Season, Show, SeasonNumber from media_manager.movies.schemas import Movie @@ -29,7 +30,10 @@ class TvdbMetadataProvider(AbstractMetadataProvider): return requests.get(f"{self.url}/tv/seasons/{id}").json() def __search_tv(self, query: str) -> dict: - return requests.get(f"{self.url}/tv/search", params={"query": query}).json() + return requests.get( + f"{self.url}/tv/search", + params={"query": remove_special_chars_and_parentheses(query)}, + ).json() def __get_trending_tv(self) -> dict: return requests.get(f"{self.url}/tv/trending").json() @@ -38,7 +42,10 @@ class TvdbMetadataProvider(AbstractMetadataProvider): return requests.get(f"{self.url}/movies/{id}").json() def __search_movie(self, query: str) -> dict: - return requests.get(f"{self.url}/movies/search", params={"query": query}).json() + return requests.get( + f"{self.url}/movies/search", + params={"query": remove_special_chars_and_parentheses(query)}, + ).json() def __get_trending_movies(self) -> dict: return requests.get(f"{self.url}/movies/trending").json() From ed78bde6040af930b9daf809b38ecce537e68d78 Mon Sep 17 00:00:00 2001 From: maxid <97409287+maxdorninger@users.noreply.github.com> Date: Sat, 20 Dec 2025 20:22:35 +0100 Subject: [PATCH 06/15] reduce logs --- media_manager/torrent/utils.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/media_manager/torrent/utils.py b/media_manager/torrent/utils.py index f2b5e0e..25666c4 100644 --- a/media_manager/torrent/utils.py +++ b/media_manager/torrent/utils.py @@ -219,15 +219,11 @@ def get_importable_media_directories(path: Path) -> list[Path]: libraries.extend(AllEncompassingConfig().misc.tv_libraries) unfiltered_dirs = path.glob("*") - log.debug(f"Using Directory {path}") media_dirs = [] for media_dir in unfiltered_dirs: - if media_dir.absolute() in [ + if media_dir.absolute() not in [ Path(library.path).absolute() for library in libraries ] or media_dir.name.startswith("."): - log.debug(f"MediaManager directory detected: {media_dir.name}") - else: - log.info(f"Detected unknown media directory: {media_dir.name}") media_dirs.append(media_dir) return media_dirs From ecc030238e72dace48d2826b1d3172b32c577166 Mon Sep 17 00:00:00 2001 From: maxid <97409287+maxdorninger@users.noreply.github.com> Date: Sat, 20 Dec 2025 20:30:33 +0100 Subject: [PATCH 07/15] fix bug: importable shows saves to importable movies variable --- web/src/routes/dashboard/+layout.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/routes/dashboard/+layout.svelte b/web/src/routes/dashboard/+layout.svelte index 39993e7..4d4ca94 100644 --- a/web/src/routes/dashboard/+layout.svelte +++ b/web/src/routes/dashboard/+layout.svelte @@ -29,7 +29,7 @@ }); client.GET('/api/v1/tv/importable').then(({ data, error }) => { if (!error) { - importableMovies = data; + importableShows = data; } }); } From 12fe84017cb6ef7857a9569b63ab36efe329ca66 Mon Sep 17 00:00:00 2001 From: maxid <97409287+maxdorninger@users.noreply.github.com> Date: Sat, 20 Dec 2025 20:31:31 +0100 Subject: [PATCH 08/15] fix bug: hidden directories included in import suggestions --- media_manager/torrent/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/media_manager/torrent/utils.py b/media_manager/torrent/utils.py index 25666c4..0ddd0f5 100644 --- a/media_manager/torrent/utils.py +++ b/media_manager/torrent/utils.py @@ -223,7 +223,7 @@ def get_importable_media_directories(path: Path) -> list[Path]: for media_dir in unfiltered_dirs: if media_dir.absolute() not in [ Path(library.path).absolute() for library in libraries - ] or media_dir.name.startswith("."): + ] and not media_dir.name.startswith("."): media_dirs.append(media_dir) return media_dirs From 5c8cff00a9ce627b8564dc88d7e4162c95f544ea Mon Sep 17 00:00:00 2001 From: Maximilian Dorninger <97409287+maxdorninger@users.noreply.github.com> Date: Sat, 20 Dec 2025 20:58:06 +0100 Subject: [PATCH 09/15] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- media_manager/torrent/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/media_manager/torrent/utils.py b/media_manager/torrent/utils.py index 0ddd0f5..64823a2 100644 --- a/media_manager/torrent/utils.py +++ b/media_manager/torrent/utils.py @@ -210,6 +210,8 @@ def remove_special_chars_and_parentheses(title: str) -> str: # Remove special characters sanitized = remove_special_characters(sanitized) + # Collapse multiple whitespace characters and trim the result + sanitized = re.sub(r"\s+", " ", sanitized).strip() return sanitized @@ -235,7 +237,7 @@ def extract_external_id_from_string(input_string: str) -> tuple[str | None, int :param input_string: The string to extract the ID from. :return: The extracted Metadata Provider and ID or None if not found. """ - match = re.search(r"(tmdb|tvdb)(?:id)?[-_]?([0-9]+)", input_string, re.IGNORECASE) + match = re.search(r"\b(tmdb|tvdb)(?:id)?[-_]?([0-9]+)\b", input_string, re.IGNORECASE) if match: return match.group(1).lower(), int(match.group(2)) From b854a13338d59ed53dd403776744705af248bf98 Mon Sep 17 00:00:00 2001 From: Maximilian Dorninger <97409287+maxdorninger@users.noreply.github.com> Date: Sat, 20 Dec 2025 20:59:28 +0100 Subject: [PATCH 10/15] Update media_manager/torrent/utils.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- media_manager/torrent/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/media_manager/torrent/utils.py b/media_manager/torrent/utils.py index 64823a2..40b2170 100644 --- a/media_manager/torrent/utils.py +++ b/media_manager/torrent/utils.py @@ -220,12 +220,12 @@ def get_importable_media_directories(path: Path) -> list[Path]: libraries.extend(AllEncompassingConfig().misc.movie_libraries) libraries.extend(AllEncompassingConfig().misc.tv_libraries) + library_paths = {Path(library.path).absolute() for library in libraries} + unfiltered_dirs = path.glob("*") media_dirs = [] for media_dir in unfiltered_dirs: - if media_dir.absolute() not in [ - Path(library.path).absolute() for library in libraries - ] and not media_dir.name.startswith("."): + if media_dir.absolute() not in library_paths and not media_dir.name.startswith("."): media_dirs.append(media_dir) return media_dirs From e0a04bb040267a9abeb03f6ef19e90d259d0821e Mon Sep 17 00:00:00 2001 From: maxid <97409287+maxdorninger@users.noreply.github.com> Date: Sat, 20 Dec 2025 21:01:48 +0100 Subject: [PATCH 11/15] fix: filter out non-directory items from import suggestions --- media_manager/torrent/utils.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/media_manager/torrent/utils.py b/media_manager/torrent/utils.py index 40b2170..f0e5566 100644 --- a/media_manager/torrent/utils.py +++ b/media_manager/torrent/utils.py @@ -222,10 +222,13 @@ def get_importable_media_directories(path: Path) -> list[Path]: library_paths = {Path(library.path).absolute() for library in libraries} - unfiltered_dirs = path.glob("*") + unfiltered_dirs = [d for d in path.glob("*") if d.is_dir()] + media_dirs = [] for media_dir in unfiltered_dirs: - if media_dir.absolute() not in library_paths and not media_dir.name.startswith("."): + if media_dir.absolute() not in library_paths and not media_dir.name.startswith( + "." + ): media_dirs.append(media_dir) return media_dirs @@ -237,7 +240,9 @@ def extract_external_id_from_string(input_string: str) -> tuple[str | None, int :param input_string: The string to extract the ID from. :return: The extracted Metadata Provider and ID or None if not found. """ - match = re.search(r"\b(tmdb|tvdb)(?:id)?[-_]?([0-9]+)\b", input_string, re.IGNORECASE) + match = re.search( + r"\b(tmdb|tvdb)(?:id)?[-_]?([0-9]+)\b", input_string, re.IGNORECASE + ) if match: return match.group(1).lower(), int(match.group(2)) From 6b2f426ff9f905db50a5795c5625da1775331125 Mon Sep 17 00:00:00 2001 From: maxid <97409287+maxdorninger@users.noreply.github.com> Date: Sat, 20 Dec 2025 21:10:40 +0100 Subject: [PATCH 12/15] update docs --- Writerside/topics/Importing-existing-media.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Writerside/topics/Importing-existing-media.md b/Writerside/topics/Importing-existing-media.md index 78a0e56..ff91d41 100644 --- a/Writerside/topics/Importing-existing-media.md +++ b/Writerside/topics/Importing-existing-media.md @@ -1,11 +1,10 @@ # Importing existing media In order for MediaManager to be able to import existing media (e.g. downloaded by Sonarr or Radarr) -3 conditions have to be met: +2 conditions have to be met: -1. The folder's name must not contain `[tmdbid-xxxxx]` or `[tvdbid-xxxxx]`. -2. The folder's name must not start with a dot. -3. The media must be in the root tv/movie library +1. The folder's name must not start with a dot. +2. The media must be in the root tv/movie library Here is an example, using these rules: @@ -14,9 +13,8 @@ Here is an example, using these rules: └── data/ ├── tv/ │ ├── Rick and Morty # WILL be imported - │ ├── Stranger Things (2016) # WILL be imported - │ │ - │ ├── Breaking Bad (2008) [tmdbid-1396] # WILL NOT be imported + │ ├── Stranger Things (2016) {tvdb_12345} [x265] # WILL be imported + │ ├── Breaking Bad (2008) [tmdbid-1396] # WILL be imported │ ├── .The Office (2013) # WILL NOT │ │ │ └── my-custom-library/ @@ -44,16 +42,18 @@ So after importing, the directory would look like this (using the above director │ ├── .Rick and Morty # RENAMED │ ├── Rick and Morty (2013) [tmdbid-60625] # IMPORTED │ │ - │ ├── .Stranger Things (2016) # RENAMED + │ ├── .Stranger Things (2016) {tvdb_12345} [x265] │ ├── Stranger Things (2016) [tmdbid-66732] # IMPORTED │ │ │ ├── .The Office (2013) # IGNORED - │ ├── Breaking Bad (2008) [tmdbid-1396] # IGNORED + │ │ + │ ├── .Breaking Bad (2008) [tmdbid-1396] + │ ├── Breaking Bad (2008) [tmdbid-1396] # IMPORTED │ │ │ └── my-custom-library/ │ └── The Simpsons # IGNORED └── movie/ - ├── .Oppenheimer (2023) # RENAMED + ├── .Oppenheimer (2023) └── Oppenheimer (2023) [tmdbid-872585] # IMPORTED ``` From 1293cc692cc435b73e7f198b35e6d3ebacd78aa3 Mon Sep 17 00:00:00 2001 From: maxid <97409287+maxdorninger@users.noreply.github.com> Date: Sat, 20 Dec 2025 21:16:26 +0100 Subject: [PATCH 13/15] fix: rename source directory before importing existing movies and TV shows --- media_manager/movies/service.py | 20 ++++++++++---------- media_manager/tv/service.py | 15 ++++++++------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/media_manager/movies/service.py b/media_manager/movies/service.py index 646023b..0782f51 100644 --- a/media_manager/movies/service.py +++ b/media_manager/movies/service.py @@ -654,8 +654,17 @@ class MovieService: 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.error( + f"Failed to rename directory '{source_directory}' to '{new_source_path}': {e}" + ) + raise Exception("Failed to rename directory") from e + video_files, subtitle_files, all_files = get_files_for_import( - directory=source_directory + directory=new_source_path ) success = self.import_movie( @@ -674,15 +683,6 @@ class MovieService: ) ) - new_source_path = source_directory.parent / ("." + source_directory.name) - try: - source_directory.rename(new_source_path) - except Exception as e: - log.error( - f"Failed to rename directory '{source_directory}' to '{new_source_path}': {e}" - ) - return False - return success def update_movie_metadata( diff --git a/media_manager/tv/service.py b/media_manager/tv/service.py index cea477f..0916e93 100644 --- a/media_manager/tv/service.py +++ b/media_manager/tv/service.py @@ -888,8 +888,15 @@ class TvService: return import_candidates def import_existing_tv_show(self, tv_show: Show, source_directory: Path) -> None: + new_source_path = source_directory.parent / ("." + source_directory.name) + try: + source_directory.rename(new_source_path) + except Exception as e: + log.error(f"Failed to rename {source_directory} to {new_source_path}: {e}") + raise Exception("Failed to rename source directory") from e + video_files, subtitle_files, all_files = get_files_for_import( - directory=source_directory + directory=new_source_path ) for season in tv_show.seasons: success, imported_episode_count = self.import_season( @@ -908,12 +915,6 @@ class TvService: if success or imported_episode_count > (len(season.episodes) / 2): self.tv_repository.add_season_file(season_file=season_file) - new_source_path = source_directory.parent / ("." + source_directory.name) - try: - source_directory.rename(new_source_path) - except Exception as e: - log.error(f"Failed to rename {source_directory} to {new_source_path}: {e}") - def get_importable_tv_shows( self, metadata_provider: AbstractMetadataProvider ) -> list[MediaImportSuggestion]: From 7d7f7b4fd51ab3f08fc0b57fc0f6c19f471c9f57 Mon Sep 17 00:00:00 2001 From: Maximilian Dorninger <97409287+maxdorninger@users.noreply.github.com> Date: Sun, 21 Dec 2025 14:34:47 +0100 Subject: [PATCH 14/15] Create CONTRIBUTING.md for project guidelines Added a contributing guide to help users understand how to contribute to the project. --- CONTRIBUTING.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e996117 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,30 @@ +# Contributing to MediaManager + +First off, thank you for considering contributing to MediaManager. + +## Why + +Following this guide helps me merge your PRs faster, prevents unnecessary back-and-forth and wasted effort. + +## How to suggest a feature or enhancement + +Open an issue which describes the feature you would like to +see, why you need it, and how it should work. +There we can discuss its scope and implementation. + +## How to contribute + +Generally, if you have any questions or need help on the implementation side of MediaManager, +just ask in the issue, or in a draft PR. + +### For something that is a one or two line fix: + +Make the change, and open a PR with a short description of what you changed and why. + +### For something that is bigger than a one or two line fix: + +Explain why you are making the change. +Be sure to give a rough overview on how your implementation works, and maybe any design decisions you made. +Also include any relevant limitations or trade-offs you made. + +It's best to also open an issue first to discuss larger changes before you start working on them. From 4fc033828e949549e670698df6a39dd4b53e95ba Mon Sep 17 00:00:00 2001 From: Maximilian Dorninger <97409287+maxdorninger@users.noreply.github.com> Date: Sun, 21 Dec 2025 14:36:55 +0100 Subject: [PATCH 15/15] Update CONTRIBUTING.md with developer guide link Added a link to the developer guide for setting up the dev environment. --- CONTRIBUTING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e996117..66ca907 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,6 +17,9 @@ There we can discuss its scope and implementation. Generally, if you have any questions or need help on the implementation side of MediaManager, just ask in the issue, or in a draft PR. +Also, see the contribution guide in the docs for information on how to setup the dev environment: +https://maxdorninger.github.io/MediaManager/developer-guide.html + ### For something that is a one or two line fix: Make the change, and open a PR with a short description of what you changed and why.