From ff4a550cbbea4f3201895862076680a0ee5ae0dd Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 6 Dec 2018 20:24:11 -0800 Subject: [PATCH] New: Include OriginalFilePath with Episode Files Closes #2336 --- .../EpisodeFiles/EpisodeFileResource.cs | 6 +- .../Extensions/PathExtensions.cs | 18 ++++ .../ImportApprovedEpisodesFixture.cs | 92 +++++++++++++++++-- ..._relative_original_path_to_episode_file.cs | 14 +++ src/NzbDrone.Core/MediaFiles/EpisodeFile.cs | 3 +- .../EpisodeImport/ImportApprovedEpisodes.cs | 32 +++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + 7 files changed, 157 insertions(+), 9 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/129_add_relative_original_path_to_episode_file.cs diff --git a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs index 55e01c41f..ed85d7119 100644 --- a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs +++ b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Api.EpisodeFiles public string SceneName { get; set; } public QualityModel Quality { get; set; } public MediaInfoResource MediaInfo { get; set; } + public string OriginalFilePath { get; set; } public bool QualityCutoffNotMet { get; set; } } @@ -38,8 +39,8 @@ namespace NzbDrone.Api.EpisodeFiles DateAdded = model.DateAdded, SceneName = model.SceneName, Quality = model.Quality, - MediaInfo = model.MediaInfo.ToResource(model.SceneName) - //QualityCutoffNotMet + MediaInfo = model.MediaInfo.ToResource(model.SceneName), + OriginalFilePath = model.OriginalFilePath }; } @@ -61,6 +62,7 @@ namespace NzbDrone.Api.EpisodeFiles Quality = model.Quality, QualityCutoffNotMet = qualityUpgradableSpecification.CutoffNotMet(series.Profile.Value, model.Quality), MediaInfo = model.MediaInfo.ToResource(model.SceneName), + OriginalFilePath = model.OriginalFilePath }; } } diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 14da5fd51..c7fc857e8 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -191,6 +191,24 @@ namespace NzbDrone.Common.Extensions return directories; } + public static string GetAncestorPath(this string path, string ancestorName) + { + var parent = Path.GetDirectoryName(path); + + while (parent != null) + { + var currentPath = parent; + parent = Path.GetDirectoryName(parent); + + if (Path.GetFileName(currentPath) == ancestorName) + { + return currentPath; + } + } + + return null; + } + public static string GetAppDataPath(this IAppFolderInfo appFolderInfo) { return appFolderInfo.AppDataFolder; diff --git a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs index 4b2bd7745..3c510140c 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs @@ -5,6 +5,7 @@ using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Common.Disk; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.MediaFiles; @@ -34,6 +35,8 @@ namespace NzbDrone.Core.Test.MediaFiles _rejectedDecisions = new List(); _approvedDecisions = new List(); + var outputPath = @"C:\Test\Unsorted\TV\30.Rock.S01E01".AsOsAgnostic(); + var series = Builder.CreateNew() .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .With(s => s.Path = @"C:\Test\TV\30 Rock".AsOsAgnostic()) @@ -66,7 +69,14 @@ namespace NzbDrone.Core.Test.MediaFiles .Setup(s => s.UpgradeEpisodeFile(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new EpisodeFileMoveResult()); - _downloadClientItem = Builder.CreateNew().Build(); + _downloadClientItem = Builder.CreateNew() + .With(d => d.OutputPath = new OsPath(outputPath)) + .Build(); + } + + private void GivenNewDownload() + { + _approvedDecisions.ForEach(a => a.LocalEpisode.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), Path.GetFileName(a.LocalEpisode.Path))); } [Test] @@ -140,6 +150,7 @@ namespace NzbDrone.Core.Test.MediaFiles [Test] public void should_use_nzb_title_as_scene_name() { + GivenNewDownload(); _downloadClientItem.Title = "malcolm.in.the.middle.s02e05.dvdrip.xvid-ingot"; Subject.Import(new List { _approvedDecisions.First() }, true, _downloadClientItem); @@ -152,6 +163,7 @@ namespace NzbDrone.Core.Test.MediaFiles [TestCase(".nzb")] public void should_remove_extension_from_nzb_title_for_scene_name(string extension) { + GivenNewDownload(); var title = "malcolm.in.the.middle.s02e05.dvdrip.xvid-ingot"; _downloadClientItem.Title = title + extension; @@ -164,7 +176,8 @@ namespace NzbDrone.Core.Test.MediaFiles [Test] public void should_not_use_nzb_title_as_scene_name_if_full_season() { - _approvedDecisions.First().LocalEpisode.Path = "c:\\tv\\season1\\malcolm.in.the.middle.s02e23.dvdrip.xvid-ingot.mkv".AsOsAgnostic(); + GivenNewDownload(); + _approvedDecisions.First().LocalEpisode.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), "malcolm.in.the.middle.s02e23.dvdrip.xvid-ingot.mkv"); _downloadClientItem.Title = "malcolm.in.the.middle.s02.dvdrip.xvid-ingot"; Subject.Import(new List { _approvedDecisions.First() }, true, _downloadClientItem); @@ -175,7 +188,8 @@ namespace NzbDrone.Core.Test.MediaFiles [Test] public void should_use_file_name_as_scenename_only_if_it_looks_like_scenename() { - _approvedDecisions.First().LocalEpisode.Path = "c:\\tv\\malcolm.in.the.middle.s02e23.dvdrip.xvid-ingot.mkv".AsOsAgnostic(); + GivenNewDownload(); + _approvedDecisions.First().LocalEpisode.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), "malcolm.in.the.middle.s02e23.dvdrip.xvid-ingot.mkv"); Subject.Import(new List { _approvedDecisions.First() }, true); @@ -185,7 +199,8 @@ namespace NzbDrone.Core.Test.MediaFiles [Test] public void should_not_use_file_name_as_scenename_if_it_doesnt_looks_like_scenename() { - _approvedDecisions.First().LocalEpisode.Path = "c:\\tv\\aaaaa.mkv".AsOsAgnostic(); + GivenNewDownload(); + _approvedDecisions.First().LocalEpisode.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), "aaaaa.mkv"); Subject.Import(new List { _approvedDecisions.First() }, true); @@ -223,7 +238,11 @@ namespace NzbDrone.Core.Test.MediaFiles [Test] public void should_copy_when_cannot_move_files_downloads() { - Subject.Import(new List { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "30.Rock.S01E01", CanMoveFiles = false}); + GivenNewDownload(); + _downloadClientItem.Title = "30.Rock.S01E01"; + _downloadClientItem.CanMoveFiles = false; + + Subject.Import(new List { _approvedDecisions.First() }, true, _downloadClientItem); Mocker.GetMock() .Verify(v => v.UpgradeEpisodeFile(It.IsAny(), _approvedDecisions.First().LocalEpisode, true), Times.Once()); @@ -232,10 +251,71 @@ namespace NzbDrone.Core.Test.MediaFiles [Test] public void should_use_override_importmode() { - Subject.Import(new List { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "30.Rock.S01E01", CanMoveFiles = false }, ImportMode.Move); + GivenNewDownload(); + _downloadClientItem.Title = "30.Rock.S01E01"; + _downloadClientItem.CanMoveFiles = false; + + Subject.Import(new List { _approvedDecisions.First() }, true, _downloadClientItem, ImportMode.Move); Mocker.GetMock() .Verify(v => v.UpgradeEpisodeFile(It.IsAny(), _approvedDecisions.First().LocalEpisode, false), Times.Once()); } + + [Test] + public void should_use_file_name_only_for_download_client_item_without_a_job_folder() + { + var fileName = "Series.Title.S01E01.720p.HDTV.x264-Sonarr.mkv"; + var path = Path.Combine(@"C:\Test\Unsorted\TV\".AsOsAgnostic(), fileName); + + _downloadClientItem.OutputPath = new OsPath(path); + _approvedDecisions.First().LocalEpisode.Path = path; + + Subject.Import(new List { _approvedDecisions.First() }, true, _downloadClientItem); + + Mocker.GetMock().Verify(v => v.Add(It.Is(c => c.OriginalFilePath == fileName))); + } + + [Test] + public void should_use_folder_and_file_name_only_for_download_client_item_with_a_job_folder() + { + var name = "Series.Title.S01E01.720p.HDTV.x264-Sonarr"; + var outputPath = Path.Combine(@"C:\Test\Unsorted\TV\".AsOsAgnostic(), name); + + _downloadClientItem.OutputPath = new OsPath(outputPath); + _approvedDecisions.First().LocalEpisode.Path = Path.Combine(outputPath, name + ".mkv"); + + Subject.Import(new List { _approvedDecisions.First() }, true, _downloadClientItem); + + Mocker.GetMock().Verify(v => v.Add(It.Is(c => c.OriginalFilePath == $"{name}\\{name}.mkv".AsOsAgnostic()))); + } + + [Test] + public void should_include_intermediate_folders_for_download_client_item_with_a_job_folder() + { + var name = "Series.Title.S01E01.720p.HDTV.x264-Sonarr"; + var outputPath = Path.Combine(@"C:\Test\Unsorted\TV\".AsOsAgnostic(), name); + + _downloadClientItem.OutputPath = new OsPath(outputPath); + _approvedDecisions.First().LocalEpisode.Path = Path.Combine(outputPath, "subfolder", name + ".mkv"); + + Subject.Import(new List { _approvedDecisions.First() }, true, _downloadClientItem); + + Mocker.GetMock().Verify(v => v.Add(It.Is(c => c.OriginalFilePath == $"{name}\\subfolder\\{name}.mkv".AsOsAgnostic()))); + } + + [Test] + public void should_use_folder_info_release_title_to_find_relative_path() + { + var name = "Series.Title.S01E01.720p.HDTV.x264-Sonarr"; + var outputPath = Path.Combine(@"C:\Test\Unsorted\TV\".AsOsAgnostic(), name); + var localEpisode = _approvedDecisions.First().LocalEpisode; + + localEpisode.FolderEpisodeInfo = new ParsedEpisodeInfo { ReleaseTitle = name }; + localEpisode.Path = Path.Combine(outputPath, "subfolder", name + ".mkv"); + + Subject.Import(new List { _approvedDecisions.First() }, true, null); + + Mocker.GetMock().Verify(v => v.Add(It.Is(c => c.OriginalFilePath == $"{name}\\subfolder\\{name}.mkv".AsOsAgnostic()))); + } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/129_add_relative_original_path_to_episode_file.cs b/src/NzbDrone.Core/Datastore/Migration/129_add_relative_original_path_to_episode_file.cs new file mode 100644 index 000000000..6537f6b93 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/129_add_relative_original_path_to_episode_file.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(129)] + public class add_relative_original_path_to_episode_file : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("EpisodeFiles").AddColumn("OriginalFilePath").AsString().Nullable(); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs index 267d5aa58..58dd7605b 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Marr.Data; using NzbDrone.Core.Datastore; @@ -17,6 +17,7 @@ namespace NzbDrone.Core.MediaFiles public string Path { get; set; } public long Size { get; set; } public DateTime DateAdded { get; set; } + public string OriginalFilePath { get; set; } public string SceneName { get; set; } public string ReleaseGroup { get; set; } public QualityModel Quality { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index c894daaab..fb88ad8bf 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -102,6 +102,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport if (newDownload) { + episodeFile.OriginalFilePath = GetOriginalFilePath(downloadClientItem, localEpisode); episodeFile.SceneName = GetSceneName(downloadClientItem, localEpisode); var moveResult = _episodeFileUpgrader.UpgradeEpisodeFile(episodeFile, localEpisode, copyOnly); @@ -148,6 +149,37 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return importResults; } + private string GetOriginalFilePath(DownloadClientItem downloadClientItem, LocalEpisode localEpisode) + { + if (downloadClientItem != null) + { + return downloadClientItem.OutputPath.Directory.ToString().GetRelativePath(localEpisode.Path); + } + + var path = localEpisode.Path; + var folderEpisodeInfo = localEpisode.FolderEpisodeInfo; + + if (folderEpisodeInfo != null) + { + var folderPath = path.GetAncestorPath(folderEpisodeInfo.ReleaseTitle); + + if (folderPath != null) + { + return folderPath.GetParentPath().GetRelativePath(path); + } + } + + var parentPath = path.GetParentPath(); + var grandparentPath = parentPath.GetParentPath(); + + if (grandparentPath != null) + { + return grandparentPath.GetRelativePath(path); + } + + return Path.Combine(Path.GetFileName(parentPath), Path.GetFileName(path)); + } + private string GetSceneName(DownloadClientItem downloadClientItem, LocalEpisode localEpisode) { if (downloadClientItem != null) diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index b1b48df42..b7451a357 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -143,6 +143,7 @@ +