From 0ea18bdecd376963eff245b810300d0db42a4792 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 1 Mar 2026 09:05:56 -0800 Subject: [PATCH] New: Delete files for Select Series Closes #5110 --- frontend/src/Commands/CommandNames.ts | 1 + .../Delete/DeleteSeriesModalContent.tsx | 86 +++--------------- .../Delete/Files/DeleteSeriesFilesModal.tsx | 22 +++++ .../Files/DeleteSeriesFilesModalContent.css | 23 +++++ .../DeleteSeriesFilesModalContent.css.d.ts | 11 +++ .../Files/DeleteSeriesFilesModalContent.tsx | 66 ++++++++++++++ .../Index/Select/Delete/SeriesDeleteList.tsx | 73 +++++++++++++++ .../Select/Delete/useSelectedSeriesStats.ts | 45 ++++++++++ .../Index/Select/SeriesIndexSelectFooter.tsx | 27 ++++++ .../DeleteEpisodeFileFixture.cs | 5 ++ src/NzbDrone.Core/Localization/Core/en.json | 3 + .../Commands/DeleteSeriesFilesCommand.cs | 18 ++++ .../MediaFiles/MediaFileDeletionService.cs | 88 ++++++++++++++++++- src/NzbDrone.Core/Tv/RefreshSeriesService.cs | 2 +- .../EpisodeFiles/EpisodeFileController.cs | 2 +- .../Queue/QueueActionController.cs | 4 +- .../Series/SeriesEditorController.cs | 2 +- 17 files changed, 399 insertions(+), 79 deletions(-) create mode 100644 frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModal.tsx create mode 100644 frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.css create mode 100644 frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.css.d.ts create mode 100644 frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.tsx create mode 100644 frontend/src/Series/Index/Select/Delete/SeriesDeleteList.tsx create mode 100644 frontend/src/Series/Index/Select/Delete/useSelectedSeriesStats.ts create mode 100644 src/NzbDrone.Core/MediaFiles/Commands/DeleteSeriesFilesCommand.cs diff --git a/frontend/src/Commands/CommandNames.ts b/frontend/src/Commands/CommandNames.ts index 8cee7d38c..301a72d1f 100644 --- a/frontend/src/Commands/CommandNames.ts +++ b/frontend/src/Commands/CommandNames.ts @@ -5,6 +5,7 @@ enum CommandNames { ClearLog = 'ClearLog', CutoffUnmetEpisodeSearch = 'CutoffUnmetEpisodeSearch', DeleteLogFiles = 'DeleteLogFiles', + DeleteSeriesFiles = 'DeleteSeriesFiles', DeleteUpdateLogFiles = 'DeleteUpdateLogFiles', DownloadedEpisodesScan = 'DownloadedEpisodesScan', EpisodeSearch = 'EpisodeSearch', diff --git a/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx b/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx index ae270078a..03c58ebeb 100644 --- a/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx +++ b/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx @@ -1,6 +1,4 @@ -import { orderBy } from 'lodash'; -import React, { useCallback, useMemo, useState } from 'react'; -import { useSelect } from 'App/Select/SelectContext'; +import React, { useCallback, useState } from 'react'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; import FormLabel from 'Components/Form/FormLabel'; @@ -10,15 +8,15 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes, kinds } from 'Helpers/Props'; -import Series from 'Series/Series'; import { setSeriesDeleteOptions, useSeriesDeleteOptions, } from 'Series/seriesOptionsStore'; -import useSeries, { useBulkDeleteSeries } from 'Series/useSeries'; +import { useBulkDeleteSeries } from 'Series/useSeries'; import { InputChanged } from 'typings/inputs'; -import formatBytes from 'Utilities/Number/formatBytes'; import translate from 'Utilities/String/translate'; +import SeriesDeleteList from './SeriesDeleteList'; +import useSelectedSeriesStats from './useSelectedSeriesStats'; import styles from './DeleteSeriesModalContent.css'; export interface DeleteSeriesModalContentProps { @@ -29,19 +27,10 @@ function DeleteSeriesModalContent({ onModalClose, }: DeleteSeriesModalContentProps) { const { addImportListExclusion } = useSeriesDeleteOptions(); - const { data: allSeries } = useSeries(); const { bulkDeleteSeries } = useBulkDeleteSeries(); const [deleteFiles, setDeleteFiles] = useState(false); - const { useSelectedIds } = useSelect(); - const seriesIds = useSelectedIds(); - - const series = useMemo((): Series[] => { - const seriesList = seriesIds.map((id) => { - return allSeries.find((s) => s.id === id); - }) as Series[]; - - return orderBy(seriesList, ['sortTitle']); - }, [allSeries, seriesIds]); + const { series, seriesIds, totalEpisodeFileCount, totalSizeOnDisk } = + useSelectedSeriesStats(); const onDeleteFilesChange = useCallback( ({ value }: InputChanged) => { @@ -78,23 +67,6 @@ function DeleteSeriesModalContent({ onModalClose, ]); - const { totalEpisodeFileCount, totalSizeOnDisk } = useMemo(() => { - return series.reduce( - (acc, { statistics = {} }) => { - const { episodeFileCount = 0, sizeOnDisk = 0 } = statistics; - - acc.totalEpisodeFileCount += episodeFileCount; - acc.totalSizeOnDisk += sizeOnDisk; - - return acc; - }, - { - totalEpisodeFileCount: 0, - totalSizeOnDisk: 0, - } - ); - }, [series]); - return ( {translate('DeleteSelectedSeries')} @@ -145,45 +117,13 @@ function DeleteSeriesModalContent({ })} -
    - {series.map(({ title, path, statistics = {} }) => { - const { episodeFileCount = 0, sizeOnDisk = 0 } = statistics; - - return ( -
  • - {title} - - {deleteFiles && ( - - - -{path} - - - {!!episodeFileCount && ( - - ( - {translate('DeleteSeriesFolderEpisodeCount', { - episodeFileCount, - size: formatBytes(sizeOnDisk), - })} - ) - - )} - - )} -
  • - ); - })} -
