From 166fc9045493a80e54a0db2bd72ce51ea50a1ffa Mon Sep 17 00:00:00 2001 From: ta264 Date: Sat, 8 Jun 2019 20:13:58 +0100 Subject: [PATCH] New: Only scan files that are new or updated (#760) * New: Only scan files that are new or updated Pass through filter correctly Add more tests Add tests for migration 30 * Fix windows disk provider * Don't publish deleted event for unmapped file * Fix test on windows --- .../FileSystem/FileSystemModule.cs | 7 +- src/Lidarr.Api.V1/Lidarr.Api.V1.csproj | 3 + .../ManualImport/ManualImportModule.cs | 5 +- .../TrackFiles/TrackFileModule.cs | 9 +- .../TrackFiles/TrackFileResource.cs | 10 +- .../DirectoryLookupServiceFixture.cs | 5 +- .../DiskTests/DiskProviderFixtureBase.cs | 1 + .../DiskTests/DiskTransferServiceFixture.cs | 35 +- .../NzbDrone.Common.Test.csproj | 6 + .../PathExtensionFixture.cs | 1 + src/NzbDrone.Common.Test/packages.config | 4 +- src/NzbDrone.Common/Disk/DiskProviderBase.cs | 95 +++-- .../Disk/DiskTransferService.cs | 5 +- src/NzbDrone.Common/Disk/DriveInfoMount.cs | 5 +- src/NzbDrone.Common/Disk/IDiskProvider.cs | 7 +- .../Extensions/PathExtensions.cs | 5 + src/NzbDrone.Common/NzbDrone.Common.csproj | 3 + src/NzbDrone.Common/packages.config | 1 + ...30_add_mediafilerepository_mtimeFixture.cs | 321 +++++++++++++++++ .../DeletedTrackFileSpecificationFixture.cs | 6 +- .../Blackhole/ScanWatchFolderFixture.cs | 6 +- .../Blackhole/TorrentBlackholeFixture.cs | 4 + .../Blackhole/UsenetBlackholeFixture.cs | 4 + .../Framework/DirectDataMapper.cs | 2 +- .../Framework/FileSystemTest.cs | 22 ++ .../CleanupOrphanedTrackFilesFixture.cs | 21 +- .../Instrumentation/DatabaseTargetFixture.cs | 4 +- .../MediaFiles/AudioTagServiceFixture.cs | 5 +- .../DiskScanServiceTests/ScanFixture.cs | 339 +++++++++++++----- .../DownloadedAlbumsCommandServiceFixture.cs | 16 +- .../DownloadedTracksImportServiceFixture.cs | 130 +++---- .../MediaFiles/ImportApprovedTracksFixture.cs | 8 +- .../DeleteTrackFileFixture.cs | 3 +- .../MediaFiles/MediaFileRepositoryFixture.cs | 22 -- .../MediaFileServiceTests/FilterFixture.cs | 271 ++++++++++---- .../MediaFileTableCleanupServiceFixture.cs | 34 +- .../MoveTrackFileFixture.cs | 3 +- .../TrackImport/ImportDecisionMakerFixture.cs | 52 +-- .../UpgradeMediaFileServiceFixture.cs | 11 +- .../SynologyIndexerFixture.cs | 10 +- .../NzbDrone.Core.Test.csproj | 16 + .../FileNameBuilderFixture.cs | 21 +- .../GetAudioFilesFixture.cs | 27 +- src/NzbDrone.Core.Test/packages.config | 1 + .../030_add_mediafilerepository_mtime.cs | 64 ++++ src/NzbDrone.Core/Datastore/TableMapping.cs | 1 - .../RssSync/DeletedTrackFileSpecification.cs | 6 +- .../Clients/Blackhole/ScanWatchFolder.cs | 12 +- .../Extras/Files/ExtraFileManager.cs | 8 +- .../Extras/Lyrics/LyricService.cs | 2 +- .../Consumers/Roksbox/RoksboxMetadata.cs | 6 +- .../Metadata/Consumers/Wdtv/WdtvMetadata.cs | 6 +- .../Metadata/Consumers/Xbmc/XbmcMetadata.cs | 2 +- .../Extras/Metadata/MetadataBase.cs | 2 +- .../Extras/Metadata/MetadataService.cs | 6 +- src/NzbDrone.Core/History/HistoryService.cs | 9 +- .../Housekeepers/CleanupOrphanedTrackFiles.cs | 14 +- .../MediaFiles/AudioTagService.cs | 9 +- .../MediaFiles/DiskScanService.cs | 99 +++-- .../DownloadedTracksImportService.cs | 37 +- .../MediaFiles/FilterFilesType.cs | 9 + .../MediaFiles/MediaFileDeletionService.cs | 2 +- .../MediaFiles/MediaFileRepository.cs | 16 +- .../MediaFiles/MediaFileService.cs | 68 +++- .../MediaFileTableCleanupService.cs | 10 +- .../MediaFiles/RenameTrackFileService.cs | 6 +- src/NzbDrone.Core/MediaFiles/TrackFile.cs | 9 +- .../MediaFiles/TrackFileMovingService.cs | 6 +- .../Identification/IdentificationService.cs | 14 +- .../TrackImport/ImportApprovedTracks.cs | 16 +- .../TrackImport/ImportDecisionMaker.cs | 36 +- .../TrackImport/Manual/ManualImportService.cs | 20 +- .../MediaFiles/UpdateTrackFileService.cs | 2 +- .../MediaFiles/UpgradeMediaFileService.cs | 2 +- .../Music/RefreshAlbumService.cs | 32 +- .../Music/RefreshArtistService.cs | 48 ++- .../Music/RefreshTrackService.cs | 9 +- .../CustomScript/CustomScript.cs | 9 +- .../Notifications/NotificationService.cs | 2 +- .../Notifications/Synology/SynologyIndexer.cs | 4 +- .../Notifications/Webhook/WebhookTrackFile.cs | 2 - src/NzbDrone.Core/NzbDrone.Core.csproj | 5 + .../Organizer/FileNameBuilder.cs | 7 +- .../Organizer/FileNameSampleService.cs | 2 +- src/NzbDrone.Core/Parser/Model/LocalTrack.cs | 3 +- src/NzbDrone.Core/Parser/ParsingService.cs | 5 +- src/NzbDrone.Core/packages.config | 1 + .../DiskProviderTests/FreeSpaceFixture.cs | 5 +- src/NzbDrone.Mono/Disk/DiskProvider.cs | 18 +- src/NzbDrone.Mono/NzbDrone.Mono.csproj | 5 +- src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs | 29 +- .../NzbDrone.Test.Common.csproj | 14 +- src/NzbDrone.Test.Common/TestBase.cs | 2 +- src/NzbDrone.Windows/Disk/DiskProvider.cs | 11 + src/NzbDrone.Windows/NzbDrone.Windows.csproj | 5 +- 95 files changed, 1575 insertions(+), 708 deletions(-) create mode 100644 src/NzbDrone.Core.Test/Datastore/Migration/030_add_mediafilerepository_mtimeFixture.cs create mode 100644 src/NzbDrone.Core.Test/Framework/FileSystemTest.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/030_add_mediafilerepository_mtime.cs create mode 100644 src/NzbDrone.Core/MediaFiles/FilterFilesType.cs diff --git a/src/Lidarr.Api.V1/FileSystem/FileSystemModule.cs b/src/Lidarr.Api.V1/FileSystem/FileSystemModule.cs index 5fb003601..fe368b671 100644 --- a/src/Lidarr.Api.V1/FileSystem/FileSystemModule.cs +++ b/src/Lidarr.Api.V1/FileSystem/FileSystemModule.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.IO.Abstractions; using System.Linq; using Nancy; using NzbDrone.Common.Disk; @@ -62,9 +63,9 @@ namespace Lidarr.Api.V1.FileSystem } return _diskScanService.GetAudioFiles(path).Select(f => new { - Path = f, - RelativePath = path.GetRelativePath(f), - Name = Path.GetFileName(f) + Path = f.FullName, + RelativePath = path.GetRelativePath(f.FullName), + Name = f.Name }).AsResponse(); } } diff --git a/src/Lidarr.Api.V1/Lidarr.Api.V1.csproj b/src/Lidarr.Api.V1/Lidarr.Api.V1.csproj index f102ae7ce..e6f2a8644 100644 --- a/src/Lidarr.Api.V1/Lidarr.Api.V1.csproj +++ b/src/Lidarr.Api.V1/Lidarr.Api.V1.csproj @@ -72,6 +72,9 @@ ..\packages\Ical.Net.2.2.32\lib\net46\NodaTime.dll + + ..\packages\System.IO.Abstractions.4.0.11\lib\net40\System.IO.Abstractions.dll + diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs index e0540fc22..3c41232c5 100644 --- a/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs +++ b/src/Lidarr.Api.V1/ManualImport/ManualImportModule.cs @@ -7,6 +7,7 @@ using NzbDrone.Core.Music; using NLog; using Nancy; using Lidarr.Http; +using NzbDrone.Core.MediaFiles; namespace Lidarr.Api.V1.ManualImport { @@ -43,10 +44,10 @@ namespace Lidarr.Api.V1.ManualImport { var folder = (string)Request.Query.folder; var downloadId = (string)Request.Query.downloadId; - var filterExistingFiles = Request.GetBooleanQueryParameter("filterExistingFiles", true); + var filter = Request.GetBooleanQueryParameter("filterExistingFiles", true) ? FilterFilesType.Matched : FilterFilesType.None; var replaceExistingFiles = Request.GetBooleanQueryParameter("replaceExistingFiles", true); - return _manualImportService.GetMediaFiles(folder, downloadId, filterExistingFiles, replaceExistingFiles).ToResource().Select(AddQualityWeight).ToList(); + return _manualImportService.GetMediaFiles(folder, downloadId, filter, replaceExistingFiles).ToResource().Select(AddQualityWeight).ToList(); } private ManualImportResource AddQualityWeight(ManualImportResource item) diff --git a/src/Lidarr.Api.V1/TrackFiles/TrackFileModule.cs b/src/Lidarr.Api.V1/TrackFiles/TrackFileModule.cs index 1d0203242..a7be00e44 100644 --- a/src/Lidarr.Api.V1/TrackFiles/TrackFileModule.cs +++ b/src/Lidarr.Api.V1/TrackFiles/TrackFileModule.cs @@ -150,7 +150,6 @@ namespace Lidarr.Api.V1.TrackFiles } var artist = trackFile.Artist.Value; - var fullPath = Path.Combine(artist.Path, trackFile.RelativePath); _mediaFileDeletionService.DeleteTrackFile(artist, trackFile); } @@ -163,8 +162,6 @@ namespace Lidarr.Api.V1.TrackFiles foreach (var trackFile in trackFiles) { - var fullPath = Path.Combine(artist.Path, trackFile.RelativePath); - _mediaFileDeletionService.DeleteTrackFile(artist, trackFile); } @@ -173,6 +170,12 @@ namespace Lidarr.Api.V1.TrackFiles public void Handle(TrackFileAddedEvent message) { + // don't process files that are added but not matched + if (message.TrackFile.AlbumId == 0) + { + return; + } + BroadcastResourceChange(ModelAction.Updated, message.TrackFile.ToResource(message.TrackFile.Artist.Value, _upgradableSpecification)); } diff --git a/src/Lidarr.Api.V1/TrackFiles/TrackFileResource.cs b/src/Lidarr.Api.V1/TrackFiles/TrackFileResource.cs index 38e57ff14..4470d01bf 100644 --- a/src/Lidarr.Api.V1/TrackFiles/TrackFileResource.cs +++ b/src/Lidarr.Api.V1/TrackFiles/TrackFileResource.cs @@ -1,10 +1,10 @@ using System; -using System.IO; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Qualities; using Lidarr.Http.REST; +using NzbDrone.Common.Extensions; namespace Lidarr.Api.V1.TrackFiles { @@ -38,8 +38,8 @@ namespace Lidarr.Api.V1.TrackFiles ArtistId = model.Artist.Value.Id, AlbumId = model.AlbumId, - RelativePath = model.RelativePath, - //Path + RelativePath = model.Artist.Value.Path.GetRelativePath(model.Path), + Path = model.Path, Size = model.Size, DateAdded = model.DateAdded, // SceneName = model.SceneName, @@ -61,8 +61,8 @@ namespace Lidarr.Api.V1.TrackFiles ArtistId = artist.Id, AlbumId = model.AlbumId, - RelativePath = model.RelativePath, - Path = Path.Combine(artist.Path, model.RelativePath), + Path = model.Path, + RelativePath = artist.Path.GetRelativePath(model.Path), Size = model.Size, DateAdded = model.DateAdded, //SceneName = model.SceneName, diff --git a/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs b/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs index 78aa99f7d..48afe91c3 100644 --- a/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs +++ b/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Linq; using FluentAssertions; using Moq; @@ -15,7 +16,7 @@ namespace NzbDrone.Common.Test.DiskTests private const string RECYCLING_BIN = "$Recycle.Bin"; private const string SYSTEM_VOLUME_INFORMATION = "System Volume Information"; private const string WINDOWS = "Windows"; - private List _folders; + private List _folders; private void SetupFolders(string root) { @@ -36,7 +37,7 @@ namespace NzbDrone.Common.Test.DiskTests WINDOWS }; - _folders = folders.Select(f => new DirectoryInfo(Path.Combine(root, f))).ToList(); + _folders = folders.Select(f => (DirectoryInfoBase)new DirectoryInfo(Path.Combine(root, f))).ToList(); } [Test] diff --git a/src/NzbDrone.Common.Test/DiskTests/DiskProviderFixtureBase.cs b/src/NzbDrone.Common.Test/DiskTests/DiskProviderFixtureBase.cs index 266a89ba3..1d750e6c6 100644 --- a/src/NzbDrone.Common.Test/DiskTests/DiskProviderFixtureBase.cs +++ b/src/NzbDrone.Common.Test/DiskTests/DiskProviderFixtureBase.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.IO.Abstractions; using FluentAssertions; using NUnit.Framework; using NzbDrone.Common.Disk; diff --git a/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs b/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs index ca6e539f5..5a216c719 100644 --- a/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs +++ b/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Linq; using FluentAssertions; using Moq; @@ -981,48 +982,50 @@ namespace NzbDrone.Common.Test.DiskTests // Note: never returns anything. Mocker.GetMock() - .Setup(v => v.GetDirectoryInfos(It.IsAny())) - .Returns(new List()); + .Setup(v => v.GetDirectoryInfos(It.IsAny())) + .Returns(new List()); // Note: never returns anything. Mocker.GetMock() - .Setup(v => v.GetFileInfos(It.IsAny())) - .Returns(new List()); + .Setup(v => v.GetFileInfos(It.IsAny(), It.IsAny())) + .Returns(new List()); } private void WithRealDiskProvider() { + IFileSystem _fileSystem = new FileSystem(); + Mocker.GetMock() .Setup(v => v.FolderExists(It.IsAny())) - .Returns(v => Directory.Exists(v)); + .Returns(v => _fileSystem.Directory.Exists(v)); Mocker.GetMock() .Setup(v => v.FileExists(It.IsAny())) - .Returns(v => File.Exists(v)); + .Returns(v => _fileSystem.File.Exists(v)); Mocker.GetMock() .Setup(v => v.CreateFolder(It.IsAny())) - .Callback(v => Directory.CreateDirectory(v)); + .Callback(v => _fileSystem.Directory.CreateDirectory(v)); Mocker.GetMock() .Setup(v => v.DeleteFolder(It.IsAny(), It.IsAny())) - .Callback((v, r) => Directory.Delete(v, r)); + .Callback((v, r) => _fileSystem.Directory.Delete(v, r)); Mocker.GetMock() .Setup(v => v.DeleteFile(It.IsAny())) - .Callback(v => File.Delete(v)); + .Callback(v => _fileSystem.File.Delete(v)); Mocker.GetMock() .Setup(v => v.GetDirectoryInfos(It.IsAny())) - .Returns(v => new DirectoryInfo(v).GetDirectories().ToList()); + .Returns(v => _fileSystem.DirectoryInfo.FromDirectoryName(v).GetDirectories().ToList()); Mocker.GetMock() - .Setup(v => v.GetFileInfos(It.IsAny())) - .Returns(v => new DirectoryInfo(v).GetFiles().ToList()); + .Setup(v => v.GetFileInfos(It.IsAny(), It.IsAny())) + .Returns((string v, SearchOption option) => _fileSystem.DirectoryInfo.FromDirectoryName(v).GetFiles("*", option).ToList()); Mocker.GetMock() .Setup(v => v.GetFileSize(It.IsAny())) - .Returns(v => new FileInfo(v).Length); + .Returns(v => _fileSystem.FileInfo.FromFileName(v).Length); Mocker.GetMock() .Setup(v => v.TryCreateHardLink(It.IsAny(), It.IsAny())) @@ -1030,13 +1033,13 @@ namespace NzbDrone.Common.Test.DiskTests Mocker.GetMock() .Setup(v => v.CopyFile(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((s, d, o) => File.Copy(s, d, o)); + .Callback((s, d, o) => _fileSystem.File.Copy(s, d, o)); Mocker.GetMock() .Setup(v => v.MoveFile(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((s, d, o) => { - if (File.Exists(d) && o) File.Delete(d); - File.Move(s, d); + if (_fileSystem.File.Exists(d) && o) _fileSystem.File.Delete(d); + _fileSystem.File.Move(s, d); }); Mocker.GetMock() diff --git a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj index 412384a51..f5c852b3a 100644 --- a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj +++ b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj @@ -55,6 +55,12 @@ ..\packages\NUnit.3.11.0\lib\net45\nunit.framework.dll + + ..\packages\System.IO.Abstractions.4.0.11\lib\net40\System.IO.Abstractions.dll + + + ..\packages\Unity.2.1.505.2\lib\NET35\Microsoft.Practices.Unity.dll + diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs index 42ad67aac..269e4145c 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -43,6 +43,7 @@ namespace NzbDrone.Common.Test result.Should().Be(clean); } + [TestCase(@"/", @"/")] [TestCase(@"/test/", @"/test")] [TestCase(@"//test/", @"/test")] [TestCase(@"//test//", @"/test")] diff --git a/src/NzbDrone.Common.Test/packages.config b/src/NzbDrone.Common.Test/packages.config index 37ac7a8c1..dd34c366c 100644 --- a/src/NzbDrone.Common.Test/packages.config +++ b/src/NzbDrone.Common.Test/packages.config @@ -4,4 +4,6 @@ - \ No newline at end of file + + + diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index 29b37fb26..de37a7ac3 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Linq; using System.Security.AccessControl; using System.Security.Principal; @@ -15,6 +16,12 @@ namespace NzbDrone.Common.Disk public abstract class DiskProviderBase : IDiskProvider { private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(DiskProviderBase)); + protected readonly IFileSystem _fileSystem; + + public DiskProviderBase(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } public static StringComparison PathStringComparison { @@ -38,7 +45,7 @@ namespace NzbDrone.Common.Disk { CheckFolderExists(path); - return new DirectoryInfo(path).CreationTimeUtc; + return _fileSystem.DirectoryInfo.FromDirectoryName(path).CreationTimeUtc; } public DateTime FolderGetLastWrite(string path) @@ -49,17 +56,17 @@ namespace NzbDrone.Common.Disk if (!dirFiles.Any()) { - return new DirectoryInfo(path).LastWriteTimeUtc; + return _fileSystem.DirectoryInfo.FromDirectoryName(path).LastWriteTimeUtc; } - return dirFiles.Select(f => new FileInfo(f)).Max(c => c.LastWriteTimeUtc); + return dirFiles.Select(f => _fileSystem.FileInfo.FromFileName(f)).Max(c => c.LastWriteTimeUtc); } public DateTime FileGetLastWrite(string path) { CheckFileExists(path); - return new FileInfo(path).LastWriteTimeUtc; + return _fileSystem.FileInfo.FromFileName(path).LastWriteTimeUtc; } private void CheckFolderExists(string path) @@ -93,7 +100,7 @@ namespace NzbDrone.Common.Disk public bool FolderExists(string path) { Ensure.That(path, () => path).IsValidPath(); - return Directory.Exists(path); + return _fileSystem.Directory.Exists(path); } public bool FileExists(string path) @@ -112,11 +119,11 @@ namespace NzbDrone.Common.Disk case StringComparison.InvariantCulture: case StringComparison.Ordinal: { - return File.Exists(path) && path == path.GetActualCasing(); + return _fileSystem.File.Exists(path) && path == path.GetActualCasing(); } default: { - return File.Exists(path); + return _fileSystem.File.Exists(path); } } } @@ -129,8 +136,8 @@ namespace NzbDrone.Common.Disk { var testPath = Path.Combine(path, "lidarr_write_test.txt"); var testContent = $"This file was created to verify if '{path}' is writable. It should've been automatically deleted. Feel free to delete it."; - File.WriteAllText(testPath, testContent); - File.Delete(testPath); + _fileSystem.File.WriteAllText(testPath, testContent); + _fileSystem.File.Delete(testPath); return true; } catch (Exception e) @@ -144,21 +151,21 @@ namespace NzbDrone.Common.Disk { Ensure.That(path, () => path).IsValidPath(); - return Directory.GetDirectories(path); + return _fileSystem.Directory.GetDirectories(path); } public string[] GetFiles(string path, SearchOption searchOption) { Ensure.That(path, () => path).IsValidPath(); - return Directory.GetFiles(path, "*.*", searchOption); + return _fileSystem.Directory.GetFiles(path, "*.*", searchOption); } public long GetFolderSize(string path) { Ensure.That(path, () => path).IsValidPath(); - return GetFiles(path, SearchOption.AllDirectories).Sum(e => new FileInfo(e).Length); + return GetFiles(path, SearchOption.AllDirectories).Sum(e => _fileSystem.FileInfo.FromFileName(e).Length); } public long GetFileSize(string path) @@ -170,14 +177,14 @@ namespace NzbDrone.Common.Disk throw new FileNotFoundException("File doesn't exist: " + path); } - var fi = new FileInfo(path); + var fi = _fileSystem.FileInfo.FromFileName(path); return fi.Length; } public void CreateFolder(string path) { Ensure.That(path, () => path).IsValidPath(); - Directory.CreateDirectory(path); + _fileSystem.Directory.CreateDirectory(path); } public void DeleteFile(string path) @@ -187,7 +194,7 @@ namespace NzbDrone.Common.Disk RemoveReadOnly(path); - File.Delete(path); + _fileSystem.File.Delete(path); } public void CopyFile(string source, string destination, bool overwrite = false) @@ -205,7 +212,7 @@ namespace NzbDrone.Common.Disk protected virtual void CopyFileInternal(string source, string destination, bool overwrite = false) { - File.Copy(source, destination, overwrite); + _fileSystem.File.Copy(source, destination, overwrite); } public void MoveFile(string source, string destination, bool overwrite = false) @@ -237,7 +244,7 @@ namespace NzbDrone.Common.Disk protected virtual void MoveFileInternal(string source, string destination) { - File.Move(source, destination); + _fileSystem.File.Move(source, destination); } public abstract bool TryCreateHardLink(string source, string destination); @@ -246,45 +253,45 @@ namespace NzbDrone.Common.Disk { Ensure.That(path, () => path).IsValidPath(); - var files = Directory.GetFiles(path, "*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); + var files = _fileSystem.Directory.GetFiles(path, "*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); Array.ForEach(files, RemoveReadOnly); - Directory.Delete(path, recursive); + _fileSystem.Directory.Delete(path, recursive); } public string ReadAllText(string filePath) { Ensure.That(filePath, () => filePath).IsValidPath(); - return File.ReadAllText(filePath); + return _fileSystem.File.ReadAllText(filePath); } public void WriteAllText(string filename, string contents) { Ensure.That(filename, () => filename).IsValidPath(); RemoveReadOnly(filename); - File.WriteAllText(filename, contents); + _fileSystem.File.WriteAllText(filename, contents); } public void FolderSetLastWriteTime(string path, DateTime dateTime) { Ensure.That(path, () => path).IsValidPath(); - Directory.SetLastWriteTimeUtc(path, dateTime); + _fileSystem.Directory.SetLastWriteTimeUtc(path, dateTime); } public void FileSetLastWriteTime(string path, DateTime dateTime) { Ensure.That(path, () => path).IsValidPath(); - File.SetLastWriteTime(path, dateTime); + _fileSystem.File.SetLastWriteTime(path, dateTime); } public bool IsFileLocked(string file) { try { - using (File.Open(file, FileMode.Open, FileAccess.Read, FileShare.None)) + using (_fileSystem.File.Open(file, FileMode.Open, FileAccess.Read, FileShare.None)) { return false; } @@ -306,7 +313,7 @@ namespace NzbDrone.Common.Disk { Ensure.That(path, () => path).IsValidPath(); - var parent = Directory.GetParent(path.TrimEnd(Path.DirectorySeparatorChar)); + var parent = _fileSystem.Directory.GetParent(path.TrimEnd(Path.DirectorySeparatorChar)); if (parent == null) { @@ -322,7 +329,7 @@ namespace NzbDrone.Common.Disk { var sid = new SecurityIdentifier(accountSid, null); - var directoryInfo = new DirectoryInfo(filename); + var directoryInfo = _fileSystem.DirectoryInfo.FromDirectoryName(filename); var directorySecurity = directoryInfo.GetAccessControl(AccessControlSections.Access); var rules = directorySecurity.GetAccessRules(true, false, typeof(SecurityIdentifier)); @@ -368,7 +375,7 @@ namespace NzbDrone.Common.Disk public FileAttributes GetFileAttributes(string path) { - return File.GetAttributes(path); + return _fileSystem.File.GetAttributes(path); } public void EmptyFolder(string path) @@ -410,12 +417,12 @@ namespace NzbDrone.Common.Disk throw new FileNotFoundException("Unable to find file: " + path, path); } - return new FileStream(path, FileMode.Open, FileAccess.Read); + return (FileStream) _fileSystem.FileStream.Create(path, FileMode.Open, FileAccess.Read); } public FileStream OpenWriteStream(string path) { - return new FileStream(path, FileMode.Create); + return (FileStream) _fileSystem.FileStream.Create(path, FileMode.Create); } public List GetMounts() @@ -454,29 +461,41 @@ namespace NzbDrone.Common.Disk } } - protected List GetDriveInfoMounts() + protected List GetDriveInfoMounts() { - return DriveInfo.GetDrives() - .Where(d => d.IsReady) - .ToList(); + return _fileSystem.DriveInfo.GetDrives() + .Where(d => d.IsReady) + .ToList(); } - public List GetDirectoryInfos(string path) + public List GetDirectoryInfos(string path) { Ensure.That(path, () => path).IsValidPath(); - var di = new DirectoryInfo(path); + var di = _fileSystem.DirectoryInfo.FromDirectoryName(path); return di.GetDirectories().ToList(); } - public List GetFileInfos(string path) + public IDirectoryInfo GetDirectoryInfo(string path) { Ensure.That(path, () => path).IsValidPath(); + return _fileSystem.DirectoryInfo.FromDirectoryName(path); + } - var di = new DirectoryInfo(path); + public List GetFileInfos(string path, SearchOption searchOption = SearchOption.TopDirectoryOnly) + { + Ensure.That(path, () => path).IsValidPath(); - return di.GetFiles().ToList(); + var di = _fileSystem.DirectoryInfo.FromDirectoryName(path); + + return di.GetFiles("*", searchOption).ToList(); + } + + public IFileInfo GetFileInfo(string path) + { + Ensure.That(path, () => path).IsValidPath(); + return _fileSystem.FileInfo.FromFileName(path); } public void RemoveEmptySubfolders(string path) diff --git a/src/NzbDrone.Common/Disk/DiskTransferService.cs b/src/NzbDrone.Common/Disk/DiskTransferService.cs index 1f210b977..947ad16d3 100644 --- a/src/NzbDrone.Common/Disk/DiskTransferService.cs +++ b/src/NzbDrone.Common/Disk/DiskTransferService.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.IO.Abstractions; using System.Linq; using System.Threading; using NLog; @@ -594,7 +595,7 @@ namespace NzbDrone.Common.Disk } } - private bool ShouldIgnore(DirectoryInfo folder) + private bool ShouldIgnore(IDirectoryInfo folder) { if (folder.Name.StartsWith(".nfs")) { @@ -605,7 +606,7 @@ namespace NzbDrone.Common.Disk return false; } - private bool ShouldIgnore(FileInfo file) + private bool ShouldIgnore(IFileInfo file) { if (file.Name.StartsWith(".nfs") || file.Name == "debug.log" || file.Name.EndsWith(".socket")) { diff --git a/src/NzbDrone.Common/Disk/DriveInfoMount.cs b/src/NzbDrone.Common/Disk/DriveInfoMount.cs index 1ddd9f43f..d80513666 100644 --- a/src/NzbDrone.Common/Disk/DriveInfoMount.cs +++ b/src/NzbDrone.Common/Disk/DriveInfoMount.cs @@ -1,15 +1,16 @@ using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Disk { public class DriveInfoMount : IMount { - private readonly DriveInfo _driveInfo; + private readonly IDriveInfo _driveInfo; private readonly DriveType _driveType; - public DriveInfoMount(DriveInfo driveInfo, DriveType driveType = DriveType.Unknown, MountOptions mountOptions = null) + public DriveInfoMount(IDriveInfo driveInfo, DriveType driveType = DriveType.Unknown, MountOptions mountOptions = null) { _driveInfo = driveInfo; _driveType = driveType; diff --git a/src/NzbDrone.Common/Disk/IDiskProvider.cs b/src/NzbDrone.Common/Disk/IDiskProvider.cs index 20f36c67d..c4cec3fc0 100644 --- a/src/NzbDrone.Common/Disk/IDiskProvider.cs +++ b/src/NzbDrone.Common/Disk/IDiskProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Security.AccessControl; using System.Security.Principal; @@ -46,8 +47,10 @@ namespace NzbDrone.Common.Disk FileStream OpenWriteStream(string path); List GetMounts(); IMount GetMount(string path); - List GetDirectoryInfos(string path); - List GetFileInfos(string path); + IDirectoryInfo GetDirectoryInfo(string path); + List GetDirectoryInfos(string path); + IFileInfo GetFileInfo(string path); + List GetFileInfos(string path, SearchOption searchOption = SearchOption.TopDirectoryOnly); void RemoveEmptySubfolders(string path); void SaveStream(Stream stream, string path); } diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 68d1eea33..a6a62042d 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -38,6 +38,11 @@ namespace NzbDrone.Common.Extensions return info.FullName.TrimEnd('/', '\\', ' '); } + if (OsInfo.IsNotWindows && info.FullName.TrimEnd('/').Length == 0) + { + return "/"; + } + return info.FullName.TrimEnd('/').Trim('\\', ' '); } diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index 05e2a0eaf..1441389c6 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -66,6 +66,9 @@ ..\packages\System.Collections.Immutable.1.5.0\lib\netstandard2.0\System.Collections.Immutable.dll + + ..\packages\System.IO.Abstractions.4.0.11\lib\net40\System.IO.Abstractions.dll + ..\packages\DotNet4.SocksProxy.1.3.4.0\lib\net40\SocksWebProxy.dll True diff --git a/src/NzbDrone.Common/packages.config b/src/NzbDrone.Common/packages.config index 56cd6e4e1..f23ec5743 100644 --- a/src/NzbDrone.Common/packages.config +++ b/src/NzbDrone.Common/packages.config @@ -8,4 +8,5 @@ + diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/030_add_mediafilerepository_mtimeFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/030_add_mediafilerepository_mtimeFixture.cs new file mode 100644 index 000000000..b79248a61 --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/030_add_mediafilerepository_mtimeFixture.cs @@ -0,0 +1,321 @@ +using NUnit.Framework; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.Test.Framework; +using System.Collections.Generic; +using System; +using NzbDrone.Core.Qualities; +using NzbDrone.Common.Serializer; +using NzbDrone.Test.Common; +using System.Linq; +using FluentAssertions; +using System.IO; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class add_mediafilerepository_mtimeFixture : MigrationTest + { + private string _artistPath = null; + + private void GivenArtist(add_mediafilerepository_mtime c, int id, string name) + { + _artistPath = $"/mnt/data/path/{name}".AsOsAgnostic(); + c.Insert.IntoTable("Artists").Row(new + { + Id = id, + CleanName = name, + Path = _artistPath, + Monitored = 1, + AlbumFolder = 1, + LanguageProfileId = 1, + MetadataProfileId = 1, + ArtistMetadataId = id + }); + } + + private void GivenAlbum(add_mediafilerepository_mtime c, int id, int artistMetadataId, string title) + { + c.Insert.IntoTable("Albums").Row(new + { + Id = id, + ForeignAlbumId = id.ToString(), + ArtistMetadataId = artistMetadataId, + Title = title, + CleanTitle = title, + Images = "", + Monitored = 1, + AlbumType = "Studio", + AnyReleaseOk = 1 + }); + } + + private void GivenAlbumRelease(add_mediafilerepository_mtime c, int id, int albumId, bool monitored) + { + c.Insert.IntoTable("AlbumReleases").Row(new + { + Id = id, + ForeignReleaseId = id.ToString(), + AlbumId = albumId, + Title = "Title", + Status = "Status", + Duration = 0, + Monitored = monitored + }); + } + + private void GivenTrackFiles(add_mediafilerepository_mtime c, List tracks, int albumReleaseId, int albumId, int firstId = 1, bool addTracks = true) + { + int id = firstId; + foreach (var track in tracks) + { + c.Insert.IntoTable("TrackFiles").Row(new + { + Id = id, + RelativePath = track?.AsOsAgnostic(), + Size = 100, + DateAdded = DateTime.UtcNow, + Quality = new QualityModel(Quality.FLAC).ToJson(), + Language = 1, + AlbumId = albumId + }); + + if (addTracks) + { + c.Insert.IntoTable("Tracks").Row(new + { + Id = id, + ForeignTrackId = id.ToString(), + Explicit = 0, + TrackFileId = id, + Duration = 100, + MediumNumber = 1, + AbsoluteTrackNumber = 1, + ForeignRecordingId = id.ToString(), + AlbumReleaseId = albumReleaseId, + ArtistMetadataId = 0 + }); + } + + id++; + } + } + + private void VerifyTracksFiles(IDirectDataMapper db, int albumId, List expectedPaths) + { + var tracks = db.Query("SELECT TrackFiles.* FROM TrackFiles " + + "WHERE TrackFiles.AlbumId = " + albumId); + + TestLogger.Debug($"Got {tracks.Count} tracks"); + + tracks.Select(x => x["Path"]).Should().BeEquivalentTo(expectedPaths); + } + + [Test] + public void migration_030_simple_case() + { + var tracks = new List { + "folder/track1.mp3", + "folder/track2.mp3", + }; + + var db = WithMigrationTestDb(c => { + GivenArtist(c, 1, "TestArtist"); + GivenAlbum(c, 1, 1, "TestAlbum"); + GivenAlbumRelease(c, 1, 1, true); + GivenTrackFiles(c, tracks, 1, 1); + }); + + var expected = tracks.Select(x => Path.Combine(_artistPath, x)).ToList(); + + VerifyTracksFiles(db, 1, expected); + } + + [Test] + public void migration_030_missing_path() + { + var tracks = new List { + "folder/track1.mp3", + null, + }; + + var db = WithMigrationTestDb(c => { + GivenArtist(c, 1, "TestArtist"); + GivenAlbum(c, 1, 1, "TestAlbum"); + GivenAlbumRelease(c, 1, 1, true); + GivenTrackFiles(c, tracks, 1, 1); + }); + + var expected = tracks.GetRange(0, 1).Select(x => Path.Combine(_artistPath, x)).ToList(); + + VerifyTracksFiles(db, 1, expected); + } + + [Test] + public void migration_030_bad_albumrelease_id() + { + var tracks = new List { + "folder/track1.mp3", + "folder/track2.mp3" + }; + + var db = WithMigrationTestDb(c => { + GivenArtist(c, 1, "TestArtist"); + GivenAlbum(c, 1, 1, "TestAlbum"); + GivenAlbumRelease(c, 1, 1, true); + GivenTrackFiles(c, tracks, 2, 1); + }); + + VerifyTracksFiles(db, 1, new List()); + } + + [Test] + public void migration_030_bad_album_id() + { + var tracks = new List { + "folder/track1.mp3", + "folder/track2.mp3" + }; + + var db = WithMigrationTestDb(c => { + GivenArtist(c, 1, "TestArtist"); + GivenAlbum(c, 1, 1, "TestAlbum"); + GivenAlbumRelease(c, 1, 1, true); + GivenTrackFiles(c, tracks, 1, 2); + }); + + VerifyTracksFiles(db, 1, new List()); + } + + [Test] + public void migration_030_bad_artist_metadata_id() + { + var tracks = new List { + "folder/track1.mp3", + "folder/track2.mp3" + }; + + var db = WithMigrationTestDb(c => { + GivenArtist(c, 1, "TestArtist"); + GivenAlbum(c, 1, 2, "TestAlbum"); + GivenAlbumRelease(c, 1, 1, true); + GivenTrackFiles(c, tracks, 1, 1); + }); + + VerifyTracksFiles(db, 1, new List()); + } + + [Test] + public void migration_030_missing_artist() + { + var tracks = new List { + "folder/track1.mp3", + "folder/track2.mp3" + }; + + var db = WithMigrationTestDb(c => { + GivenAlbum(c, 1, 1, "TestAlbum"); + GivenAlbumRelease(c, 1, 1, true); + GivenTrackFiles(c, tracks, 1, 1); + }); + + VerifyTracksFiles(db, 1, new List()); + } + + [Test] + public void migration_030_missing_tracks() + { + var tracks = new List { + "folder/track1.mp3", + "folder/track2.mp3" + }; + + var db = WithMigrationTestDb(c => { + GivenArtist(c, 1, "TestArtist"); + GivenAlbum(c, 1, 1, "TestAlbum"); + GivenAlbumRelease(c, 1, 1, true); + GivenTrackFiles(c, tracks, 1, 1, addTracks: false); + }); + + VerifyTracksFiles(db, 1, new List()); + } + + [Test] + public void migration_030_duplicate_files() + { + var tracks = new List { + "folder/track1.mp3", + "folder/track2.mp3", + "folder/track1.mp3", + }; + + var db = WithMigrationTestDb(c => { + GivenArtist(c, 1, "TestArtist"); + GivenAlbum(c, 1, 1, "TestAlbum"); + GivenAlbumRelease(c, 1, 1, true); + GivenTrackFiles(c, tracks, 1, 1); + }); + + var expected = tracks.GetRange(0, 2).Select(x => Path.Combine(_artistPath, x)).ToList(); + + VerifyTracksFiles(db, 1, expected); + } + + [Test] + public void migration_030_unmonitored_release_duplicate() + { + var monitored_tracks = new List { + "folder/track1.mp3", + "folder/track2.mp3", + }; + + var unmonitored_tracks = new List { + "folder/track1.mp3", + "folder/track2.mp3", + }; + + var db = WithMigrationTestDb(c => { + GivenArtist(c, 1, "TestArtist"); + GivenAlbum(c, 1, 1, "TestAlbum"); + + GivenAlbumRelease(c, 1, 1, true); + GivenTrackFiles(c, monitored_tracks, 1, 1); + + GivenAlbumRelease(c, 2, 1, false); + GivenTrackFiles(c, unmonitored_tracks, 2, 1, firstId: 100); + }); + + var expected = monitored_tracks.Select(x => Path.Combine(_artistPath, x)).ToList(); + + VerifyTracksFiles(db, 1, expected); + } + + [Test] + public void migration_030_unmonitored_release_distinct() + { + var monitored_tracks = new List { + "folder/track1.mp3", + "folder/track2.mp3", + }; + + var unmonitored_tracks = new List { + "folder/track3.mp3", + "folder/track4.mp3", + }; + + var db = WithMigrationTestDb(c => { + GivenArtist(c, 1, "TestArtist"); + GivenAlbum(c, 1, 1, "TestAlbum"); + + GivenAlbumRelease(c, 1, 1, true); + GivenTrackFiles(c, monitored_tracks, 1, 1); + + GivenAlbumRelease(c, 2, 1, false); + GivenTrackFiles(c, unmonitored_tracks, 2, 1, firstId: 100); + }); + + var expected = monitored_tracks.Select(x => Path.Combine(_artistPath, x)).ToList(); + + VerifyTracksFiles(db, 1, expected); + } + } +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedTrackFileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedTrackFileSpecificationFixture.cs index 698eafce2..d581c5194 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedTrackFileSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedTrackFileSpecificationFixture.cs @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _firstFile = new TrackFile{ Id = 1, - RelativePath = "My.Artist.S01E01.mp3", + Path = "/My.Artist.S01E01.mp3", Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), DateAdded = DateTime.Now, AlbumId = 1 @@ -43,7 +43,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _secondFile = new TrackFile{ Id = 2, - RelativePath = "My.Artist.S01E02.mp3", + Path = "/My.Artist.S01E02.mp3", Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), DateAdded = DateTime.Now, AlbumId = 2 @@ -97,7 +97,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync private void WithExistingFile(TrackFile trackFile) { - var path = Path.Combine(@"C:\Music\My.Artist".AsOsAgnostic(), trackFile.RelativePath); + var path = trackFile.Path; Mocker.GetMock() .Setup(v => v.FileExists(path)) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/ScanWatchFolderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/ScanWatchFolderFixture.cs index a63bfba6a..227102792 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/ScanWatchFolderFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/ScanWatchFolderFixture.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Linq; using System.Threading; using FluentAssertions; @@ -37,7 +38,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole .Returns(1000000); Mocker.GetMock().Setup(c => c.FilterFiles(It.IsAny(), It.IsAny>())) - .Returns>((b, s) => s.ToList()); + .Returns>((b, s) => s.ToList()); + + Mocker.GetMock().Setup(c => c.FilterFiles(It.IsAny(), It.IsAny>())) + .Returns>((b, s) => s.ToList()); } protected void GivenChangedItem() diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs index e972fc474..6fd6c6042 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Linq; using System.Net; using FluentAssertions; @@ -52,6 +53,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole Mocker.GetMock().Setup(c => c.FilterFiles(It.IsAny(), It.IsAny>())) .Returns>((b, s) => s.ToList()); + + Mocker.GetMock().Setup(c => c.FilterFiles(It.IsAny(), It.IsAny>())) + .Returns>((b, s) => s.ToList()); } protected void GivenFailedDownload() diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs index 291846222..04786acbc 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Linq; using System.Net; using FluentAssertions; @@ -46,6 +47,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole Mocker.GetMock().Setup(c => c.FilterFiles(It.IsAny(), It.IsAny>())) .Returns>((b, s) => s.ToList()); + + Mocker.GetMock().Setup(c => c.FilterFiles(It.IsAny(), It.IsAny>())) + .Returns>((b, s) => s.ToList()); } protected void GivenFailedDownload() diff --git a/src/NzbDrone.Core.Test/Framework/DirectDataMapper.cs b/src/NzbDrone.Core.Test/Framework/DirectDataMapper.cs index a75daf167..ccc469e21 100644 --- a/src/NzbDrone.Core.Test/Framework/DirectDataMapper.cs +++ b/src/NzbDrone.Core.Test/Framework/DirectDataMapper.cs @@ -88,7 +88,7 @@ namespace NzbDrone.Core.Test.Framework value = dataRow.ItemArray[i]; } - item[columnName] = dataRow.ItemArray[i]; + item[columnName] = value; } return item; diff --git a/src/NzbDrone.Core.Test/Framework/FileSystemTest.cs b/src/NzbDrone.Core.Test/Framework/FileSystemTest.cs new file mode 100644 index 000000000..8e43591a2 --- /dev/null +++ b/src/NzbDrone.Core.Test/Framework/FileSystemTest.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using System.IO.Abstractions.TestingHelpers; +using Microsoft.Practices.Unity; +using NzbDrone.Common.Disk; +namespace NzbDrone.Core.Test.Framework +{ + public abstract class FileSystemTest : CoreTest where TSubject : class + { + protected MockFileSystem FileSystem { get; private set; } + protected IDiskProvider DiskProvider { get; private set; } + + [SetUp] + public void FileSystemTestSetup() + { + FileSystem = new MockFileSystem(); + + DiskProvider = Mocker.Resolve("ActualDiskProvider", new ResolverOverride[] { + new ParameterOverride("fileSystem", FileSystem) + }); + } + } +} diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedTrackFilesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedTrackFilesFixture.cs index 95c824745..fa3fbcae9 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedTrackFilesFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedTrackFilesFixture.cs @@ -14,24 +14,26 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers public class CleanupOrphanedTrackFilesFixture : DbTest { [Test] - public void should_delete_orphaned_track_files() + public void should_unlink_orphaned_track_files() { var trackFile = Builder.CreateNew() - .With(h => h.Quality = new QualityModel()) - .BuildNew(); + .With(h => h.Quality = new QualityModel()) + .With(h => h.AlbumId = 1) + .BuildNew(); Db.Insert(trackFile); Subject.Clean(); - AllStoredModels.Should().BeEmpty(); + AllStoredModels[0].AlbumId.Should().Be(0); } [Test] - public void should_not_delete_unorphaned_track_files() + public void should_not_unlink_unorphaned_track_files() { var trackFiles = Builder.CreateListOfSize(2) - .All() - .With(h => h.Quality = new QualityModel()) - .BuildListOfNew(); + .All() + .With(h => h.Quality = new QualityModel()) + .With(h => h.AlbumId = 1) + .BuildListOfNew(); Db.InsertMany(trackFiles); @@ -42,7 +44,8 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers Db.Insert(track); Subject.Clean(); - AllStoredModels.Should().HaveCount(1); + AllStoredModels.Where(x => x.AlbumId == 1).Should().HaveCount(1); + Db.All().Should().Contain(e => e.TrackFileId == AllStoredModels.First().Id); } } diff --git a/src/NzbDrone.Core.Test/Instrumentation/DatabaseTargetFixture.cs b/src/NzbDrone.Core.Test/Instrumentation/DatabaseTargetFixture.cs index d67035ffb..41c4707ea 100644 --- a/src/NzbDrone.Core.Test/Instrumentation/DatabaseTargetFixture.cs +++ b/src/NzbDrone.Core.Test/Instrumentation/DatabaseTargetFixture.cs @@ -119,11 +119,11 @@ namespace NzbDrone.Core.Test.Instrumentation public void null_string_as_arg_should_not_fail() { var epFile = new TrackFile(); - _logger.Debug("File {0} no longer exists on disk. removing from database.", epFile.RelativePath); + _logger.Debug("File {0} no longer exists on disk. removing from database.", epFile.Path); Thread.Sleep(600); - epFile.RelativePath.Should().BeNull(); + epFile.Path.Should().BeNull(); } diff --git a/src/NzbDrone.Core.Test/MediaFiles/AudioTagServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/AudioTagServiceFixture.cs index 637c03afa..867f40e98 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/AudioTagServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/AudioTagServiceFixture.cs @@ -186,10 +186,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture var path = copiedFile; var track = new TrackFile { - Artist = new Artist { - Path = Path.GetDirectoryName(path) - }, - RelativePath = Path.GetFileName(path) + Path = path }; testTags.Write(path); diff --git a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs index 4b9afe5c0..4d465ebdf 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DiskScanServiceTests/ScanFixture.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Linq; using FizzWare.NBuilder; using Moq; @@ -13,11 +14,16 @@ using NzbDrone.Core.Music; using NzbDrone.Core.RootFolders; using NzbDrone.Test.Common; using NzbDrone.Core.Parser.Model; +using FluentAssertions; +using System.IO.Abstractions.TestingHelpers; +using NzbDrone.Core.DecisionEngine; +using System; +using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests { [TestFixture] - public class ScanFixture : CoreTest + public class ScanFixture : FileSystemTest { private Artist _artist; private string _rootFolder; @@ -34,42 +40,34 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests .With(s => s.Path = artistFolder) .Build(); - Mocker.GetMock() - .Setup(s => s.FolderExists(It.IsAny())) - .Returns(false); - - Mocker.GetMock() - .Setup(s => s.GetParentFolder(It.IsAny())) - .Returns((string path) => Directory.GetParent(path).FullName); - Mocker.GetMock() .Setup(s => s.GetBestRootFolderPath(It.IsAny())) .Returns(_rootFolder); Mocker.GetMock() - .Setup(v => v.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny())) + .Setup(v => v.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List>()); Mocker.GetMock() .Setup(v => v.GetFilesByArtist(It.IsAny())) .Returns(new List()); + + Mocker.GetMock() + .Setup(v => v.GetFilesWithBasePath(It.IsAny())) + .Returns(new List()); + + Mocker.GetMock() + .Setup(v => v.FilterUnchangedFiles(It.IsAny>(), It.IsAny(), It.IsAny())) + .Returns((List files, Artist artist) => files); } private void GivenRootFolder(params string[] subfolders) { - Mocker.GetMock() - .Setup(s => s.FolderExists(_rootFolder)) - .Returns(true); - - Mocker.GetMock() - .Setup(s => s.GetDirectories(_rootFolder)) - .Returns(subfolders); + FileSystem.AddDirectory(_rootFolder); foreach (var folder in subfolders) { - Mocker.GetMock() - .Setup(s => s.FolderExists(folder)) - .Returns(true); + FileSystem.AddDirectory(folder); } } @@ -78,11 +76,36 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenRootFolder(_artist.Path); } - private void GivenFiles(IEnumerable files) + private List GivenFiles(IEnumerable files, DateTimeOffset? lastWrite = null) { - Mocker.GetMock() - .Setup(s => s.GetFiles(It.IsAny(), SearchOption.AllDirectories)) - .Returns(files.ToArray()); + if (lastWrite == null) + { + TestLogger.Debug("Using default lastWrite"); + lastWrite = new DateTime(2019, 1, 1, 0, 0, 0, DateTimeKind.Utc); + } + + foreach (var file in files) + { + FileSystem.AddFile(file, new MockFileData(string.Empty) { LastWriteTime = lastWrite.Value }); + } + + return files.Select(x => DiskProvider.GetFileInfo(x)).ToList(); + } + + private void GivenKnownFiles(IEnumerable files, DateTimeOffset? lastWrite = null) + { + if (lastWrite == null) + { + TestLogger.Debug("Using default lastWrite"); + lastWrite = new DateTime(2019, 1, 1, 0, 0, 0, DateTimeKind.Utc); + } + + Mocker.GetMock() + .Setup(x => x.GetFilesWithBasePath(_artist.Path)) + .Returns(files.Select(x => new TrackFile { + Path = x, + Modified = lastWrite.Value.UtcDateTime + }).ToList()); } [Test] @@ -115,7 +138,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests .Verify(v => v.Clean(It.IsAny(), It.IsAny>()), Times.Never()); Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.IsAny>(), _artist, false), Times.Never()); + .Verify(v => v.GetImportDecisions(It.IsAny>(), _artist, FilterFilesType.Known, true), Times.Never()); } [Test] @@ -129,8 +152,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Subject.Scan(_artist); - Mocker.GetMock() - .Verify(v => v.CreateFolder(_artist.Path), Times.Once()); + DiskProvider.FolderExists(_artist.Path).Should().BeTrue(); } [Test] @@ -144,8 +166,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Subject.Scan(_artist); - Mocker.GetMock() - .Verify(v => v.CreateFolder(_artist.Path), Times.Never()); + DiskProvider.FolderExists(_artist.Path).Should().BeFalse(); } [Test] @@ -155,14 +176,13 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests Subject.Scan(_artist); - Mocker.GetMock() - .Verify(v => v.FolderExists(_artist.Path), Times.Once()); + DiskProvider.FolderExists(_artist.Path).Should().BeFalse(); Mocker.GetMock() .Verify(v => v.Clean(It.IsAny(), It.IsAny>()), Times.Once()); Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.IsAny>(), _artist, false), Times.Never()); + .Verify(v => v.GetImportDecisions(It.IsAny>(), _artist, FilterFilesType.Known, true), Times.Never()); } [Test] @@ -180,7 +200,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests .Verify(v => v.Clean(It.IsAny(), It.IsAny>()), Times.Once()); Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.IsAny>(), _artist, false), Times.Never()); + .Verify(v => v.GetImportDecisions(It.IsAny>(), _artist, FilterFilesType.Known, true), Times.Never()); } [Test] @@ -190,14 +210,14 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, "file1.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "s01e01.flac").AsOsAgnostic() + Path.Combine(_artist.Path, "file1.flac"), + Path.Combine(_artist.Path, "s01e01.flac") }); Subject.Scan(_artist); Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 2), _artist, false), Times.Once()); + .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 2), _artist, FilterFilesType.Known, true), Times.Once()); } [Test] @@ -207,20 +227,17 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, "EXTRAS", "file1.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "Extras", "file2.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "EXTRAs", "file3.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "ExTrAs", "file4.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac").AsOsAgnostic() + Path.Combine(_artist.Path, "EXTRAS", "file1.flac"), + Path.Combine(_artist.Path, "Extras", "file2.flac"), + Path.Combine(_artist.Path, "EXTRAs", "file3.flac"), + Path.Combine(_artist.Path, "ExTrAs", "file4.flac"), + Path.Combine(_artist.Path, "Season 1", "s01e01.flac") }); Subject.Scan(_artist); - Mocker.GetMock() - .Verify(v => v.GetFiles(It.IsAny(), It.IsAny()), Times.Once()); - Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, false), Times.Once()); + .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once()); } [Test] @@ -230,15 +247,15 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, ".AppleDouble", "file1.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, ".appledouble", "file2.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac").AsOsAgnostic() + Path.Combine(_artist.Path, ".AppleDouble", "file1.flac"), + Path.Combine(_artist.Path, ".appledouble", "file2.flac"), + Path.Combine(_artist.Path, "Season 1", "s01e01.flac") }); Subject.Scan(_artist); Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, false), Times.Once()); + .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once()); } [Test] @@ -250,18 +267,18 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, "Extras", "file1.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, ".AppleDouble", "file2.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "Season 1", "s01e02.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "Season 2", "s02e01.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "Season 2", "s02e02.flac").AsOsAgnostic(), + Path.Combine(_artist.Path, "Extras", "file1.flac"), + Path.Combine(_artist.Path, ".AppleDouble", "file2.flac"), + Path.Combine(_artist.Path, "Season 1", "s01e01.flac"), + Path.Combine(_artist.Path, "Season 1", "s01e02.flac"), + Path.Combine(_artist.Path, "Season 2", "s02e01.flac"), + Path.Combine(_artist.Path, "Season 2", "s02e02.flac"), }); Subject.Scan(_artist); Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 4), _artist, false), Times.Once()); + .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 4), _artist, FilterFilesType.Known, true), Times.Once()); } [Test] @@ -271,13 +288,13 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, "Album 1", ".t01.mp3").AsOsAgnostic() + Path.Combine(_artist.Path, "Album 1", ".t01.mp3") }); Subject.Scan(_artist); Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, false), Times.Once()); + .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once()); } [Test] @@ -287,16 +304,16 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, ".@__thumb", "file1.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, ".@__THUMB", "file2.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, ".hidden", "file2.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac").AsOsAgnostic() + Path.Combine(_artist.Path, ".@__thumb", "file1.flac"), + Path.Combine(_artist.Path, ".@__THUMB", "file2.flac"), + Path.Combine(_artist.Path, ".hidden", "file2.flac"), + Path.Combine(_artist.Path, "Season 1", "s01e01.flac") }); Subject.Scan(_artist); Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, false), Times.Once()); + .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once()); } [Test] @@ -306,17 +323,17 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, "Season 1", ".@__thumb", "file1.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "Season 1", ".@__THUMB", "file2.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "Season 1", ".hidden", "file2.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "Season 1", ".AppleDouble", "s01e01.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac").AsOsAgnostic() + Path.Combine(_artist.Path, "Season 1", ".@__thumb", "file1.flac"), + Path.Combine(_artist.Path, "Season 1", ".@__THUMB", "file2.flac"), + Path.Combine(_artist.Path, "Season 1", ".hidden", "file2.flac"), + Path.Combine(_artist.Path, "Season 1", ".AppleDouble", "s01e01.flac"), + Path.Combine(_artist.Path, "Season 1", "s01e01.flac") }); Subject.Scan(_artist); Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, false), Times.Once()); + .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once()); } [Test] @@ -326,14 +343,14 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, "@eaDir", "file1.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac").AsOsAgnostic() + Path.Combine(_artist.Path, "@eaDir", "file1.flac"), + Path.Combine(_artist.Path, "Season 1", "s01e01.flac") }); Subject.Scan(_artist); Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, false), Times.Once()); + .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once()); } [Test] @@ -343,14 +360,14 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, ".@__thumb", "file1.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac").AsOsAgnostic() + Path.Combine(_artist.Path, ".@__thumb", "file1.flac"), + Path.Combine(_artist.Path, "Season 1", "s01e01.flac") }); Subject.Scan(_artist); Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, false), Times.Once()); + .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once()); } [Test] @@ -362,14 +379,14 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, "Season 1", "file1.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "Season 1", "s01e01.flac").AsOsAgnostic() + Path.Combine(_artist.Path, "Season 1", "file1.flac"), + Path.Combine(_artist.Path, "Season 1", "s01e01.flac") }); Subject.Scan(_artist); Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 2), _artist, false), Times.Once()); + .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 2), _artist, FilterFilesType.Known, true), Times.Once()); } [Test] @@ -379,15 +396,173 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests GivenFiles(new List { - Path.Combine(_artist.Path, ".DS_STORE").AsOsAgnostic(), - Path.Combine(_artist.Path, "._24 The Status Quo Combustion.flac").AsOsAgnostic(), - Path.Combine(_artist.Path, "24 The Status Quo Combustion.flac").AsOsAgnostic() + Path.Combine(_artist.Path, ".DS_STORE"), + Path.Combine(_artist.Path, "._24 The Status Quo Combustion.flac"), + Path.Combine(_artist.Path, "24 The Status Quo Combustion.flac") }); Subject.Scan(_artist); Mocker.GetMock() - .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, false), Times.Once()); + .Verify(v => v.GetImportDecisions(It.Is>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once()); + } + + private void GivenRejections() + { + Mocker.GetMock() + .Setup(x => x.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((List fileList, Artist artist, FilterFilesType filter, bool includeExisting) => + fileList.Select(x => new LocalTrack { + Artist = artist, + Path = x.FullName, + Modified = x.LastWriteTimeUtc, + FileTrackInfo = new ParsedTrackInfo() + }) + .Select(x => new ImportDecision(x, new Rejection("Reject"))) + .ToList()); + } + + [Test] + public void should_insert_new_unmatched_files_when_all_new() + { + var files = new List { + Path.Combine(_artist.Path, "Season 1", "file1.flac"), + Path.Combine(_artist.Path, "Season 1", "s01e01.flac") + }; + + GivenFiles(files); + GivenKnownFiles(new List()); + GivenRejections(); + + Subject.Scan(_artist); + + Mocker.GetMock() + .Verify(x => x.AddMany(It.Is>(l => l.Select(t => t.Path).SequenceEqual(files))), + Times.Once()); + } + + [Test] + public void should_insert_new_unmatched_files_when_some_known() + { + var files = new List { + Path.Combine(_artist.Path, "Season 1", "file1.flac"), + Path.Combine(_artist.Path, "Season 1", "s01e01.flac") + }; + + GivenFiles(files); + GivenKnownFiles(files.GetRange(1, 1)); + GivenRejections(); + + Subject.Scan(_artist); + + Mocker.GetMock() + .Verify(x => x.AddMany(It.Is>(l => l.Select(t => t.Path).SequenceEqual(files.GetRange(0, 1)))), + Times.Once()); + } + + [Test] + public void should_not_insert_files_when_all_known() + { + var files = new List { + Path.Combine(_artist.Path, "Season 1", "file1.flac"), + Path.Combine(_artist.Path, "Season 1", "s01e01.flac") + }; + + GivenFiles(files); + GivenKnownFiles(files); + GivenRejections(); + + Subject.Scan(_artist); + + Mocker.GetMock() + .Verify(x => x.AddMany(It.Is>(l => l.Count == 0)), + Times.Once()); + + Mocker.GetMock() + .Verify(x => x.AddMany(It.Is>(l => l.Count > 0)), + Times.Never()); + } + + [Test] + public void should_not_update_info_for_unchanged_known_files() + { + var files = new List { + Path.Combine(_artist.Path, "Season 1", "file1.flac"), + Path.Combine(_artist.Path, "Season 1", "s01e01.flac") + }; + + GivenFiles(files); + GivenKnownFiles(files); + GivenRejections(); + + Subject.Scan(_artist); + + Mocker.GetMock() + .Verify(x => x.Update(It.Is>(l => l.Count == 0)), + Times.Once()); + + Mocker.GetMock() + .Verify(x => x.Update(It.Is>(l => l.Count > 0)), + Times.Never()); + + } + + [Test] + public void should_update_info_for_changed_known_files() + { + var files = new List { + Path.Combine(_artist.Path, "Season 1", "file1.flac"), + Path.Combine(_artist.Path, "Season 1", "s01e01.flac") + }; + + GivenFiles(files, new DateTime(2019, 2, 1)); + GivenKnownFiles(files); + GivenRejections(); + + Subject.Scan(_artist); + + Mocker.GetMock() + .Verify(x => x.Update(It.Is>(l => l.Count == 2)), + Times.Once()); + } + + [Test] + public void should_update_fields_for_updated_files() + { + var files = new List { + Path.Combine(_artist.Path, "Season 1", "file1.flac"), + }; + + GivenKnownFiles(files); + + FileSystem.AddFile(files[0], new MockFileData("".PadRight(100)) { LastWriteTime = new DateTime(2019, 2, 1) }); + + var localTrack = Builder.CreateNew() + .With(x => x.Path = files[0]) + .With(x => x.Modified = new DateTime(2019, 2, 1)) + .With(x => x.Size = 100) + .With(x => x.Quality = new QualityModel(Quality.FLAC)) + .With(x => x.FileTrackInfo = new ParsedTrackInfo { + MediaInfo = Builder.CreateNew().Build() + }) + .Build(); + + Mocker.GetMock() + .Setup(x => x.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new List> { new ImportDecision(localTrack, new Rejection("Reject")) }); + + Subject.Scan(_artist); + + Mocker.GetMock() + .Verify(x => x.Update(It.Is>( + l => l.Count == 1 && + l[0].Path == localTrack.Path && + l[0].Modified == localTrack.Modified && + l[0].Size == localTrack.Size && + l[0].Quality.Equals(localTrack.Quality) && + l[0].MediaInfo.AudioFormat == localTrack.FileTrackInfo.MediaInfo.AudioFormat + )), + Times.Once()); } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedAlbumsCommandServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedAlbumsCommandServiceFixture.cs index a5360f84c..dacf30aaf 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedAlbumsCommandServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedAlbumsCommandServiceFixture.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using System.IO; +using System.IO.Abstractions; using FizzWare.NBuilder; using Moq; using NUnit.Framework; -using NzbDrone.Common.Disk; using NzbDrone.Core.Download; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.MediaFiles; @@ -14,11 +13,12 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; using NzbDrone.Test.Common; +using System.IO.Abstractions.TestingHelpers; namespace NzbDrone.Core.Test.MediaFiles { [TestFixture] - public class DownloadedAlbumsCommandServiceFixture : CoreTest + public class DownloadedAlbumsCommandServiceFixture : FileSystemTest { private string _downloadFolder = "c:\\drop_other\\Show.S01E01\\".AsOsAgnostic(); private string _downloadFile = "c:\\drop_other\\Show.S01E01.mkv".AsOsAgnostic(); @@ -30,7 +30,7 @@ namespace NzbDrone.Core.Test.MediaFiles { Mocker.GetMock() - .Setup(v => v.ProcessRootFolder(It.IsAny())) + .Setup(v => v.ProcessRootFolder(It.IsAny())) .Returns(new List()); Mocker.GetMock() @@ -56,14 +56,12 @@ namespace NzbDrone.Core.Test.MediaFiles private void GivenExistingFolder(string path) { - Mocker.GetMock().Setup(c => c.FolderExists(It.IsAny())) - .Returns(true); + FileSystem.AddDirectory(path); } private void GivenExistingFile(string path) { - Mocker.GetMock().Setup(c => c.FileExists(It.IsAny())) - .Returns(true); + FileSystem.AddFile(path, new MockFileData(string.Empty)); } private void GivenValidQueueItem() @@ -78,7 +76,7 @@ namespace NzbDrone.Core.Test.MediaFiles { Assert.Throws(() => Subject.Execute(new DownloadedAlbumsScanCommand())); - Mocker.GetMock().Verify(c => c.ProcessRootFolder(It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(c => c.ProcessRootFolder(It.IsAny()), Times.Never()); } diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs index 134e354ab..1aada9a68 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedTracksImportServiceFixture.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using System.IO; +using System.IO.Abstractions; using System.Linq; using FizzWare.NBuilder; using FluentAssertions; @@ -12,36 +12,33 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.TrackImport; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; using NzbDrone.Test.Common; +using System.IO.Abstractions.TestingHelpers; +using System.IO; namespace NzbDrone.Core.Test.MediaFiles { [TestFixture] - public class DownloadedTracksImportServiceFixture : CoreTest + public class DownloadedTracksImportServiceFixture : FileSystemTest { private string _droneFactory = "c:\\drop\\".AsOsAgnostic(); - private string[] _subFolders = new[] { "c:\\root\\foldername".AsOsAgnostic() }; - private string[] _audioFiles = new[] { "c:\\root\\foldername\\01 the first track.ext".AsOsAgnostic() }; + private string[] _subFolders = new[] { "c:\\drop\\foldername".AsOsAgnostic() }; + private string[] _audioFiles = new[] { "c:\\drop\\foldername\\01 the first track.ext".AsOsAgnostic() }; private TrackedDownload _trackedDownload; [SetUp] public void Setup() { + GivenAudioFiles(_audioFiles, 10); + Mocker.GetMock().Setup(c => c.GetAudioFiles(It.IsAny(), It.IsAny())) - .Returns(_audioFiles); - - Mocker.GetMock().Setup(c => c.FilterFiles(It.IsAny(), It.IsAny>())) - .Returns>((b, s) => s.ToList()); - - Mocker.GetMock().Setup(c => c.GetDirectories(It.IsAny())) - .Returns(_subFolders); + .Returns(_audioFiles.Select(x => DiskProvider.GetFileInfo(x)).ToArray()); - Mocker.GetMock().Setup(c => c.FolderExists(It.IsAny())) - .Returns(true); + Mocker.GetMock().Setup(c => c.FilterFiles(It.IsAny(), It.IsAny>())) + .Returns>((b, s) => s.ToList()); Mocker.GetMock() .Setup(s => s.Import(It.IsAny>>(), true, null, ImportMode.Auto)) @@ -65,6 +62,14 @@ namespace NzbDrone.Core.Test.MediaFiles }; } + private void GivenAudioFiles(string[] files, long filesize) + { + foreach (var file in files) + { + FileSystem.AddFile(file, new MockFileData("".PadRight((int)filesize))); + } + } + private void GivenValidArtist() { Mocker.GetMock() @@ -80,7 +85,7 @@ namespace NzbDrone.Core.Test.MediaFiles imported.Add(new ImportDecision(localTrack)); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null)) .Returns(imported); Mocker.GetMock() @@ -92,13 +97,13 @@ namespace NzbDrone.Core.Test.MediaFiles private void WasImportedResponse() { Mocker.GetMock().Setup(c => c.GetAudioFiles(It.IsAny(), It.IsAny())) - .Returns(new string[0]); + .Returns(new IFileInfo[0]); } [Test] public void should_search_for_artist_using_folder_name() { - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + Subject.ProcessRootFolder(DiskProvider.GetDirectoryInfo(_droneFactory)); Mocker.GetMock().Verify(c => c.GetArtist("foldername"), Times.Once()); } @@ -108,10 +113,12 @@ namespace NzbDrone.Core.Test.MediaFiles { GivenValidArtist(); - Mocker.GetMock().Setup(c => c.IsFileLocked(It.IsAny())) - .Returns(true); + foreach (var file in _audioFiles) + { + FileSystem.AddFile(file, new MockFileData("".PadRight(10)) { AllowedFileShare = FileShare.None }); + } - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + Subject.ProcessRootFolder(DiskProvider.GetDirectoryInfo(_droneFactory)); VerifyNoImport(); } @@ -121,10 +128,10 @@ namespace NzbDrone.Core.Test.MediaFiles { Mocker.GetMock().Setup(c => c.GetArtist("foldername")).Returns((Artist)null); - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + Subject.ProcessRootFolder(DiskProvider.GetDirectoryInfo(_droneFactory)); Mocker.GetMock() - .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny()), + .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny()), Times.Never()); VerifyNoImport(); @@ -141,9 +148,9 @@ namespace NzbDrone.Core.Test.MediaFiles Mocker.GetMock() .Setup(c => c.GetAudioFiles(It.IsAny(), It.IsAny())) - .Returns(new string[0]); + .Returns(new IFileInfo[0]); - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + Subject.ProcessRootFolder(DiskProvider.GetDirectoryInfo(_droneFactory)); Mocker.GetMock() .Verify(v => v.GetAudioFiles(It.IsAny(), true), Times.Never()); @@ -158,7 +165,7 @@ namespace NzbDrone.Core.Test.MediaFiles .Setup(s => s.Import(It.IsAny>>(), false, null, ImportMode.Auto)) .Returns(new List()); - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + Subject.ProcessRootFolder(DiskProvider.GetDirectoryInfo(_droneFactory)); Mocker.GetMock() .Verify(v => v.GetFolderSize(It.IsAny()), Times.Never()); @@ -175,14 +182,14 @@ namespace NzbDrone.Core.Test.MediaFiles imported.Add(new ImportDecision(localTrack)); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null)) .Returns(imported); Mocker.GetMock() .Setup(s => s.Import(It.IsAny>>(), true, null, ImportMode.Auto)) .Returns(imported.Select(i => new ImportResult(i)).ToList()); - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + Subject.ProcessRootFolder(DiskProvider.GetDirectoryInfo(_droneFactory)); Mocker.GetMock() .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Never()); @@ -195,13 +202,9 @@ namespace NzbDrone.Core.Test.MediaFiles public void should_remove_unpack_from_folder_name(string prefix) { var folderName = "Alien Ant Farm - Truant (2003)"; - var folders = new[] { string.Format(@"C:\Test\Unsorted\{0}{1}", prefix, folderName).AsOsAgnostic() }; - - Mocker.GetMock() - .Setup(c => c.GetDirectories(It.IsAny())) - .Returns(folders); + FileSystem.AddDirectory(string.Format(@"C:\drop\{0}{1}", prefix, folderName).AsOsAgnostic()); - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + Subject.ProcessRootFolder(DiskProvider.GetDirectoryInfo(_droneFactory)); Mocker.GetMock() .Verify(v => v.GetArtist(folderName), Times.Once()); @@ -213,13 +216,8 @@ namespace NzbDrone.Core.Test.MediaFiles [Test] public void should_return_importresult_on_unknown_artist() { - Mocker.GetMock().Setup(c => c.FolderExists(It.IsAny())) - .Returns(false); - - Mocker.GetMock().Setup(c => c.FileExists(It.IsAny())) - .Returns(true); - var fileName = @"C:\folder\file.mkv".AsOsAgnostic(); + FileSystem.AddFile(fileName, new MockFileData(string.Empty)); var result = Subject.ProcessPath(fileName); @@ -241,33 +239,18 @@ namespace NzbDrone.Core.Test.MediaFiles imported.Add(new ImportDecision(localTrack)); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null)) .Returns(imported); Mocker.GetMock() .Setup(s => s.Import(It.IsAny>>(), true, null, ImportMode.Auto)) .Returns(imported.Select(i => new ImportResult(i)).ToList()); - //Mocker.GetMock() - // .Setup(s => s.IsSample(It.IsAny(), - // It.IsAny(), - // It.IsAny(), - // It.IsAny(), - // It.IsAny())) - // .Returns(true); + GivenAudioFiles(new []{ _audioFiles.First().Replace(".ext", ".rar") }, 15.Megabytes()); - Mocker.GetMock() - .Setup(s => s.GetFiles(It.IsAny(), SearchOption.AllDirectories)) - .Returns(new []{ _audioFiles.First().Replace(".ext", ".rar") }); - - Mocker.GetMock() - .Setup(s => s.GetFileSize(It.IsAny())) - .Returns(15.Megabytes()); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + Subject.ProcessRootFolder(DiskProvider.GetDirectoryInfo(_droneFactory)); - Mocker.GetMock() - .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Never()); + DiskProvider.FolderExists(_subFolders[0]).Should().BeTrue(); ExceptionVerification.ExpectedWarns(1); } @@ -277,12 +260,6 @@ namespace NzbDrone.Core.Test.MediaFiles { var folderName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\[HorribleSubs] Maria the Virgin Witch - 09 [720p]".AsOsAgnostic(); - Mocker.GetMock().Setup(c => c.FolderExists(folderName)) - .Returns(false); - - Mocker.GetMock().Setup(c => c.FileExists(folderName)) - .Returns(false); - Subject.ProcessPath(folderName).Should().BeEmpty(); Mocker.GetMock() @@ -302,26 +279,16 @@ namespace NzbDrone.Core.Test.MediaFiles imported.Add(new ImportDecision(localTrack)); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), null)) .Returns(imported); Mocker.GetMock() .Setup(s => s.Import(It.IsAny>>(), true, null, ImportMode.Auto)) .Returns(new List()); - //Mocker.GetMock() - // .Setup(s => s.IsSample(It.IsAny(), - // It.IsAny(), - // It.IsAny(), - // It.IsAny(), - // It.IsAny())) - // .Returns(true); + Subject.ProcessRootFolder(DiskProvider.GetDirectoryInfo(_droneFactory)); - Mocker.GetMock() - .Setup(s => s.GetFileSize(It.IsAny())) - .Returns(15.Megabytes()); - - Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); + DiskProvider.FolderExists(_subFolders[0]).Should().BeTrue(); Mocker.GetMock() .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Never()); @@ -338,8 +305,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.ProcessPath(_droneFactory, ImportMode.Auto, _trackedDownload.RemoteAlbum.Artist, _trackedDownload.DownloadItem); - Mocker.GetMock() - .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Never()); + DiskProvider.FolderExists(_subFolders[0]).Should().BeTrue(); } [Test] @@ -353,8 +319,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.ProcessPath(_droneFactory, ImportMode.Move, _trackedDownload.RemoteAlbum.Artist, _trackedDownload.DownloadItem); - Mocker.GetMock() - .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Once()); + DiskProvider.FolderExists(_subFolders[0]).Should().BeFalse(); } [Test] @@ -368,8 +333,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.ProcessPath(_droneFactory, ImportMode.Copy, _trackedDownload.RemoteAlbum.Artist, _trackedDownload.DownloadItem); - Mocker.GetMock() - .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Never()); + DiskProvider.FolderExists(_subFolders[0]).Should().BeTrue(); } private void VerifyNoImport() diff --git a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedTracksFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedTracksFixture.cs index 0dff9bb93..34979e6b8 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedTracksFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedTracksFixture.cs @@ -89,10 +89,6 @@ namespace NzbDrone.Core.Test.MediaFiles _downloadClientItem = Builder.CreateNew().Build(); - Mocker.GetMock() - .Setup(s => s.GetFilesWithRelativePath(It.IsAny(), It.IsAny())) - .Returns(new List()); - Mocker.GetMock() .Setup(s => s.GetFilesByAlbum(It.IsAny())) .Returns(new List()); @@ -220,8 +216,8 @@ namespace NzbDrone.Core.Test.MediaFiles public void should_delete_existing_trackfiles_with_the_same_path() { Mocker.GetMock() - .Setup(s => s.GetFilesWithRelativePath(It.IsAny(), It.IsAny())) - .Returns(Builder.CreateListOfSize(1).BuildList()); + .Setup(s => s.GetFileWithPath(It.IsAny())) + .Returns(Builder.CreateNew().Build()); var track = _approvedDecisions.First(); track.Item.ExistingFile = true; diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteTrackFileFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteTrackFileFixture.cs index 316afbc77..edaf152ce 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteTrackFileFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteTrackFileFixture.cs @@ -26,8 +26,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileDeletionService .Build(); _trackFile = Builder.CreateNew() - .With(f => f.RelativePath = "Artist Name - Track01") - .With(f => f.Path = Path.Combine(_artist.Path, "Artist Name - Track01")) + .With(f => f.Path = "/Artist Name - Track01") .Build(); Mocker.GetMock() diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileRepositoryFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileRepositoryFixture.cs index 43040950f..8cd50a027 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaFileRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileRepositoryFixture.cs @@ -117,28 +117,6 @@ namespace NzbDrone.Core.Test.MediaFiles files.Should().HaveCount(4); } - [Test] - public void get_files_by_relative_path() - { - VerifyData(); - var files = Subject.GetFilesWithRelativePath(artist.Id, "RelativePath2"); - VerifyEagerLoaded(files); - - files.Should().OnlyContain(c => c.AlbumId == album.Id); - files.Should().OnlyContain(c => c.RelativePath == "RelativePath2"); - } - - [Test] - public void get_files_by_relative_path_should_only_contain_monitored_releases() - { - VerifyData(); - - // file 5 is linked to an unmonitored release - var files = Subject.GetFilesWithRelativePath(artist.Id, "RelativePath5"); - - files.Should().BeEmpty(); - } - private void VerifyData() { Db.All().Should().HaveCount(1); diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTests/FilterFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTests/FilterFixture.cs index 96aee6685..c65aea5c2 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTests/FilterFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileServiceTests/FilterFixture.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.IO; using System.Linq; using FluentAssertions; using Moq; @@ -8,13 +7,18 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; using NzbDrone.Test.Common; +using System.IO.Abstractions.TestingHelpers; +using System.IO.Abstractions; +using System; +using FizzWare.NBuilder; namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests { [TestFixture] - public class FilterFixture : CoreTest + public class FilterFixture : FileSystemTest { private Artist _artist; + private DateTime _lastWrite = new DateTime(2019, 1, 1); [SetUp] public void Setup() @@ -26,125 +30,252 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests }; } - [Test] - public void filter_should_return_all_files_if_no_existing_files() + private List GivenFiles(string[] files) { - var files = new List() + foreach (var file in files) { - "C:\\file1.avi".AsOsAgnostic(), - "C:\\file2.avi".AsOsAgnostic(), - "C:\\file3.avi".AsOsAgnostic() - }; + FileSystem.AddFile(file, new MockFileData(string.Empty) { LastWriteTime = _lastWrite }); + } + + return files.Select(x => DiskProvider.GetFileInfo(x)).ToList(); + } + + [TestCase(FilterFilesType.Known)] + [TestCase(FilterFilesType.Matched)] + public void filter_should_return_all_files_if_no_existing_files(FilterFilesType filter) + { + var files = GivenFiles(new [] + { + "C:\\file1.avi".AsOsAgnostic(), + "C:\\file2.avi".AsOsAgnostic(), + "C:\\file3.avi".AsOsAgnostic() + }); Mocker.GetMock() - .Setup(c => c.GetFilesByArtist(It.IsAny())) + .Setup(c => c.GetFilesWithBasePath(It.IsAny())) .Returns(new List()); - - Subject.FilterExistingFiles(files, _artist).Should().BeEquivalentTo(files); + Subject.FilterUnchangedFiles(files, _artist, filter).Should().BeEquivalentTo(files); } - [Test] - public void filter_should_return_none_if_all_files_exist() + [TestCase(FilterFilesType.Known)] + [TestCase(FilterFilesType.Matched)] + public void filter_should_return_nothing_if_all_files_exist(FilterFilesType filter) { - var files = new List() - { - "C:\\file1.avi".AsOsAgnostic(), - "C:\\file2.avi".AsOsAgnostic(), - "C:\\file3.avi".AsOsAgnostic() - }; + var files = GivenFiles(new [] + { + "C:\\file1.avi".AsOsAgnostic(), + "C:\\file2.avi".AsOsAgnostic(), + "C:\\file3.avi".AsOsAgnostic() + }); Mocker.GetMock() - .Setup(c => c.GetFilesByArtist(It.IsAny())) - .Returns(files.Select(f => new TrackFile { RelativePath = Path.GetFileName(f) }).ToList()); - + .Setup(c => c.GetFilesWithBasePath(It.IsAny())) + .Returns(files.Select(f => new TrackFile { + Path = f.FullName, + Modified = _lastWrite + }).ToList()); - Subject.FilterExistingFiles(files, _artist).Should().BeEmpty(); + Subject.FilterUnchangedFiles(files, _artist, filter).Should().BeEmpty(); } - [Test] - public void filter_should_return_none_existing_files() + [TestCase(FilterFilesType.Known)] + [TestCase(FilterFilesType.Matched)] + public void filter_should_not_return_existing_files(FilterFilesType filter) { - var files = new List() - { - "C:\\file1.avi".AsOsAgnostic(), - "C:\\file2.avi".AsOsAgnostic(), - "C:\\file3.avi".AsOsAgnostic() - }; + var files = GivenFiles(new [] + { + "C:\\file1.avi".AsOsAgnostic(), + "C:\\file2.avi".AsOsAgnostic(), + "C:\\file3.avi".AsOsAgnostic() + }); Mocker.GetMock() - .Setup(c => c.GetFilesByArtist(It.IsAny())) + .Setup(c => c.GetFilesWithBasePath(It.IsAny())) .Returns(new List { - new TrackFile{ RelativePath = "file2.avi".AsOsAgnostic()} + new TrackFile{ + Path = "C:\\file2.avi".AsOsAgnostic(), + Modified = _lastWrite + } }); - - Subject.FilterExistingFiles(files, _artist).Should().HaveCount(2); - Subject.FilterExistingFiles(files, _artist).Should().NotContain("C:\\file2.avi".AsOsAgnostic()); + Subject.FilterUnchangedFiles(files, _artist, filter).Should().HaveCount(2); + Subject.FilterUnchangedFiles(files, _artist, filter).Select(x => x.FullName).Should().NotContain("C:\\file2.avi".AsOsAgnostic()); } - [Test] - public void filter_should_return_none_existing_files_ignoring_case() + [TestCase(FilterFilesType.Known)] + [TestCase(FilterFilesType.Matched)] + public void filter_should_return_none_existing_files_ignoring_case(FilterFilesType filter) { WindowsOnly(); - var files = new List() - { - "C:\\file1.avi".AsOsAgnostic(), - "C:\\FILE2.avi".AsOsAgnostic(), - "C:\\file3.avi".AsOsAgnostic() - }; + var files = GivenFiles(new [] + { + "C:\\file1.avi".AsOsAgnostic(), + "C:\\FILE2.avi".AsOsAgnostic(), + "C:\\file3.avi".AsOsAgnostic() + }); Mocker.GetMock() - .Setup(c => c.GetFilesByArtist(It.IsAny())) + .Setup(c => c.GetFilesWithBasePath(It.IsAny())) .Returns(new List { - new TrackFile{ RelativePath = "file2.avi".AsOsAgnostic()} + new TrackFile{ + Path = "C:\\file2.avi".AsOsAgnostic(), + Modified = _lastWrite + } }); - Subject.FilterExistingFiles(files, _artist).Should().HaveCount(2); - Subject.FilterExistingFiles(files, _artist).Should().NotContain("C:\\file2.avi".AsOsAgnostic()); + Subject.FilterUnchangedFiles(files, _artist, filter).Should().HaveCount(2); + Subject.FilterUnchangedFiles(files, _artist, filter).Select(x => x.FullName).Should().NotContain("C:\\file2.avi".AsOsAgnostic()); } - [Test] - public void filter_should_return_none_existing_files_not_ignoring_case() + + [TestCase(FilterFilesType.Known)] + [TestCase(FilterFilesType.Matched)] + public void filter_should_return_none_existing_files_not_ignoring_case(FilterFilesType filter) { MonoOnly(); - var files = new List() - { - "C:\\file1.avi".AsOsAgnostic(), - "C:\\FILE2.avi".AsOsAgnostic(), - "C:\\file3.avi".AsOsAgnostic() - }; + var files = GivenFiles(new [] + { + "C:\\file1.avi".AsOsAgnostic(), + "C:\\FILE2.avi".AsOsAgnostic(), + "C:\\file3.avi".AsOsAgnostic() + }); Mocker.GetMock() - .Setup(c => c.GetFilesByArtist(It.IsAny())) + .Setup(c => c.GetFilesWithBasePath(It.IsAny())) .Returns(new List { - new TrackFile{ RelativePath = "file2.avi".AsOsAgnostic()} + new TrackFile{ + Path = "C:\\file2.avi".AsOsAgnostic(), + Modified = _lastWrite + } }); - Subject.FilterExistingFiles(files, _artist).Should().HaveCount(3); + Subject.FilterUnchangedFiles(files, _artist, filter).Should().HaveCount(3); } - [Test] - public void filter_should_not_change_casing() + [TestCase(FilterFilesType.Known)] + [TestCase(FilterFilesType.Matched)] + public void filter_should_not_change_casing(FilterFilesType filter) { - var files = new List() - { - "C:\\FILE1.avi".AsOsAgnostic() - }; + var files = GivenFiles(new [] + { + "C:\\FILE1.avi".AsOsAgnostic() + }); Mocker.GetMock() - .Setup(c => c.GetFilesByArtist(It.IsAny())) + .Setup(c => c.GetFilesWithBasePath(It.IsAny())) .Returns(new List()); - Subject.FilterExistingFiles(files, _artist).Should().HaveCount(1); - Subject.FilterExistingFiles(files, _artist).Should().NotContain(files.First().ToLower()); - Subject.FilterExistingFiles(files, _artist).Should().Contain(files.First()); + Subject.FilterUnchangedFiles(files, _artist, filter).Should().HaveCount(1); + Subject.FilterUnchangedFiles(files, _artist, filter).Select(x => x.FullName).Should().NotContain(files.First().FullName.ToLower()); + Subject.FilterUnchangedFiles(files, _artist, filter).Should().Contain(files.First()); + } + + + [TestCase(FilterFilesType.Known)] + [TestCase(FilterFilesType.Matched)] + public void filter_should_not_return_existing_file_if_size_unchanged(FilterFilesType filter) + { + FileSystem.AddFile("C:\\file1.avi".AsOsAgnostic(), new MockFileData("".PadRight(10)) { LastWriteTime = _lastWrite }); + FileSystem.AddFile("C:\\file2.avi".AsOsAgnostic(), new MockFileData("".PadRight(10)) { LastWriteTime = _lastWrite }); + FileSystem.AddFile("C:\\file3.avi".AsOsAgnostic(), new MockFileData("".PadRight(10)) { LastWriteTime = _lastWrite }); + + var files = FileSystem.AllFiles.Select(x => DiskProvider.GetFileInfo(x)).ToList(); + + Mocker.GetMock() + .Setup(c => c.GetFilesWithBasePath(It.IsAny())) + .Returns(new List + { + new TrackFile{ + Path = "C:\\file2.avi".AsOsAgnostic(), + Size = 10, + Modified = _lastWrite + } + }); + + Subject.FilterUnchangedFiles(files, _artist, filter).Should().HaveCount(2); + Subject.FilterUnchangedFiles(files, _artist, filter).Select(x => x.FullName).Should().NotContain("C:\\file2.avi".AsOsAgnostic()); + } + + [TestCase(FilterFilesType.Matched)] + public void filter_unmatched_should_return_existing_file_if_unmatched(FilterFilesType filter) + { + FileSystem.AddFile("C:\\file1.avi".AsOsAgnostic(), new MockFileData("".PadRight(10)) { LastWriteTime = _lastWrite }); + FileSystem.AddFile("C:\\file2.avi".AsOsAgnostic(), new MockFileData("".PadRight(10)) { LastWriteTime = _lastWrite }); + FileSystem.AddFile("C:\\file3.avi".AsOsAgnostic(), new MockFileData("".PadRight(10)) { LastWriteTime = _lastWrite }); + + var files = FileSystem.AllFiles.Select(x => DiskProvider.GetFileInfo(x)).ToList(); + + Mocker.GetMock() + .Setup(c => c.GetFilesWithBasePath(It.IsAny())) + .Returns(new List + { + new TrackFile{ + Path = "C:\\file2.avi".AsOsAgnostic(), + Size = 10, + Modified = _lastWrite, + Tracks = new List() + } + }); + + Subject.FilterUnchangedFiles(files, _artist, filter).Should().HaveCount(3); + Subject.FilterUnchangedFiles(files, _artist, filter).Select(x => x.FullName).Should().Contain("C:\\file2.avi".AsOsAgnostic()); + } + + [TestCase(FilterFilesType.Matched)] + public void filter_unmatched_should_not_return_existing_file_if_matched(FilterFilesType filter) + { + FileSystem.AddFile("C:\\file1.avi".AsOsAgnostic(), new MockFileData("".PadRight(10)) { LastWriteTime = _lastWrite }); + FileSystem.AddFile("C:\\file2.avi".AsOsAgnostic(), new MockFileData("".PadRight(10)) { LastWriteTime = _lastWrite }); + FileSystem.AddFile("C:\\file3.avi".AsOsAgnostic(), new MockFileData("".PadRight(10)) { LastWriteTime = _lastWrite }); + + var files = FileSystem.AllFiles.Select(x => DiskProvider.GetFileInfo(x)).ToList(); + + Mocker.GetMock() + .Setup(c => c.GetFilesWithBasePath(It.IsAny())) + .Returns(new List + { + new TrackFile{ + Path = "C:\\file2.avi".AsOsAgnostic(), + Size = 10, + Modified = _lastWrite, + Tracks = Builder.CreateListOfSize(1).Build() as List + } + }); + + Subject.FilterUnchangedFiles(files, _artist, filter).Should().HaveCount(2); + Subject.FilterUnchangedFiles(files, _artist, filter).Select(x => x.FullName).Should().NotContain("C:\\file2.avi".AsOsAgnostic()); + } + + [TestCase(FilterFilesType.Known)] + [TestCase(FilterFilesType.Matched)] + public void filter_should_return_existing_file_if_size_changed(FilterFilesType filter) + { + FileSystem.AddFile("C:\\file1.avi".AsOsAgnostic(), new MockFileData("".PadRight(10)) { LastWriteTime = _lastWrite }); + FileSystem.AddFile("C:\\file2.avi".AsOsAgnostic(), new MockFileData("".PadRight(11)) { LastWriteTime = _lastWrite }); + FileSystem.AddFile("C:\\file3.avi".AsOsAgnostic(), new MockFileData("".PadRight(10)) { LastWriteTime = _lastWrite }); + + var files = FileSystem.AllFiles.Select(x => DiskProvider.GetFileInfo(x)).ToList(); + + Mocker.GetMock() + .Setup(c => c.GetFilesWithBasePath(It.IsAny())) + .Returns(new List + { + new TrackFile{ + Path = "C:\\file2.avi".AsOsAgnostic(), + Size = 10, + Modified = _lastWrite + } + }); + + Subject.FilterUnchangedFiles(files, _artist, filter).Should().HaveCount(3); + Subject.FilterUnchangedFiles(files, _artist, filter).Select(x => x.FullName).Should().Contain("C:\\file2.avi".AsOsAgnostic()); } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs index db296276a..2b5b297a3 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Core.Test.MediaFiles { public class MediaFileTableCleanupServiceFixture : CoreTest { - private const string DELETED_PATH = "ANY FILE WITH THIS PATH IS CONSIDERED DELETED!"; + private readonly string DELETED_PATH = @"c:\ANY FILE WITH THIS PATH IS CONSIDERED DELETED!".AsOsAgnostic(); private List _tracks; private Artist _artist; @@ -56,13 +56,15 @@ namespace NzbDrone.Core.Test.MediaFiles private List FilesOnDisk(IEnumerable trackFiles) { - return trackFiles.Select(e => Path.Combine(_artist.Path, e.RelativePath)).ToList(); + return trackFiles.Select(e => e.Path).ToList(); } [Test] - public void should_skip_files_that_exist_in_disk() + public void should_skip_files_that_exist_on_disk() { var trackFiles = Builder.CreateListOfSize(10) + .All() + .With(x => x.Path = Path.Combine(@"c:\test".AsOsAgnostic(), Path.GetRandomFileName())) .Build(); GivenTrackFiles(trackFiles); @@ -76,31 +78,17 @@ namespace NzbDrone.Core.Test.MediaFiles public void should_delete_non_existent_files() { var trackFiles = Builder.CreateListOfSize(10) + .All() + .With(x => x.Path = Path.Combine(@"c:\test".AsOsAgnostic(), Path.GetRandomFileName())) .Random(2) - .With(c => c.RelativePath = DELETED_PATH) + .With(c => c.Path = DELETED_PATH) .Build(); GivenTrackFiles(trackFiles); - Subject.Clean(_artist, FilesOnDisk(trackFiles.Where(e => e.RelativePath != DELETED_PATH))); + Subject.Clean(_artist, FilesOnDisk(trackFiles.Where(e => e.Path != DELETED_PATH))); - Mocker.GetMock().Verify(c => c.Delete(It.Is(e => e.RelativePath == DELETED_PATH), DeleteMediaFileReason.MissingFromDisk), Times.Exactly(2)); - } - - [Test] - public void should_delete_files_that_dont_belong_to_any_tracks() - { - var trackFiles = Builder.CreateListOfSize(10) - .Random(10) - .With(c => c.RelativePath = "ExistingPath") - .Build(); - - GivenTrackFiles(trackFiles); - GivenFilesAreNotAttachedToTrack(); - - Subject.Clean(_artist, FilesOnDisk(trackFiles)); - - Mocker.GetMock().Verify(c => c.Delete(It.IsAny(), DeleteMediaFileReason.NoLinkedEpisodes), Times.Exactly(10)); + Mocker.GetMock().Verify(c => c.Delete(It.Is(e => e.Path == DELETED_PATH), DeleteMediaFileReason.MissingFromDisk), Times.Exactly(2)); } [Test] @@ -118,7 +106,7 @@ namespace NzbDrone.Core.Test.MediaFiles { var trackFiles = Builder.CreateListOfSize(10) .Random(10) - .With(c => c.RelativePath = "ExistingPath") + .With(c => c.Path = "/ExistingPath".AsOsAgnostic()) .Build(); GivenTrackFiles(trackFiles); diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackFileMovingServiceTests/MoveTrackFileFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackFileMovingServiceTests/MoveTrackFileFixture.cs index 2d3c03135..c5c0fa630 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackFileMovingServiceTests/MoveTrackFileFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackFileMovingServiceTests/MoveTrackFileFixture.cs @@ -14,6 +14,7 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; using NzbDrone.Test.Common; +using System.IO; namespace NzbDrone.Core.Test.MediaFiles.TrackFileMovingServiceTests { @@ -33,7 +34,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackFileMovingServiceTests _trackFile = Builder.CreateNew() .With(f => f.Path = null) - .With(f => f.RelativePath = @"Album\File.mp3") + .With(f => f.Path = Path.Combine(_artist.Path, @"Album\File.mp3")) .Build(); _localtrack = Builder.CreateNew() diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs index ecbdd3c8b..ddfcee8d9 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs @@ -3,11 +3,11 @@ using System.Linq; using FluentAssertions; using Moq; using NUnit.Framework; +using System.IO.Abstractions; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.TrackImport; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; @@ -18,13 +18,14 @@ using NzbDrone.Core.MediaFiles.TrackImport.Aggregation; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Profiles.Languages; using NzbDrone.Core.MediaFiles.TrackImport.Identification; +using System.IO.Abstractions.TestingHelpers; namespace NzbDrone.Core.Test.MediaFiles.TrackImport { [TestFixture] - public class ImportDecisionMakerFixture : CoreTest + public class ImportDecisionMakerFixture : FileSystemTest { - private List _audioFiles; + private List _fileInfos; private LocalTrack _localTrack; private Artist _artist; private AlbumRelease _albumRelease; @@ -112,8 +113,8 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport }); Mocker.GetMock() - .Setup(c => c.FilterExistingFiles(It.IsAny>(), It.IsAny())) - .Returns((List files, Artist artist) => files); + .Setup(c => c.FilterUnchangedFiles(It.IsAny>(), It.IsAny(), It.IsAny())) + .Returns((List files, Artist artist) => files); GivenSpecifications(_albumpass1); } @@ -125,11 +126,12 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport private void GivenAudioFiles(IEnumerable videoFiles) { - _audioFiles = videoFiles.ToList(); + foreach (var file in videoFiles) + { + FileSystem.AddFile(file, new MockFileData(string.Empty)); + } - Mocker.GetMock() - .Setup(c => c.FilterExistingFiles(_audioFiles, It.IsAny())) - .Returns(_audioFiles); + _fileInfos = videoFiles.Select(x => DiskProvider.GetFileInfo(x)).ToList(); } private void GivenAugmentationSuccess() @@ -149,7 +151,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport GivenAugmentationSuccess(); GivenSpecifications(_albumpass1, _albumpass2, _albumpass3, _albumfail1, _albumfail2, _albumfail3); - Subject.GetImportDecisions(_audioFiles, new Artist(), null, null, downloadClientItem, null, false, false, false, false); + Subject.GetImportDecisions(_fileInfos, new Artist(), null, null, downloadClientItem, null, FilterFilesType.None, false, false, false); _albumfail1.Verify(c => c.IsSatisfiedBy(It.IsAny()), Times.Once()); _albumfail2.Verify(c => c.IsSatisfiedBy(It.IsAny()), Times.Once()); @@ -166,7 +168,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport GivenAugmentationSuccess(); GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3); - Subject.GetImportDecisions(_audioFiles, new Artist(), null, null, downloadClientItem, null, false, false, false, false); + Subject.GetImportDecisions(_fileInfos, new Artist(), null, null, downloadClientItem, null, FilterFilesType.None, false, false, false); _fail1.Verify(c => c.IsSatisfiedBy(It.IsAny()), Times.Once()); _fail2.Verify(c => c.IsSatisfiedBy(It.IsAny()), Times.Once()); @@ -184,7 +186,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport GivenSpecifications(_albumpass1, _albumpass2, _albumpass3, _albumfail1, _albumfail2, _albumfail3); GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3); - Subject.GetImportDecisions(_audioFiles, new Artist(), null, null, downloadClientItem, null, false, false, false, false); + Subject.GetImportDecisions(_fileInfos, new Artist(), null, null, downloadClientItem, null, FilterFilesType.None, false, false, false); _fail1.Verify(c => c.IsSatisfiedBy(It.IsAny()), Times.Never()); _fail2.Verify(c => c.IsSatisfiedBy(It.IsAny()), Times.Never()); @@ -200,7 +202,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport GivenSpecifications(_albumfail1); GivenSpecifications(_pass1); - var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false); + var result = Subject.GetImportDecisions(_fileInfos, new Artist(), FilterFilesType.None, false); result.Single().Approved.Should().BeFalse(); } @@ -211,7 +213,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport GivenSpecifications(_albumpass1); GivenSpecifications(_fail1); - var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false); + var result = Subject.GetImportDecisions(_fileInfos, new Artist(), FilterFilesType.None, false); result.Single().Approved.Should().BeFalse(); } @@ -222,7 +224,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport GivenSpecifications(_albumpass1, _albumfail1, _albumpass2, _albumpass3); GivenSpecifications(_pass1, _pass2, _pass3); - var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false); + var result = Subject.GetImportDecisions(_fileInfos, new Artist(), FilterFilesType.None, false); result.Single().Approved.Should().BeFalse(); } @@ -233,7 +235,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport GivenSpecifications(_albumpass1, _albumpass2, _albumpass3); GivenSpecifications(_pass1, _fail1, _pass2, _pass3); - var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false); + var result = Subject.GetImportDecisions(_fileInfos, new Artist(), FilterFilesType.None, false); result.Single().Approved.Should().BeFalse(); } @@ -245,7 +247,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport GivenSpecifications(_albumpass1, _albumpass2, _albumpass3); GivenSpecifications(_pass1, _pass2, _pass3); - var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false); + var result = Subject.GetImportDecisions(_fileInfos, new Artist(), FilterFilesType.None, false); result.Single().Approved.Should().BeTrue(); } @@ -256,7 +258,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport GivenAugmentationSuccess(); GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3); - var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false); + var result = Subject.GetImportDecisions(_fileInfos, new Artist(), FilterFilesType.None, false); result.Single().Rejections.Should().HaveCount(3); } @@ -276,10 +278,10 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic() }); - Subject.GetImportDecisions(_audioFiles, _artist, false); + Subject.GetImportDecisions(_fileInfos, _artist, FilterFilesType.None, false); Mocker.GetMock() - .Verify(c => c.Augment(It.IsAny(), It.IsAny()), Times.Exactly(_audioFiles.Count)); + .Verify(c => c.Augment(It.IsAny(), It.IsAny()), Times.Exactly(_fileInfos.Count)); ExceptionVerification.ExpectedErrors(3); } @@ -302,10 +304,10 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport return new List { new LocalAlbumRelease(tracks) }; }); - var decisions = Subject.GetImportDecisions(_audioFiles, _artist, false); + var decisions = Subject.GetImportDecisions(_fileInfos, _artist, FilterFilesType.None, false); Mocker.GetMock() - .Verify(c => c.Augment(It.IsAny(), It.IsAny()), Times.Exactly(_audioFiles.Count)); + .Verify(c => c.Augment(It.IsAny(), It.IsAny()), Times.Exactly(_fileInfos.Count)); decisions.Should().HaveCount(3); decisions.First().Rejections.Should().NotBeEmpty(); @@ -323,10 +325,10 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic() }); - var decisions = Subject.GetImportDecisions(_audioFiles, _artist, false); + var decisions = Subject.GetImportDecisions(_fileInfos, _artist, FilterFilesType.None, false); Mocker.GetMock() - .Verify(c => c.Augment(It.IsAny(), It.IsAny()), Times.Exactly(_audioFiles.Count)); + .Verify(c => c.Augment(It.IsAny(), It.IsAny()), Times.Exactly(_fileInfos.Count)); decisions.Should().HaveCount(3); decisions.First().Rejections.Should().NotBeEmpty(); @@ -344,7 +346,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic() }); - Subject.GetImportDecisions(_audioFiles, _artist, false).Should().HaveCount(1); + Subject.GetImportDecisions(_fileInfos, _artist, FilterFilesType.None, false).Should().HaveCount(1); ExceptionVerification.ExpectedErrors(1); } diff --git a/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs index f933e9502..0d5bc318a 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs @@ -18,6 +18,7 @@ namespace NzbDrone.Core.Test.MediaFiles { private TrackFile _trackFile; private LocalTrack _localTrack; + private string rootPath = @"C:\Test\Music\Artist".AsOsAgnostic(); [SetUp] public void Setup() @@ -25,7 +26,7 @@ namespace NzbDrone.Core.Test.MediaFiles _localTrack = new LocalTrack(); _localTrack.Artist = new Artist { - Path = @"C:\Test\Music\Artist".AsOsAgnostic() + Path = rootPath }; _trackFile = Builder @@ -55,7 +56,7 @@ namespace NzbDrone.Core.Test.MediaFiles new TrackFile { Id = 1, - RelativePath = @"Season 01\30.rock.s01e01.avi", + Path = Path.Combine(rootPath, @"Season 01\30.rock.s01e01.avi"), })) .Build() .ToList(); @@ -70,7 +71,7 @@ namespace NzbDrone.Core.Test.MediaFiles new TrackFile { Id = 1, - RelativePath = @"Season 01\30.rock.s01e01.avi", + Path = Path.Combine(rootPath, @"Season 01\30.rock.s01e01.avi"), })) .Build() .ToList(); @@ -84,14 +85,14 @@ namespace NzbDrone.Core.Test.MediaFiles new TrackFile { Id = 1, - RelativePath = @"Season 01\30.rock.s01e01.avi", + Path = Path.Combine(rootPath, @"Season 01\30.rock.s01e01.avi"), })) .TheNext(1) .With(e => e.TrackFile = new LazyLoaded( new TrackFile { Id = 2, - RelativePath = @"Season 01\30.rock.s01e02.avi", + Path = Path.Combine(rootPath, @"Season 01\30.rock.s01e02.avi"), })) .Build() .ToList(); diff --git a/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs index d80090859..08df42c62 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/SynologyIndexerFixture.cs @@ -7,6 +7,7 @@ using NzbDrone.Core.Notifications.Synology; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; using NzbDrone.Test.Common; +using System.IO; namespace NzbDrone.Core.Test.NotificationTests { @@ -15,13 +16,14 @@ namespace NzbDrone.Core.Test.NotificationTests { private Artist _artist; private AlbumDownloadMessage _upgrade; + private string rootPath = @"C:\Test\".AsOsAgnostic(); [SetUp] public void SetUp() { _artist = new Artist() { - Path = @"C:\Test\".AsOsAgnostic() + Path = rootPath, }; _upgrade = new AlbumDownloadMessage() @@ -32,7 +34,7 @@ namespace NzbDrone.Core.Test.NotificationTests { new TrackFile { - RelativePath = "file1.S01E01E02.mkv" + Path = Path.Combine(rootPath, "file1.S01E01E02.mkv") } }, @@ -41,11 +43,11 @@ namespace NzbDrone.Core.Test.NotificationTests { new TrackFile { - RelativePath = "file1.S01E01.mkv" + Path = Path.Combine(rootPath, "file1.S01E01.mkv") }, new TrackFile { - RelativePath = "file1.S01E02.mkv" + Path = Path.Combine(rootPath, "file1.S01E02.mkv") } } }; diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 79086b6ba..757eebced 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -91,6 +91,12 @@ ..\packages\TagLibSharp-Lidarr.2.2.0.19\lib\netstandard2.0\TagLibSharp.dll True + + ..\packages\System.IO.Abstractions.4.0.11\lib\net40\System.IO.Abstractions.dll + + + ..\packages\System.IO.Abstractions.TestingHelpers.4.0.11\lib\net40\System.IO.Abstractions.TestingHelpers.dll + @@ -127,6 +133,7 @@ + @@ -206,6 +213,7 @@ + @@ -432,6 +440,14 @@ {CADDFCE0-7509-4430-8364-2074E1EEFCA2} NzbDrone.Test.Common + + {15AD7579-A314-4626-B556-663F51D97CD1} + NzbDrone.Mono + + + {911284D3-F130-459E-836C-2430B6FBF21D} + NzbDrone.Windows + diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs index ed83ff51f..f08dec808 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs @@ -391,28 +391,27 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests public void use_file_name_when_sceneName_is_null() { _namingConfig.RenameTracks = false; - _trackFile.RelativePath = "Linkin Park - 06 - Test"; + _trackFile.Path = "Linkin Park - 06 - Test"; Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be(Path.GetFileNameWithoutExtension(_trackFile.RelativePath)); + .Should().Be(Path.GetFileNameWithoutExtension(_trackFile.Path)); } [Test] public void use_file_name_when_sceneName_is_not_null() { _namingConfig.RenameTracks = false; - _trackFile.RelativePath = "Linkin Park - 06 - Test"; + _trackFile.Path = "Linkin Park - 06 - Test"; _trackFile.SceneName = "SceneName"; Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be(Path.GetFileNameWithoutExtension(_trackFile.RelativePath)); + .Should().Be(Path.GetFileNameWithoutExtension(_trackFile.Path)); } [Test] public void use_path_when_sceneName_and_relative_path_are_null() { _namingConfig.RenameTracks = false; - _trackFile.RelativePath = null; _trackFile.Path = @"C:\Test\Unsorted\Artist - 01 - Test"; Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) @@ -447,7 +446,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _namingConfig.StandardTrackFormat = "{Artist Name} - {Original Title} - {Track Title}"; _trackFile.SceneName = "Linkin.Park.Meteora.320-LOL"; - _trackFile.RelativePath = "30 Rock - 01 - Test"; + _trackFile.Path = "30 Rock - 01 - Test"; Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) .Should().Be("Linkin Park - Linkin.Park.Meteora.320-LOL - City Sushi"); @@ -520,10 +519,10 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _namingConfig.StandardTrackFormat = "{Original Title}"; _trackFile.SceneName = null; - _trackFile.RelativePath = "existing.file.mkv"; + _trackFile.Path = "existing.file.mkv"; Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) - .Should().Be(Path.GetFileNameWithoutExtension(_trackFile.RelativePath)); + .Should().Be(Path.GetFileNameWithoutExtension(_trackFile.Path)); } [Test] @@ -533,7 +532,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _namingConfig.StandardTrackFormat = "{Original Title}"; _trackFile.SceneName = "30.Rock.S01E01.xvid-LOL"; - _trackFile.RelativePath = "30 Rock - S01E01 - Test"; + _trackFile.Path = "30 Rock - S01E01 - Test"; Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) .Should().Be("30.Rock.S01E01.xvid-LOL"); @@ -599,7 +598,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _namingConfig.StandardTrackFormat = "{Artist Name} - {Original Filename}"; _trackFile.SceneName = "30.Rock.S01E01.xvid-LOL"; - _trackFile.RelativePath = "30 Rock - S01E01 - Test"; + _trackFile.Path = "30 Rock - S01E01 - Test"; Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) .Should().Be("30 Rock - 30 Rock - S01E01 - Test"); @@ -612,7 +611,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _namingConfig.StandardTrackFormat = "{Original Filename}"; _trackFile.SceneName = "30.Rock.S01E01.xvid-LOL"; - _trackFile.RelativePath = "30 Rock - S01E01 - Test"; + _trackFile.Path = "30 Rock - S01E01 - Test"; Subject.BuildTrackFileName(new List { _track1 }, _artist, _album, _trackFile) .Should().Be("30 Rock - S01E01 - Test"); diff --git a/src/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/GetAudioFilesFixture.cs b/src/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/GetAudioFilesFixture.cs index ac6eaa253..bd54f2f88 100644 --- a/src/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/GetAudioFilesFixture.cs +++ b/src/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/GetAudioFilesFixture.cs @@ -8,6 +8,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Test.Common; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Test.Framework; +using System.IO.Abstractions; namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests { @@ -29,6 +30,10 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests @"movie.exe", @"movie" }; + + Mocker.GetMock() + .Setup(s => s.GetFileInfos(It.IsAny(), It.IsAny())) + .Returns(new List()); } private IEnumerable GetFiles(string folder, string subFolder = "") @@ -38,9 +43,15 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests private void GivenFiles(IEnumerable files) { - var filesToReturn = files.ToArray(); + var filesToReturn = files.Select(x => (FileInfoBase)new FileInfo(x)).ToList(); + + foreach (var file in filesToReturn) + { + TestLogger.Debug(file.Name); + } + Mocker.GetMock() - .Setup(s => s.GetFiles(It.IsAny(), SearchOption.AllDirectories)) + .Setup(s => s.GetFileInfos(It.IsAny(), SearchOption.AllDirectories)) .Returns(filesToReturn); } @@ -49,8 +60,8 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests { Subject.GetAudioFiles(path); - Mocker.GetMock().Verify(s => s.GetFiles(path, SearchOption.AllDirectories), Times.Once()); - Mocker.GetMock().Verify(s => s.GetFiles(path, SearchOption.TopDirectoryOnly), Times.Never()); + Mocker.GetMock().Verify(s => s.GetFileInfos(path, SearchOption.AllDirectories), Times.Once()); + Mocker.GetMock().Verify(s => s.GetFileInfos(path, SearchOption.TopDirectoryOnly), Times.Never()); } [Test] @@ -58,8 +69,8 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests { Subject.GetAudioFiles(path, true); - Mocker.GetMock().Verify(s => s.GetFiles(path, SearchOption.AllDirectories), Times.Once()); - Mocker.GetMock().Verify(s => s.GetFiles(path, SearchOption.TopDirectoryOnly), Times.Never()); + Mocker.GetMock().Verify(s => s.GetFileInfos(path, SearchOption.AllDirectories), Times.Once()); + Mocker.GetMock().Verify(s => s.GetFileInfos(path, SearchOption.TopDirectoryOnly), Times.Never()); } [Test] @@ -67,8 +78,8 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests { Subject.GetAudioFiles(path, false); - Mocker.GetMock().Verify(s => s.GetFiles(path, SearchOption.AllDirectories), Times.Never()); - Mocker.GetMock().Verify(s => s.GetFiles(path, SearchOption.TopDirectoryOnly), Times.Once()); + Mocker.GetMock().Verify(s => s.GetFileInfos(path, SearchOption.AllDirectories), Times.Never()); + Mocker.GetMock().Verify(s => s.GetFileInfos(path, SearchOption.TopDirectoryOnly), Times.Once()); } [Test] diff --git a/src/NzbDrone.Core.Test/packages.config b/src/NzbDrone.Core.Test/packages.config index 89a0c270b..39f39bf19 100644 --- a/src/NzbDrone.Core.Test/packages.config +++ b/src/NzbDrone.Core.Test/packages.config @@ -15,4 +15,5 @@ + diff --git a/src/NzbDrone.Core/Datastore/Migration/030_add_mediafilerepository_mtime.cs b/src/NzbDrone.Core/Datastore/Migration/030_add_mediafilerepository_mtime.cs new file mode 100644 index 000000000..0645eb54b --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/030_add_mediafilerepository_mtime.cs @@ -0,0 +1,64 @@ +using System; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(30)] + public class add_mediafilerepository_mtime : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("TrackFiles").AddColumn("Modified").AsDateTime().WithDefaultValue(new DateTime(2000, 1, 1)); + Alter.Table("TrackFiles").AddColumn("Path").AsString().Nullable(); + + // Remove anything where RelativePath is null + Execute.Sql(@"DELETE FROM TrackFiles WHERE RelativePath IS NULL"); + + // Remove anything not linked to a track (these shouldn't be present in version < 30) + Execute.Sql(@"DELETE FROM TrackFiles + WHERE Id IN ( + SELECT TrackFiles.Id FROM TrackFiles + LEFT JOIN Tracks ON TrackFiles.Id = Tracks.TrackFileId + WHERE Tracks.Id IS NULL)"); + + // Remove anything where we can't get an artist path (i.e. we don't know where it is) + Execute.Sql(@"DELETE FROM TrackFiles + WHERE Id IN ( + SELECT TrackFiles.Id FROM TrackFiles + LEFT JOIN Albums ON TrackFiles.AlbumId = Albums.Id + LEFT JOIN Artists on Artists.ArtistMetadataId = Albums.ArtistMetadataId + WHERE Artists.Path IS NULL)"); + + // Remove anything linked to unmonitored or unidentified releases. This should ensure uniqueness of track files. + Execute.Sql(@"DELETE FROM TrackFiles + WHERE Id IN ( + SELECT TrackFiles.Id FROM TrackFiles + LEFT JOIN Tracks ON TrackFiles.Id = Tracks.TrackFileId + LEFT JOIN AlbumReleases ON Tracks.AlbumReleaseId = AlbumReleases.Id + WHERE AlbumReleases.Monitored = 0 + OR AlbumReleases.Monitored IS NULL)"); + + // Populate the full paths + Execute.Sql(@"UPDATE TrackFiles + SET Path = (SELECT Artists.Path || '" + System.IO.Path.DirectorySeparatorChar + @"' || TrackFiles.RelativePath + FROM Artists + JOIN Albums ON Albums.ArtistMetadataId = Artists.ArtistMetadataId + WHERE TrackFiles.AlbumId = Albums.Id)"); + + // Belt and braces to ensure uniqueness + Execute.Sql(@"DELETE FROM TrackFiles + WHERE rowid NOT IN ( + SELECT min(rowid) + FROM TrackFiles + GROUP BY Path + )"); + + // Now enforce the uniqueness constraint + Alter.Table("TrackFiles").AlterColumn("Path").AsString().NotNullable().Unique(); + + // Finally delete the relative path column + Delete.Column("RelativePath").FromTable("TrackFiles"); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index f2eab0ca2..2122b8422 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -147,7 +147,6 @@ namespace NzbDrone.Core.Datastore .SingleOrDefault()); Mapper.Entity().RegisterModel("TrackFiles") - .Ignore(f => f.Path) .Relationship() .HasOne(f => f.Album, f => f.AlbumId) .For(f => f.Tracks) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedTrackFileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedTrackFileSpecification.cs index 6c98a9ea9..45a25ce31 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedTrackFileSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedTrackFileSpecification.cs @@ -56,7 +56,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { foreach (var missingTrackFile in missingTrackFiles) { - _logger.Trace("Track file {0} is missing from disk.", missingTrackFile.RelativePath); + _logger.Trace("Track file {0} is missing from disk.", missingTrackFile.Path); } _logger.Debug("Files for this album exist in the database but not on disk, will be unmonitored on next diskscan. skipping."); @@ -68,9 +68,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync private bool IsTrackFileMissing(Artist artist, TrackFile trackFile) { - var fullPath = Path.Combine(artist.Path, trackFile.RelativePath); - - return !_diskProvider.FileExists(fullPath); + return !_diskProvider.FileExists(trackFile.Path); } } } diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/ScanWatchFolder.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/ScanWatchFolder.cs index 924436600..c19f37d35 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/ScanWatchFolder.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/ScanWatchFolder.cs @@ -88,14 +88,14 @@ namespace NzbDrone.Core.Download.Clients.Blackhole foreach (var audioFile in _diskScanService.FilterFiles(watchFolder, _diskScanService.GetAudioFiles(watchFolder, false))) { - var title = FileNameBuilder.CleanFileName(Path.GetFileName(audioFile)); + var title = FileNameBuilder.CleanFileName(audioFile.Name); var newWatchItem = new WatchFolderItem { - DownloadId = Path.GetFileName(audioFile) + "_" + _diskProvider.FileGetLastWrite(audioFile).Ticks, + DownloadId = audioFile.Name + "_" + audioFile.LastWriteTimeUtc.Ticks, Title = title, - OutputPath = new OsPath(audioFile), + OutputPath = new OsPath(audioFile.FullName), Status = DownloadItemStatus.Completed, RemainingTime = TimeSpan.Zero @@ -105,10 +105,10 @@ namespace NzbDrone.Core.Download.Clients.Blackhole if (PreCheckWatchItemExpiry(newWatchItem, oldWatchItem)) { - newWatchItem.TotalSize = _diskProvider.GetFileSize(audioFile); - newWatchItem.Hash = GetHash(audioFile); + newWatchItem.TotalSize = audioFile.Length; + newWatchItem.Hash = GetHash(audioFile.FullName); - if (_diskProvider.IsFileLocked(audioFile)) + if (_diskProvider.IsFileLocked(audioFile.FullName)) { newWatchItem.Status = DownloadItemStatus.Downloading; } diff --git a/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs b/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs index 187e8e2b6..360bbb13c 100644 --- a/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs +++ b/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs @@ -50,8 +50,8 @@ namespace NzbDrone.Core.Extras.Files protected TExtraFile ImportFile(Artist artist, TrackFile trackFile, string path, bool readOnly, string extension, string fileNameSuffix = null) { - var newFolder = Path.GetDirectoryName(Path.Combine(artist.Path, trackFile.RelativePath)); - var filenameBuilder = new StringBuilder(Path.GetFileNameWithoutExtension(trackFile.RelativePath)); + var newFolder = Path.GetDirectoryName(trackFile.Path); + var filenameBuilder = new StringBuilder(Path.GetFileNameWithoutExtension(trackFile.Path)); if (fileNameSuffix.IsNotNullOrWhiteSpace()) { @@ -82,8 +82,8 @@ namespace NzbDrone.Core.Extras.Files protected TExtraFile MoveFile(Artist artist, TrackFile trackFile, TExtraFile extraFile, string fileNameSuffix = null) { - var newFolder = Path.GetDirectoryName(Path.Combine(artist.Path, trackFile.RelativePath)); - var filenameBuilder = new StringBuilder(Path.GetFileNameWithoutExtension(trackFile.RelativePath)); + var newFolder = Path.GetDirectoryName(trackFile.Path); + var filenameBuilder = new StringBuilder(Path.GetFileNameWithoutExtension(trackFile.Path)); if (fileNameSuffix.IsNotNullOrWhiteSpace()) { diff --git a/src/NzbDrone.Core/Extras/Lyrics/LyricService.cs b/src/NzbDrone.Core/Extras/Lyrics/LyricService.cs index b476c5490..a18b63883 100644 --- a/src/NzbDrone.Core/Extras/Lyrics/LyricService.cs +++ b/src/NzbDrone.Core/Extras/Lyrics/LyricService.cs @@ -65,7 +65,7 @@ namespace NzbDrone.Core.Extras.Lyrics if (groupCount > 1) { - _logger.Warn("Multiple lyric files found with the same language and extension for {0}", Path.Combine(artist.Path, trackFile.RelativePath)); + _logger.Warn("Multiple lyric files found with the same language and extension for {0}", trackFile.Path); } foreach (var subtitleFile in group) diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs index daf1368e0..801b57c62 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs @@ -37,7 +37,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox public override string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile) { - var trackFilePath = Path.Combine(artist.Path, trackFile.RelativePath); + var trackFilePath = trackFile.Path; if (metadataFile.Type == MetadataType.TrackMetadata) { @@ -122,7 +122,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox return null; } - _logger.Debug("Generating Track Metadata for: {0}", trackFile.RelativePath); + _logger.Debug("Generating Track Metadata for: {0}", trackFile.Path); var xmlResult = string.Empty; foreach (var track in trackFile.Tracks.Value) @@ -148,7 +148,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox } } - return new MetadataFileResult(GetTrackMetadataFilename(trackFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray())); + return new MetadataFileResult(GetTrackMetadataFilename(artist.Path.GetRelativePath(trackFile.Path)), xmlResult.Trim(Environment.NewLine.ToCharArray())); } public override List ArtistImages(Artist artist) diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs index 9dfb6756a..7ce21dbda 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs @@ -35,7 +35,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv public override string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile) { - var trackFilePath = Path.Combine(artist.Path, trackFile.RelativePath); + var trackFilePath = trackFile.Path; if (metadataFile.Type == MetadataType.TrackMetadata) { @@ -94,7 +94,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv return null; } - _logger.Debug("Generating Track Metadata for: {0}", Path.Combine(artist.Path, trackFile.RelativePath)); + _logger.Debug("Generating Track Metadata for: {0}", trackFile.Path); var xmlResult = string.Empty; foreach (var track in trackFile.Tracks.Value) @@ -124,7 +124,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv } } - var filename = GetTrackMetadataFilename(trackFile.RelativePath); + var filename = GetTrackMetadataFilename(artist.Path.GetRelativePath(trackFile.Path)); return new MetadataFileResult(filename, xmlResult.Trim(Environment.NewLine.ToCharArray())); } diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs index c849b867e..91a2392e5 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs @@ -37,7 +37,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc public override string GetFilenameAfterMove(Artist artist, TrackFile trackFile, MetadataFile metadataFile) { - var trackFilePath = Path.Combine(artist.Path, trackFile.RelativePath); + var trackFilePath = trackFile.Path; if (metadataFile.Type == MetadataType.TrackMetadata) { diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs index fc6899f99..7650402a5 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs @@ -30,7 +30,7 @@ namespace NzbDrone.Core.Extras.Metadata { var existingFilename = Path.Combine(artist.Path, metadataFile.RelativePath); var extension = Path.GetExtension(existingFilename).TrimStart('.'); - var newFileName = Path.ChangeExtension(Path.Combine(artist.Path, trackFile.RelativePath), extension); + var newFileName = Path.ChangeExtension(trackFile.Path, extension); return newFileName; } diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs index 8beccbce0..ef1b2884d 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs @@ -80,7 +80,7 @@ namespace NzbDrone.Core.Extras.Metadata files.AddIfNotNull(ProcessArtistMetadata(consumer, artist, consumerFiles)); files.AddRange(ProcessArtistImages(consumer, artist, consumerFiles)); - var albumGroups = trackFiles.GroupBy(s => Path.GetDirectoryName(s.RelativePath)).ToList(); + var albumGroups = trackFiles.GroupBy(s => Path.GetDirectoryName(s.Path)).ToList(); foreach (var group in albumGroups) { @@ -146,7 +146,7 @@ namespace NzbDrone.Core.Extras.Metadata { var metadataFiles = _metadataFileService.GetFilesByArtist(artist.Id); var movedFiles = new List(); - var distinctTrackFilePaths = trackFiles.DistinctBy(s => Path.GetDirectoryName(s.RelativePath)).ToList(); + var distinctTrackFilePaths = trackFiles.DistinctBy(s => Path.GetDirectoryName(s.Path)).ToList(); // TODO: Move EpisodeImage and EpisodeMetadata metadata files, instead of relying on consumers to do it // (Xbmc's EpisodeImage is more than just the extension) @@ -162,7 +162,7 @@ namespace NzbDrone.Core.Extras.Metadata foreach (var metadataFile in metadataFilesForConsumer) { - var newFileName = consumer.GetFilenameAfterMove(artist, Path.GetDirectoryName(filePath.RelativePath), metadataFile); + var newFileName = consumer.GetFilenameAfterMove(artist, Path.GetDirectoryName(filePath.Path), metadataFile); var existingFileName = Path.Combine(artist.Path, metadataFile.RelativePath); if (newFileName.PathNotEquals(existingFileName)) diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index e34d3c7ee..b2bba0e01 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -234,7 +234,7 @@ namespace NzbDrone.Core.History //Won't have a value since we publish this event before saving to DB. //history.Data.Add("FileId", message.ImportedEpisode.Id.ToString()); history.Data.Add("DroppedPath", message.TrackInfo.Path); - history.Data.Add("ImportedPath", Path.Combine(message.TrackInfo.Artist.Path, message.ImportedTrack.RelativePath)); + history.Data.Add("ImportedPath", message.ImportedTrack.Path); history.Data.Add("DownloadClient", message.DownloadClient); _historyRepository.Insert(history); @@ -321,8 +321,7 @@ namespace NzbDrone.Core.History { var sourcePath = message.OriginalPath; var sourceRelativePath = message.Artist.Path.GetRelativePath(message.OriginalPath); - var path = Path.Combine(message.Artist.Path, message.TrackFile.RelativePath); - var relativePath = message.TrackFile.RelativePath; + var path = message.TrackFile.Path; foreach (var track in message.TrackFile.Tracks.Value) { @@ -340,7 +339,6 @@ namespace NzbDrone.Core.History history.Data.Add("SourcePath", sourcePath); history.Data.Add("SourceRelativePath", sourceRelativePath); history.Data.Add("Path", path); - history.Data.Add("RelativePath", relativePath); _historyRepository.Insert(history); } @@ -348,8 +346,7 @@ namespace NzbDrone.Core.History public void Handle(TrackFileRetaggedEvent message) { - var path = Path.Combine(message.Artist.Path, message.TrackFile.RelativePath); - var relativePath = message.TrackFile.RelativePath; + var path = message.TrackFile.Path; foreach (var track in message.TrackFile.Tracks.Value) { diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTrackFiles.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTrackFiles.cs index 4d24b8269..013df88b5 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTrackFiles.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTrackFiles.cs @@ -15,23 +15,15 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { var mapper = _database.GetDataMapper(); - // Delete where track no longer exists - mapper.ExecuteNonQuery(@"DELETE FROM TrackFiles + // Unlink where track no longer exists + mapper.ExecuteNonQuery(@"UPDATE TrackFiles + SET AlbumId = 0 WHERE Id IN ( SELECT TrackFiles.Id FROM TrackFiles LEFT OUTER JOIN Tracks ON TrackFiles.Id = Tracks.TrackFileId WHERE Tracks.Id IS NULL)"); - // Delete trackfiles associated with releases that are not currently selected - mapper.ExecuteNonQuery(@"DELETE FROM TrackFiles - WHERE Id IN ( - SELECT TrackFiles.Id FROM TrackFiles - JOIN Tracks ON TrackFiles.Id = Tracks.TrackFileId - JOIN AlbumReleases ON Tracks.AlbumReleaseId = AlbumReleases.Id - JOIN Albums ON AlbumReleases.AlbumId = Albums.Id - WHERE AlbumReleases.Monitored = 0)"); - // Unlink Tracks where the Trackfiles entry no longer exists mapper.ExecuteNonQuery(@"UPDATE Tracks SET TrackFileId = 0 diff --git a/src/NzbDrone.Core/MediaFiles/AudioTagService.cs b/src/NzbDrone.Core/MediaFiles/AudioTagService.cs index a1fe3f91e..8aae5cacb 100644 --- a/src/NzbDrone.Core/MediaFiles/AudioTagService.cs +++ b/src/NzbDrone.Core/MediaFiles/AudioTagService.cs @@ -15,6 +15,7 @@ using NLog.Fluent; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using TagLib; +using NzbDrone.Common.Extensions; namespace NzbDrone.Core.MediaFiles { @@ -176,7 +177,7 @@ namespace NzbDrone.Core.MediaFiles } var newTags = GetTrackMetadata(trackfile); - var path = Path.Combine(trackfile.Artist.Value.Path, trackfile.RelativePath); + var path = trackfile.Path; var diff = ReadAudioTag(path).Diff(newTags); @@ -270,7 +271,7 @@ namespace NzbDrone.Core.MediaFiles return; } - var path = Path.Combine(trackfile.Artist.Value.Path, trackfile.RelativePath); + var path = trackfile.Path; _logger.Debug($"Removing MusicBrainz tags for {path}"); RemoveMusicBrainzTags(path); @@ -312,7 +313,7 @@ namespace NzbDrone.Core.MediaFiles continue; } - var oldTags = ReadAudioTag(Path.Combine(f.Artist.Value.Path, f.RelativePath)); + var oldTags = ReadAudioTag(f.Path); var newTags = GetTrackMetadata(f); var diff = oldTags.Diff(newTags); @@ -323,7 +324,7 @@ namespace NzbDrone.Core.MediaFiles AlbumId = file.Album.Value.Id, TrackNumbers = file.Tracks.Value.Select(e => e.AbsoluteTrackNumber).ToList(), TrackFileId = file.Id, - RelativePath = file.RelativePath, + RelativePath = file.Artist.Value.Path.GetRelativePath(file.Path), Changes = diff }; } diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 5fb32a8bb..5817681fb 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.IO.Abstractions; using System.Linq; using System.Text.RegularExpressions; using NLog; @@ -15,18 +16,17 @@ using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Music; -using NzbDrone.Core.Music.Events; using NzbDrone.Core.MediaFiles.TrackImport; -using NzbDrone.Core.Parser.Model; using NzbDrone.Common; namespace NzbDrone.Core.MediaFiles { public interface IDiskScanService { - void Scan(Artist artist); - string[] GetAudioFiles(string path, bool allDirectories = true); + void Scan(Artist artist, FilterFilesType filter = FilterFilesType.Known); + IFileInfo[] GetAudioFiles(string path, bool allDirectories = true); string[] GetNonAudioFiles(string path, bool allDirectories = true); + List FilterFiles(string basePath, IEnumerable files); List FilterFiles(string basePath, IEnumerable files); } @@ -70,7 +70,7 @@ namespace NzbDrone.Core.MediaFiles private static readonly Regex ExcludedSubFoldersRegex = new Regex(@"(?:\\|\/|^)(?:extras|@eadir|extrafanart|plex versions|\.[^\\/]+)(?:\\|\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ExcludedFilesRegex = new Regex(@"^\._|^Thumbs\.db$|^\.DS_store$|\.partial~$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public void Scan(Artist artist) + public void Scan(Artist artist, FilterFilesType filter = FilterFilesType.Known) { var rootFolder = _rootFolderService.GetBestRootFolderPath(artist.Path); @@ -113,44 +113,70 @@ namespace NzbDrone.Core.MediaFiles var mediaFileList = FilterFiles(artist.Path, GetAudioFiles(artist.Path)).ToList(); musicFilesStopwatch.Stop(); _logger.Trace("Finished getting track files for: {0} [{1}]", artist, musicFilesStopwatch.Elapsed); - - CleanMediaFiles(artist, mediaFileList); + + CleanMediaFiles(artist, mediaFileList.Select(x => x.FullName).ToList()); var decisionsStopwatch = Stopwatch.StartNew(); - var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, artist, false); + var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, artist, filter, true); decisionsStopwatch.Stop(); _logger.Debug("Import decisions complete for: {0} [{1}]", artist, decisionsStopwatch.Elapsed); var importStopwatch = Stopwatch.StartNew(); _importApprovedTracks.Import(decisions, false); + + // decisions may have been filtered to just new files. Anything new and approved will have been inserted. + // Now we need to make sure anything new but not approved gets inserted + // Note that knownFiles will include anything imported just now + var knownFiles = _mediaFileService.GetFilesWithBasePath(artist.Path); + + var newFiles = decisions + .ExceptBy(x => x.Item.Path, knownFiles, x => x.Path, PathEqualityComparer.Instance) + .Select(decision => new TrackFile { + Path = decision.Item.Path, + Size = decision.Item.Size, + Modified = decision.Item.Modified, + DateAdded = DateTime.UtcNow, + Quality = decision.Item.Quality, + MediaInfo = decision.Item.FileTrackInfo.MediaInfo, + Language = decision.Item.Language + }) + .ToList(); + _mediaFileService.AddMany(newFiles); + + _logger.Debug($"Inserted {newFiles.Count} new unmatched trackfiles"); + + // finally update info on size/modified for existing files + var updatedFiles = knownFiles + .Join(decisions, + x => x.Path, + x => x.Item.Path, + (file, decision) => new { + File = file, + Item = decision.Item + }, + PathEqualityComparer.Instance) + .Where(x => x.File.Size != x.Item.Size || + Math.Abs((x.File.Modified - x.Item.Modified).TotalSeconds) > 1 ) + .Select(x => { + x.File.Size = x.Item.Size; + x.File.Modified = x.Item.Modified; + x.File.MediaInfo = x.Item.FileTrackInfo.MediaInfo; + x.File.Quality = x.Item.Quality; + return x.File; + }) + .ToList(); + + _mediaFileService.Update(updatedFiles); + + _logger.Debug($"Updated info for {updatedFiles.Count} known files"); + RemoveEmptyArtistFolder(artist.Path); - UpdateMediaInfo(artist, decisions.Select(x => x.Item).ToList()); + CompletedScanning(artist); importStopwatch.Stop(); _logger.Debug("Track import complete for: {0} [{1}]", artist, importStopwatch.Elapsed); } - private void UpdateMediaInfo(Artist artist, List mediaFiles) - { - var existingFiles = _mediaFileService.GetFilesByArtist(artist.Id); - var toUpdate = new List(existingFiles.Count); - - foreach (var file in existingFiles) - { - var path = Path.Combine(artist.Path, file.RelativePath); - var scannedFile = mediaFiles.FirstOrDefault(x => PathEqualityComparer.Instance.Equals(path, x.Path)); - - if (scannedFile != null) - { - file.MediaInfo = scannedFile.FileTrackInfo.MediaInfo; - toUpdate.Add(file); - } - } - - _logger.Debug($"Updating Media Info for:\n{string.Join("\n", toUpdate)}"); - _mediaFileService.UpdateMediaInfo(toUpdate); - } - private void CleanMediaFiles(Artist artist, List mediaFileList) { _logger.Debug("{0} Cleaning up media files in DB", artist); @@ -163,14 +189,14 @@ namespace NzbDrone.Core.MediaFiles _eventAggregator.PublishEvent(new ArtistScannedEvent(artist)); } - public string[] GetAudioFiles(string path, bool allDirectories = true) + public IFileInfo[] GetAudioFiles(string path, bool allDirectories = true) { _logger.Debug("Scanning '{0}' for music files", path); var searchOption = allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; - var filesOnDisk = _diskProvider.GetFiles(path, searchOption).ToList(); + var filesOnDisk = _diskProvider.GetFileInfos(path, searchOption); - var mediaFileList = filesOnDisk.Where(file => MediaFileExtensions.Extensions.Contains(Path.GetExtension(file))) + var mediaFileList = filesOnDisk.Where(file => MediaFileExtensions.Extensions.Contains(file.Extension)) .ToList(); _logger.Trace("{0} files were found in {1}", filesOnDisk.Count, path); @@ -202,6 +228,13 @@ namespace NzbDrone.Core.MediaFiles .ToList(); } + public List FilterFiles(string basePath, IEnumerable files) + { + return files.Where(file => !ExcludedSubFoldersRegex.IsMatch(basePath.GetRelativePath(file.FullName))) + .Where(file => !ExcludedFilesRegex.IsMatch(file.Name)) + .ToList(); + } + private void SetPermissions(string path) { if (!_configService.SetPermissionsLinux) diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedTracksImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedTracksImportService.cs index 6e8abeb4c..17b922164 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedTracksImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedTracksImportService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Linq; using NLog; using NzbDrone.Common.Disk; @@ -17,9 +18,9 @@ namespace NzbDrone.Core.MediaFiles { public interface IDownloadedTracksImportService { - List ProcessRootFolder(DirectoryInfo directoryInfo); + List ProcessRootFolder(IDirectoryInfo directoryInfo); List ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Artist artist = null, DownloadClientItem downloadClientItem = null); - bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Artist artist); + bool ShouldDeleteFolder(IDirectoryInfo directoryInfo, Artist artist); } public class DownloadedTracksImportService : IDownloadedTracksImportService @@ -52,19 +53,19 @@ namespace NzbDrone.Core.MediaFiles _logger = logger; } - public List ProcessRootFolder(DirectoryInfo directoryInfo) + public List ProcessRootFolder(IDirectoryInfo directoryInfo) { var results = new List(); - foreach (var subFolder in _diskProvider.GetDirectories(directoryInfo.FullName)) + foreach (var subFolder in _diskProvider.GetDirectoryInfos(directoryInfo.FullName)) { - var folderResults = ProcessFolder(new DirectoryInfo(subFolder), ImportMode.Auto, null); + var folderResults = ProcessFolder(subFolder, ImportMode.Auto, null); results.AddRange(folderResults); } foreach (var audioFile in _diskScanService.GetAudioFiles(directoryInfo.FullName, false)) { - var fileResults = ProcessFile(new FileInfo(audioFile), ImportMode.Auto, null); + var fileResults = ProcessFile(audioFile, ImportMode.Auto, null); results.AddRange(fileResults); } @@ -75,7 +76,7 @@ namespace NzbDrone.Core.MediaFiles { if (_diskProvider.FolderExists(path)) { - var directoryInfo = new DirectoryInfo(path); + var directoryInfo = _diskProvider.GetDirectoryInfo(path); if (artist == null) { @@ -87,7 +88,7 @@ namespace NzbDrone.Core.MediaFiles if (_diskProvider.FileExists(path)) { - var fileInfo = new FileInfo(path); + var fileInfo = _diskProvider.GetFileInfo(path); if (artist == null) { @@ -103,14 +104,14 @@ namespace NzbDrone.Core.MediaFiles return new List(); } - public bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Artist artist) + public bool ShouldDeleteFolder(IDirectoryInfo directoryInfo, Artist artist) { var audioFiles = _diskScanService.GetAudioFiles(directoryInfo.FullName); var rarFiles = _diskProvider.GetFiles(directoryInfo.FullName, SearchOption.AllDirectories).Where(f => Path.GetExtension(f).Equals(".rar", StringComparison.OrdinalIgnoreCase)); foreach (var audioFile in audioFiles) { - var albumParseResult = Parser.Parser.ParseMusicTitle(Path.GetFileName(audioFile)); + var albumParseResult = Parser.Parser.ParseMusicTitle(audioFile.Name); if (albumParseResult == null) { @@ -131,7 +132,7 @@ namespace NzbDrone.Core.MediaFiles return true; } - private List ProcessFolder(DirectoryInfo directoryInfo, ImportMode importMode, DownloadClientItem downloadClientItem) + private List ProcessFolder(IDirectoryInfo directoryInfo, ImportMode importMode, DownloadClientItem downloadClientItem) { var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); var artist = _parsingService.GetArtist(cleanedUpName); @@ -149,7 +150,7 @@ namespace NzbDrone.Core.MediaFiles return ProcessFolder(directoryInfo, importMode, artist, downloadClientItem); } - private List ProcessFolder(DirectoryInfo directoryInfo, ImportMode importMode, Artist artist, DownloadClientItem downloadClientItem) + private List ProcessFolder(IDirectoryInfo directoryInfo, ImportMode importMode, Artist artist, DownloadClientItem downloadClientItem) { if (_artistService.ArtistPathExists(directoryInfo.FullName)) { @@ -185,17 +186,17 @@ namespace NzbDrone.Core.MediaFiles { foreach (var audioFile in audioFiles) { - if (_diskProvider.IsFileLocked(audioFile)) + if (_diskProvider.IsFileLocked(audioFile.FullName)) { return new List { - FileIsLockedResult(audioFile) + FileIsLockedResult(audioFile.FullName) }; } } } - var decisions = _importDecisionMaker.GetImportDecisions(audioFiles.ToList(), artist, trackInfo); + var decisions = _importDecisionMaker.GetImportDecisions(audioFiles, artist, trackInfo); var importResults = _importApprovedTracks.Import(decisions, true, downloadClientItem, importMode); if (importMode == ImportMode.Auto) @@ -214,7 +215,7 @@ namespace NzbDrone.Core.MediaFiles return importResults; } - private List ProcessFile(FileInfo fileInfo, ImportMode importMode, DownloadClientItem downloadClientItem) + private List ProcessFile(IFileInfo fileInfo, ImportMode importMode, DownloadClientItem downloadClientItem) { var artist = _parsingService.GetArtist(Path.GetFileNameWithoutExtension(fileInfo.Name)); @@ -231,7 +232,7 @@ namespace NzbDrone.Core.MediaFiles return ProcessFile(fileInfo, importMode, artist, downloadClientItem); } - private List ProcessFile(FileInfo fileInfo, ImportMode importMode, Artist artist, DownloadClientItem downloadClientItem) + private List ProcessFile(IFileInfo fileInfo, ImportMode importMode, Artist artist, DownloadClientItem downloadClientItem) { if (Path.GetFileNameWithoutExtension(fileInfo.Name).StartsWith("._")) { @@ -254,7 +255,7 @@ namespace NzbDrone.Core.MediaFiles } } - var decisions = _importDecisionMaker.GetImportDecisions(new List() { fileInfo.FullName }, artist, null); + var decisions = _importDecisionMaker.GetImportDecisions(new List() { fileInfo }, artist, null); return _importApprovedTracks.Import(decisions, true, downloadClientItem, importMode); } diff --git a/src/NzbDrone.Core/MediaFiles/FilterFilesType.cs b/src/NzbDrone.Core/MediaFiles/FilterFilesType.cs new file mode 100644 index 000000000..76342b47f --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/FilterFilesType.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.MediaFiles +{ + public enum FilterFilesType + { + None, + Matched, + Known + } +} diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs index 9315b12cf..7b3e314cf 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Core.MediaFiles public void DeleteTrackFile(Artist artist, TrackFile trackFile) { - var fullPath = Path.Combine(artist.Path, trackFile.RelativePath); + var fullPath = trackFile.Path; var rootFolder = _diskProvider.GetParentFolder(artist.Path); if (!_diskProvider.FolderExists(rootFolder)) diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs index 6ef9319d6..ea5d19ecd 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Marr.Data.QGen; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; @@ -11,7 +12,8 @@ namespace NzbDrone.Core.MediaFiles List GetFilesByArtist(int artistId); List GetFilesByAlbum(int albumId); List GetFilesByRelease(int releaseId); - List GetFilesWithRelativePath(int artistId, string relativePath); + List GetFilesWithBasePath(string path); + TrackFile GetFileWithPath(string path); } @@ -55,15 +57,17 @@ namespace NzbDrone.Core.MediaFiles .Where(x => x.AlbumReleaseId == releaseId) .ToList(); } - - public List GetFilesWithRelativePath(int artistId, string relativePath) + public List GetFilesWithBasePath(string path) { return Query - .Join(JoinType.Inner, t => t.AlbumRelease, (t, r) => t.AlbumReleaseId == r.Id) - .Where(r => r.Monitored == true) - .AndWhere(t => t.Artist.Value.Id == artistId && t.RelativePath == relativePath) + .Where(x => x.Path.StartsWith(path)) .ToList(); } + + public TrackFile GetFileWithPath(string path) + { + return Query.Where(x => x.Path == path).SingleOrDefault(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs index 129a1bc39..4bada76e4 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Linq; using NLog; using NzbDrone.Core.MediaFiles.Events; @@ -8,6 +9,7 @@ using NzbDrone.Common; using NzbDrone.Core.Music; using System; using NzbDrone.Core.Music.Events; +using NzbDrone.Common.Extensions; namespace NzbDrone.Core.MediaFiles { @@ -21,10 +23,11 @@ namespace NzbDrone.Core.MediaFiles List GetFilesByArtist(int artistId); List GetFilesByAlbum(int albumId); List GetFilesByRelease(int releaseId); - List FilterExistingFiles(List files, Artist artist); + List FilterUnchangedFiles(List files, Artist artist, FilterFilesType filter); TrackFile Get(int id); List Get(IEnumerable ids); - List GetFilesWithRelativePath(int artistId, string relativePath); + List GetFilesWithBasePath(string path); + TrackFile GetFileWithPath(string path); void UpdateMediaInfo(List trackFiles); } @@ -70,19 +73,57 @@ namespace NzbDrone.Core.MediaFiles public void Delete(TrackFile trackFile, DeleteMediaFileReason reason) { - trackFile.Path = Path.Combine(trackFile.Artist.Value.Path, trackFile.RelativePath); - _mediaFileRepository.Delete(trackFile); - _eventAggregator.PublishEvent(new TrackFileDeletedEvent(trackFile, reason)); + // If the trackfile wasn't mapped to a track, don't publish an event + if (trackFile.AlbumId > 0) + { + _eventAggregator.PublishEvent(new TrackFileDeletedEvent(trackFile, reason)); + } } - public List FilterExistingFiles(List files, Artist artist) + public List FilterUnchangedFiles(List files, Artist artist, FilterFilesType filter) { - var artistFiles = GetFilesByArtist(artist.Id).Select(f => Path.Combine(artist.Path, f.RelativePath)).ToList(); + _logger.Debug($"Filtering {files.Count} files for unchanged files"); + + var knownFiles = GetFilesWithBasePath(artist.Path); + _logger.Trace($"Got {knownFiles.Count} existing files"); - if (!artistFiles.Any()) return files; + if (!knownFiles.Any()) return files; - return files.Except(artistFiles, PathEqualityComparer.Instance).ToList(); + var combined = files + .Join(knownFiles, + f => f.FullName, + af => af.Path, + (f, af) => new { DiskFile = f, DbFile = af}, + PathEqualityComparer.Instance) + .ToList(); + + List unwanted = null; + if (filter == FilterFilesType.Known) + { + unwanted = combined + .Where(x => x.DiskFile.Length == x.DbFile.Size && + Math.Abs((x.DiskFile.LastWriteTimeUtc - x.DbFile.Modified).TotalSeconds) <= 1) + .Select(x => x.DiskFile) + .ToList(); + _logger.Trace($"{unwanted.Count} unchanged existing files"); + } + else if (filter == FilterFilesType.Matched) + { + unwanted = combined + .Where(x => x.DiskFile.Length == x.DbFile.Size && + Math.Abs((x.DiskFile.LastWriteTimeUtc - x.DbFile.Modified).TotalSeconds) <= 1 && + (x.DbFile.Tracks == null || (x.DbFile.Tracks.IsLoaded && x.DbFile.Tracks.Value.Any()))) + .Select(x => x.DiskFile) + .ToList(); + _logger.Trace($"{unwanted.Count} unchanged and matched files"); + } + else + { + throw new ArgumentException("Unrecognised value of FilterFilesType filter"); + } + + return files.Except(unwanted).ToList(); } public TrackFile Get(int id) @@ -95,9 +136,14 @@ namespace NzbDrone.Core.MediaFiles return _mediaFileRepository.Get(ids).ToList(); } - public List GetFilesWithRelativePath(int artistId, string relativePath) + public List GetFilesWithBasePath(string path) + { + return _mediaFileRepository.GetFilesWithBasePath(path); + } + + public TrackFile GetFileWithPath(string path) { - return _mediaFileRepository.GetFilesWithRelativePath(artistId, relativePath); + return _mediaFileRepository.GetFileWithPath(path); } public void HandleAsync(AlbumDeletedEvent message) diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs index 93ee9bc5a..08bfc038d 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs @@ -39,7 +39,7 @@ namespace NzbDrone.Core.MediaFiles foreach (var artistFile in artistFiles) { var trackFile = artistFile; - var trackFilePath = Path.Combine(artist.Path, trackFile.RelativePath); + var trackFilePath = trackFile.Path; try { @@ -49,15 +49,7 @@ namespace NzbDrone.Core.MediaFiles _mediaFileService.Delete(artistFile, DeleteMediaFileReason.MissingFromDisk); continue; } - - if (tracks.None(e => e.TrackFileId == trackFile.Id)) - { - _logger.Debug("File [{0}] is not assigned to any artist, removing from db", trackFilePath); - _mediaFileService.Delete(trackFile, DeleteMediaFileReason.NoLinkedEpisodes); - continue; - } } - catch (Exception ex) { _logger.Error(ex, "Unable to cleanup TrackFile in DB: {0}", trackFile.Id); diff --git a/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs b/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs index 682b0468b..0e5fa572f 100644 --- a/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/RenameTrackFileService.cs @@ -84,7 +84,7 @@ namespace NzbDrone.Core.MediaFiles { var file = f; var tracksInFile = tracks.Where(e => e.TrackFileId == file.Id).ToList(); - var trackFilePath = Path.Combine(artist.Path, file.RelativePath); + var trackFilePath = file.Path; if (!tracksInFile.Any()) { @@ -105,7 +105,7 @@ namespace NzbDrone.Core.MediaFiles AlbumId = album.Id, TrackNumbers = tracksInFile.Select(e => e.AbsoluteTrackNumber).ToList(), TrackFileId = file.Id, - ExistingPath = file.RelativePath, + ExistingPath = artist.Path.GetRelativePath(file.Path), NewPath = artist.Path.GetRelativePath(newPath) }; } @@ -118,7 +118,7 @@ namespace NzbDrone.Core.MediaFiles foreach (var trackFile in trackFiles) { - var trackFilePath = Path.Combine(artist.Path, trackFile.RelativePath); + var trackFilePath = trackFile.Path; try { diff --git a/src/NzbDrone.Core/MediaFiles/TrackFile.cs b/src/NzbDrone.Core/MediaFiles/TrackFile.cs index 3d6df7a8e..f12f693a9 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackFile.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackFile.cs @@ -13,9 +13,9 @@ namespace NzbDrone.Core.MediaFiles public class TrackFile : ModelBase { // these are model properties - public string RelativePath { get; set; } public string Path { get; set; } public long Size { get; set; } + public DateTime Modified { get; set; } public DateTime DateAdded { get; set; } public string SceneName { get; set; } public string ReleaseGroup { get; set; } @@ -31,7 +31,7 @@ namespace NzbDrone.Core.MediaFiles public override string ToString() { - return string.Format("[{0}] {1}", Id, RelativePath); + return string.Format("[{0}] {1}", Id, Path); } public string GetSceneOrFileName() @@ -41,11 +41,6 @@ namespace NzbDrone.Core.MediaFiles return SceneName; } - if (RelativePath.IsNotNullOrWhiteSpace()) - { - return System.IO.Path.GetFileName(RelativePath); - } - if (Path.IsNotNullOrWhiteSpace()) { return System.IO.Path.GetFileName(Path); diff --git a/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs index 055a8a739..17e9d285f 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs @@ -66,7 +66,7 @@ namespace NzbDrone.Core.MediaFiles var tracks = _trackService.GetTracksByFileId(trackFile.Id); var album = _albumService.GetAlbum(trackFile.AlbumId); var newFileName = _buildFileNames.BuildTrackFileName(tracks, artist, album, trackFile); - var filePath = _buildFileNames.BuildTrackFilePath(artist, album, newFileName, Path.GetExtension(trackFile.RelativePath)); + var filePath = _buildFileNames.BuildTrackFilePath(artist, album, newFileName, Path.GetExtension(trackFile.Path)); EnsureTrackFolder(trackFile, artist, album, filePath); @@ -112,7 +112,7 @@ namespace NzbDrone.Core.MediaFiles Ensure.That(artist, () => artist).IsNotNull(); Ensure.That(destinationFilePath, () => destinationFilePath).IsValidPath(); - var trackFilePath = trackFile.Path ?? Path.Combine(artist.Path, trackFile.RelativePath); + var trackFilePath = trackFile.Path; if (!_diskProvider.FileExists(trackFilePath)) { @@ -126,7 +126,7 @@ namespace NzbDrone.Core.MediaFiles _diskTransferService.TransferFile(trackFilePath, destinationFilePath, mode); - trackFile.RelativePath = artist.Path.GetRelativePath(destinationFilePath); + trackFile.Path = destinationFilePath; _updateTrackFileService.ChangeFileDateForFile(trackFile, artist, tracks); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs index 08997ce80..e1225d352 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs @@ -177,9 +177,12 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification { var localTracks = trackfiles.Select(x => new LocalTrack { Path = x.Path, + Size = x.Size, + Modified = x.Modified, FileTrackInfo = _audioTagService.ReadTags(x.Path), ExistingFile = true, - AdditionalFile = true + AdditionalFile = true, + Quality = x.Quality }) .ToList(); @@ -335,15 +338,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification .Distinct() .ToDictionary(id => id, id => includeExisting ? _mediaFileService.GetFilesByAlbum(id) : new List()); - // populate the path. Artist will have been returned by mediaFileService - foreach (var trackfiles in albumTracks.Values) - { - foreach (var trackfile in trackfiles) - { - trackfile.Path = Path.Combine(trackfile.Artist.Value.Path, trackfile.RelativePath); - } - } - return releases.Select(x => new CandidateAlbumRelease { AlbumRelease = x, ExistingTracks = albumTracks[x.AlbumId] diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs index 27eadafe2..59cbe2bd7 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs @@ -92,12 +92,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport foreach (var previousFile in previousFiles) { - var trackFilePath = Path.Combine(artist.Path, previousFile.RelativePath); - var subfolder = rootFolder.GetRelativePath(_diskProvider.GetParentFolder(trackFilePath)); - if (_diskProvider.FileExists(trackFilePath)) + var subfolder = rootFolder.GetRelativePath(_diskProvider.GetParentFolder(previousFile.Path)); + if (_diskProvider.FileExists(previousFile.Path)) { _logger.Debug("Removing existing track file: {0}", previousFile); - _recycleBinProvider.DeleteFile(trackFilePath, subfolder); + _recycleBinProvider.DeleteFile(previousFile.Path, subfolder); } _mediaFileService.Delete(previousFile, DeleteMediaFileReason.Upgrade); } @@ -154,7 +153,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport var trackFile = new TrackFile { Path = localTrack.Path.CleanFilePath(), - Size = _diskProvider.GetFileSize(localTrack.Path), + Size = localTrack.Size, + Modified = localTrack.Modified, DateAdded = DateTime.UtcNow, ReleaseGroup = localTrack.ReleaseGroup, Quality = localTrack.Quality, @@ -190,12 +190,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport } else { - trackFile.RelativePath = localTrack.Artist.Path.GetRelativePath(trackFile.Path); - // Delete existing files from the DB mapped to this path - var previousFiles = _mediaFileService.GetFilesWithRelativePath(localTrack.Artist.Id, trackFile.RelativePath); + var previousFile = _mediaFileService.GetFileWithPath(trackFile.Path); - foreach (var previousFile in previousFiles) + if (previousFile != null) { _mediaFileService.Delete(previousFile, DeleteMediaFileReason.ManualOverride); } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs index fb8c0f830..f2e7c5ad7 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs @@ -11,14 +11,15 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Download; using NzbDrone.Core.MediaFiles.TrackImport.Aggregation; using NzbDrone.Core.MediaFiles.TrackImport.Identification; +using System.IO.Abstractions; namespace NzbDrone.Core.MediaFiles.TrackImport { public interface IMakeImportDecision { - List> GetImportDecisions(List musicFiles, Artist artist, bool includeExisting); - List> GetImportDecisions(List musicFiles, Artist artist, ParsedTrackInfo folderInfo); - List> GetImportDecisions(List musicFiles, Artist artist, Album album, AlbumRelease albumRelease, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, bool filterExistingFiles, bool newDownload, bool singleRelease, bool includeExisting); + List> GetImportDecisions(List musicFiles, Artist artist, FilterFilesType filter, bool includeExisting); + List> GetImportDecisions(List musicFiles, Artist artist, ParsedTrackInfo folderInfo); + List> GetImportDecisions(List musicFiles, Artist artist, Album album, AlbumRelease albumRelease, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, FilterFilesType filter, bool newDownload, bool singleRelease, bool includeExisting); } public class ImportDecisionMaker : IMakeImportDecision @@ -60,25 +61,33 @@ namespace NzbDrone.Core.MediaFiles.TrackImport _logger = logger; } - public List> GetImportDecisions(List musicFiles, Artist artist, bool includeExisting) + public List> GetImportDecisions(List musicFiles, Artist artist, FilterFilesType filter, bool includeExisting) { - return GetImportDecisions(musicFiles, artist, null, null, null, null, false, false, false, true); + return GetImportDecisions(musicFiles, artist, null, null, null, null, filter, false, false, true); } - public List> GetImportDecisions(List musicFiles, Artist artist, ParsedTrackInfo folderInfo) + public List> GetImportDecisions(List musicFiles, Artist artist, ParsedTrackInfo folderInfo) { - return GetImportDecisions(musicFiles, artist, null, null, null, folderInfo, false, true, false, false); + return GetImportDecisions(musicFiles, artist, null, null, null, folderInfo, FilterFilesType.None, true, false, false); } - public List> GetImportDecisions(List musicFiles, Artist artist, Album album, AlbumRelease albumRelease, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, bool filterExistingFiles, bool newDownload, bool singleRelease, bool includeExisting) + public List> GetImportDecisions(List musicFiles, Artist artist, Album album, AlbumRelease albumRelease, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, FilterFilesType filter, bool newDownload, bool singleRelease, bool includeExisting) { var watch = new System.Diagnostics.Stopwatch(); watch.Start(); - var files = filterExistingFiles && (artist != null) ? _mediaFileService.FilterExistingFiles(musicFiles, artist) : musicFiles; + var files = filter != FilterFilesType.None && (artist != null) ? _mediaFileService.FilterUnchangedFiles(musicFiles, artist, filter) : musicFiles; + + var localTracks = new List(); + var decisions = new List>(); _logger.Debug("Analyzing {0}/{1} files.", files.Count, musicFiles.Count); + if (!files.Any()) + { + return decisions; + } + ParsedAlbumInfo downloadClientItemInfo = null; if (downloadClientItem != null) @@ -86,9 +95,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport downloadClientItemInfo = Parser.Parser.ParseAlbumTitle(downloadClientItem.Title); } - var localTracks = new List(); - var decisions = new List>(); - foreach (var file in files) { var localTrack = new LocalTrack @@ -97,8 +103,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport Album = album, DownloadClientAlbumInfo = downloadClientItemInfo, FolderTrackInfo = folderInfo, - Path = file, - FileTrackInfo = _audioTagService.ReadTags(file), + Path = file.FullName, + Size = file.Length, + Modified = file.LastWriteTimeUtc, + FileTrackInfo = _audioTagService.ReadTags(file.FullName), ExistingFile = !newDownload, AdditionalFile = false }; diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index d9d5a5422..e24febb47 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Linq; using NLog; using NzbDrone.Common.Disk; @@ -20,7 +21,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual { public interface IManualImportService { - List GetMediaFiles(string path, string downloadId, bool filterExistingFiles, bool replaceExistingFiles); + List GetMediaFiles(string path, string downloadId, FilterFilesType filter, bool replaceExistingFiles); List UpdateItems(List item); } @@ -72,7 +73,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual _logger = logger; } - public List GetMediaFiles(string path, string downloadId, bool filterExistingFiles, bool replaceExistingFiles) + public List GetMediaFiles(string path, string downloadId, FilterFilesType filter, bool replaceExistingFiles) { if (downloadId.IsNotNullOrWhiteSpace()) { @@ -93,16 +94,16 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual return new List(); } - var decision = _importDecisionMaker.GetImportDecisions(new List { path }, null, null, null, null, null, false, true, false, !replaceExistingFiles); + var decision = _importDecisionMaker.GetImportDecisions(new List { _diskProvider.GetFileInfo(path) }, null, null, null, null, null, FilterFilesType.None, true, false, !replaceExistingFiles); var result = MapItem(decision.First(), Path.GetDirectoryName(path), downloadId, replaceExistingFiles, false); return new List { result }; } - return ProcessFolder(path, downloadId, filterExistingFiles, replaceExistingFiles); + return ProcessFolder(path, downloadId, filter, replaceExistingFiles); } - private List ProcessFolder(string folder, string downloadId, bool filterExistingFiles, bool replaceExistingFiles) + private List ProcessFolder(string folder, string downloadId, FilterFilesType filter, bool replaceExistingFiles) { var directoryInfo = new DirectoryInfo(folder); var artist = _parsingService.GetArtist(directoryInfo.Name); @@ -115,11 +116,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual var folderInfo = Parser.Parser.ParseMusicTitle(directoryInfo.Name); var artistFiles = _diskScanService.GetAudioFiles(folder).ToList(); - var decisions = _importDecisionMaker.GetImportDecisions(artistFiles, artist, null, null, null, folderInfo, filterExistingFiles, true, false, !replaceExistingFiles); + var decisions = _importDecisionMaker.GetImportDecisions(artistFiles, artist, null, null, null, folderInfo, filter, true, false, !replaceExistingFiles); // paths will be different for new and old files which is why we need to map separately var newFiles = artistFiles.Join(decisions, - f => f, + f => f.FullName, d => d.Item.Path, (f, d) => new { File = f, Decision = d }, PathEqualityComparer.Instance); @@ -145,7 +146,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual var disableReleaseSwitching = group.First().DisableReleaseSwitching; - var decisions = _importDecisionMaker.GetImportDecisions(group.Select(x => x.Path).ToList(), group.First().Artist, group.First().Album, group.First().Release, null, null, false, true, true, !replaceExistingFiles); + var decisions = _importDecisionMaker.GetImportDecisions(group.Select(x => _diskProvider.GetFileInfo(x.Path)).ToList(), group.First().Artist, group.First().Album, group.First().Release, null, null, FilterFilesType.None, true, true, !replaceExistingFiles); var existingItems = group.Join(decisions, i => i.Path, @@ -260,7 +261,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual ExistingFile = artist.Path.IsParentPath(file.Path), Tracks = tracks, FileTrackInfo = fileTrackInfo, - MediaInfo = fileTrackInfo.MediaInfo, Path = file.Path, Quality = file.Quality, Language = file.Language, @@ -305,7 +305,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath.FullPath)) { if (_downloadedTracksImportService.ShouldDeleteFolder( - new DirectoryInfo(trackedDownload.DownloadItem.OutputPath.FullPath), + _diskProvider.GetDirectoryInfo(trackedDownload.DownloadItem.OutputPath.FullPath), trackedDownload.RemoteAlbum.Artist) && trackedDownload.DownloadItem.CanMoveFiles) { _diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath.FullPath, true); diff --git a/src/NzbDrone.Core/MediaFiles/UpdateTrackFileService.cs b/src/NzbDrone.Core/MediaFiles/UpdateTrackFileService.cs index e7767aa3c..08bbe6de3 100644 --- a/src/NzbDrone.Core/MediaFiles/UpdateTrackFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpdateTrackFileService.cs @@ -49,7 +49,7 @@ namespace NzbDrone.Core.MediaFiles private bool ChangeFileDate(TrackFile trackFile, Artist artist, List tracks) { - var trackFilePath = Path.Combine(artist.Path, trackFile.RelativePath); + var trackFilePath = trackFile.Path; switch (_configService.FileDate) { diff --git a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs index 67e3f223b..ac7a010ea 100644 --- a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs @@ -57,7 +57,7 @@ namespace NzbDrone.Core.MediaFiles foreach (var existingFile in existingFiles) { var file = existingFile.First(); - var trackFilePath = Path.Combine(localTrack.Artist.Path, file.RelativePath); + var trackFilePath = file.Path; var subfolder = rootFolder.GetRelativePath(_diskProvider.GetParentFolder(trackFilePath)); if (_diskProvider.FileExists(trackFilePath)) diff --git a/src/NzbDrone.Core/Music/RefreshAlbumService.cs b/src/NzbDrone.Core/Music/RefreshAlbumService.cs index 3e0567498..2ca351cdd 100644 --- a/src/NzbDrone.Core/Music/RefreshAlbumService.cs +++ b/src/NzbDrone.Core/Music/RefreshAlbumService.cs @@ -17,8 +17,8 @@ namespace NzbDrone.Core.Music { public interface IRefreshAlbumService { - void RefreshAlbumInfo(Album album, bool forceUpdateFileTags); - void RefreshAlbumInfo(List albums, bool forceAlbumRefresh, bool forceUpdateFileTags); + bool RefreshAlbumInfo(Album album, bool forceUpdateFileTags); + bool RefreshAlbumInfo(List albums, bool forceAlbumRefresh, bool forceUpdateFileTags); } public class RefreshAlbumService : IRefreshAlbumService, IExecute @@ -57,20 +57,23 @@ namespace NzbDrone.Core.Music _logger = logger; } - public void RefreshAlbumInfo(List albums, bool forceAlbumRefresh, bool forceUpdateFileTags) + public bool RefreshAlbumInfo(List albums, bool forceAlbumRefresh, bool forceUpdateFileTags) { + bool updated = false; foreach (var album in albums) { if (forceAlbumRefresh || _checkIfAlbumShouldBeRefreshed.ShouldRefresh(album)) { - RefreshAlbumInfo(album, forceUpdateFileTags); + updated |= RefreshAlbumInfo(album, forceUpdateFileTags); } } + return updated; } - public void RefreshAlbumInfo(Album album, bool forceUpdateFileTags) + public bool RefreshAlbumInfo(Album album, bool forceUpdateFileTags) { _logger.ProgressInfo("Updating Info for {0}", album.Title); + bool updated = false; Tuple> tuple; @@ -81,14 +84,14 @@ namespace NzbDrone.Core.Music catch (AlbumNotFoundException) { _logger.Error($"{album} was not found, it may have been removed from Metadata sources."); - return; + return updated; } if (tuple.Item2.AlbumReleases.Value.Count == 0) { _logger.Debug($"{album} has no valid releases, removing."); _albumService.DeleteMany(new List { album }); - return; + return true; } var remoteMetadata = tuple.Item3.DistinctBy(x => x.ForeignArtistId).ToList(); @@ -124,6 +127,7 @@ namespace NzbDrone.Core.Music _artistMetadataRepository.InsertMany(addMetadataList); forceUpdateFileTags |= updateMetadataList.Any(); + updated |= updateMetadataList.Any() || addMetadataList.Any(); var albumInfo = tuple.Item2; @@ -137,6 +141,7 @@ namespace NzbDrone.Core.Music // the only thing written to tags from the album object is the title forceUpdateFileTags |= album.Title != (albumInfo.Title ?? "Unknown"); + updated |= forceUpdateFileTags; album.LastInfoSync = DateTime.UtcNow; album.CleanTitle = albumInfo.CleanTitle; @@ -229,11 +234,14 @@ namespace NzbDrone.Core.Music // if we have updated a monitored release, refresh all file tags forceUpdateFileTags |= updateReleaseList.Any(x => x.Monitored); + updated |= existingReleases.Any() || updateReleaseList.Any() || newReleaseList.Any(); - _refreshTrackService.RefreshTrackInfo(album, forceUpdateFileTags); + updated |= _refreshTrackService.RefreshTrackInfo(album, forceUpdateFileTags); _albumService.UpdateMany(new List{album}); _logger.Debug("Finished album refresh for {0}", album.Title); + + return updated; } public void Execute(RefreshAlbumCommand message) @@ -242,10 +250,12 @@ namespace NzbDrone.Core.Music { var album = _albumService.GetAlbum(message.AlbumId.Value); var artist = _artistService.GetArtistByMetadataId(album.ArtistMetadataId); - RefreshAlbumInfo(album, false); - _eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist)); + var updated = RefreshAlbumInfo(album, false); + if (updated) + { + _eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist)); + } } - } } } diff --git a/src/NzbDrone.Core/Music/RefreshArtistService.cs b/src/NzbDrone.Core/Music/RefreshArtistService.cs index 63360419a..0a96d4f78 100644 --- a/src/NzbDrone.Core/Music/RefreshArtistService.cs +++ b/src/NzbDrone.Core/Music/RefreshArtistService.cs @@ -63,9 +63,10 @@ namespace NzbDrone.Core.Music _logger = logger; } - private void RefreshArtistInfo(Artist artist, bool forceAlbumRefresh) + private bool RefreshArtistInfo(Artist artist, bool forceAlbumRefresh) { _logger.ProgressInfo("Updating Info for {0}", artist.Name); + bool updated = false; Artist artistInfo; @@ -76,10 +77,11 @@ namespace NzbDrone.Core.Music catch (ArtistNotFoundException) { _logger.Error($"Artist {artist} was not found, it may have been removed from Metadata sources."); - return; + return updated; } var forceUpdateFileTags = artist.Name != artistInfo.Name; + updated |= forceUpdateFileTags; if (artist.Metadata.Value.ForeignArtistId != artistInfo.Metadata.Value.ForeignArtistId) { @@ -96,6 +98,7 @@ namespace NzbDrone.Core.Music artist.Metadata.Value.ForeignArtistId = artistInfo.Metadata.Value.ForeignArtistId; forceUpdateFileTags = true; + updated = true; } artist.Metadata.Value.ApplyChanges(artistInfo.Metadata.Value); @@ -153,14 +156,23 @@ namespace NzbDrone.Core.Music newAlbumsList = UpdateAlbums(artist, newAlbumsList); _addAlbumService.AddAlbums(newAlbumsList); - _refreshAlbumService.RefreshAlbumInfo(updateAlbumsList, forceAlbumRefresh, forceUpdateFileTags); + updated |= existingAlbums.Any() || newAlbumsList.Any(); + + updated |= _refreshAlbumService.RefreshAlbumInfo(updateAlbumsList, forceAlbumRefresh, forceUpdateFileTags); // Do this last so artist only marked as refreshed if refresh of tracks / albums completed successfully _artistService.UpdateArtist(artist); _eventAggregator.PublishEvent(new AlbumInfoRefreshedEvent(artist, newAlbumsList, updateAlbumsList)); - _eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist)); + + if (updated) + { + _eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist)); + } + _logger.Debug("Finished artist refresh for {0}", artist.Name); + + return updated; } private List UpdateAlbums(Artist artist, List albumsToUpdate) @@ -174,26 +186,26 @@ namespace NzbDrone.Core.Music return albumsToUpdate; } - private void RescanArtist(Artist artist, bool isNew, CommandTrigger trigger) + private void RescanArtist(Artist artist, bool isNew, CommandTrigger trigger, bool infoUpdated) { var rescanAfterRefresh = _configService.RescanAfterRefresh; var shouldRescan = true; if (isNew) { - _logger.Trace("Forcing refresh of {0}. Reason: New artist", artist); + _logger.Trace("Forcing rescan of {0}. Reason: New artist", artist); shouldRescan = true; } else if (rescanAfterRefresh == RescanAfterRefreshType.Never) { - _logger.Trace("Skipping refresh of {0}. Reason: never recan after refresh", artist); + _logger.Trace("Skipping rescan of {0}. Reason: never recan after refresh", artist); shouldRescan = false; } else if (rescanAfterRefresh == RescanAfterRefreshType.AfterManual && trigger != CommandTrigger.Manual) { - _logger.Trace("Skipping refresh of {0}. Reason: not after automatic scans", artist); + _logger.Trace("Skipping rescan of {0}. Reason: not after automatic scans", artist); shouldRescan = false; } @@ -204,7 +216,10 @@ namespace NzbDrone.Core.Music try { - _diskScanService.Scan(artist); + // If some metadata has been updated then rescan unmatched files. + // Otherwise only scan files that haven't been seen before. + var filter = infoUpdated ? FilterFilesType.Matched : FilterFilesType.Known; + _diskScanService.Scan(artist, filter); } catch (Exception e) { @@ -221,16 +236,16 @@ namespace NzbDrone.Core.Music if (message.ArtistId.HasValue) { var artist = _artistService.GetArtist(message.ArtistId.Value); - + bool updated = false; try { - RefreshArtistInfo(artist, true); - RescanArtist(artist, isNew, trigger); + updated = RefreshArtistInfo(artist, true); + RescanArtist(artist, isNew, trigger, updated); } catch (Exception e) { _logger.Error(e, "Couldn't refresh info for {0}", artist); - RescanArtist(artist, isNew, trigger); + RescanArtist(artist, isNew, trigger, updated); throw; } } @@ -244,22 +259,23 @@ namespace NzbDrone.Core.Music if (manualTrigger || _checkIfArtistShouldBeRefreshed.ShouldRefresh(artist)) { + bool updated = false; try { - RefreshArtistInfo(artist, manualTrigger); + updated = RefreshArtistInfo(artist, manualTrigger); } catch (Exception e) { _logger.Error(e, "Couldn't refresh info for {0}", artist); } - RescanArtist(artist, false, trigger); + RescanArtist(artist, false, trigger, updated); } else { _logger.Info("Skipping refresh of artist: {0}", artist.Name); - RescanArtist(artist, false, trigger); + RescanArtist(artist, false, trigger, false); } } } diff --git a/src/NzbDrone.Core/Music/RefreshTrackService.cs b/src/NzbDrone.Core/Music/RefreshTrackService.cs index 8e6e46cfa..f193ea7b4 100644 --- a/src/NzbDrone.Core/Music/RefreshTrackService.cs +++ b/src/NzbDrone.Core/Music/RefreshTrackService.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Music { public interface IRefreshTrackService { - void RefreshTrackInfo(Album rg, bool forceUpdateFileTags); + bool RefreshTrackInfo(Album rg, bool forceUpdateFileTags); } public class RefreshTrackService : IRefreshTrackService @@ -37,11 +37,12 @@ namespace NzbDrone.Core.Music _logger = logger; } - public void RefreshTrackInfo(Album album, bool forceUpdateFileTags) + public bool RefreshTrackInfo(Album album, bool forceUpdateFileTags) { _logger.Info("Starting track info refresh for: {0}", album); var successCount = 0; var failCount = 0; + bool updated = false; foreach (var release in album.AlbumReleases.Value) { @@ -115,6 +116,8 @@ namespace NzbDrone.Core.Music _trackService.DeleteMany(existingTracks); _trackService.UpdateMany(updateList); _trackService.InsertMany(newList); + + updated |= existingTracks.Any() || updateList.Any() || newList.Any(); } if (failCount != 0) @@ -126,6 +129,8 @@ namespace NzbDrone.Core.Music { _logger.Info("Finished track refresh for album: {0}.", album); } + + return updated; } } } diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index e78f63811..a5b92e7c8 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -79,14 +79,12 @@ namespace NzbDrone.Core.Notifications.CustomScript if (message.TrackFiles.Any()) { - environmentVariables.Add("Lidarr_AddedTrackRelativePaths", string.Join("|", message.TrackFiles.Select(e => e.RelativePath))); - environmentVariables.Add("Lidarr_AddedTrackPaths", string.Join("|", message.TrackFiles.Select(e => Path.Combine(artist.Path, e.RelativePath)))); + environmentVariables.Add("Lidarr_AddedTrackPaths", string.Join("|", message.TrackFiles.Select(e => e.Path))); } if (message.OldFiles.Any()) { - environmentVariables.Add("Lidarr_DeletedRelativePaths", string.Join("|", message.OldFiles.Select(e => e.RelativePath))); - environmentVariables.Add("Lidarr_DeletedPaths", string.Join("|", message.OldFiles.Select(e => Path.Combine(artist.Path, e.RelativePath)))); + environmentVariables.Add("Lidarr_DeletedPaths", string.Join("|", message.OldFiles.Select(e => e.Path))); } ExecuteScript(environmentVariables); @@ -127,8 +125,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Lidarr_Album_ReleaseDate", album.ReleaseDate.ToString()); environmentVariables.Add("Lidarr_TrackFile_Id", trackFile.Id.ToString()); environmentVariables.Add("Lidarr_TrackFile_TrackCount", trackFile.Tracks.Value.Count.ToString()); - environmentVariables.Add("Lidarr_TrackFile_RelativePath", trackFile.RelativePath); - environmentVariables.Add("Lidarr_TrackFile_Path", Path.Combine(artist.Path, trackFile.RelativePath)); + environmentVariables.Add("Lidarr_TrackFile_Path", trackFile.Path); environmentVariables.Add("Lidarr_TrackFile_TrackNumbers", string.Join(",", trackFile.Tracks.Value.Select(e => e.TrackNumber))); environmentVariables.Add("Lidarr_TrackFile_TrackTitles", string.Join("|", trackFile.Tracks.Value.Select(e => e.Title))); environmentVariables.Add("Lidarr_TrackFile_Quality", trackFile.Quality.Quality.Name); diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index 68cee8c51..947521120 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -74,7 +74,7 @@ namespace NzbDrone.Core.Notifications private string GetTrackRetagMessage(Artist artist, TrackFile trackFile, Dictionary> diff) { return string.Format("{0}:\n{1}", - Path.Combine(artist.Path, trackFile.RelativePath), + trackFile.Path, string.Join("\n", diff.Select(x => $"{x.Key}: {FormatMissing(x.Value.Item1)} → {FormatMissing(x.Value.Item2)}"))); } diff --git a/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs index dd90807a7..e412d77af 100644 --- a/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs +++ b/src/NzbDrone.Core/Notifications/Synology/SynologyIndexer.cs @@ -26,14 +26,14 @@ namespace NzbDrone.Core.Notifications.Synology { foreach (var oldFile in message.OldFiles) { - var fullPath = Path.Combine(message.Artist.Path, oldFile.RelativePath); + var fullPath = oldFile.Path; _indexerProxy.DeleteFile(fullPath); } foreach (var newFile in message.TrackFiles) { - var fullPath = Path.Combine(message.Artist.Path, newFile.RelativePath); + var fullPath = newFile.Path; _indexerProxy.AddFile(fullPath); } diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookTrackFile.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookTrackFile.cs index a51bd520a..eeefbaf09 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookTrackFile.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookTrackFile.cs @@ -9,7 +9,6 @@ namespace NzbDrone.Core.Notifications.Webhook public WebhookTrackFile(TrackFile trackFile) { Id = trackFile.Id; - RelativePath = trackFile.RelativePath; Path = trackFile.Path; Quality = trackFile.Quality.Quality.Name; QualityVersion = trackFile.Quality.Revision.Version; @@ -18,7 +17,6 @@ namespace NzbDrone.Core.Notifications.Webhook } public int Id { get; set; } - public string RelativePath { get; set; } public string Path { get; set; } public string Quality { get; set; } public int QualityVersion { get; set; } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index c22855552..fe9e1c003 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -92,6 +92,9 @@ ..\packages\RestSharp.105.2.3\lib\net46\RestSharp.dll + + ..\packages\System.IO.Abstractions.4.0.11\lib\net40\System.IO.Abstractions.dll + @@ -204,6 +207,7 @@ + @@ -805,6 +809,7 @@ + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index f5b27ff4a..68e360832 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -549,12 +549,7 @@ namespace NzbDrone.Core.Organizer private string GetOriginalFileName(TrackFile trackFile) { - if (trackFile.RelativePath.IsNullOrWhiteSpace()) - { - return Path.GetFileNameWithoutExtension(trackFile.Path); - } - - return Path.GetFileNameWithoutExtension(trackFile.RelativePath); + return Path.GetFileNameWithoutExtension(trackFile.Path); } } diff --git a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs index 3fa99fbae..35a9c42a2 100644 --- a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs @@ -86,7 +86,7 @@ namespace NzbDrone.Core.Organizer _singleTrackFile = new TrackFile { Quality = new QualityModel(Quality.MP3_256, new Revision(2)), - RelativePath = "Artist.Name.Album.Name.TrackNum.Track.Title.MP3256.mp3", + Path = "/music/Artist.Name.Album.Name.TrackNum.Track.Title.MP3256.mp3", SceneName = "Artist.Name.Album.Name.TrackNum.Track.Title.MP3256", ReleaseGroup = "RlsGrp", MediaInfo = mediaInfo diff --git a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs index ab77faff0..50a4f205d 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs @@ -3,6 +3,7 @@ using NzbDrone.Core.Qualities; using System.Collections.Generic; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles.TrackImport.Identification; +using System; namespace NzbDrone.Core.Parser.Model { @@ -15,6 +16,7 @@ namespace NzbDrone.Core.Parser.Model public string Path { get; set; } public long Size { get; set; } + public DateTime Modified { get; set; } public ParsedTrackInfo FileTrackInfo { get; set; } public ParsedTrackInfo FolderTrackInfo { get; set; } public ParsedAlbumInfo DownloadClientAlbumInfo { get; set; } @@ -26,7 +28,6 @@ namespace NzbDrone.Core.Parser.Model public Distance Distance { get; set; } public QualityModel Quality { get; set; } public Language Language { get; set; } - public MediaInfoModel MediaInfo { get; set; } public bool ExistingFile { get; set; } public bool AdditionalFile { get; set; } public bool SceneSource { get; set; } diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index f8c658bbf..81927ae13 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -220,16 +220,13 @@ namespace NzbDrone.Core.Parser public Album GetLocalAlbum(string filename, Artist artist) { - if (Path.HasExtension(filename)) { filename = Path.GetDirectoryName(filename); } - filename = artist.Path.GetRelativePath(filename); - var tracksInAlbum = _mediaFileService.GetFilesByArtist(artist.Id) - .FindAll(s => Path.GetDirectoryName(s.RelativePath) == filename) + .FindAll(s => Path.GetDirectoryName(s.Path) == filename) .DistinctBy(s => s.AlbumId) .ToList(); diff --git a/src/NzbDrone.Core/packages.config b/src/NzbDrone.Core/packages.config index 2c5921b11..592928cbc 100644 --- a/src/NzbDrone.Core/packages.config +++ b/src/NzbDrone.Core/packages.config @@ -12,4 +12,5 @@ + diff --git a/src/NzbDrone.Mono.Test/DiskProviderTests/FreeSpaceFixture.cs b/src/NzbDrone.Mono.Test/DiskProviderTests/FreeSpaceFixture.cs index 25d3b419f..1f61f6c65 100644 --- a/src/NzbDrone.Mono.Test/DiskProviderTests/FreeSpaceFixture.cs +++ b/src/NzbDrone.Mono.Test/DiskProviderTests/FreeSpaceFixture.cs @@ -1,4 +1,7 @@ -using NUnit.Framework; +using System.Collections.Generic; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Disk; using NzbDrone.Common.Test.DiskTests; using NzbDrone.Mono.Disk; diff --git a/src/NzbDrone.Mono/Disk/DiskProvider.cs b/src/NzbDrone.Mono/Disk/DiskProvider.cs index 2f63b0dfa..2eb525966 100644 --- a/src/NzbDrone.Mono/Disk/DiskProvider.cs +++ b/src/NzbDrone.Mono/Disk/DiskProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Linq; using Mono.Unix; using Mono.Unix.Native; @@ -23,7 +24,16 @@ namespace NzbDrone.Mono.Disk // `unchecked((uint)-1)` and `uint.MaxValue` are the same thing. private const uint UNCHANGED_ID = uint.MaxValue; - public DiskProvider(IProcMountProvider procMountProvider, ISymbolicLinkResolver symLinkResolver) + public DiskProvider(IProcMountProvider procMountProvider, + ISymbolicLinkResolver symLinkResolver) + : this(new FileSystem(), procMountProvider, symLinkResolver) + { + } + + public DiskProvider(IFileSystem fileSystem, + IProcMountProvider procMountProvider, + ISymbolicLinkResolver symLinkResolver) + : base(fileSystem) { _procMountProvider = procMountProvider; _symLinkResolver = symLinkResolver; @@ -40,6 +50,8 @@ namespace NzbDrone.Mono.Disk { Ensure.That(path, () => path).IsValidPath(); + Logger.Debug($"path: {path}"); + var mount = GetMount(path); if (mount == null) @@ -57,9 +69,9 @@ namespace NzbDrone.Mono.Disk try { - var fs = File.GetAccessControl(filename); + var fs = _fileSystem.File.GetAccessControl(filename); fs.SetAccessRuleProtection(false, false); - File.SetAccessControl(filename, fs); + _fileSystem.File.SetAccessControl(filename, fs); } catch (NotImplementedException) { diff --git a/src/NzbDrone.Mono/NzbDrone.Mono.csproj b/src/NzbDrone.Mono/NzbDrone.Mono.csproj index 571047c04..4cc9b51b4 100644 --- a/src/NzbDrone.Mono/NzbDrone.Mono.csproj +++ b/src/NzbDrone.Mono/NzbDrone.Mono.csproj @@ -58,6 +58,9 @@ ..\packages\NLog.4.5.4\lib\net45\NLog.dll + + ..\packages\System.IO.Abstractions.4.0.11\lib\net40\System.IO.Abstractions.dll + @@ -102,4 +105,4 @@ --> - \ No newline at end of file + diff --git a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs index d8ef41b9a..f83e1d789 100644 --- a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs +++ b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; +using System.IO.Abstractions; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -9,6 +9,7 @@ using System.Runtime.CompilerServices; using Microsoft.Practices.Unity; using Moq; using Moq.Language.Flow; +using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Test.Common.AutoMoq.Unity; @@ -50,6 +51,15 @@ namespace NzbDrone.Test.Common.AutoMoq return result; } + public virtual T Resolve(string name, params ResolverOverride[] resolverOverrides) + { + ResolveType = typeof(T); + var result = _container.Resolve(name, resolverOverrides); + SetConstant(result); + ResolveType = null; + return result; + } + public virtual Mock GetMock() where T : class { return GetMock(DefaultBehavior); @@ -135,9 +145,9 @@ namespace NzbDrone.Test.Common.AutoMoq _container = container; container.RegisterInstance(this); - RegisterPlatformLibrary(container); - _registeredMocks = new Dictionary(); + + RegisterPlatformLibrary(container); AddTheAutoMockingContainerExtensionToTheContainer(container); } @@ -178,12 +188,15 @@ namespace NzbDrone.Test.Common.AutoMoq assemblyName = "Lidarr.Mono"; } - if (!File.Exists(assemblyName + ".dll")) - { - return; - } + var assembly = Assembly.Load(assemblyName); + + // This allows us to resolve the platform specific disk provider in FileSystemTest + var diskProvider = assembly.GetTypes().Where(x => x.Name == "DiskProvider").SingleOrDefault(); + container.RegisterType(typeof(IDiskProvider), diskProvider, "ActualDiskProvider"); - Assembly.Load(assemblyName); + // This tells the mocker to resolve IFileSystem using an actual filesystem (and not a mock) + // if not specified, giving the old behaviour before we switched to System.IO.Abstractions. + SetConstant(new FileSystem()); } #endregion diff --git a/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj b/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj index feddd4f24..ddadf11ef 100644 --- a/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj +++ b/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj @@ -73,6 +73,9 @@ ..\packages\RestSharp.105.2.3\lib\net46\RestSharp.dll + + ..\packages\System.IO.Abstractions.4.0.11\lib\net40\System.IO.Abstractions.dll + @@ -80,15 +83,6 @@ - - ..\packages\Unity.2.1.505.2\lib\NET35\Microsoft.Practices.Unity.dll - - - ..\packages\Unity.2.1.505.2\lib\NET35\Microsoft.Practices.Unity.Configuration.dll - - - ..\packages\Moq.4.0.10827\lib\NET40\Moq.dll - @@ -138,4 +132,4 @@ --> - \ No newline at end of file + diff --git a/src/NzbDrone.Test.Common/TestBase.cs b/src/NzbDrone.Test.Common/TestBase.cs index df23f75c6..eb9436991 100644 --- a/src/NzbDrone.Test.Common/TestBase.cs +++ b/src/NzbDrone.Test.Common/TestBase.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Test.Common private TSubject _subject; [SetUp] - public void CoreTestSetup() + public virtual void CoreTestSetup() { _subject = null; diff --git a/src/NzbDrone.Windows/Disk/DiskProvider.cs b/src/NzbDrone.Windows/Disk/DiskProvider.cs index 83ee14c50..92a2567a1 100644 --- a/src/NzbDrone.Windows/Disk/DiskProvider.cs +++ b/src/NzbDrone.Windows/Disk/DiskProvider.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.IO.Abstractions; using System.Runtime.InteropServices; using NLog; using NzbDrone.Common.Disk; @@ -23,6 +24,16 @@ namespace NzbDrone.Windows.Disk [return: MarshalAs(UnmanagedType.Bool)] static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes); + public DiskProvider() + : this(new FileSystem()) + { + } + + public DiskProvider(IFileSystem fileSystem) + : base(fileSystem) + { + } + public override long? GetAvailableSpace(string path) { Ensure.That(path, () => path).IsValidPath(); diff --git a/src/NzbDrone.Windows/NzbDrone.Windows.csproj b/src/NzbDrone.Windows/NzbDrone.Windows.csproj index c2f609ddf..effcb8fac 100644 --- a/src/NzbDrone.Windows/NzbDrone.Windows.csproj +++ b/src/NzbDrone.Windows/NzbDrone.Windows.csproj @@ -59,6 +59,9 @@ ..\packages\NLog.4.5.4\lib\net45\NLog.dll + + ..\packages\System.IO.Abstractions.4.0.11\lib\net40\System.IO.Abstractions.dll + @@ -91,4 +94,4 @@ --> - \ No newline at end of file +