moved cleanup of deleted files to their own service.

detaching of episodes when files are deleted is done through events now.
This commit is contained in:
kay.one 2013-03-07 13:34:56 +09:00
parent 40a959a309
commit 9a8414edde
19 changed files with 195 additions and 193 deletions

View File

@ -13,7 +13,7 @@ using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Test.IndexerSearchTests
{
public abstract class IndexerSearchTestBase<TSearch> : CoreTest<TSearch>
where TSearch : SearchBase
where TSearch : IndexerSearchBase
{
protected Series _series;
protected Episode _episode;

View File

@ -13,7 +13,7 @@ using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Test.IndexerSearchTests
{
public class TestSearch : SearchBase
public class TestSearch : IndexerSearchBase
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();

View File

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using FizzWare.NBuilder;
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using System.Linq;
namespace NzbDrone.Core.Test.MediaFileTests
{
public class GhostFileCleanupFixture : CoreTest<GhostFileCleanupService>
{
private void GiveEpisodeFiles(IEnumerable<EpisodeFile> episodeFiles)
{
Mocker.GetMock<IMediaFileService>()
.Setup(c => c.GetFilesBySeries(It.IsAny<int>()))
.Returns(episodeFiles.ToList());
}
private const string DeletedPath = "ANY FILE WITH THIS PATH IS CONSIDERED DELETED!";
[SetUp]
public void SetUp()
{
Mocker.GetMock<DiskProvider>()
.Setup(e => e.FileExists(It.Is<String>(c => c != DeletedPath)))
.Returns(true);
}
[Test]
public void should_skip_files_that_exist_in_disk()
{
var episodeFiles = Builder<EpisodeFile>.CreateListOfSize(10)
.Build();
GiveEpisodeFiles(episodeFiles);
Subject.RemoveNonExistingFiles(0);
Mocker.GetMock<IEpisodeService>().Verify(c => c.UpdateEpisode(It.IsAny<Episode>()), Times.Never());
}
[Test]
public void should_delete_none_existing_files()
{
var episodeFiles = Builder<EpisodeFile>.CreateListOfSize(10)
.Random(2)
.With(c => c.Path = DeletedPath)
.Build();
GiveEpisodeFiles(episodeFiles);
Subject.RemoveNonExistingFiles(0);
Mocker.GetMock<IMediaFileService>().Verify(c => c.Delete(It.Is<EpisodeFile>(e => e.Path == DeletedPath)), Times.Exactly(2));
}
}
}

View File

@ -151,6 +151,7 @@
<Compile Include="JobTests\RenameSeasonJobFixture.cs" />
<Compile Include="DecisionEngineTests\LanguageSpecificationFixture.cs" />
<Compile Include="MediaCoverTests\MediaCoverServiceFixture.cs" />
<Compile Include="MediaFileTests\GhostFileCleanupFixture.cs" />
<Compile Include="MediaFileTests\MediaFileRepositoryFixture.cs" />
<Compile Include="ProviderTests\DownloadClientTests\NzbgetProviderTests\DownloadNzbFixture.cs" />
<Compile Include="ProviderTests\DownloadClientTests\NzbgetProviderTests\QueueFixture.cs" />
@ -172,7 +173,6 @@
<Compile Include="Configuration\ConfigCachingFixture.cs" />
<Compile Include="DecisionEngineTests\AllowedReleaseGroupSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\CustomStartDateSpecificationFixture.cs" />
<Compile Include="ProviderTests\DiskScanProviderTests\CleanUpFixture.cs" />
<Compile Include="ProviderTests\DiskScanProviderTests\CleanUpDropFolderFixture.cs" />
<Compile Include="ProviderTests\DiskScanProviderTests\GetVideoFilesFixture.cs" />
<Compile Include="ProviderTests\DiskScanProviderTests\ScanFixture.cs" />

View File

@ -1,117 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Model;
using NzbDrone.Core.Providers;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
using NzbDrone.Test.Common.AutoMoq;
namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests
{
// ReSharper disable InconsistentNaming
public class CleanUpFixture : CoreTest
{
[Test]
public void should_skip_existing_files()
{
var episodes = Builder<EpisodeFile>.CreateListOfSize(10).Build();
Mocker.GetMock<DiskProvider>()
.Setup(e => e.FileExists(It.IsAny<String>()))
.Returns(true);
//Act
Mocker.Resolve<DiskScanProvider>().CleanUp(episodes);
//Assert
Mocker.VerifyAllMocks();
}
[Test]
public void should_delete_none_existing_files()
{
var episodes = Builder<EpisodeFile>.CreateListOfSize(10).Build();
Mocker.GetMock<DiskProvider>()
.Setup(e => e.FileExists(It.IsAny<String>()))
.Returns(false);
Mocker.GetMock<IEpisodeService>()
.Setup(e => e.GetEpisodesByFileId(It.IsAny<int>()))
.Returns(new List<Episode>());
Mocker.GetMock<IMediaFileService>()
.Setup(e => e.Delete(It.IsAny<int>()));
//Act
Mocker.Resolve<DiskScanProvider>().CleanUp(episodes);
//Assert
Mocker.VerifyAllMocks();
Mocker.GetMock<IEpisodeService>()
.Verify(e => e.GetEpisodesByFileId(It.IsAny<int>()), Times.Exactly(10));
Mocker.GetMock<IMediaFileService>()
.Verify(e => e.Delete(It.IsAny<int>()), Times.Exactly(10));
}
[Test]
public void should_delete_none_existing_files_remove_links_to_episodes()
{
var episodes = Builder<EpisodeFile>.CreateListOfSize(10).Build();
Mocker.GetMock<DiskProvider>()
.Setup(e => e.FileExists(It.IsAny<String>()))
.Returns(false);
Mocker.GetMock<IEpisodeService>()
.Setup(e => e.GetEpisodesByFileId(It.IsAny<int>()))
.Returns(new List<Episode> { new Episode { EpisodeFile = new EpisodeFile { Id = 10 } }, new Episode { EpisodeFile = new EpisodeFile { Id = 10 } } });
Mocker.GetMock<IEpisodeService>()
.Setup(e => e.UpdateEpisode(It.IsAny<Episode>()));
Mocker.GetMock<IMediaFileService>()
.Setup(e => e.Delete(It.IsAny<int>()));
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.AutoIgnorePreviouslyDownloadedEpisodes)
.Returns(true);
//Act
Mocker.Resolve<DiskScanProvider>().CleanUp(episodes);
//Assert
Mocker.VerifyAllMocks();
Mocker.GetMock<IEpisodeService>()
.Verify(e => e.GetEpisodesByFileId(It.IsAny<int>()), Times.Exactly(10));
Mocker.GetMock<IEpisodeService>()
.Verify(e => e.UpdateEpisode(It.Is<Episode>(g => g.EpisodeFileId == 0)), Times.Exactly(20));
Mocker.GetMock<IMediaFileService>()
.Verify(e => e.Delete(It.IsAny<int>()), Times.Exactly(10));
Mocker.GetMock<IMediaFileService>()
.Verify(e => e.Delete(It.IsAny<int>()), Times.Exactly(10));
}
}
}

View File

@ -42,8 +42,8 @@ namespace NzbDrone.Core
.As<IndexerBase>().SingleInstance();
container.RegisterAssemblyTypes(assembly)
.Where(t => t.IsSubclassOf(typeof(SearchBase)))
.As<SearchBase>().SingleInstance();
.Where(t => t.IsSubclassOf(typeof(IndexerSearchBase)))
.As<IndexerSearchBase>().SingleInstance();
container.RegisterAssemblyTypes(assembly)
.Where(t => t.IsSubclassOf(typeof(ExternalNotificationBase)))

View File

@ -14,6 +14,7 @@ namespace NzbDrone.Core.Datastore
TModel Update(TModel model);
TModel UpSert(TModel model);
void Delete(int id);
void Delete(TModel model);
IList<TModel> InsertMany(IList<TModel> model);
IList<TModel> UpdateMany(IList<TModel> model);
void DeleteMany(List<TModel> model);
@ -67,6 +68,11 @@ namespace NzbDrone.Core.Datastore
return ObjectDatabase.Update(model);
}
public void Delete(TModel model)
{
ObjectDatabase.Delete(model);
}
public IList<TModel> InsertMany(IList<TModel> model)
{
return ObjectDatabase.InsertMany(model);

View File

@ -7,11 +7,10 @@ using System.Net.NetworkInformation;
using System.Text.RegularExpressions;
using NLog;
using NzbDrone.Core.Model;
using NzbDrone.Core.Download.Clients.Sabnzbd;
namespace NzbDrone.Core.Providers
namespace NzbDrone.Core.Download.Clients.Sabnzbd
{
public class AutoConfigureProvider
public class SabAutoConfigureService
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

View File

@ -14,7 +14,7 @@ using NzbDrone.Core.Tv;
namespace NzbDrone.Core.IndexerSearch
{
public class DailyEpisodeSearch : SearchBase
public class DailyEpisodeSearch : IndexerSearchBase
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();

View File

@ -13,7 +13,7 @@ using NzbDrone.Core.Tv;
namespace NzbDrone.Core.IndexerSearch
{
public class EpisodeSearch : SearchBase
public class EpisodeSearch : IndexerSearchBase
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();

View File

@ -13,18 +13,19 @@ using NzbDrone.Core.Tv;
namespace NzbDrone.Core.IndexerSearch
{
public abstract class SearchBase
public abstract class IndexerSearchBase
{
private readonly ISeriesRepository _seriesRepository;
protected readonly IEpisodeService _episodeService;
protected readonly DownloadProvider _downloadProvider;
private readonly IEpisodeService _episodeService;
private readonly DownloadProvider _downloadProvider;
private readonly ISceneMappingService _sceneMappingService;
private readonly IDownloadDirector DownloadDirector;
protected readonly IIndexerService _indexerService;
protected readonly ISceneMappingService _sceneMappingService;
protected readonly IDownloadDirector DownloadDirector;
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
protected SearchBase(ISeriesRepository seriesRepository, IEpisodeService episodeService, DownloadProvider downloadProvider,
protected IndexerSearchBase(ISeriesRepository seriesRepository, IEpisodeService episodeService, DownloadProvider downloadProvider,
IIndexerService indexerService, ISceneMappingService sceneMappingService,
IDownloadDirector downloadDirector)
{
@ -36,7 +37,7 @@ namespace NzbDrone.Core.IndexerSearch
DownloadDirector = downloadDirector;
}
protected SearchBase()
protected IndexerSearchBase()
{
}

View File

@ -13,7 +13,7 @@ using NzbDrone.Core.Tv;
namespace NzbDrone.Core.IndexerSearch
{
public class PartialSeasonSearch : SearchBase
public class PartialSeasonSearch : IndexerSearchBase
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();

View File

@ -0,0 +1,14 @@
using NzbDrone.Common.Eventing;
namespace NzbDrone.Core.MediaFiles.Events
{
public class EpisodeFileDeletedEvent : IEvent
{
public EpisodeFile EpisodeFile { get; private set; }
public EpisodeFileDeletedEvent(EpisodeFile episodeFile)
{
EpisodeFile = episodeFile;
}
}
}

View File

@ -0,0 +1,48 @@
using System;
using NLog;
using NzbDrone.Common;
namespace NzbDrone.Core.MediaFiles
{
public interface ICleanGhostFiles
{
void RemoveNonExistingFiles(int seriesId);
}
public class GhostFileCleanupService : ICleanGhostFiles
{
private readonly IMediaFileService _mediaFileService;
private readonly DiskProvider _diskProvider;
private readonly Logger _logger;
public GhostFileCleanupService(IMediaFileService mediaFileService, DiskProvider diskProvider, Logger logger)
{
_mediaFileService = mediaFileService;
_diskProvider = diskProvider;
_logger = logger;
}
public void RemoveNonExistingFiles(int seriesId)
{
var seriesFile = _mediaFileService.GetFilesBySeries(seriesId);
foreach (var episodeFile in seriesFile)
{
try
{
if (!_diskProvider.FileExists(episodeFile.Path))
{
_logger.Trace("File [{0}] no longer exists on disk. removing from db", episodeFile.Path);
_mediaFileService.Delete(episodeFile);
}
}
catch (Exception ex)
{
var message = String.Format("Unable to cleanup EpisodeFile in DB: {0}", episodeFile.Id);
_logger.ErrorException(message, ex);
}
}
}
}
}

View File

@ -3,6 +3,7 @@ using System.IO;
using NLog;
using NzbDrone.Common.Eventing;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events;
@ -12,7 +13,7 @@ namespace NzbDrone.Core.MediaFiles
{
EpisodeFile Add(EpisodeFile episodeFile);
void Update(EpisodeFile episodeFile);
void Delete(int episodeFileId);
void Delete(EpisodeFile episodeFile);
bool Exists(string path);
EpisodeFile GetFileByPath(string path);
List<EpisodeFile> GetFilesBySeries(int seriesId);
@ -23,15 +24,17 @@ namespace NzbDrone.Core.MediaFiles
{
private readonly IConfigService _configService;
private readonly IEpisodeService _episodeService;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
private readonly IMediaFileRepository _mediaFileRepository;
public MediaFileService(IMediaFileRepository mediaFileRepository, IConfigService configService, IEpisodeService episodeService, Logger logger)
public MediaFileService(IMediaFileRepository mediaFileRepository, IConfigService configService, IEpisodeService episodeService, IEventAggregator eventAggregator, Logger logger)
{
_mediaFileRepository = mediaFileRepository;
_configService = configService;
_episodeService = episodeService;
_eventAggregator = eventAggregator;
_logger = logger;
}
@ -45,16 +48,10 @@ namespace NzbDrone.Core.MediaFiles
_mediaFileRepository.Update(episodeFile);
}
public void Delete(int episodeFileId)
public void Delete(EpisodeFile episodeFile)
{
_mediaFileRepository.Delete(episodeFileId);
var ep = _episodeService.GetEpisodesByFileId(episodeFileId);
foreach (var episode in ep)
{
_episodeService.SetEpisodeIgnore(episode.Id, true);
}
_mediaFileRepository.Delete(episodeFile);
_eventAggregator.Publish(new EpisodeFileDeletedEvent(episodeFile));
}
public bool Exists(string path)

View File

@ -201,6 +201,7 @@
<Compile Include="DecisionEngine\QualityUpgradableSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\UpgradeDiskSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\UpgradeHistorySpecification.cs" />
<Compile Include="Download\Clients\Sabnzbd\SabAutoConfigureService.cs" />
<Compile Include="Download\EpisodeDownloadedEvent.cs" />
<Compile Include="Download\EpisodeGrabbedEvent.cs" />
<Compile Include="Download\SeriesRenamedEvent.cs" />
@ -215,7 +216,7 @@
<Compile Include="IndexerSearch\DailyEpisodeSearch.cs" />
<Compile Include="IndexerSearch\EpisodeSearch.cs" />
<Compile Include="IndexerSearch\PartialSeasonSearch.cs" />
<Compile Include="IndexerSearch\SearchBase.cs" />
<Compile Include="IndexerSearch\IndexerSearchBase.cs" />
<Compile Include="Indexers\IndexerRepository.cs" />
<Compile Include="Indexers\NewznabRepository.cs" />
<Compile Include="Instrumentation\LogInjectionModule.cs" />
@ -249,6 +250,8 @@
<Compile Include="Lifecycle\AppRestartJob.cs" />
<Compile Include="Lifecycle\IInitializable.cs" />
<Compile Include="MediaCover\MediaCover.cs" />
<Compile Include="MediaFiles\Events\EpisodeFileDeletedEvent.cs" />
<Compile Include="MediaFiles\GhostFileCleanupService.cs" />
<Compile Include="MediaFiles\MediaFileRepository.cs" />
<Compile Include="Organizer\EpisodeSortingType.cs" />
<Compile Include="Organizer\FileNameBuilder.cs" />
@ -338,9 +341,6 @@
<Compile Include="Providers\XemProvider.cs" />
<Compile Include="Qualities\Quality.cs" />
<Compile Include="Tv\Season.cs" />
<Compile Include="Providers\AutoConfigureProvider.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Providers\BackupProvider.cs">
<SubType>Code</SubType>
</Compile>

View File

@ -20,21 +20,21 @@ namespace NzbDrone.Core.Providers
private static readonly string[] MediaExtensions = new[] { ".mkv", ".avi", ".wmv", ".mp4", ".mpg", ".mpeg", ".xvid", ".flv", ".mov", ".rm", ".rmvb", ".divx", ".dvr-ms", ".ts", ".ogm", ".m4v", ".strm" };
private readonly DiskProvider _diskProvider;
private readonly IEpisodeService _episodeService;
private readonly ICleanGhostFiles _ghostFileCleaner;
private readonly IMediaFileService _mediaFileService;
private readonly IConfigService _configService;
private readonly IBuildFileNames _buildFileNames;
private readonly RecycleBinProvider _recycleBinProvider;
private readonly MediaInfoProvider _mediaInfoProvider;
private readonly ISeriesRepository _seriesRepository;
private readonly IEventAggregator _eventAggregator;
public DiskScanProvider(DiskProvider diskProvider, IEpisodeService episodeService, IMediaFileService mediaFileService, IConfigService configService, IBuildFileNames buildFileNames,
public DiskScanProvider(DiskProvider diskProvider, IEpisodeService episodeService, ICleanGhostFiles ghostFileCleaner, IMediaFileService mediaFileService, IConfigService configService, IBuildFileNames buildFileNames,
RecycleBinProvider recycleBinProvider, MediaInfoProvider mediaInfoProvider, ISeriesRepository seriesRepository, IEventAggregator eventAggregator)
{
_diskProvider = diskProvider;
_episodeService = episodeService;
_ghostFileCleaner = ghostFileCleaner;
_mediaFileService = mediaFileService;
_configService = configService;
_buildFileNames = buildFileNames;
_recycleBinProvider = recycleBinProvider;
_mediaInfoProvider = mediaInfoProvider;
@ -74,8 +74,7 @@ namespace NzbDrone.Core.Providers
return new List<EpisodeFile>();
}
var seriesFile = _mediaFileService.GetFilesBySeries(series.Id);
CleanUp(seriesFile);
_ghostFileCleaner.RemoveNonExistingFiles(series.Id);
var mediaFileList = GetVideoFiles(path);
var importedFiles = new List<EpisodeFile>();
@ -237,43 +236,7 @@ namespace NzbDrone.Core.Providers
return episodeFile;
}
/// <summary>
/// Removes files that no longer exist on disk from the database
/// </summary>
/// <param name = "files">list of files to verify</param>
public virtual void CleanUp(IList<EpisodeFile> files)
{
foreach (var episodeFile in files)
{
try
{
if (!_diskProvider.FileExists(episodeFile.Path))
{
Logger.Trace("File [{0}] no longer exists on disk. removing from db", episodeFile.Path);
//Set the EpisodeFileId for each episode attached to this file to 0
foreach (var episode in _episodeService.GetEpisodesByFileId(episodeFile.Id))
{
Logger.Trace("Detaching episode {0} from file.", episode.Id);
episode.EpisodeFile = null;
episode.Ignored = _configService.AutoIgnorePreviouslyDownloadedEpisodes;
episode.GrabDate = null;
episode.PostDownloadStatus = PostDownloadStatusType.Unknown;
_episodeService.UpdateEpisode(episode);
}
//Delete it from the DB
Logger.Trace("Removing EpisodeFile from DB.");
_mediaFileService.Delete(episodeFile.Id);
}
}
catch (Exception ex)
{
var message = String.Format("Unable to cleanup EpisodeFile in DB: {0}", episodeFile.Id);
Logger.ErrorException(message, ex);
}
}
}
public virtual void CleanUpDropFolder(string path)
{

View File

@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Eventing;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Model;
using NzbDrone.Core.Providers;
@ -37,6 +39,7 @@ namespace NzbDrone.Core.Tv
public class EpisodeService : IEpisodeService,
IHandle<EpisodeGrabbedEvent>,
IHandle<EpisodeFileDeletedEvent>,
IHandleAsync<SeriesDeletedEvent>
{
@ -46,13 +49,17 @@ namespace NzbDrone.Core.Tv
private readonly ISeasonRepository _seasonRepository;
private readonly IEpisodeRepository _episodeRepository;
private readonly IEventAggregator _eventAggregator;
private readonly IConfigService _configService;
private readonly Logger _logger;
public EpisodeService(TvDbProxy tvDbProxyProxy, ISeasonRepository seasonRepository, IEpisodeRepository episodeRepository, IEventAggregator eventAggregator)
public EpisodeService(TvDbProxy tvDbProxyProxy, ISeasonRepository seasonRepository, IEpisodeRepository episodeRepository, IEventAggregator eventAggregator, IConfigService configService, Logger logger)
{
_tvDbProxy = tvDbProxyProxy;
_seasonRepository = seasonRepository;
_episodeRepository = episodeRepository;
_eventAggregator = eventAggregator;
_configService = configService;
_logger = logger;
}
public void AddEpisode(Episode episode)
@ -379,5 +386,18 @@ namespace NzbDrone.Core.Tv
var episodes = GetEpisodeBySeries(message.Series.Id);
_episodeRepository.DeleteMany(episodes);
}
public void Handle(EpisodeFileDeletedEvent message)
{
foreach (var episode in GetEpisodesByFileId(message.EpisodeFile.Id))
{
_logger.Trace("Detaching episode {0} from file.", episode.Id);
episode.EpisodeFile = null;
episode.Ignored = _configService.AutoIgnorePreviouslyDownloadedEpisodes;
episode.GrabDate = null;
episode.PostDownloadStatus = PostDownloadStatusType.Unknown;
UpdateEpisode(episode);
}
}
}
}

View File

@ -102,9 +102,16 @@ namespace NzbDrone.Test.Common
return Path.Combine(Directory.GetCurrentDirectory(), "Files", fileName);
}
protected void VerifyEventPublished<TEvent>() where TEvent : IEvent
{
Mocker.GetMock<IEventAggregator>().Verify(c => c.Publish(It.IsAny<TEvent>()), Times.Once());
VerifyEventPublished<TEvent>(Times.Once());
}
protected void VerifyEventPublished<TEvent>(Times times) where TEvent : IEvent
{
Mocker.GetMock<IEventAggregator>().Verify(c => c.Publish(It.IsAny<TEvent>()), times);
}
protected void VerifyEventNotPublished<TEvent>() where TEvent : IEvent