- - {deleteFiles && !!totalEpisodeFileCount ? ( -
- {translate('DeleteSeriesFolderEpisodeCount', { - episodeFileCount: totalEpisodeFileCount, - size: formatBytes(totalSizeOnDisk), - })} -
- ) : null} + diff --git a/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModal.tsx b/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModal.tsx new file mode 100644 index 000000000..2cac5b3bd --- /dev/null +++ b/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModal.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import DeleteSeriesModalContent, { + DeleteSeriesFilesModalContentProps, +} from './DeleteSeriesFilesModalContent'; + +interface DeleteSeriesFilesModalProps + extends DeleteSeriesFilesModalContentProps { + isOpen: boolean; +} + +function DeleteSeriesFilesModal(props: DeleteSeriesFilesModalProps) { + const { isOpen, onModalClose } = props; + + return ( + + + + ); +} + +export default DeleteSeriesFilesModal; diff --git a/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.css b/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.css new file mode 100644 index 000000000..7e9ce40cb --- /dev/null +++ b/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.css @@ -0,0 +1,23 @@ +.message { + margin-bottom: 10px; +} + +.pathContainer { + margin-left: 5px; +} + +.path { + margin-left: 5px; + color: var(--dangerColor); + font-weight: bold; +} + +.statistics { + margin-left: 5px; + color: var(--warningColor); +} + +.deleteFilesMessage { + margin-top: 20px; + color: var(--warningColor); +} diff --git a/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.css.d.ts b/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.css.d.ts new file mode 100644 index 000000000..ca4650422 --- /dev/null +++ b/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.css.d.ts @@ -0,0 +1,11 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'deleteFilesMessage': string; + 'message': string; + 'path': string; + 'pathContainer': string; + 'statistics': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.tsx b/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.tsx new file mode 100644 index 000000000..a6c227fe3 --- /dev/null +++ b/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.tsx @@ -0,0 +1,66 @@ +import React, { useCallback } from 'react'; +import CommandNames from 'Commands/CommandNames'; +import { useExecuteCommand } from 'Commands/useCommands'; +import Button from 'Components/Link/Button'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import SeriesDeleteList from '../SeriesDeleteList'; +import useSelectedSeriesStats from '../useSelectedSeriesStats'; +import styles from './DeleteSeriesFilesModalContent.css'; + +export interface DeleteSeriesFilesModalContentProps { + onModalClose(): void; +} + +function DeleteSeriesFilesModalContent({ + onModalClose, +}: DeleteSeriesFilesModalContentProps) { + const { series, seriesIds, totalEpisodeFileCount, totalSizeOnDisk } = + useSelectedSeriesStats(); + const executeCommand = useExecuteCommand(); + + const onDeleteSeriesConfirmed = useCallback(() => { + executeCommand({ + name: CommandNames.DeleteSeriesFiles, + seriesIds, + }); + + onModalClose(); + }, [seriesIds, executeCommand, onModalClose]); + + return ( + + {translate('DeleteSelectedSeriesFiles')} + + +
+ {translate('DeleteSeriesFilesConfirmation', { + count: series.length, + })} +
+ + +
+ + + + + + +
+ ); +} + +export default DeleteSeriesFilesModalContent; diff --git a/frontend/src/Series/Index/Select/Delete/SeriesDeleteList.tsx b/frontend/src/Series/Index/Select/Delete/SeriesDeleteList.tsx new file mode 100644 index 000000000..15c8cca47 --- /dev/null +++ b/frontend/src/Series/Index/Select/Delete/SeriesDeleteList.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import Series from 'Series/Series'; +import formatBytes from 'Utilities/Number/formatBytes'; +import translate from 'Utilities/String/translate'; + +interface SeriesDeleteListStyles { + pathContainer: string; + path: string; + statistics: string; + deleteFilesMessage: string; +} + +interface SeriesDeleteListProps { + series: Series[]; + showFileDetails: boolean; + totalEpisodeFileCount: number; + totalSizeOnDisk: number; + styles: SeriesDeleteListStyles; +} + +function SeriesDeleteList({ + series, + showFileDetails, + totalEpisodeFileCount, + totalSizeOnDisk, + styles, +}: SeriesDeleteListProps) { + return ( + <> +
    + {series.map(({ title, path, statistics = {} }) => { + const { episodeFileCount = 0, sizeOnDisk = 0 } = statistics; + + return ( +
  • + {title} + + {showFileDetails ? ( + + + -{path} + + + {episodeFileCount ? ( + + ( + {translate('DeleteSeriesFolderEpisodeCount', { + episodeFileCount, + size: formatBytes(sizeOnDisk), + })} + ) + + ) : null} + + ) : null} +
  • + ); + })} +
+ + {showFileDetails && totalEpisodeFileCount ? ( +
+ {translate('DeleteSeriesFolderEpisodeCount', { + episodeFileCount: totalEpisodeFileCount, + size: formatBytes(totalSizeOnDisk), + })} +
+ ) : null} + + ); +} + +export default SeriesDeleteList; diff --git a/frontend/src/Series/Index/Select/Delete/useSelectedSeriesStats.ts b/frontend/src/Series/Index/Select/Delete/useSelectedSeriesStats.ts new file mode 100644 index 000000000..0748cbdcd --- /dev/null +++ b/frontend/src/Series/Index/Select/Delete/useSelectedSeriesStats.ts @@ -0,0 +1,45 @@ +import { useMemo } from 'react'; +import { useSelect } from 'App/Select/SelectContext'; +import Series from 'Series/Series'; +import useSeries from 'Series/useSeries'; +import sortByProp from 'Utilities/Array/sortByProp'; + +function useSelectedSeriesStats() { + const { data: allSeries } = useSeries(); + const { useSelectedIds } = useSelect(); + const seriesIds = useSelectedIds(); + + const series = useMemo((): Series[] => { + const seriesList = seriesIds.map((id) => { + return allSeries.find((s) => s.id === id); + }) as Series[]; + + return seriesList.sort(sortByProp('sortTitle')); + }, [allSeries, seriesIds]); + + const { totalEpisodeFileCount, totalSizeOnDisk } = useMemo(() => { + return series.reduce( + (acc, { statistics = {} }) => { + const { episodeFileCount = 0, sizeOnDisk = 0 } = statistics; + + acc.totalEpisodeFileCount += episodeFileCount; + acc.totalSizeOnDisk += sizeOnDisk; + + return acc; + }, + { + totalEpisodeFileCount: 0, + totalSizeOnDisk: 0, + } + ); + }, [series]); + + return { + series, + seriesIds, + totalEpisodeFileCount, + totalSizeOnDisk, + }; +} + +export default useSelectedSeriesStats; diff --git a/frontend/src/Series/Index/Select/SeriesIndexSelectFooter.tsx b/frontend/src/Series/Index/Select/SeriesIndexSelectFooter.tsx index 0d3cc20f2..df6a1aede 100644 --- a/frontend/src/Series/Index/Select/SeriesIndexSelectFooter.tsx +++ b/frontend/src/Series/Index/Select/SeriesIndexSelectFooter.tsx @@ -14,6 +14,7 @@ import { } from 'Series/useSeries'; import translate from 'Utilities/String/translate'; import DeleteSeriesModal from './Delete/DeleteSeriesModal'; +import DeleteSeriesFilesModal from './Delete/Files/DeleteSeriesFilesModal'; import EditSeriesModal from './Edit/EditSeriesModal'; import OrganizeSeriesModal from './Organize/OrganizeSeriesModal'; import ChangeMonitoringModal from './SeasonPass/ChangeMonitoringModal'; @@ -34,6 +35,9 @@ function SeriesIndexSelectFooter() { const { updateSeriesMonitor, isUpdatingSeriesMonitor } = useUpdateSeriesMonitor(); const { isBulkDeleting, bulkDeleteError } = useBulkDeleteSeries(); + const isDeleteFilesCommandExecuting = useCommandExecuting( + CommandNames.DeleteSeriesFiles + ); const isOrganizingSeries = useCommandExecuting(CommandNames.RenameSeries); @@ -46,6 +50,7 @@ function SeriesIndexSelectFooter() { const [isTagsModalOpen, setIsTagsModalOpen] = useState(false); const [isMonitoringModalOpen, setIsMonitoringModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isDeleteFilesModalOpen, setIsDeleteFilesModalOpen] = useState(false); const [isSavingSeries, setIsSavingSeries] = useState(false); const [isSavingTags, setIsSavingTags] = useState(false); const [isSavingMonitoring, setIsSavingMonitoring] = useState(false); @@ -132,6 +137,14 @@ function SeriesIndexSelectFooter() { setIsDeleteModalOpen(false); }, []); + const onDeleteFilesPress = useCallback(() => { + setIsDeleteFilesModalOpen(true); + }, []); + + const onDeleteFilesModalClose = useCallback(() => { + setIsDeleteFilesModalOpen(false); + }, []); + useEffect(() => { if (!isSaving) { setIsSavingSeries(false); @@ -195,6 +208,15 @@ function SeriesIndexSelectFooter() { > {translate('Delete')} + + + {translate('DeleteFiles')} + @@ -229,6 +251,11 @@ function SeriesIndexSelectFooter() { isOpen={isDeleteModalOpen} onModalClose={onDeleteModalClose} /> + + ); } diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteEpisodeFileFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteEpisodeFileFixture.cs index 6bbd36434..2bec1811e 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteEpisodeFileFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteEpisodeFileFixture.cs @@ -5,6 +5,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Core.Exceptions; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.RootFolders; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; @@ -41,6 +42,10 @@ public void Setup() private void GivenRootFolderExists() { + Mocker.GetMock() + .Setup(s => s.GetBestRootFolderPath(_series.Path)) + .Returns(ROOT_FOLDER); + Mocker.GetMock() .Setup(s => s.FolderExists(ROOT_FOLDER)) .Returns(true); diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index d5e2d43a1..04e91e4f2 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -364,6 +364,7 @@ "DeleteEpisodeFromDisk": "Delete episode from disk", "DeleteEpisodesFiles": "Delete {episodeFileCount} Episode Files", "DeleteEpisodesFilesHelpText": "Delete the episode files and series folder", + "DeleteFiles": "Delete Files", "DeleteImportList": "Delete Import List", "DeleteImportListExclusion": "Delete Import List Exclusion", "DeleteImportListExclusionMessageText": "Are you sure you want to delete this import list exclusion?", @@ -391,6 +392,8 @@ "DeleteSelectedIndexers": "Delete Indexer(s)", "DeleteSelectedIndexersMessageText": "Are you sure you want to delete {count} selected indexer(s)?", "DeleteSelectedSeries": "Delete Selected Series", + "DeleteSelectedSeriesFiles": "Delete Selected Series Files", + "DeleteSeriesFilesConfirmation": "Are you sure you want to delete all tracked episode files for {count} selected series?", "DeleteSeriesFolder": "Delete Series Folder", "DeleteSeriesFolderConfirmation": "The series folder `{path}` and all of its content will be deleted.", "DeleteSeriesFolderCountConfirmation": "Are you sure you want to delete {count} selected series?", diff --git a/src/NzbDrone.Core/MediaFiles/Commands/DeleteSeriesFilesCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/DeleteSeriesFilesCommand.cs new file mode 100644 index 000000000..a3ee57b91 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Commands/DeleteSeriesFilesCommand.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.MediaFiles.Commands +{ + public class DeleteSeriesFilesCommand : Command + { + public List SeriesIds { get; set; } + + public override bool SendUpdatesToClient => true; + public override bool RequiresDiskAccess => true; + + public DeleteSeriesFilesCommand() + { + SeriesIds = new List(); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs index bd8d66025..463346976 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs @@ -4,11 +4,15 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Exceptions; +using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging; +using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.RootFolders; using NzbDrone.Core.Tv; using NzbDrone.Core.Tv.Events; @@ -20,6 +24,7 @@ public interface IDeleteMediaFiles } public class MediaFileDeletionService : IDeleteMediaFiles, + IExecute, IHandleAsync, IHandle { @@ -27,7 +32,9 @@ public class MediaFileDeletionService : IDeleteMediaFiles, private readonly IRecycleBinProvider _recycleBinProvider; private readonly IMediaFileService _mediaFileService; private readonly ISeriesService _seriesService; + private readonly IRootFolderService _rootFolderService; private readonly IConfigService _configService; + private readonly ICommandResultReporter _commandResultReporter; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; @@ -35,7 +42,9 @@ public MediaFileDeletionService(IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, IMediaFileService mediaFileService, ISeriesService seriesService, + IRootFolderService rootFolderService, IConfigService configService, + ICommandResultReporter commandResultReporter, IEventAggregator eventAggregator, Logger logger) { @@ -43,7 +52,9 @@ public MediaFileDeletionService(IDiskProvider diskProvider, _recycleBinProvider = recycleBinProvider; _mediaFileService = mediaFileService; _seriesService = seriesService; + _rootFolderService = rootFolderService; _configService = configService; + _commandResultReporter = commandResultReporter; _eventAggregator = eventAggregator; _logger = logger; } @@ -51,7 +62,7 @@ public MediaFileDeletionService(IDiskProvider diskProvider, public void DeleteEpisodeFile(Series series, EpisodeFile episodeFile) { var fullPath = Path.Combine(series.Path, episodeFile.RelativePath); - var rootFolder = _diskProvider.GetParentFolder(series.Path); + var rootFolder = _rootFolderService.GetBestRootFolderPath(series.Path); if (!_diskProvider.FolderExists(rootFolder)) { @@ -88,6 +99,81 @@ public void DeleteEpisodeFile(Series series, EpisodeFile episodeFile) _eventAggregator.PublishEvent(new DeleteCompletedEvent()); } + public void Execute(DeleteSeriesFilesCommand message) + { + foreach (var seriesId in message.SeriesIds) + { + try + { + var series = _seriesService.GetSeries(seriesId); + var mediaFiles = _mediaFileService.GetFilesBySeries(seriesId); + + _logger.ProgressDebug("{0}: Deleting episode files}", series.Title); + + if (mediaFiles.Count == 0) + { + _logger.Debug("No files found for series: {0}", series.Title); + continue; + } + + var rootFolder = _rootFolderService.GetBestRootFolderPath(series.Path); + + if (!_diskProvider.FolderExists(rootFolder)) + { + _logger.Warn("Series' root folder ({0}) doesn't exist.", rootFolder); + _commandResultReporter.Report(CommandResult.Indeterminate); + continue; + } + + if (_diskProvider.GetDirectories(rootFolder).Empty()) + { + _logger.Warn("Series' root folder ({0}) is empty.", rootFolder); + _commandResultReporter.Report(CommandResult.Indeterminate); + continue; + } + + if (!_diskProvider.FolderExists(series.Path)) + { + _logger.Warn("Series' folder ({0}) does not exist.", series.Path); + _commandResultReporter.Report(CommandResult.Indeterminate); + continue; + } + + foreach (var episodeFile in mediaFiles) + { + var fullPath = Path.Combine(series.Path, episodeFile.RelativePath); + + if (_diskProvider.FileExists(fullPath)) + { + _logger.Info("Deleting episode file: {0}", fullPath); + + var subfolder = _diskProvider.GetParentFolder(series.Path).GetRelativePath(_diskProvider.GetParentFolder(fullPath)); + + try + { + _recycleBinProvider.DeleteFile(fullPath, subfolder); + } + catch (Exception e) + { + _logger.Error(e, "Unable to delete episode file"); + _commandResultReporter.Report(CommandResult.Indeterminate); + continue; + } + + _mediaFileService.Delete(episodeFile, DeleteMediaFileReason.Manual); + } + } + + _logger.ProgressDebug("{0}: Deleted episode files", series.Title); + } + catch (Exception ex) + { + _logger.Warn(ex, "Unable to delete files for series with ID: {0}", seriesId); + _commandResultReporter.Report(CommandResult.Indeterminate); + } + } + } + public void HandleAsync(SeriesDeletedEvent message) { if (message.DeleteFiles) diff --git a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs index 63cbf7257..f18f8e7f9 100644 --- a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs +++ b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs @@ -231,7 +231,7 @@ public void Execute(RefreshSeriesCommand message) _logger.Error("Series '{0}' (tvdbid {1}) was not found, it may have been removed from TheTVDB.", series.Title, series.TvdbId); // Mark the result as indeterminate so it's not marked as a full success, - // // but we can still process other series if needed. + // but we can still process other series if needed. _commandResultReporter.Report(CommandResult.Indeterminate); } catch (Exception e) diff --git a/src/Sonarr.Api.V5/EpisodeFiles/EpisodeFileController.cs b/src/Sonarr.Api.V5/EpisodeFiles/EpisodeFileController.cs index aed000529..1bfef1f4c 100644 --- a/src/Sonarr.Api.V5/EpisodeFiles/EpisodeFileController.cs +++ b/src/Sonarr.Api.V5/EpisodeFiles/EpisodeFileController.cs @@ -135,7 +135,7 @@ public object DeleteEpisodeFiles([FromBody] EpisodeFileListResource resource) _mediaFileDeletionService.DeleteEpisodeFile(series, episodeFile); } - return new { }; + return NoContent(); } [HttpPut("bulk")] diff --git a/src/Sonarr.Api.V5/Queue/QueueActionController.cs b/src/Sonarr.Api.V5/Queue/QueueActionController.cs index 959cebc7a..c9e79e5ee 100644 --- a/src/Sonarr.Api.V5/Queue/QueueActionController.cs +++ b/src/Sonarr.Api.V5/Queue/QueueActionController.cs @@ -31,7 +31,7 @@ public async Task Grab([FromRoute] int id) await _downloadService.DownloadReport(pendingRelease.RemoteEpisode, null); - return new { }; + return NoContent(); } [HttpPost("grab/bulk")] @@ -50,7 +50,7 @@ public async Task Grab([FromBody] QueueBulkResource resource) await _downloadService.DownloadReport(pendingRelease.RemoteEpisode, null); } - return new { }; + return NoContent(); } } } diff --git a/src/Sonarr.Api.V5/Series/SeriesEditorController.cs b/src/Sonarr.Api.V5/Series/SeriesEditorController.cs index 9ce021a3c..ae30ff4c3 100644 --- a/src/Sonarr.Api.V5/Series/SeriesEditorController.cs +++ b/src/Sonarr.Api.V5/Series/SeriesEditorController.cs @@ -109,6 +109,6 @@ public object DeleteSeries([FromBody] SeriesEditorResource resource) { _seriesService.DeleteSeries(resource.SeriesIds, resource.DeleteFiles, resource.AddImportListExclusion); - return new { }; + return NoContent(); } }