diff --git a/media_manager/movies/router.py b/media_manager/movies/router.py index 9076afd..b0dc4e5 100644 --- a/media_manager/movies/router.py +++ b/media_manager/movies/router.py @@ -27,7 +27,6 @@ from media_manager.movies.schemas import ( RichMovieRequest, ) from media_manager.movies.dependencies import ( - movie_repository_dep, movie_service_dep, movie_dep, ) @@ -73,10 +72,19 @@ def add_a_movie( @router.delete( "/{movie_id}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(current_active_user)], + dependencies=[Depends(current_superuser)], ) -def delete_a_movie(movie_service: movie_repository_dep, movie: movie_dep): - movie_service.delete_movie(movie_id=movie.id) +def delete_a_movie( + movie_service: movie_service_dep, + movie: movie_dep, + delete_files_on_disk: bool = False, + delete_torrents: bool = False, +): + movie_service.delete_movie( + movie_id=movie.id, + delete_files_on_disk=delete_files_on_disk, + delete_torrents=delete_torrents, + ) diff --git a/media_manager/movies/service.py b/media_manager/movies/service.py index 0931a5c..5914923 100644 --- a/media_manager/movies/service.py +++ b/media_manager/movies/service.py @@ -1,4 +1,5 @@ import re +import shutil from pathlib import Path from sqlalchemy.exc import IntegrityError @@ -113,6 +114,51 @@ class MovieService: """ self.movie_repository.delete_movie_request(movie_request_id=movie_request_id) + def delete_movie( + self, + movie_id: MovieId, + delete_files_on_disk: bool = False, + delete_torrents: bool = False, + ) -> None: + """ + Delete a movie from the database, optionally deleting files and torrents. + + :param movie_id: The ID of 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: + movie = self.movie_repository.get_movie(movie_id=movie_id) + + if delete_files_on_disk and movie.library: + log.info("Attempting to delete movie files from disk.") + # Get the movie's directory path + library_config = next( + (lib for lib in AllEncompassingConfig().misc.movie_libraries if lib.name == movie.library), + None + ) + log.debug(f"Library config for movie deletion: {library_config}") + if library_config: + movie_path = Path(library_config.path) / movie.folder_name + if movie_path.exists() and movie_path.is_dir(): + shutil.rmtree(movie_path) + log.info(f"Deleted movie directory: {movie_path}") + + if delete_torrents: + # Get all torrents associated with this movie + 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 + ) + log.info(f"Deleted torrent: {torrent.hash}") + except Exception as e: + log.warning(f"Failed to delete torrent {torrent.hash}: {e}") + + # Delete from database + self.movie_repository.delete_movie(movie_id=movie_id) + def get_public_movie_files_by_movie_id( self, movie_id: MovieId ) -> list[PublicMovieFile]: diff --git a/media_manager/tv/router.py b/media_manager/tv/router.py index 16956f4..69d99b5 100644 --- a/media_manager/tv/router.py +++ b/media_manager/tv/router.py @@ -34,7 +34,6 @@ from media_manager.schemas import MediaImportSuggestion from media_manager.tv.dependencies import ( season_dep, show_dep, - tv_repository_dep, tv_service_dep, ) from media_manager.metadataProvider.dependencies import metadata_provider_dep @@ -87,10 +86,19 @@ def get_total_count_of_downloaded_episodes(tv_service: tv_service_dep): @router.delete( "/shows/{show_id}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(current_active_user)], + dependencies=[Depends(current_superuser)], ) -def delete_a_show(tv_repository: tv_repository_dep, show: show_dep): - tv_repository.delete_show(show_id=show.id) +def delete_a_show( + tv_service: tv_service_dep, + show: show_dep, + delete_files_on_disk: bool = False, + delete_torrents: bool = False, +): + tv_service.delete_show( + show_id=show.id, + delete_files_on_disk=delete_files_on_disk, + delete_torrents=delete_torrents, + ) # -------------------------------- diff --git a/media_manager/tv/service.py b/media_manager/tv/service.py index 2640194..96339af 100644 --- a/media_manager/tv/service.py +++ b/media_manager/tv/service.py @@ -1,4 +1,5 @@ import re +import shutil from sqlalchemy.exc import IntegrityError @@ -129,6 +130,53 @@ class TvService: """ self.tv_repository.delete_season_request(season_request_id=season_request_id) + def delete_show( + self, + show_id: ShowId, + delete_files_on_disk: bool = False, + delete_torrents: bool = False, + ) -> None: + """ + Delete a show from the database, optionally deleting files and torrents. + + :param show_id: The ID of the show to delete. + :param delete_files_on_disk: Whether to delete the show's files from disk. + :param delete_torrents: Whether to delete associated torrents from the torrent client. + """ + if delete_files_on_disk or delete_torrents: + show = self.tv_repository.get_show_by_id(show_id) + + log.debug(f"ID: {show.id} - Name: {show.name} - Library: {show.library}") + + if delete_files_on_disk and show.library: + log.info("Attempting to delete show files from disk.") + # Get the show's directory path + library_config = next( + (lib for lib in AllEncompassingConfig().misc.tv_libraries if lib.name == show.library), + None + ) + log.debug(f"Library config for show deletion: {library_config}") + if library_config: + show_path = Path(library_config.path) / show.folder_name + if show_path.exists() and show_path.is_dir(): + shutil.rmtree(show_path) + log.info(f"Deleted show directory: {show_path}") + + if delete_torrents: + # Get all torrents associated with this show + 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 + ) + log.info(f"Deleted torrent: {torrent.hash}") + except Exception as e: + log.warning(f"Failed to delete torrent {torrent.hash}: {e}") + + # Delete from database + self.tv_repository.delete_show(show_id=show_id) + def get_public_season_files_by_season_id( self, season_id: SeasonId ) -> list[PublicSeasonFile]: diff --git a/web/src/lib/api/api.d.ts b/web/src/lib/api/api.d.ts index 8740b41..743730b 100644 --- a/web/src/lib/api/api.d.ts +++ b/web/src/lib/api/api.d.ts @@ -1309,6 +1309,8 @@ export interface components { added: boolean; /** Vote Average */ vote_average?: number | null; + /** Id */ + id?: string | null; }; /** Movie */ Movie: { @@ -2645,7 +2647,10 @@ export interface operations { }; delete_a_show_api_v1_tv_shows__show_id__delete: { parameters: { - query?: never; + query?: { + delete_files_on_disk?: boolean; + delete_torrents?: boolean; + }; header?: never; path: { /** @description The ID of the show */ @@ -3473,7 +3478,10 @@ export interface operations { }; delete_a_movie_api_v1_movies__movie_id__delete: { parameters: { - query?: never; + query?: { + delete_files_on_disk?: boolean; + delete_torrents?: boolean; + }; header?: never; path: { /** @description The ID of the movie */ diff --git a/web/src/lib/components/login-card.svelte b/web/src/lib/components/login-card.svelte index bafa273..d3f2cbc 100644 --- a/web/src/lib/components/login-card.svelte +++ b/web/src/lib/components/login-card.svelte @@ -22,6 +22,7 @@ let email = $state(''); let password = $state(''); let errorMessage = $state(''); + let successMessage = $state(''); let isLoading = $state(false); async function handleLogin(event: Event) { @@ -29,6 +30,7 @@ isLoading = true; errorMessage = ''; + successMessage = ''; const { error, response } = await client.POST('/api/v1/auth/cookie/login', { body: { @@ -45,8 +47,8 @@ if (!error) { console.log('Login successful!'); console.log('Received User Data: ', response); - errorMessage = 'Login successful! Redirecting...'; - toast.success(errorMessage); + successMessage = 'Login successful! Redirecting...'; + toast.success(successMessage); goto(resolve('/dashboard', {})); } else { toast.error('Login failed!'); @@ -100,6 +102,13 @@ {/if} + {#if successMessage} + + Success + {successMessage} + + {/if} + {#if isLoading} {/if} diff --git a/web/src/routes/dashboard/movies/[movieId=uuid]/+page.svelte b/web/src/routes/dashboard/movies/[movieId=uuid]/+page.svelte index db8f2f8..eb52ff8 100644 --- a/web/src/routes/dashboard/movies/[movieId=uuid]/+page.svelte +++ b/web/src/routes/dashboard/movies/[movieId=uuid]/+page.svelte @@ -27,17 +27,17 @@ let user: () => components['schemas']['UserRead'] = getContext('user'); let deleteDialogOpen = $state(false); let deleteFilesOnDisk = $state(false); + let deleteTorrents = $state(false); async function delete_movie() { if (!movie.id) { toast.error('Movie ID is missing'); return; } - // TODO: Implement delete_files_on_disk parameter in backend API const { response } = await client.DELETE('/api/v1/movies/{movie_id}', { params: { - path: { movie_id: movie.id } - // query: { delete_files_on_disk: deleteFilesOnDisk } // Not yet implemented + path: { movie_id: movie.id }, + query: { delete_files_on_disk: deleteFilesOnDisk, delete_torrents: deleteTorrents } } }); if (!response.ok) { @@ -130,7 +130,9 @@ - Delete {getFullyQualifiedMediaName(movie)}? + Delete - {getFullyQualifiedMediaName(movie)}? This action cannot be undone. This will permanently delete {getFullyQualifiedMediaName(movie)} from the database. @@ -142,7 +144,14 @@ for="delete-files" class="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" > - Also delete files on disk (not yet implemented) + Also delete files on disk + + + diff --git a/web/src/routes/dashboard/tv/[showId=uuid]/+page.svelte b/web/src/routes/dashboard/tv/[showId=uuid]/+page.svelte index 059ed64..cf70bc1 100644 --- a/web/src/routes/dashboard/tv/[showId=uuid]/+page.svelte +++ b/web/src/routes/dashboard/tv/[showId=uuid]/+page.svelte @@ -32,6 +32,7 @@ let continuousDownloadEnabled = $state(show().continuous_download); let deleteDialogOpen = $state(false); let deleteFilesOnDisk = $state(false); + let deleteTorrents = $state(false); async function toggle_continuous_download() { const { response } = await client.POST('/api/v1/tv/shows/{show_id}/continuousDownload', { @@ -56,11 +57,10 @@ } async function delete_show() { - // TODO: Implement delete_files_on_disk parameter in backend API const { response } = await client.DELETE('/api/v1/tv/shows/{show_id}', { params: { - path: { show_id: show().id } - // query: { delete_files_on_disk: deleteFilesOnDisk } // Not yet implemented + path: { show_id: show().id }, + query: { delete_files_on_disk: deleteFilesOnDisk, delete_torrents: deleteTorrents } } }); if (!response.ok) { @@ -164,21 +164,33 @@ - Delete {getFullyQualifiedMediaName(show())}?Delete - {getFullyQualifiedMediaName(show())}? This action cannot be undone. This will permanently delete {getFullyQualifiedMediaName(show())} from the database. -
- - +
+
+ + +
+
+ + +
Cancel