mirror of
https://github.com/Sonarr/Sonarr.git
synced 2024-12-16 11:37:58 +02:00
Fixed: Deleting an episode file from the UI that was already deleted from disk
Fixes #1782
This commit is contained in:
parent
0d782e1cac
commit
c2b66cf524
@ -1,16 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using NLog;
|
||||
using NzbDrone.Api.REST;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.SignalR;
|
||||
using HttpStatusCode = System.Net.HttpStatusCode;
|
||||
|
||||
namespace NzbDrone.Api.EpisodeFiles
|
||||
{
|
||||
@ -18,27 +15,21 @@ public class EpisodeFileModule : NzbDroneRestModuleWithSignalR<EpisodeFileResour
|
||||
IHandle<EpisodeFileAddedEvent>
|
||||
{
|
||||
private readonly IMediaFileService _mediaFileService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IRecycleBinProvider _recycleBinProvider;
|
||||
private readonly IDeleteMediaFiles _mediaFileDeletionService;
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public EpisodeFileModule(IBroadcastSignalRMessage signalRBroadcaster,
|
||||
IMediaFileService mediaFileService,
|
||||
IDiskProvider diskProvider,
|
||||
IRecycleBinProvider recycleBinProvider,
|
||||
IDeleteMediaFiles mediaFileDeletionService,
|
||||
ISeriesService seriesService,
|
||||
IQualityUpgradableSpecification qualityUpgradableSpecification,
|
||||
Logger logger)
|
||||
IQualityUpgradableSpecification qualityUpgradableSpecification)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_mediaFileService = mediaFileService;
|
||||
_diskProvider = diskProvider;
|
||||
_recycleBinProvider = recycleBinProvider;
|
||||
_mediaFileDeletionService = mediaFileDeletionService;
|
||||
_seriesService = seriesService;
|
||||
_qualityUpgradableSpecification = qualityUpgradableSpecification;
|
||||
_logger = logger;
|
||||
GetResourceById = GetEpisodeFile;
|
||||
GetResourceAll = GetEpisodeFiles;
|
||||
UpdateResource = SetQuality;
|
||||
@ -77,13 +68,15 @@ private void SetQuality(EpisodeFileResource episodeFileResource)
|
||||
private void DeleteEpisodeFile(int id)
|
||||
{
|
||||
var episodeFile = _mediaFileService.Get(id);
|
||||
var series = _seriesService.GetSeries(episodeFile.SeriesId);
|
||||
var fullPath = Path.Combine(series.Path, episodeFile.RelativePath);
|
||||
var subfolder = _diskProvider.GetParentFolder(series.Path).GetRelativePath(_diskProvider.GetParentFolder(fullPath));
|
||||
|
||||
_logger.Info("Deleting episode file: {0}", fullPath);
|
||||
_recycleBinProvider.DeleteFile(fullPath, subfolder);
|
||||
_mediaFileService.Delete(episodeFile, DeleteMediaFileReason.Manual);
|
||||
if (episodeFile == null)
|
||||
{
|
||||
throw new NzbDroneClientException(HttpStatusCode.NotFound, "Episode file not found");
|
||||
}
|
||||
|
||||
var series = _seriesService.GetSeries(episodeFile.SeriesId);
|
||||
|
||||
_mediaFileDeletionService.DeleteEpisodeFile(series, episodeFile);
|
||||
}
|
||||
|
||||
public void Handle(EpisodeFileAddedEvent message)
|
||||
|
@ -0,0 +1,140 @@
|
||||
using System.IO;
|
||||
using FizzWare.NBuilder;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.MediaFiles.MediaFileDeletionService
|
||||
{
|
||||
[TestFixture]
|
||||
public class DeleteEpisodeFileFixture : CoreTest<Core.MediaFiles.MediaFileDeletionService>
|
||||
{
|
||||
private static readonly string RootFolder = @"C:\Test\TV";
|
||||
private Series _series;
|
||||
private EpisodeFile _episodeFile;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_series = Builder<Series>.CreateNew()
|
||||
.With(s => s.Path = Path.Combine(RootFolder, "Series Title"))
|
||||
.Build();
|
||||
|
||||
_episodeFile = Builder<EpisodeFile>.CreateNew()
|
||||
.With(f => f.RelativePath = "Series Title - S01E01")
|
||||
.With(f => f.Path = Path.Combine(_series.Path, "Series Title - S01E01"))
|
||||
.Build();
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetParentFolder(_series.Path))
|
||||
.Returns(RootFolder);
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetParentFolder(_episodeFile.Path))
|
||||
.Returns(_series.Path);
|
||||
}
|
||||
|
||||
private void GivenRootFolderExists()
|
||||
{
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.FolderExists(RootFolder))
|
||||
.Returns(true);
|
||||
}
|
||||
|
||||
private void GivenRootFolderHasFolders()
|
||||
{
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.GetDirectories(RootFolder))
|
||||
.Returns(new[] { _series.Path });
|
||||
}
|
||||
|
||||
private void GivenSeriesFolderExists()
|
||||
{
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.FolderExists(_series.Path))
|
||||
.Returns(true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_throw_if_root_folder_does_not_exist()
|
||||
{
|
||||
Assert.Throws<NzbDroneClientException>(() => Subject.DeleteEpisodeFile(_series, _episodeFile));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_should_throw_if_root_folder_is_empty()
|
||||
{
|
||||
GivenRootFolderExists();
|
||||
Assert.Throws<NzbDroneClientException>(() => Subject.DeleteEpisodeFile(_series, _episodeFile));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_delete_from_db_if_series_folder_does_not_exist()
|
||||
{
|
||||
GivenRootFolderExists();
|
||||
GivenRootFolderHasFolders();
|
||||
|
||||
Subject.DeleteEpisodeFile(_series, _episodeFile);
|
||||
|
||||
Mocker.GetMock<IMediaFileService>().Verify(v => v.Delete(_episodeFile, DeleteMediaFileReason.Manual), Times.Once());
|
||||
Mocker.GetMock<IRecycleBinProvider>().Verify(v => v.DeleteFile(_episodeFile.Path, It.IsAny<string>()), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_delete_from_db_if_episode_file_does_not_exist()
|
||||
{
|
||||
GivenRootFolderExists();
|
||||
GivenRootFolderHasFolders();
|
||||
GivenSeriesFolderExists();
|
||||
|
||||
Subject.DeleteEpisodeFile(_series, _episodeFile);
|
||||
|
||||
Mocker.GetMock<IMediaFileService>().Verify(v => v.Delete(_episodeFile, DeleteMediaFileReason.Manual), Times.Once());
|
||||
Mocker.GetMock<IRecycleBinProvider>().Verify(v => v.DeleteFile(_episodeFile.Path, It.IsAny<string>()), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_delete_from_disk_and_db_if_episode_file_exists()
|
||||
{
|
||||
GivenRootFolderExists();
|
||||
GivenRootFolderHasFolders();
|
||||
GivenSeriesFolderExists();
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.FileExists(_episodeFile.Path))
|
||||
.Returns(true);
|
||||
|
||||
Subject.DeleteEpisodeFile(_series, _episodeFile);
|
||||
|
||||
Mocker.GetMock<IRecycleBinProvider>().Verify(v => v.DeleteFile(_episodeFile.Path, "Series Title"), Times.Once());
|
||||
Mocker.GetMock<IMediaFileService>().Verify(v => v.Delete(_episodeFile, DeleteMediaFileReason.Manual), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_handle_error_deleting_episode_file()
|
||||
{
|
||||
GivenRootFolderExists();
|
||||
GivenRootFolderHasFolders();
|
||||
GivenSeriesFolderExists();
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(s => s.FileExists(_episodeFile.Path))
|
||||
.Returns(true);
|
||||
|
||||
Mocker.GetMock<IRecycleBinProvider>()
|
||||
.Setup(s => s.DeleteFile(_episodeFile.Path, "Series Title"))
|
||||
.Throws(new IOException());
|
||||
|
||||
Assert.Throws<NzbDroneClientException>(() => Subject.DeleteEpisodeFile(_series, _episodeFile));
|
||||
|
||||
ExceptionVerification.ExpectedErrors(1);
|
||||
Mocker.GetMock<IRecycleBinProvider>().Verify(v => v.DeleteFile(_episodeFile.Path, "Series Title"), Times.Once());
|
||||
Mocker.GetMock<IMediaFileService>().Verify(v => v.Delete(_episodeFile, DeleteMediaFileReason.Manual), Times.Never());
|
||||
}
|
||||
}
|
||||
}
|
@ -290,6 +290,7 @@
|
||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotUnpackingSpecificationFixture.cs" />
|
||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\UpgradeSpecificationFixture.cs" />
|
||||
<Compile Include="MediaFiles\ImportApprovedEpisodesFixture.cs" />
|
||||
<Compile Include="MediaFiles\MediaFileDeletionService\DeleteEpisodeFileFixture.cs" />
|
||||
<Compile Include="MediaFiles\MediaFileRepositoryFixture.cs" />
|
||||
<Compile Include="MediaFiles\MediaInfo\MediaInfoFormatterTests\FormatAudioCodecFixture.cs" />
|
||||
<Compile Include="MediaFiles\MediaInfo\MediaInfoFormatterTests\FormatVideoCodecFixture.cs" />
|
||||
|
84
src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs
Normal file
84
src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs
Normal file
@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Core.Tv.Events;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles
|
||||
{
|
||||
public interface IDeleteMediaFiles
|
||||
{
|
||||
void DeleteEpisodeFile(Series series, EpisodeFile episodeFile);
|
||||
}
|
||||
|
||||
public class MediaFileDeletionService : IDeleteMediaFiles, IHandleAsync<SeriesDeletedEvent>
|
||||
{
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IRecycleBinProvider _recycleBinProvider;
|
||||
private readonly IMediaFileService _mediaFileService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public MediaFileDeletionService(IDiskProvider diskProvider,
|
||||
IRecycleBinProvider recycleBinProvider,
|
||||
IMediaFileService mediaFileService,
|
||||
Logger logger)
|
||||
{
|
||||
_diskProvider = diskProvider;
|
||||
_recycleBinProvider = recycleBinProvider;
|
||||
_mediaFileService = mediaFileService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void DeleteEpisodeFile(Series series, EpisodeFile episodeFile)
|
||||
{
|
||||
var fullPath = Path.Combine(series.Path, episodeFile.RelativePath);
|
||||
var rootFolder = _diskProvider.GetParentFolder(series.Path);
|
||||
|
||||
if (!_diskProvider.FolderExists(rootFolder))
|
||||
{
|
||||
throw new NzbDroneClientException(HttpStatusCode.Conflict, "Series' root folder ({0}) doesn't exist.", rootFolder);
|
||||
}
|
||||
|
||||
if (_diskProvider.GetDirectories(rootFolder).Empty())
|
||||
{
|
||||
throw new NzbDroneClientException(HttpStatusCode.Conflict, "Series' root folder ({0}) is empty.", rootFolder);
|
||||
}
|
||||
|
||||
if (_diskProvider.FolderExists(series.Path) && _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");
|
||||
throw new NzbDroneClientException(HttpStatusCode.InternalServerError, "Unable to delete episode file");
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the episode file from the database to clean it up even if the file was already deleted
|
||||
_mediaFileService.Delete(episodeFile, DeleteMediaFileReason.Manual);
|
||||
}
|
||||
|
||||
public void HandleAsync(SeriesDeletedEvent message)
|
||||
{
|
||||
if (message.DeleteFiles)
|
||||
{
|
||||
if (_diskProvider.FolderExists(message.Series.Path))
|
||||
{
|
||||
_recycleBinProvider.DeleteFolder(message.Series.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ public interface IRecycleBinProvider
|
||||
void Cleanup();
|
||||
}
|
||||
|
||||
public class RecycleBinProvider : IHandleAsync<SeriesDeletedEvent>, IExecute<CleanUpRecycleBinCommand>, IRecycleBinProvider
|
||||
public class RecycleBinProvider : IExecute<CleanUpRecycleBinCommand>, IRecycleBinProvider
|
||||
{
|
||||
private readonly IDiskTransferService _diskTransferService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
@ -192,17 +192,6 @@ public void Cleanup()
|
||||
_logger.Debug("Recycling Bin has been cleaned up.");
|
||||
}
|
||||
|
||||
public void HandleAsync(SeriesDeletedEvent message)
|
||||
{
|
||||
if (message.DeleteFiles)
|
||||
{
|
||||
if (_diskProvider.FolderExists(message.Series.Path))
|
||||
{
|
||||
DeleteFolder(message.Series.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Execute(CleanUpRecycleBinCommand message)
|
||||
{
|
||||
Cleanup();
|
||||
|
@ -777,6 +777,7 @@
|
||||
<Compile Include="MediaFiles\Events\SeriesScannedEvent.cs" />
|
||||
<Compile Include="MediaFiles\FileDateType.cs" />
|
||||
<Compile Include="MediaFiles\MediaFileAttributeService.cs" />
|
||||
<Compile Include="MediaFiles\MediaFileDeletionService.cs" />
|
||||
<Compile Include="MediaFiles\MediaFileExtensions.cs" />
|
||||
<Compile Include="MediaFiles\MediaFileRepository.cs" />
|
||||
<Compile Include="MediaFiles\MediaFileService.cs">
|
||||
|
@ -19,7 +19,7 @@ module.exports = Backgrid.Cell.extend({
|
||||
var self = this;
|
||||
|
||||
if (window.confirm('Are you sure you want to delete \'{0}\' from disk?'.format(this.model.get('path')))) {
|
||||
this.model.destroy().done(function() {
|
||||
this.model.destroy({ wait: true }).done(function() {
|
||||
vent.trigger(vent.Events.EpisodeFileDeleted, { episodeFile : self.model });
|
||||
});
|
||||
}
|
||||
|
@ -176,7 +176,7 @@ module.exports = Marionette.Layout.extend({
|
||||
if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) {
|
||||
var episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, episodeFileId);
|
||||
|
||||
episodeFile.destroy();
|
||||
episodeFile.destroy({ wait: true });
|
||||
}
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user