diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 000000000..97626ba45 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml index f5ca71492..b8387eb1b 100644 --- a/.idea/jsLibraryMappings.xml +++ b/.idea/jsLibraryMappings.xml @@ -1,7 +1,6 @@ - \ No newline at end of file diff --git a/src/NzbDrone.Api/Config/MediaManagementConfigModule.cs b/src/NzbDrone.Api/Config/MediaManagementConfigModule.cs index e8d3b9eb4..8b35e53ed 100644 --- a/src/NzbDrone.Api/Config/MediaManagementConfigModule.cs +++ b/src/NzbDrone.Api/Config/MediaManagementConfigModule.cs @@ -1,5 +1,4 @@ -using System; -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Configuration; using NzbDrone.Core.Validation.Paths; diff --git a/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs b/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs index f18c1f731..a51e8b4d3 100644 --- a/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs +++ b/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs @@ -1,5 +1,4 @@ -using System; -using NzbDrone.Api.REST; +using NzbDrone.Api.REST; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles; @@ -21,6 +20,7 @@ namespace NzbDrone.Api.Config public bool SkipFreeSpaceCheckWhenImporting { get; set; } public bool CopyUsingHardlinks { get; set; } + public string ExtraFileExtensions { get; set; } public bool EnableMediaInfo { get; set; } } diff --git a/src/NzbDrone.Api/Metadata/MetadataModule.cs b/src/NzbDrone.Api/Metadata/MetadataModule.cs index 77c828093..7b4607abe 100644 --- a/src/NzbDrone.Api/Metadata/MetadataModule.cs +++ b/src/NzbDrone.Api/Metadata/MetadataModule.cs @@ -1,5 +1,5 @@ using System; -using NzbDrone.Core.Metadata; +using NzbDrone.Core.Extras.Metadata; namespace NzbDrone.Api.Metadata { diff --git a/src/NzbDrone.Common.Test/ExtensionTests/IEnumerableExtensionTests/ExceptByFixture.cs b/src/NzbDrone.Common.Test/ExtensionTests/IEnumerableExtensionTests/ExceptByFixture.cs new file mode 100644 index 000000000..1688fd8c4 --- /dev/null +++ b/src/NzbDrone.Common.Test/ExtensionTests/IEnumerableExtensionTests/ExceptByFixture.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Common.Test.ExtensionTests.IEnumerableExtensionTests +{ + [TestFixture] + public class ExceptByFixture + { + public class Object1 + { + public string Prop1 { get; set; } + } + + public class Object2 + { + public string Prop1 { get; set; } + } + + [Test] + public void should_return_empty_when_object_with_property_exists_in_both_lists() + { + var first = new List + { + new Object1 { Prop1 = "one" }, + new Object1 { Prop1 = "two" } + }; + + var second = new List + { + new Object1 { Prop1 = "two" }, + new Object1 { Prop1 = "one" } + }; + + first.ExceptBy(o => o.Prop1, second, o => o.Prop1, StringComparer.InvariantCultureIgnoreCase).Should().BeEmpty(); + } + + [Test] + public void should_return_objects_that_do_not_have_a_match_in_the_second_list() + { + var first = new List + { + new Object1 { Prop1 = "one" }, + new Object1 { Prop1 = "two" } + }; + + var second = new List + { + new Object1 { Prop1 = "one" }, + new Object1 { Prop1 = "four" } + }; + + var result = first.ExceptBy(o => o.Prop1, second, o => o.Prop1, StringComparer.InvariantCultureIgnoreCase).ToList(); + + result.Should().HaveCount(1); + result.First().GetType().Should().Be(typeof (Object1)); + result.First().Prop1.Should().Be("two"); + } + } +} diff --git a/src/NzbDrone.Common.Test/ExtensionTests/IEnumerableExtensionTests/IntersectByFixture.cs b/src/NzbDrone.Common.Test/ExtensionTests/IEnumerableExtensionTests/IntersectByFixture.cs new file mode 100644 index 000000000..f88b9ed40 --- /dev/null +++ b/src/NzbDrone.Common.Test/ExtensionTests/IEnumerableExtensionTests/IntersectByFixture.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Common.Test.ExtensionTests.IEnumerableExtensionTests +{ + [TestFixture] + public class IntersectByFixture + { + public class Object1 + { + public string Prop1 { get; set; } + } + + public class Object2 + { + public string Prop1 { get; set; } + } + + [Test] + public void should_return_empty_when_no_intersections() + { + var first = new List + { + new Object1 { Prop1 = "one" }, + new Object1 { Prop1 = "two" } + }; + + var second = new List + { + new Object1 { Prop1 = "three" }, + new Object1 { Prop1 = "four" } + }; + + first.IntersectBy(o => o.Prop1, second, o => o.Prop1, StringComparer.InvariantCultureIgnoreCase).Should().BeEmpty(); + } + + [Test] + public void should_return_objects_with_intersecting_values() + { + var first = new List + { + new Object1 { Prop1 = "one" }, + new Object1 { Prop1 = "two" } + }; + + var second = new List + { + new Object1 { Prop1 = "one" }, + new Object1 { Prop1 = "four" } + }; + + var result = first.IntersectBy(o => o.Prop1, second, o => o.Prop1, StringComparer.InvariantCultureIgnoreCase).ToList(); + + result.Should().HaveCount(1); + result.First().Prop1.Should().Be("one"); + } + } +} diff --git a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj index 867f6adaf..ec929d47b 100644 --- a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj +++ b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj @@ -80,6 +80,8 @@ + + diff --git a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs index 1593c69b9..a1beecaa9 100644 --- a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs +++ b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs @@ -13,6 +13,44 @@ namespace NzbDrone.Common.Extensions return source.Where(element => knownKeys.Add(keySelector(element))); } + public static IEnumerable IntersectBy(this IEnumerable first, Func firstKeySelector, + IEnumerable second, Func secondKeySelector, + IEqualityComparer keyComparer) + { + var keys = new HashSet(second.Select(secondKeySelector), keyComparer); + + foreach (var element in first) + { + var key = firstKeySelector(element); + + // Remove the key so we only yield once + if (keys.Remove(key)) + { + yield return element; + } + } + } + + public static IEnumerable ExceptBy(this IEnumerable first, Func firstKeySelector, + IEnumerable second, Func secondKeySelector, + IEqualityComparer keyComparer) + { + var keys = new HashSet(second.Select(secondKeySelector), keyComparer); + var matchedKeys = new HashSet(); + + foreach (var element in first) + { + var key = firstKeySelector(element); + + if (!keys.Contains(key) && !matchedKeys.Contains(key)) + { + // Store the key so we only yield once + matchedKeys.Add(key); + yield return element; + } + } + } + public static void AddIfNotNull(this List source, TSource item) { if (item == null) diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 5d7ff3e19..c37243439 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -37,6 +37,11 @@ namespace NzbDrone.Common.Extensions return info.FullName.TrimEnd('/').Trim('\\', ' '); } + public static bool PathNotEquals(this string firstPath, string secondPath, StringComparison? comparison = null) + { + return !PathEquals(firstPath, secondPath, comparison); + } + public static bool PathEquals(this string firstPath, string secondPath, StringComparison? comparison = null) { if (!comparison.HasValue) diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/099_extra_and_subtitle_filesFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/099_extra_and_subtitle_filesFixture.cs new file mode 100644 index 000000000..43396618d --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/099_extra_and_subtitle_filesFixture.cs @@ -0,0 +1,33 @@ +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class metadata_files_extensionFixture : MigrationTest + { + [Test] + public void should_set_extension_using_relative_path() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("MetadataFiles").Row(new + { + SeriesId = 1, + RelativePath = "banner.jpg", + LastUpdated = "2016-05-30 20:23:02.3725923", + Type = 3, + Consumer = "XbmcMetadata" + }); + }); + + var items = db.Query("SELECT * FROM MetadataFiles"); + + items.Should().HaveCount(1); + items.First().Extension.Should().Be(".jpg"); + } + } +} diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/DeleteBadMediaCovers.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/DeleteBadMediaCovers.cs index 8dfd295cf..b9d69fc61 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/DeleteBadMediaCovers.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/DeleteBadMediaCovers.cs @@ -7,9 +7,11 @@ using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Extras.Metadata; +using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Housekeeping.Housekeepers; -using NzbDrone.Core.Metadata; -using NzbDrone.Core.Metadata.Files; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; @@ -19,7 +21,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks [TestFixture] public class DeleteBadMediaCoversFixture : CoreTest { - private List _metaData; + private List _metadata; private List _series; [SetUp] @@ -31,7 +33,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks .Build().ToList(); - _metaData = Builder.CreateListOfSize(1) + _metadata = Builder.CreateListOfSize(1) .Build().ToList(); Mocker.GetMock() @@ -41,7 +43,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks Mocker.GetMock() .Setup(c => c.GetFilesBySeries(_series.First().Id)) - .Returns(_metaData); + .Returns(_metadata); Mocker.GetMock().SetupGet(c => c.CleanupMetadataImages).Returns(true); @@ -51,8 +53,8 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks [Test] public void should_not_process_non_image_files() { - _metaData.First().RelativePath = "season\\file.xml".AsOsAgnostic(); - _metaData.First().Type = MetadataType.EpisodeMetadata; + _metadata.First().RelativePath = "season\\file.xml".AsOsAgnostic(); + _metadata.First().Type = MetadataType.EpisodeMetadata; Subject.Clean(); @@ -63,7 +65,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks [Test] public void should_not_process_images_before_tvdb_switch() { - _metaData.First().LastUpdated = new DateTime(2014, 12, 25); + _metadata.First().LastUpdated = new DateTime(2014, 12, 25); Subject.Clean(); @@ -89,7 +91,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks [Test] public void should_set_clean_flag_to_false() { - _metaData.First().LastUpdated = new DateTime(2014, 12, 25); + _metadata.First().LastUpdated = new DateTime(2014, 12, 25); Subject.Clean(); @@ -102,9 +104,9 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks { var imagePath = "C:\\TV\\Season\\image.jpg".AsOsAgnostic(); - _metaData.First().LastUpdated = new DateTime(2014, 12, 29); - _metaData.First().RelativePath = "Season\\image.jpg".AsOsAgnostic(); - _metaData.First().Type = MetadataType.SeriesImage; + _metadata.First().LastUpdated = new DateTime(2014, 12, 29); + _metadata.First().RelativePath = "Season\\image.jpg".AsOsAgnostic(); + _metadata.First().Type = MetadataType.SeriesImage; Mocker.GetMock() .Setup(c => c.OpenReadStream(imagePath)) @@ -115,7 +117,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks Mocker.GetMock().Verify(c => c.DeleteFile(imagePath), Times.Once()); - Mocker.GetMock().Verify(c => c.Delete(_metaData.First().Id), Times.Once()); + Mocker.GetMock().Verify(c => c.Delete(_metadata.First().Id), Times.Once()); } @@ -124,9 +126,9 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks { var imagePath = "C:\\TV\\Season\\image.jpg".AsOsAgnostic(); - _metaData.First().LastUpdated = new DateTime(2014, 12, 29); - _metaData.First().Type = MetadataType.SeasonImage; - _metaData.First().RelativePath = "Season\\image.jpg".AsOsAgnostic(); + _metadata.First().LastUpdated = new DateTime(2014, 12, 29); + _metadata.First().Type = MetadataType.SeasonImage; + _metadata.First().RelativePath = "Season\\image.jpg".AsOsAgnostic(); Mocker.GetMock() .Setup(c => c.OpenReadStream(imagePath)) @@ -136,7 +138,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks Subject.Clean(); Mocker.GetMock().Verify(c => c.DeleteFile(imagePath), Times.Once()); - Mocker.GetMock().Verify(c => c.Delete(_metaData.First().Id), Times.Once()); + Mocker.GetMock().Verify(c => c.Delete(_metadata.First().Id), Times.Once()); } @@ -145,8 +147,8 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks { var imagePath = "C:\\TV\\Season\\image.jpg".AsOsAgnostic(); - _metaData.First().LastUpdated = new DateTime(2014, 12, 29); - _metaData.First().RelativePath = "Season\\image.jpg".AsOsAgnostic(); + _metadata.First().LastUpdated = new DateTime(2014, 12, 29); + _metadata.First().RelativePath = "Season\\image.jpg".AsOsAgnostic(); Mocker.GetMock() .Setup(c => c.OpenReadStream(imagePath)) diff --git a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs index 004863cdf..c2d436ec8 100644 --- a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs +++ b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs @@ -81,7 +81,7 @@ namespace NzbDrone.Core.Test.HistoryTests Path = @"C:\Test\Unsorted\Series.s01e01.mkv" }; - Subject.Handle(new EpisodeImportedEvent(localEpisode, episodeFile, true, "sab","abcd")); + Subject.Handle(new EpisodeImportedEvent(localEpisode, episodeFile, true, "sab", "abcd", true)); Mocker.GetMock() .Verify(v => v.Insert(It.Is(h => h.SourceTitle == Path.GetFileNameWithoutExtension(localEpisode.Path)))); diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupAbsolutePathMetadataFilesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupAbsolutePathMetadataFilesFixture.cs index a17e16e1d..65b05f32d 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupAbsolutePathMetadataFilesFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupAbsolutePathMetadataFilesFixture.cs @@ -2,8 +2,8 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Housekeeping.Housekeepers; -using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common; diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDuplicateMetadataFilesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDuplicateMetadataFilesFixture.cs index 99bcb8fd6..5bfeaefc0 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDuplicateMetadataFilesFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDuplicateMetadataFilesFixture.cs @@ -1,9 +1,9 @@ using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; +using NzbDrone.Core.Extras.Metadata; +using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Housekeeping.Housekeepers; -using NzbDrone.Core.Metadata; -using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.Housekeeping.Housekeepers @@ -58,7 +58,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers public void should_not_delete_metadata_files_when_there_is_only_one_for_that_series_and_consumer() { var file = Builder.CreateNew() - .BuildNew(); + .BuildNew(); Db.Insert(file); Subject.Clean(); diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs index 7e4dc060f..27679d8d3 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs @@ -1,10 +1,10 @@ using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; +using NzbDrone.Core.Extras.Metadata; +using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Housekeeping.Housekeepers; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Metadata; -using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -94,10 +94,10 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers Db.Insert(series); var metadataFile = Builder.CreateNew() - .With(m => m.SeriesId = series.Id) - .With(m => m.Type = MetadataType.EpisodeMetadata) - .With(m => m.EpisodeFileId = 0) - .BuildNew(); + .With(m => m.SeriesId = series.Id) + .With(m => m.Type = MetadataType.EpisodeMetadata) + .With(m => m.EpisodeFileId = 0) + .BuildNew(); Db.Insert(metadataFile); Subject.Clean(); diff --git a/src/NzbDrone.Core.Test/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs b/src/NzbDrone.Core.Test/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs index 06cf6fb51..6d4328b32 100644 --- a/src/NzbDrone.Core.Test/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs +++ b/src/NzbDrone.Core.Test/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs @@ -2,8 +2,8 @@ using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; -using NzbDrone.Core.Metadata; -using NzbDrone.Core.Metadata.Consumers.Roksbox; +using NzbDrone.Core.Extras.Metadata; +using NzbDrone.Core.Extras.Metadata.Consumers.Roksbox; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; diff --git a/src/NzbDrone.Core.Test/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs b/src/NzbDrone.Core.Test/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs index 648f9641d..078744ec8 100644 --- a/src/NzbDrone.Core.Test/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs +++ b/src/NzbDrone.Core.Test/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs @@ -2,8 +2,8 @@ using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; -using NzbDrone.Core.Metadata; -using NzbDrone.Core.Metadata.Consumers.Wdtv; +using NzbDrone.Core.Extras.Metadata; +using NzbDrone.Core.Extras.Metadata.Consumers.Wdtv; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 8f4e61c4c..4c8e12548 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -119,6 +119,7 @@ + diff --git a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs index 5e6b37902..4b430e171 100644 --- a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs @@ -48,8 +48,17 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Castle.2009.S01E14.HDTV.XviD.HUN-LOL", Language.Hungarian)] public void should_parse_language(string postTitle, Language language) { - var result = Parser.Parser.ParseTitle(postTitle); - result.Language.Should().Be(language); + var result = LanguageParser.ParseLanguage(postTitle); + result.Should().Be(language); + } + + [TestCase("2 Broke Girls - S01E01 - Pilot.en.sub", Language.English)] + [TestCase("2 Broke Girls - S01E01 - Pilot.eng.sub", Language.English)] + [TestCase("2 Broke Girls - S01E01 - Pilot.sub", Language.Unknown)] + public void should_parse_subtitle_language(string fileName, Language language) + { + var result = LanguageParser.ParseSubtitleLanguage(fileName); + result.Should().Be(language); } } } diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index e8068584a..a04dba0e5 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -202,6 +202,13 @@ namespace NzbDrone.Core.Configuration set { SetValue("EnableMediaInfo", value); } } + public string ExtraFileExtensions + { + get { return GetValue("ExtraFileExtensions", ""); } + + set { SetValue("ExtraFileExtensions", value); } + } + public bool SetPermissionsLinux { get { return GetValueBoolean("SetPermissionsLinux", false); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 891b0c494..02d6756be 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -35,6 +35,7 @@ namespace NzbDrone.Core.Configuration bool SkipFreeSpaceCheckWhenImporting { get; set; } bool CopyUsingHardlinks { get; set; } bool EnableMediaInfo { get; set; } + string ExtraFileExtensions { get; set; } //Permissions (Media Management) bool SetPermissionsLinux { get; set; } diff --git a/src/NzbDrone.Core/Datastore/Migration/099_extra_and_subtitle_files.cs b/src/NzbDrone.Core/Datastore/Migration/099_extra_and_subtitle_files.cs new file mode 100644 index 000000000..2d7f54143 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/099_extra_and_subtitle_files.cs @@ -0,0 +1,56 @@ +using System; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(99)] + public class extra_and_subtitle_files : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("ExtraFiles") + .WithColumn("SeriesId").AsInt32().NotNullable() + .WithColumn("SeasonNumber").AsInt32().NotNullable() + .WithColumn("EpisodeFileId").AsInt32().NotNullable() + .WithColumn("RelativePath").AsString().NotNullable() + .WithColumn("Extension").AsString().NotNullable() + .WithColumn("Added").AsDateTime().NotNullable() + .WithColumn("LastUpdated").AsDateTime().NotNullable(); + + Create.TableForModel("SubtitleFiles") + .WithColumn("SeriesId").AsInt32().NotNullable() + .WithColumn("SeasonNumber").AsInt32().NotNullable() + .WithColumn("EpisodeFileId").AsInt32().NotNullable() + .WithColumn("RelativePath").AsString().NotNullable() + .WithColumn("Extension").AsString().NotNullable() + .WithColumn("Added").AsDateTime().NotNullable() + .WithColumn("LastUpdated").AsDateTime().NotNullable() + .WithColumn("Language").AsInt32().NotNullable(); + + Alter.Table("MetadataFiles") + .AddColumn("Added").AsDateTime().Nullable() + .AddColumn("Extension").AsString().Nullable(); + + // Set Extension using the extension from RelativePath + Execute.Sql("UPDATE MetadataFiles SET Extension = substr(RelativePath, instr(RelativePath, '.'));"); + + Alter.Table("MetadataFiles").AlterColumn("Extension").AsString().NotNullable(); + } + } + + public class MetadataFile99 + { + public int Id { get; set; } + public int SeriesId { get; set; } + public int? EpisodeFileId { get; set; } + public int? SeasonNumber { get; set; } + public string RelativePath { get; set; } + public DateTime Added { get; set; } + public DateTime LastUpdated { get; set; } + public string Extension { get; set; } + public string Hash { get; set; } + public string Consumer { get; set; } + public int Type { get; set; } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 2f2401fb8..62f6aeb8b 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -14,8 +14,6 @@ using NzbDrone.Core.Indexers; using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Jobs; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Metadata; -using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Profiles.Delay; using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.Notifications; @@ -31,6 +29,10 @@ using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Tv; using NzbDrone.Common.Disk; using NzbDrone.Core.Authentication; +using NzbDrone.Core.Extras.Metadata; +using NzbDrone.Core.Extras.Metadata.Files; +using NzbDrone.Core.Extras.Others; +using NzbDrone.Core.Extras.Subtitles; using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.Datastore @@ -92,13 +94,14 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("QualityDefinitions") .Ignore(d => d.Weight); - Mapper.Entity().RegisterModel("Profiles"); Mapper.Entity().RegisterModel("Logs"); Mapper.Entity().RegisterModel("NamingConfig"); Mapper.Entity().MapResultSet(); Mapper.Entity().RegisterModel("Blacklist"); Mapper.Entity().RegisterModel("MetadataFiles"); + Mapper.Entity().RegisterModel("SubtitleFiles"); + Mapper.Entity().RegisterModel("ExtraFiles"); Mapper.Entity().RegisterModel("PendingReleases") .Ignore(e => e.RemoteEpisode); diff --git a/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs new file mode 100644 index 000000000..c4621d1a3 --- /dev/null +++ b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Extras +{ + public class ExistingExtraFileService : IHandle + { + private readonly IDiskProvider _diskProvider; + private readonly List _existingExtraFileImporters; + private readonly List _extraFileManagers; + private readonly Logger _logger; + + public ExistingExtraFileService(IDiskProvider diskProvider, + List existingExtraFileImporters, + List extraFileManagers, + Logger logger) + { + _diskProvider = diskProvider; + _existingExtraFileImporters = existingExtraFileImporters.OrderBy(e => e.Order).ToList(); + _extraFileManagers = extraFileManagers.OrderBy(e => e.Order).ToList(); + _logger = logger; + } + + public void Handle(SeriesScannedEvent message) + { + var series = message.Series; + var extraFiles = new List(); + + if (!_diskProvider.FolderExists(series.Path)) + { + return; + } + + _logger.Debug("Looking for existing extra files in {0}", series.Path); + + var filesOnDisk = _diskProvider.GetFiles(series.Path, SearchOption.AllDirectories); + var possibleExtraFiles = filesOnDisk.Where(c => !MediaFileExtensions.Extensions.Contains(Path.GetExtension(c).ToLower()) && + !c.StartsWith(Path.Combine(series.Path, "EXTRAS"))).ToList(); + + var filteredFiles = possibleExtraFiles; + var importedFiles = new List(); + + foreach (var existingExtraFileImporter in _existingExtraFileImporters) + { + var imported = existingExtraFileImporter.ProcessFiles(series, filteredFiles, importedFiles); + + importedFiles.AddRange(imported.Select(f => Path.Combine(series.Path, f.RelativePath))); + } + + _logger.Info("Found {0} extra files", extraFiles); + } + } +} diff --git a/src/NzbDrone.Core/Extras/ExtraService.cs b/src/NzbDrone.Core/Extras/ExtraService.cs new file mode 100644 index 000000000..6881ebee6 --- /dev/null +++ b/src/NzbDrone.Core/Extras/ExtraService.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Extras +{ + public interface IExtraService + { + void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly); + } + + public class ExtraService : IExtraService, + IHandle, + IHandle, + IHandle + { + private readonly IMediaFileService _mediaFileService; + private readonly IEpisodeService _episodeService; + private readonly IDiskProvider _diskProvider; + private readonly IConfigService _configService; + private readonly List _extraFileManagers; + private readonly Logger _logger; + + public ExtraService(IMediaFileService mediaFileService, + IEpisodeService episodeService, + IDiskProvider diskProvider, + IConfigService configService, + List extraFileManagers, + Logger logger) + { + _mediaFileService = mediaFileService; + _episodeService = episodeService; + _diskProvider = diskProvider; + _configService = configService; + _extraFileManagers = extraFileManagers.OrderBy(e => e.Order).ToList(); + _logger = logger; + } + + public void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly) + { + // TODO: Remove + // Not importing files yet, testing that parsing is working properly first + return; + + var series = localEpisode.Series; + + foreach (var extraFileManager in _extraFileManagers) + { + extraFileManager.CreateAfterEpisodeImport(series, episodeFile); + } + + var sourcePath = localEpisode.Path; + var sourceFolder = _diskProvider.GetParentFolder(sourcePath); + var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath); + var files = _diskProvider.GetFiles(sourceFolder, SearchOption.TopDirectoryOnly); + + var wantedExtensions = _configService.ExtraFileExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(e => e.Trim(' ', '.')) + .ToList(); + + var matchingFilenames = files.Where(f => Path.GetFileNameWithoutExtension(f).StartsWith(sourceFileName)); + + foreach (var matchingFilename in matchingFilenames) + { + var matchingExtension = wantedExtensions.FirstOrDefault(e => matchingFilename.EndsWith(e)); + + if (matchingExtension == null) + { + continue; + } + + try + { + foreach (var extraFileManager in _extraFileManagers) + { + var extraFile = extraFileManager.Import(series, episodeFile, matchingFilename, matchingExtension, isReadOnly); + + if (extraFile != null) + { + break; + } + } + } + catch (Exception ex) + { + _logger.Warn(ex, "Failed to import extra file: {0}", matchingFilename); + } + } + } + + public void Handle(MediaCoversUpdatedEvent message) + { + var series = message.Series; + var episodeFiles = GetEpisodeFiles(series.Id); + + foreach (var extraFileManager in _extraFileManagers) + { + extraFileManager.CreateAfterSeriesScan(series, episodeFiles); + } + } + + public void Handle(EpisodeFolderCreatedEvent message) + { + var series = message.Series; + + foreach (var extraFileManager in _extraFileManagers) + { + extraFileManager.CreateAfterEpisodeImport(series, message.SeriesFolder, message.SeasonFolder); + } + } + + public void Handle(SeriesRenamedEvent message) + { + var series = message.Series; + var episodeFiles = GetEpisodeFiles(series.Id); + + foreach (var extraFileManager in _extraFileManagers) + { + extraFileManager.MoveFilesAfterRename(series, episodeFiles); + } + } + + private List GetEpisodeFiles(int seriesId) + { + var episodeFiles = _mediaFileService.GetFilesBySeries(seriesId); + var episodes = _episodeService.GetEpisodeBySeries(seriesId); + + foreach (var episodeFile in episodeFiles) + { + var localEpisodeFile = episodeFile; + episodeFile.Episodes = new LazyList(episodes.Where(e => e.EpisodeFileId == localEpisodeFile.Id)); + } + + return episodeFiles; + } + } +} diff --git a/src/NzbDrone.Core/Metadata/Files/MetadataFile.cs b/src/NzbDrone.Core/Extras/Files/ExtraFile.cs similarity index 58% rename from src/NzbDrone.Core/Metadata/Files/MetadataFile.cs rename to src/NzbDrone.Core/Extras/Files/ExtraFile.cs index 96cdd1fe0..036eaec33 100644 --- a/src/NzbDrone.Core/Metadata/Files/MetadataFile.cs +++ b/src/NzbDrone.Core/Extras/Files/ExtraFile.cs @@ -1,17 +1,16 @@ using System; using NzbDrone.Core.Datastore; -namespace NzbDrone.Core.Metadata.Files +namespace NzbDrone.Core.Extras.Files { - public class MetadataFile : ModelBase + public abstract class ExtraFile : ModelBase { public int SeriesId { get; set; } - public string Consumer { get; set; } - public MetadataType Type { get; set; } - public string RelativePath { get; set; } - public DateTime LastUpdated { get; set; } public int? EpisodeFileId { get; set; } public int? SeasonNumber { get; set; } - public string Hash { get; set; } + public string RelativePath { get; set; } + public DateTime Added { get; set; } + public DateTime LastUpdated { get; set; } + public string Extension { get; set; } } } diff --git a/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs b/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs new file mode 100644 index 000000000..e2a6f31d2 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NzbDrone.Common; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Extras.Files +{ + public interface IManageExtraFiles + { + int Order { get; } + IEnumerable CreateAfterSeriesScan(Series series, List episodeFiles); + IEnumerable CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile); + IEnumerable CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder); + IEnumerable MoveFilesAfterRename(Series series, List episodeFiles); + ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly); + } + + public abstract class ExtraFileManager : IManageExtraFiles + where TExtraFile : ExtraFile, new() + + { + private readonly IConfigService _configService; + private readonly IDiskTransferService _diskTransferService; + private readonly IExtraFileService _extraFileService; + + public ExtraFileManager(IConfigService configService, + IDiskTransferService diskTransferService, + IExtraFileService extraFileService) + { + _configService = configService; + _diskTransferService = diskTransferService; + _extraFileService = extraFileService; + } + + public abstract int Order { get; } + public abstract IEnumerable CreateAfterSeriesScan(Series series, List episodeFiles); + public abstract IEnumerable CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile); + public abstract IEnumerable CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder); + public abstract IEnumerable MoveFilesAfterRename(Series series, List episodeFiles); + public abstract ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly); + + protected TExtraFile ImportFile(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) + { + var newFileName = Path.Combine(series.Path, Path.ChangeExtension(episodeFile.RelativePath, extension)); + + var transferMode = TransferMode.Move; + + if (readOnly) + { + transferMode = _configService.CopyUsingHardlinks ? TransferMode.HardLinkOrCopy : TransferMode.Copy; + } + + _diskTransferService.TransferFile(path, newFileName, transferMode, true, false); + + return new TExtraFile + { + SeriesId = series.Id, + SeasonNumber = episodeFile.SeasonNumber, + EpisodeFileId = episodeFile.Id, + RelativePath = series.Path.GetRelativePath(newFileName), + Extension = Path.GetExtension(path) + }; + } + } +} diff --git a/src/NzbDrone.Core/Metadata/Files/MetadataFileRepository.cs b/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs similarity index 56% rename from src/NzbDrone.Core/Metadata/Files/MetadataFileRepository.cs rename to src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs index e1116d05a..7cb4644c3 100644 --- a/src/NzbDrone.Core/Metadata/Files/MetadataFileRepository.cs +++ b/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs @@ -3,22 +3,23 @@ using System.Linq; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; -namespace NzbDrone.Core.Metadata.Files +namespace NzbDrone.Core.Extras.Files { - public interface IMetadataFileRepository : IBasicRepository + public interface IExtraFileRepository : IBasicRepository where TExtraFile : ExtraFile, new() { void DeleteForSeries(int seriesId); void DeleteForSeason(int seriesId, int seasonNumber); void DeleteForEpisodeFile(int episodeFileId); - List GetFilesBySeries(int seriesId); - List GetFilesBySeason(int seriesId, int seasonNumber); - List GetFilesByEpisodeFile(int episodeFileId); - MetadataFile FindByPath(string path); + List GetFilesBySeries(int seriesId); + List GetFilesBySeason(int seriesId, int seasonNumber); + List GetFilesByEpisodeFile(int episodeFileId); + TExtraFile FindByPath(string path); } - public class MetadataFileRepository : BasicRepository, IMetadataFileRepository + public class ExtraFileRepository : BasicRepository, IExtraFileRepository + where TExtraFile : ExtraFile, new() { - public MetadataFileRepository(IMainDatabase database, IEventAggregator eventAggregator) + public ExtraFileRepository(IMainDatabase database, IEventAggregator eventAggregator) : base(database, eventAggregator) { } @@ -38,22 +39,22 @@ namespace NzbDrone.Core.Metadata.Files Delete(c => c.EpisodeFileId == episodeFileId); } - public List GetFilesBySeries(int seriesId) + public List GetFilesBySeries(int seriesId) { return Query.Where(c => c.SeriesId == seriesId); } - public List GetFilesBySeason(int seriesId, int seasonNumber) + public List GetFilesBySeason(int seriesId, int seasonNumber) { return Query.Where(c => c.SeriesId == seriesId && c.SeasonNumber == seasonNumber); } - public List GetFilesByEpisodeFile(int episodeFileId) + public List GetFilesByEpisodeFile(int episodeFileId) { return Query.Where(c => c.EpisodeFileId == episodeFileId); } - public MetadataFile FindByPath(string path) + public TExtraFile FindByPath(string path) { return Query.Where(c => c.RelativePath == path).SingleOrDefault(); } diff --git a/src/NzbDrone.Core/Extras/Files/ExtraFileService.cs b/src/NzbDrone.Core/Extras/Files/ExtraFileService.cs new file mode 100644 index 000000000..d1fafd0d1 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Files/ExtraFileService.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Tv.Events; + +namespace NzbDrone.Core.Extras.Files +{ + public interface IExtraFileService + where TExtraFile : ExtraFile, new() + { + List GetFilesBySeries(int seriesId); + List GetFilesByEpisodeFile(int episodeFileId); + TExtraFile FindByPath(string path); + void Upsert(TExtraFile extraFile); + void Upsert(List extraFiles); + void Delete(int id); + void DeleteMany(IEnumerable ids); + } + + public abstract class ExtraFileService : IExtraFileService, + IHandleAsync, + IHandleAsync + where TExtraFile : ExtraFile, new() + { + private readonly IExtraFileRepository _repository; + private readonly ISeriesService _seriesService; + private readonly IDiskProvider _diskProvider; + private readonly Logger _logger; + + public ExtraFileService(IExtraFileRepository repository, + ISeriesService seriesService, + IDiskProvider diskProvider, + Logger logger) + { + _repository = repository; + _seriesService = seriesService; + _diskProvider = diskProvider; + _logger = logger; + } + + public List GetFilesBySeries(int seriesId) + { + return _repository.GetFilesBySeries(seriesId); + } + + public List GetFilesByEpisodeFile(int episodeFileId) + { + return _repository.GetFilesByEpisodeFile(episodeFileId); + } + + public TExtraFile FindByPath(string path) + { + return _repository.FindByPath(path); + } + + public void Upsert(TExtraFile extraFile) + { + Upsert(new List { extraFile }); + } + + public void Upsert(List extraFiles) + { + extraFiles.ForEach(m => + { + m.LastUpdated = DateTime.UtcNow; + + if (m.Id == 0) + { + m.Added = m.LastUpdated; + } + }); + + _repository.InsertMany(extraFiles.Where(m => m.Id == 0).ToList()); + _repository.UpdateMany(extraFiles.Where(m => m.Id > 0).ToList()); + } + + public void Delete(int id) + { + _repository.Delete(id); + } + + public void DeleteMany(IEnumerable ids) + { + _repository.DeleteMany(ids); + } + + public void HandleAsync(SeriesDeletedEvent message) + { + _logger.Debug("Deleting Extra from database for series: {0}", message.Series); + _repository.DeleteForSeries(message.Series.Id); + } + + public void HandleAsync(EpisodeFileDeletedEvent message) + { + var episodeFile = message.EpisodeFile; + var series = _seriesService.GetSeries(message.EpisodeFile.SeriesId); + + foreach (var extra in _repository.GetFilesByEpisodeFile(episodeFile.Id)) + { + var path = Path.Combine(series.Path, extra.RelativePath); + + if (_diskProvider.FileExists(path)) + { + _diskProvider.DeleteFile(path); + } + } + + _logger.Debug("Deleting Extra from database for episode file: {0}", episodeFile); + _repository.DeleteForEpisodeFile(episodeFile.Id); + } + } +} diff --git a/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs b/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs new file mode 100644 index 000000000..ad14b60a5 --- /dev/null +++ b/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Extras +{ + public interface IImportExistingExtraFiles + { + int Order { get; } + IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles); + } +} diff --git a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs new file mode 100644 index 000000000..2c94038c6 --- /dev/null +++ b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NzbDrone.Common; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Extras +{ + public abstract class ImportExistingExtraFilesBase : IImportExistingExtraFiles + where TExtraFile : ExtraFile, new() + { + private readonly IExtraFileService _extraFileService; + + public ImportExistingExtraFilesBase(IExtraFileService extraFileService) + { + _extraFileService = extraFileService; + } + + public abstract int Order { get; } + public abstract IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles); + + public virtual List FilterAndClean(Series series, List filesOnDisk, List importedFiles) + { + var seriesFiles = _extraFileService.GetFilesBySeries(series.Id); + + Clean(series, filesOnDisk, importedFiles, seriesFiles); + + return Filter(series, filesOnDisk, importedFiles, seriesFiles); + } + + private List Filter(Series series, List filesOnDisk, List importedFiles, List seriesFiles) + { + var filteredFiles = filesOnDisk; + + filteredFiles = filteredFiles.Except(seriesFiles.Select(f => Path.Combine(series.Path, f.RelativePath)).ToList(), PathEqualityComparer.Instance).ToList(); + return filteredFiles.Except(importedFiles, PathEqualityComparer.Instance).ToList(); + } + + private void Clean(Series series, List filesOnDisk, List importedFiles, List seriesFiles) + { + var alreadyImportedFileIds = seriesFiles.IntersectBy(f => Path.Combine(series.Path, f.RelativePath), importedFiles, i => i, PathEqualityComparer.Instance) + .Select(f => f.Id); + + var deletedFiles = seriesFiles.ExceptBy(f => Path.Combine(series.Path, f.RelativePath), filesOnDisk, i => i, PathEqualityComparer.Instance) + .Select(f => f.Id); + + _extraFileService.DeleteMany(alreadyImportedFileIds); + _extraFileService.DeleteMany(deletedFiles); + } + } +} diff --git a/src/NzbDrone.Core/Metadata/Consumers/MediaBrowser/MediaBrowserMetadata.cs b/src/NzbDrone.Core/Extras/MetaData/Consumers/MediaBrowser/MediaBrowserMetadata.cs similarity index 87% rename from src/NzbDrone.Core/Metadata/Consumers/MediaBrowser/MediaBrowserMetadata.cs rename to src/NzbDrone.Core/Extras/MetaData/Consumers/MediaBrowser/MediaBrowserMetadata.cs index 9d17e5504..38d999886 100644 --- a/src/NzbDrone.Core/Metadata/Consumers/MediaBrowser/MediaBrowserMetadata.cs +++ b/src/NzbDrone.Core/Extras/MetaData/Consumers/MediaBrowser/MediaBrowserMetadata.cs @@ -6,27 +6,20 @@ using System.Text; using System.Xml; using System.Xml.Linq; using NLog; -using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; -using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.Metadata.Consumers.MediaBrowser +namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser { public class MediaBrowserMetadata : MetadataBase { - private readonly IMapCoversToLocal _mediaCoverService; - private readonly IDiskProvider _diskProvider; private readonly Logger _logger; - public MediaBrowserMetadata(IMapCoversToLocal mediaCoverService, - IDiskProvider diskProvider, + public MediaBrowserMetadata( Logger logger) { - _mediaCoverService = mediaCoverService; - _diskProvider = diskProvider; _logger = logger; } @@ -38,13 +31,6 @@ namespace NzbDrone.Core.Metadata.Consumers.MediaBrowser } } - public override List AfterRename(Series series, List existingMetadataFiles, List episodeFiles) - { - var updatedMetadataFiles = new List(); - - return updatedMetadataFiles; - } - public override MetadataFile FindMetadataFile(Series series, string path) { var filename = Path.GetFileName(path); diff --git a/src/NzbDrone.Core/Metadata/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs b/src/NzbDrone.Core/Extras/MetaData/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs similarity index 90% rename from src/NzbDrone.Core/Metadata/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs rename to src/NzbDrone.Core/Extras/MetaData/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs index e4201e8ba..c0e5d75bc 100644 --- a/src/NzbDrone.Core/Metadata/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/MetaData/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs @@ -1,10 +1,9 @@ -using System; -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; -namespace NzbDrone.Core.Metadata.Consumers.MediaBrowser +namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser { public class MediaBrowserSettingsValidator : AbstractValidator { diff --git a/src/NzbDrone.Core/Metadata/Consumers/Roksbox/RoksboxMetadata.cs b/src/NzbDrone.Core/Extras/MetaData/Consumers/Roksbox/RoksboxMetadata.cs similarity index 84% rename from src/NzbDrone.Core/Metadata/Consumers/Roksbox/RoksboxMetadata.cs rename to src/NzbDrone.Core/Extras/MetaData/Consumers/Roksbox/RoksboxMetadata.cs index 1859b7efa..072e3428d 100644 --- a/src/NzbDrone.Core/Metadata/Consumers/Roksbox/RoksboxMetadata.cs +++ b/src/NzbDrone.Core/Extras/MetaData/Consumers/Roksbox/RoksboxMetadata.cs @@ -9,12 +9,13 @@ using System.Xml.Linq; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.Metadata.Consumers.Roksbox +namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox { public class RoksboxMetadata : MetadataBase { @@ -42,49 +43,22 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox } } - public override List AfterRename(Series series, List existingMetadataFiles, List episodeFiles) + public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) { - var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList(); - var updatedMetadataFiles = new List(); + var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath); - foreach (var episodeFile in episodeFiles) + if (metadataFile.Type == MetadataType.EpisodeImage) { - var metadataFiles = episodeFilesMetadata.Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); - - foreach (var metadataFile in metadataFiles) - { - string newFilename; - - if (metadataFile.Type == MetadataType.EpisodeImage) - { - newFilename = GetEpisodeImageFilename(episodeFile.RelativePath); - } - - else if (metadataFile.Type == MetadataType.EpisodeMetadata) - { - newFilename = GetEpisodeMetadataFilename(episodeFile.RelativePath); - } - - else - { - _logger.Trace("Unknown episode file metadata: {0}", metadataFile.RelativePath); - continue; - } - - var existingFilename = Path.Combine(series.Path, metadataFile.RelativePath); - newFilename = Path.Combine(series.Path, newFilename); - - if (!newFilename.PathEquals(existingFilename)) - { - _diskProvider.MoveFile(existingFilename, newFilename); - metadataFile.RelativePath = series.Path.GetRelativePath(newFilename); - - updatedMetadataFiles.Add(metadataFile); - } - } + return GetEpisodeImageFilename(episodeFilePath); } - return updatedMetadataFiles; + if (metadataFile.Type == MetadataType.EpisodeMetadata) + { + return GetEpisodeMetadataFilename(episodeFilePath); + } + + _logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath); + return Path.Combine(series.Path, metadataFile.RelativePath); } public override MetadataFile FindMetadataFile(Series series, string path) diff --git a/src/NzbDrone.Core/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs b/src/NzbDrone.Core/Extras/MetaData/Consumers/Roksbox/RoksboxMetadataSettings.cs similarity index 93% rename from src/NzbDrone.Core/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs rename to src/NzbDrone.Core/Extras/MetaData/Consumers/Roksbox/RoksboxMetadataSettings.cs index 08858653a..5ac09ef78 100644 --- a/src/NzbDrone.Core/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/MetaData/Consumers/Roksbox/RoksboxMetadataSettings.cs @@ -1,10 +1,9 @@ -using System; -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; -namespace NzbDrone.Core.Metadata.Consumers.Roksbox +namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox { public class RoksboxSettingsValidator : AbstractValidator { diff --git a/src/NzbDrone.Core/Metadata/Consumers/Wdtv/WdtvMetadata.cs b/src/NzbDrone.Core/Extras/MetaData/Consumers/Wdtv/WdtvMetadata.cs similarity index 85% rename from src/NzbDrone.Core/Metadata/Consumers/Wdtv/WdtvMetadata.cs rename to src/NzbDrone.Core/Extras/MetaData/Consumers/Wdtv/WdtvMetadata.cs index 260d21455..4964caeb4 100644 --- a/src/NzbDrone.Core/Metadata/Consumers/Wdtv/WdtvMetadata.cs +++ b/src/NzbDrone.Core/Extras/MetaData/Consumers/Wdtv/WdtvMetadata.cs @@ -9,12 +9,13 @@ using System.Xml.Linq; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.Metadata.Consumers.Wdtv +namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv { public class WdtvMetadata : MetadataBase { @@ -41,49 +42,23 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv } } - public override List AfterRename(Series series, List existingMetadataFiles, List episodeFiles) + public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) { - var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList(); - var updatedMetadataFiles = new List(); + var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath); - foreach (var episodeFile in episodeFiles) + if (metadataFile.Type == MetadataType.EpisodeImage) { - var metadataFiles = episodeFilesMetadata.Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); - - foreach (var metadataFile in metadataFiles) - { - string newFilename; - - if (metadataFile.Type == MetadataType.EpisodeImage) - { - newFilename = GetEpisodeImageFilename(episodeFile.RelativePath); - } - - else if (metadataFile.Type == MetadataType.EpisodeMetadata) - { - newFilename = GetEpisodeMetadataFilename(episodeFile.RelativePath); - } - - else - { - _logger.Trace("Unknown episode file metadata: {0}", metadataFile.RelativePath); - continue; - } - - var existingPath = Path.Combine(series.Path, metadataFile.RelativePath); - var newPath = Path.Combine(series.Path, newFilename); - - if (!newPath.PathEquals(existingPath)) - { - _diskProvider.MoveFile(existingPath, newPath); - metadataFile.RelativePath = newFilename; - - updatedMetadataFiles.Add(metadataFile); - } - } + return GetEpisodeImageFilename(episodeFilePath); } - return updatedMetadataFiles; + if (metadataFile.Type == MetadataType.EpisodeMetadata) + { + return GetEpisodeMetadataFilename(episodeFilePath); + } + + _logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath); + return Path.Combine(series.Path, metadataFile.RelativePath); + } public override MetadataFile FindMetadataFile(Series series, string path) diff --git a/src/NzbDrone.Core/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs b/src/NzbDrone.Core/Extras/MetaData/Consumers/Wdtv/WdtvMetadataSettings.cs similarity index 94% rename from src/NzbDrone.Core/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs rename to src/NzbDrone.Core/Extras/MetaData/Consumers/Wdtv/WdtvMetadataSettings.cs index b8fcfe599..052c6deae 100644 --- a/src/NzbDrone.Core/Metadata/Consumers/Wdtv/WdtvMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/MetaData/Consumers/Wdtv/WdtvMetadataSettings.cs @@ -1,10 +1,9 @@ -using System; -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; -namespace NzbDrone.Core.Metadata.Consumers.Wdtv +namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv { public class WdtvSettingsValidator : AbstractValidator { diff --git a/src/NzbDrone.Core/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/MetaData/Consumers/Xbmc/XbmcMetadata.cs similarity index 87% rename from src/NzbDrone.Core/Metadata/Consumers/Xbmc/XbmcMetadata.cs rename to src/NzbDrone.Core/Extras/MetaData/Consumers/Xbmc/XbmcMetadata.cs index 376c11985..5a1a71fd0 100644 --- a/src/NzbDrone.Core/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/Extras/MetaData/Consumers/Xbmc/XbmcMetadata.cs @@ -9,12 +9,13 @@ using System.Xml.Linq; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.Metadata.Consumers.Xbmc +namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc { public class XbmcMetadata : MetadataBase { @@ -43,49 +44,22 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc } } - public override List AfterRename(Series series, List existingMetadataFiles, List episodeFiles) + public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) { - var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList(); - var updatedMetadataFiles = new List(); + var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath); - foreach (var episodeFile in episodeFiles) + if (metadataFile.Type == MetadataType.EpisodeImage) { - var metadataFiles = episodeFilesMetadata.Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); - - foreach (var metadataFile in metadataFiles) - { - string newFilename; - - if (metadataFile.Type == MetadataType.EpisodeImage) - { - newFilename = GetEpisodeImageFilename(episodeFile.RelativePath); - } - - else if (metadataFile.Type == MetadataType.EpisodeMetadata) - { - newFilename = GetEpisodeNfoFilename(episodeFile.RelativePath); - } - - else - { - _logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath); - continue; - } - - var existingFilename = Path.Combine(series.Path, metadataFile.RelativePath); - newFilename = Path.Combine(series.Path, newFilename); - - if (!newFilename.PathEquals(existingFilename)) - { - _diskProvider.MoveFile(existingFilename, newFilename); - metadataFile.RelativePath = series.Path.GetRelativePath(newFilename); - - updatedMetadataFiles.Add(metadataFile); - } - } + return GetEpisodeImageFilename(episodeFilePath); } - return updatedMetadataFiles; + if (metadataFile.Type == MetadataType.EpisodeMetadata) + { + return GetEpisodeMetadataFilename(episodeFilePath); + } + + _logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath); + return Path.Combine(series.Path, metadataFile.RelativePath); } public override MetadataFile FindMetadataFile(Series series, string path) @@ -328,7 +302,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc } } - return new MetadataFileResult(GetEpisodeNfoFilename(episodeFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray())); + return new MetadataFileResult(GetEpisodeMetadataFilename(episodeFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray())); } public override List SeriesImages(Series series) @@ -407,7 +381,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc } } - private string GetEpisodeNfoFilename(string episodeFilePath) + private string GetEpisodeMetadataFilename(string episodeFilePath) { return Path.ChangeExtension(episodeFilePath, "nfo"); } diff --git a/src/NzbDrone.Core/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs b/src/NzbDrone.Core/Extras/MetaData/Consumers/Xbmc/XbmcMetadataSettings.cs similarity index 94% rename from src/NzbDrone.Core/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs rename to src/NzbDrone.Core/Extras/MetaData/Consumers/Xbmc/XbmcMetadataSettings.cs index 0908ff69f..35ec5ee32 100644 --- a/src/NzbDrone.Core/Metadata/Consumers/Xbmc/XbmcMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/MetaData/Consumers/Xbmc/XbmcMetadataSettings.cs @@ -1,10 +1,9 @@ -using System; -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; -namespace NzbDrone.Core.Metadata.Consumers.Xbmc +namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc { public class XbmcSettingsValidator : AbstractValidator { diff --git a/src/NzbDrone.Core/Extras/MetaData/ExistingMetadataImporter.cs b/src/NzbDrone.Core/Extras/MetaData/ExistingMetadataImporter.cs new file mode 100644 index 000000000..eb4f8ee31 --- /dev/null +++ b/src/NzbDrone.Core/Extras/MetaData/ExistingMetadataImporter.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Extras.Metadata.Files; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Extras.Metadata +{ + public class ExistingMetadataImporter : ImportExistingExtraFilesBase + { + private readonly IExtraFileService _metadataFileService; + private readonly IParsingService _parsingService; + private readonly Logger _logger; + private readonly List _consumers; + + public ExistingMetadataImporter(IExtraFileService metadataFileService, + IEnumerable consumers, + IParsingService parsingService, + Logger logger) + : base(metadataFileService) + { + _metadataFileService = metadataFileService; + _parsingService = parsingService; + _logger = logger; + _consumers = consumers.ToList(); + } + + public override int Order + { + get + { + return 0; + } + } + + public override IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles) + { + _logger.Debug("Looking for existing metadata in {0}", series.Path); + + var metadataFiles = new List(); + var filteredFiles = FilterAndClean(series, filesOnDisk, importedFiles); + + foreach (var possibleMetadataFile in filteredFiles) + { + foreach (var consumer in _consumers) + { + var metadata = consumer.FindMetadataFile(series, possibleMetadataFile); + + if (metadata == null) + { + continue; + } + + if (metadata.Type == MetadataType.EpisodeImage || + metadata.Type == MetadataType.EpisodeMetadata) + { + var localEpisode = _parsingService.GetLocalEpisode(possibleMetadataFile, series); + + if (localEpisode == null) + { + _logger.Debug("Unable to parse extra file: {0}", possibleMetadataFile); + continue; + } + + if (localEpisode.Episodes.Empty()) + { + _logger.Debug("Cannot find related episodes for: {0}", possibleMetadataFile); + continue; + } + + if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1) + { + _logger.Debug("Extra file: {0} does not match existing files.", possibleMetadataFile); + continue; + } + + metadata.SeasonNumber = localEpisode.SeasonNumber; + metadata.EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId; + metadata.Extension = Path.GetExtension(possibleMetadataFile); + } + + metadataFiles.Add(metadata); + } + } + + _logger.Info("Found {0} existing metadata files", metadataFiles.Count); + _metadataFileService.Upsert(metadataFiles); + + return metadataFiles; + } + } +} diff --git a/src/NzbDrone.Core/Metadata/Files/CleanMetadataService.cs b/src/NzbDrone.Core/Extras/MetaData/Files/CleanMetadataFileService.cs similarity index 78% rename from src/NzbDrone.Core/Metadata/Files/CleanMetadataService.cs rename to src/NzbDrone.Core/Extras/MetaData/Files/CleanMetadataFileService.cs index 566d5a293..6166ae20b 100644 --- a/src/NzbDrone.Core/Metadata/Files/CleanMetadataService.cs +++ b/src/NzbDrone.Core/Extras/MetaData/Files/CleanMetadataFileService.cs @@ -3,22 +3,22 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.Metadata.Files +namespace NzbDrone.Core.Extras.Metadata.Files { public interface ICleanMetadataService { void Clean(Series series); } - public class CleanMetadataService : ICleanMetadataService + public class CleanExtraFileService : ICleanMetadataService { private readonly IMetadataFileService _metadataFileService; private readonly IDiskProvider _diskProvider; private readonly Logger _logger; - public CleanMetadataService(IMetadataFileService metadataFileService, - IDiskProvider diskProvider, - Logger logger) + public CleanExtraFileService(IMetadataFileService metadataFileService, + IDiskProvider diskProvider, + Logger logger) { _metadataFileService = metadataFileService; _diskProvider = diskProvider; diff --git a/src/NzbDrone.Core/Metadata/Files/ImageFileResult.cs b/src/NzbDrone.Core/Extras/MetaData/Files/ImageFileResult.cs similarity index 83% rename from src/NzbDrone.Core/Metadata/Files/ImageFileResult.cs rename to src/NzbDrone.Core/Extras/MetaData/Files/ImageFileResult.cs index 26a2e41df..be810acaa 100644 --- a/src/NzbDrone.Core/Metadata/Files/ImageFileResult.cs +++ b/src/NzbDrone.Core/Extras/MetaData/Files/ImageFileResult.cs @@ -1,6 +1,4 @@ -using System; - -namespace NzbDrone.Core.Metadata.Files +namespace NzbDrone.Core.Extras.Metadata.Files { public class ImageFileResult { diff --git a/src/NzbDrone.Core/Extras/MetaData/Files/MetadataFile.cs b/src/NzbDrone.Core/Extras/MetaData/Files/MetadataFile.cs new file mode 100644 index 000000000..efc6e6fc4 --- /dev/null +++ b/src/NzbDrone.Core/Extras/MetaData/Files/MetadataFile.cs @@ -0,0 +1,11 @@ +using NzbDrone.Core.Extras.Files; + +namespace NzbDrone.Core.Extras.Metadata.Files +{ + public class MetadataFile : ExtraFile + { + public string Hash { get; set; } + public string Consumer { get; set; } + public MetadataType Type { get; set; } + } +} diff --git a/src/NzbDrone.Core/Extras/MetaData/Files/MetadataFileRepository.cs b/src/NzbDrone.Core/Extras/MetaData/Files/MetadataFileRepository.cs new file mode 100644 index 000000000..d1f29ea75 --- /dev/null +++ b/src/NzbDrone.Core/Extras/MetaData/Files/MetadataFileRepository.cs @@ -0,0 +1,18 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Extras.Metadata.Files +{ + public interface IMetadataFileRepository : IExtraFileRepository + { + } + + public class MetadataFileRepository : ExtraFileRepository, IMetadataFileRepository + { + public MetadataFileRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Metadata/Files/MetadataFileResult.cs b/src/NzbDrone.Core/Extras/MetaData/Files/MetadataFileResult.cs similarity index 84% rename from src/NzbDrone.Core/Metadata/Files/MetadataFileResult.cs rename to src/NzbDrone.Core/Extras/MetaData/Files/MetadataFileResult.cs index fd0b69316..f76c11d33 100644 --- a/src/NzbDrone.Core/Metadata/Files/MetadataFileResult.cs +++ b/src/NzbDrone.Core/Extras/MetaData/Files/MetadataFileResult.cs @@ -1,6 +1,4 @@ -using System; - -namespace NzbDrone.Core.Metadata.Files +namespace NzbDrone.Core.Extras.Metadata.Files { public class MetadataFileResult { diff --git a/src/NzbDrone.Core/Extras/MetaData/Files/MetadataFileService.cs b/src/NzbDrone.Core/Extras/MetaData/Files/MetadataFileService.cs new file mode 100644 index 000000000..2bb3d603d --- /dev/null +++ b/src/NzbDrone.Core/Extras/MetaData/Files/MetadataFileService.cs @@ -0,0 +1,19 @@ +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Extras.Metadata.Files +{ + public interface IMetadataFileService : IExtraFileService + { + } + + public class MetadataFileService : ExtraFileService, IMetadataFileService + { + public MetadataFileService(IExtraFileRepository repository, ISeriesService seriesService, IDiskProvider diskProvider, Logger logger) + : base(repository, seriesService, diskProvider, logger) + { + } + } +} diff --git a/src/NzbDrone.Core/Metadata/IMetadata.cs b/src/NzbDrone.Core/Extras/MetaData/IMetadata.cs similarity index 75% rename from src/NzbDrone.Core/Metadata/IMetadata.cs rename to src/NzbDrone.Core/Extras/MetaData/IMetadata.cs index f9c1feae3..b631425e6 100644 --- a/src/NzbDrone.Core/Metadata/IMetadata.cs +++ b/src/NzbDrone.Core/Extras/MetaData/IMetadata.cs @@ -1,21 +1,19 @@ using System.Collections.Generic; +using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.Metadata +namespace NzbDrone.Core.Extras.Metadata { public interface IMetadata : IProvider { - List AfterRename(Series series, List existingMetadataFiles, List episodeFiles); + string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile); MetadataFile FindMetadataFile(Series series, string path); - MetadataFileResult SeriesMetadata(Series series); MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile); List SeriesImages(Series series); List SeasonImages(Series series, Season season); List EpisodeImages(Series series, EpisodeFile episodeFile); - } } diff --git a/src/NzbDrone.Core/Metadata/MetadataBase.cs b/src/NzbDrone.Core/Extras/MetaData/MetadataBase.cs similarity index 75% rename from src/NzbDrone.Core/Metadata/MetadataBase.cs rename to src/NzbDrone.Core/Extras/MetaData/MetadataBase.cs index ce6b7ad87..39a4162ce 100644 --- a/src/NzbDrone.Core/Metadata/MetadataBase.cs +++ b/src/NzbDrone.Core/Extras/MetaData/MetadataBase.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; +using System.IO; using FluentValidation.Results; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.Metadata +namespace NzbDrone.Core.Extras.Metadata { public abstract class MetadataBase : IMetadata where TSettings : IProviderConfig, new() { @@ -43,7 +45,15 @@ namespace NzbDrone.Core.Metadata return new ValidationResult(); } - public abstract List AfterRename(Series series, List existingMetadataFiles, List episodeFiles); + public virtual string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) + { + var existingFilename = Path.Combine(series.Path, metadataFile.RelativePath); + var extension = Path.GetExtension(existingFilename).TrimStart('.'); + var newFileName = Path.ChangeExtension(Path.Combine(series.Path, episodeFile.RelativePath), extension); + + return newFileName; + } + public abstract MetadataFile FindMetadataFile(Series series, string path); public abstract MetadataFileResult SeriesMetadata(Series series); diff --git a/src/NzbDrone.Core/Metadata/MetadataDefinition.cs b/src/NzbDrone.Core/Extras/MetaData/MetadataDefinition.cs similarity index 73% rename from src/NzbDrone.Core/Metadata/MetadataDefinition.cs rename to src/NzbDrone.Core/Extras/MetaData/MetadataDefinition.cs index 0784037dc..4ee8a2d4b 100644 --- a/src/NzbDrone.Core/Metadata/MetadataDefinition.cs +++ b/src/NzbDrone.Core/Extras/MetaData/MetadataDefinition.cs @@ -1,6 +1,6 @@ using NzbDrone.Core.ThingiProvider; -namespace NzbDrone.Core.Metadata +namespace NzbDrone.Core.Extras.Metadata { public class MetadataDefinition : ProviderDefinition { diff --git a/src/NzbDrone.Core/Metadata/MetadataFactory.cs b/src/NzbDrone.Core/Extras/MetaData/MetadataFactory.cs similarity index 97% rename from src/NzbDrone.Core/Metadata/MetadataFactory.cs rename to src/NzbDrone.Core/Extras/MetaData/MetadataFactory.cs index ae7056779..5fe8db3f5 100644 --- a/src/NzbDrone.Core/Metadata/MetadataFactory.cs +++ b/src/NzbDrone.Core/Extras/MetaData/MetadataFactory.cs @@ -6,7 +6,7 @@ using NzbDrone.Common.Composition; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ThingiProvider; -namespace NzbDrone.Core.Metadata +namespace NzbDrone.Core.Extras.Metadata { public interface IMetadataFactory : IProviderFactory { diff --git a/src/NzbDrone.Core/Metadata/MetadataRepository.cs b/src/NzbDrone.Core/Extras/MetaData/MetadataRepository.cs similarity index 92% rename from src/NzbDrone.Core/Metadata/MetadataRepository.cs rename to src/NzbDrone.Core/Extras/MetaData/MetadataRepository.cs index e6749799e..349da708e 100644 --- a/src/NzbDrone.Core/Metadata/MetadataRepository.cs +++ b/src/NzbDrone.Core/Extras/MetaData/MetadataRepository.cs @@ -2,12 +2,10 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ThingiProvider; - -namespace NzbDrone.Core.Metadata +namespace NzbDrone.Core.Extras.Metadata { public interface IMetadataRepository : IProviderRepository { - } public class MetadataRepository : ProviderRepository, IMetadataRepository diff --git a/src/NzbDrone.Core/Metadata/MetadataService.cs b/src/NzbDrone.Core/Extras/MetaData/MetadataService.cs similarity index 74% rename from src/NzbDrone.Core/Metadata/MetadataService.cs rename to src/NzbDrone.Core/Extras/MetaData/MetadataService.cs index 281392d68..10c04d719 100644 --- a/src/NzbDrone.Core/Metadata/MetadataService.cs +++ b/src/NzbDrone.Core/Extras/MetaData/MetadataService.cs @@ -7,159 +7,179 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.MediaFiles.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.Metadata +namespace NzbDrone.Core.Extras.Metadata { - public class MetadataService : IHandle, - IHandle, - IHandle, - IHandle + public class MetadataService : ExtraFileManager { private readonly IMetadataFactory _metadataFactory; - private readonly IMetadataFileService _metadataFileService; private readonly ICleanMetadataService _cleanMetadataService; - private readonly IMediaFileService _mediaFileService; - private readonly IEpisodeService _episodeService; private readonly IDiskTransferService _diskTransferService; private readonly IDiskProvider _diskProvider; private readonly IHttpClient _httpClient; private readonly IMediaFileAttributeService _mediaFileAttributeService; - private readonly IEventAggregator _eventAggregator; + private readonly IMetadataFileService _metadataFileService; private readonly Logger _logger; - public MetadataService(IMetadataFactory metadataFactory, - IMetadataFileService metadataFileService, - ICleanMetadataService cleanMetadataService, - IMediaFileService mediaFileService, - IEpisodeService episodeService, + public MetadataService(IConfigService configService, IDiskTransferService diskTransferService, + IMetadataFactory metadataFactory, + ICleanMetadataService cleanMetadataService, IDiskProvider diskProvider, IHttpClient httpClient, IMediaFileAttributeService mediaFileAttributeService, - IEventAggregator eventAggregator, + IMetadataFileService metadataFileService, Logger logger) + : base(configService, diskTransferService, metadataFileService) { _metadataFactory = metadataFactory; - _metadataFileService = metadataFileService; _cleanMetadataService = cleanMetadataService; - _mediaFileService = mediaFileService; - _episodeService = episodeService; _diskTransferService = diskTransferService; _diskProvider = diskProvider; _httpClient = httpClient; _mediaFileAttributeService = mediaFileAttributeService; - _eventAggregator = eventAggregator; + _metadataFileService = metadataFileService; _logger = logger; } - public void Handle(MediaCoversUpdatedEvent message) + public override int Order { - _cleanMetadataService.Clean(message.Series); + get + { + return 0; + } + } - if (!_diskProvider.FolderExists(message.Series.Path)) + public override IEnumerable CreateAfterSeriesScan(Series series, List episodeFiles) + { + var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); + _cleanMetadataService.Clean(series); + + if (!_diskProvider.FolderExists(series.Path)) { _logger.Info("Series folder does not exist, skipping metadata creation"); - return; + return Enumerable.Empty(); } - var seriesMetadataFiles = _metadataFileService.GetFilesBySeries(message.Series.Id); - var episodeFiles = GetEpisodeFiles(message.Series.Id); + var files = new List(); foreach (var consumer in _metadataFactory.Enabled()) { - var consumerFiles = GetMetadataFilesForConsumer(consumer, seriesMetadataFiles); - var files = new List(); + var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles); - files.AddIfNotNull(ProcessSeriesMetadata(consumer, message.Series, consumerFiles)); - files.AddRange(ProcessSeriesImages(consumer, message.Series, consumerFiles)); - files.AddRange(ProcessSeasonImages(consumer, message.Series, consumerFiles)); + files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles)); + files.AddRange(ProcessSeriesImages(consumer, series, consumerFiles)); + files.AddRange(ProcessSeasonImages(consumer, series, consumerFiles)); foreach (var episodeFile in episodeFiles) { - files.AddIfNotNull(ProcessEpisodeMetadata(consumer, message.Series, episodeFile, consumerFiles)); - files.AddRange(ProcessEpisodeImages(consumer, message.Series, episodeFile, consumerFiles)); + files.AddIfNotNull(ProcessEpisodeMetadata(consumer, series, episodeFile, consumerFiles)); + files.AddRange(ProcessEpisodeImages(consumer, series, episodeFile, consumerFiles)); } - - _eventAggregator.PublishEvent(new MetadataFilesUpdated(files)); } + + _metadataFileService.Upsert(files); + + return files; } - public void Handle(EpisodeImportedEvent message) + public override IEnumerable CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) { - foreach (var consumer in _metadataFactory.Enabled()) - { - var files = new List(); - - files.AddIfNotNull(ProcessEpisodeMetadata(consumer, message.EpisodeInfo.Series, message.ImportedEpisode, new List())); - files.AddRange(ProcessEpisodeImages(consumer, message.EpisodeInfo.Series, message.ImportedEpisode, new List())); - - _eventAggregator.PublishEvent(new MetadataFilesUpdated(files)); - } - } - - public void Handle(EpisodeFolderCreatedEvent message) - { - if (message.SeriesFolder.IsNullOrWhiteSpace() && message.SeasonFolder.IsNullOrWhiteSpace()) - { - return; - } - - var seriesMetadataFiles = _metadataFileService.GetFilesBySeries(message.Series.Id); + var files = new List(); foreach (var consumer in _metadataFactory.Enabled()) { - var files = new List(); - var consumerFiles = GetMetadataFilesForConsumer(consumer, seriesMetadataFiles); - if (message.SeriesFolder.IsNotNullOrWhiteSpace()) + files.AddIfNotNull(ProcessEpisodeMetadata(consumer, series, episodeFile, new List())); + files.AddRange(ProcessEpisodeImages(consumer, series, episodeFile, new List())); + } + + _metadataFileService.Upsert(files); + + return files; + } + + public override IEnumerable CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder) + { + var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); + + if (seriesFolder.IsNullOrWhiteSpace() && seasonFolder.IsNullOrWhiteSpace()) + { + return new List(); + } + + var files = new List(); + + foreach (var consumer in _metadataFactory.Enabled()) + { + var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles); + + if (seriesFolder.IsNotNullOrWhiteSpace()) { - files.AddIfNotNull(ProcessSeriesMetadata(consumer, message.Series, consumerFiles)); - files.AddRange(ProcessSeriesImages(consumer, message.Series, consumerFiles)); + files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles)); + files.AddRange(ProcessSeriesImages(consumer, series, consumerFiles)); } - if (message.SeasonFolder.IsNotNullOrWhiteSpace()) + if (seasonFolder.IsNotNullOrWhiteSpace()) { - files.AddRange(ProcessSeasonImages(consumer, message.Series, consumerFiles)); + files.AddRange(ProcessSeasonImages(consumer, series, consumerFiles)); } - - _eventAggregator.PublishEvent(new MetadataFilesUpdated(files)); } + + _metadataFileService.Upsert(files); + + return files; } - public void Handle(SeriesRenamedEvent message) + public override IEnumerable MoveFilesAfterRename(Series series, List episodeFiles) { - var seriesMetadata = _metadataFileService.GetFilesBySeries(message.Series.Id); - var episodeFiles = GetEpisodeFiles(message.Series.Id); + var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); + var movedFiles = new List(); - foreach (var consumer in _metadataFactory.Enabled()) + // TODO: Move EpisodeImage and EpisodeMetadata metadata files, instead of relying on consumers to do it + // (Xbmc's EpisodeImage is more than just the extension) + + foreach (var consumer in _metadataFactory.GetAvailableProviders()) { - var updatedMetadataFiles = consumer.AfterRename(message.Series, - GetMetadataFilesForConsumer(consumer, seriesMetadata), - episodeFiles); + foreach (var episodeFile in episodeFiles) + { + var metadataFilesForConsumer = GetMetadataFilesForConsumer(consumer, metadataFiles).Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); - _eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles)); + foreach (var metadataFile in metadataFilesForConsumer) + { + var newFileName = consumer.GetFilenameAfterMove(series, episodeFile, metadataFile); + var existingFileName = Path.Combine(series.Path, metadataFile.RelativePath); + + if (newFileName.PathNotEquals(existingFileName)) + { + try + { + _diskProvider.MoveFile(existingFileName, newFileName); + metadataFile.RelativePath = series.Path.GetRelativePath(newFileName); + movedFiles.Add(metadataFile); + } + catch (Exception ex) + { + _logger.Warn(ex, "Unable to move metadata file: {0}", existingFileName); + } + } + } + } } + + _metadataFileService.Upsert(movedFiles); + + return movedFiles; } - private List GetEpisodeFiles(int seriesId) + public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) { - var episodeFiles = _mediaFileService.GetFilesBySeries(seriesId); - var episodes = _episodeService.GetEpisodeBySeries(seriesId); - - foreach (var episodeFile in episodeFiles) - { - var localEpisodeFile = episodeFile; - episodeFile.Episodes = new LazyList(episodes.Where(e => e.EpisodeFileId == localEpisodeFile.Id)); - } - - return episodeFiles; + return null; } private List GetMetadataFilesForConsumer(IMetadata consumer, List seriesMetadata) @@ -226,7 +246,7 @@ namespace NzbDrone.Core.Metadata if (existingMetadata != null) { var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath); - if (!fullPath.PathEquals(existingFullPath)) + if (fullPath.PathNotEquals(existingFullPath)) { _diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move); existingMetadata.RelativePath = episodeMetadata.RelativePath; @@ -239,6 +259,7 @@ namespace NzbDrone.Core.Metadata new MetadataFile { SeriesId = series.Id, + SeasonNumber = episodeFile.SeasonNumber, EpisodeFileId = episodeFile.Id, Consumer = consumer.GetType().Name, Type = MetadataType.EpisodeMetadata, @@ -347,7 +368,7 @@ namespace NzbDrone.Core.Metadata if (existingMetadata != null) { var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath); - if (!fullPath.PathEquals(existingFullPath)) + if (fullPath.PathNotEquals(existingFullPath)) { _diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move); existingMetadata.RelativePath = image.RelativePath; @@ -360,6 +381,7 @@ namespace NzbDrone.Core.Metadata new MetadataFile { SeriesId = series.Id, + SeasonNumber = episodeFile.SeasonNumber, EpisodeFileId = episodeFile.Id, Consumer = consumer.GetType().Name, Type = MetadataType.EpisodeImage, diff --git a/src/NzbDrone.Core/Metadata/MetadataType.cs b/src/NzbDrone.Core/Extras/MetaData/MetadataType.cs similarity index 82% rename from src/NzbDrone.Core/Metadata/MetadataType.cs rename to src/NzbDrone.Core/Extras/MetaData/MetadataType.cs index 07a054c39..849bc31dd 100644 --- a/src/NzbDrone.Core/Metadata/MetadataType.cs +++ b/src/NzbDrone.Core/Extras/MetaData/MetadataType.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.Metadata +namespace NzbDrone.Core.Extras.Metadata { public enum MetadataType { diff --git a/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs b/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs new file mode 100644 index 000000000..d744c2259 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Extras.Others +{ + public class ExistingOtherExtraImporter : ImportExistingExtraFilesBase + { + private readonly IExtraFileService _otherExtraFileService; + private readonly IParsingService _parsingService; + private readonly Logger _logger; + + public ExistingOtherExtraImporter(IExtraFileService otherExtraFileService, + IParsingService parsingService, + Logger logger) + : base(otherExtraFileService) + { + _otherExtraFileService = otherExtraFileService; + _parsingService = parsingService; + _logger = logger; + } + + public override int Order + { + get + { + return 2; + } + } + + public override IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles) + { + _logger.Debug("Looking for existing extra files in {0}", series.Path); + + var extraFiles = new List(); + var filteredFiles = FilterAndClean(series, filesOnDisk, importedFiles); + + foreach (var possibleExtraFile in filteredFiles) + { + var localEpisode = _parsingService.GetLocalEpisode(possibleExtraFile, series); + + if (localEpisode == null) + { + _logger.Debug("Unable to parse extra file: {0}", possibleExtraFile); + continue; + } + + if (localEpisode.Episodes.Empty()) + { + _logger.Debug("Cannot find related episodes for: {0}", possibleExtraFile); + continue; + } + + if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1) + { + _logger.Debug("Extra file: {0} does not match existing files.", possibleExtraFile); + continue; + } + + var extraFile = new OtherExtraFile + { + SeriesId = series.Id, + SeasonNumber = localEpisode.SeasonNumber, + EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId, + RelativePath = series.Path.GetRelativePath(possibleExtraFile), + Extension = Path.GetExtension(possibleExtraFile) + }; + + extraFiles.Add(extraFile); + } + + _logger.Info("Found {0} existing other extra files", extraFiles.Count); + _otherExtraFileService.Upsert(extraFiles); + + return extraFiles; + } + } +} diff --git a/src/NzbDrone.Core/Extras/Others/OtherExtraFile.cs b/src/NzbDrone.Core/Extras/Others/OtherExtraFile.cs new file mode 100644 index 000000000..12187cfc1 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Others/OtherExtraFile.cs @@ -0,0 +1,8 @@ +using NzbDrone.Core.Extras.Files; + +namespace NzbDrone.Core.Extras.Others +{ + public class OtherExtraFile : ExtraFile + { + } +} diff --git a/src/NzbDrone.Core/Extras/Others/OtherExtraFileRepository.cs b/src/NzbDrone.Core/Extras/Others/OtherExtraFileRepository.cs new file mode 100644 index 000000000..3f33a3eb8 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Others/OtherExtraFileRepository.cs @@ -0,0 +1,18 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Extras.Others +{ + public interface IOtherExtraFileRepository : IExtraFileRepository + { + } + + public class OtherExtraFileRepository : ExtraFileRepository, IOtherExtraFileRepository + { + public OtherExtraFileRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Extras/Others/OtherExtraFileService.cs b/src/NzbDrone.Core/Extras/Others/OtherExtraFileService.cs new file mode 100644 index 000000000..3d4f65c61 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Others/OtherExtraFileService.cs @@ -0,0 +1,19 @@ +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Extras.Others +{ + public interface IOtherExtraFileService : IExtraFileService + { + } + + public class OtherExtraFileService : ExtraFileService, IOtherExtraFileService + { + public OtherExtraFileService(IExtraFileRepository repository, ISeriesService seriesService, IDiskProvider diskProvider, Logger logger) + : base(repository, seriesService, diskProvider, logger) + { + } + } +} diff --git a/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs b/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs new file mode 100644 index 000000000..d1eb1f0cc --- /dev/null +++ b/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Extras.Others +{ + public class OtherExtraService : ExtraFileManager + { + private readonly IOtherExtraFileService _otherExtraFileService; + private readonly IDiskProvider _diskProvider; + private readonly Logger _logger; + + public OtherExtraService(IConfigService configService, + IDiskTransferService diskTransferService, + IOtherExtraFileService otherExtraFileService, + IDiskProvider diskProvider, + Logger logger) + : base(configService, diskTransferService, otherExtraFileService) + { + _otherExtraFileService = otherExtraFileService; + _diskProvider = diskProvider; + _logger = logger; + } + + public override int Order + { + get + { + return 2; + } + } + + public override IEnumerable CreateAfterSeriesScan(Series series, List episodeFiles) + { + return Enumerable.Empty(); + } + + public override IEnumerable CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) + { + return Enumerable.Empty(); + } + + public override IEnumerable CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder) + { + return Enumerable.Empty(); + } + + public override IEnumerable MoveFilesAfterRename(Series series, List episodeFiles) + { + // TODO: Remove + // We don't want to move files after rename yet. + + return Enumerable.Empty(); + + var extraFiles = _otherExtraFileService.GetFilesBySeries(series.Id); + var movedFiles = new List(); + + foreach (var episodeFile in episodeFiles) + { + var extraFilesForEpisodeFile = extraFiles.Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); + + foreach (var extraFile in extraFilesForEpisodeFile) + { + var existingFileName = Path.Combine(series.Path, extraFile.RelativePath); + var extension = Path.GetExtension(existingFileName).TrimStart('.'); + var newFileName = Path.ChangeExtension(Path.Combine(series.Path, episodeFile.RelativePath), extension); + + if (newFileName.PathNotEquals(existingFileName)) + { + try + { + _diskProvider.MoveFile(existingFileName, newFileName); + extraFile.RelativePath = series.Path.GetRelativePath(newFileName); + movedFiles.Add(extraFile); + } + catch (Exception ex) + { + _logger.Warn(ex, "Unable to move extra file: {0}", existingFileName); + } + } + } + } + + _otherExtraFileService.Upsert(movedFiles); + + return movedFiles; + } + + public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) + { + // If the extension is .nfo we need to change it to .nfo-orig + if (Path.GetExtension(path).Equals(".nfo")) + { + extension += "-orig"; + } + + var extraFile = ImportFile(series, episodeFile, path, extension, readOnly); + + _otherExtraFileService.Upsert(extraFile); + + return extraFile; + } + } +} diff --git a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs new file mode 100644 index 000000000..11e1b7742 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Extras.Subtitles +{ + public class ExistingSubtitleImporter : ImportExistingExtraFilesBase + { + private readonly IExtraFileService _subtitleFileService; + private readonly IParsingService _parsingService; + private readonly Logger _logger; + + public ExistingSubtitleImporter(IExtraFileService subtitleFileService, + IParsingService parsingService, + Logger logger) + : base (subtitleFileService) + { + _subtitleFileService = subtitleFileService; + _parsingService = parsingService; + _logger = logger; + } + + public override int Order + { + get + { + return 1; + } + } + + public override IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles) + { + _logger.Debug("Looking for existing subtitle files in {0}", series.Path); + + var subtitleFiles = new List(); + var filteredFiles = FilterAndClean(series, filesOnDisk, importedFiles); + + foreach (var possibleSubtitleFile in filteredFiles) + { + var extension = Path.GetExtension(possibleSubtitleFile); + + if (SubtitleFileExtensions.Extensions.Contains(extension)) + { + var localEpisode = _parsingService.GetLocalEpisode(possibleSubtitleFile, series); + + if (localEpisode == null) + { + _logger.Debug("Unable to parse subtitle file: {0}", possibleSubtitleFile); + continue; + } + + if (localEpisode.Episodes.Empty()) + { + _logger.Debug("Cannot find related episodes for: {0}", possibleSubtitleFile); + continue; + } + + if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1) + { + _logger.Debug("Subtitle file: {0} does not match existing files.", possibleSubtitleFile); + continue; + } + + var subtitleFile = new SubtitleFile + { + SeriesId = series.Id, + SeasonNumber = localEpisode.SeasonNumber, + EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId, + RelativePath = series.Path.GetRelativePath(possibleSubtitleFile), + Language = LanguageParser.ParseSubtitleLanguage(possibleSubtitleFile), + Extension = extension + }; + + subtitleFiles.Add(subtitleFile); + } + } + + _logger.Info("Found {0} existing subtitle files", subtitleFiles.Count); + _subtitleFileService.Upsert(subtitleFiles); + + return subtitleFiles; + } + } +} diff --git a/src/NzbDrone.Core/Extras/Subtitles/ImportedSubtitleFiles.cs b/src/NzbDrone.Core/Extras/Subtitles/ImportedSubtitleFiles.cs new file mode 100644 index 000000000..287ebdb68 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Subtitles/ImportedSubtitleFiles.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using NzbDrone.Core.Extras.Files; + +namespace NzbDrone.Core.Extras.Subtitles +{ + public class ImportedSubtitleFiles + { + public List SourceFiles { get; set; } + public List SubtitleFiles { get; set; } + + public ImportedSubtitleFiles() + { + SourceFiles = new List(); + SubtitleFiles = new List(); + } + } +} diff --git a/src/NzbDrone.Core/Extras/Subtitles/SubtitleFile.cs b/src/NzbDrone.Core/Extras/Subtitles/SubtitleFile.cs new file mode 100644 index 000000000..0ccd3ede6 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Subtitles/SubtitleFile.cs @@ -0,0 +1,10 @@ +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Core.Extras.Subtitles +{ + public class SubtitleFile : ExtraFile + { + public Language Language { get; set; } + } +} diff --git a/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileExtensions.cs b/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileExtensions.cs new file mode 100644 index 000000000..92f07a11a --- /dev/null +++ b/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileExtensions.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Extras.Subtitles +{ + public static class SubtitleFileExtensions + { + private static HashSet _fileExtensions; + + static SubtitleFileExtensions() + { + _fileExtensions = new HashSet + { + ".aqt", + ".ass", + ".idx", + ".jss", + ".psb", + ".rt", + ".smi", + ".srt", + ".ssa", + ".sub", + ".txt", + ".utf", + ".utf8", + ".utf-8" + }; + } + + public static HashSet Extensions + { + get { return _fileExtensions; } + } + } +} diff --git a/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileRepository.cs b/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileRepository.cs new file mode 100644 index 000000000..9b87fa9e0 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileRepository.cs @@ -0,0 +1,18 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Extras.Subtitles +{ + public interface ISubtitleFileRepository : IExtraFileRepository + { + } + + public class SubtitleFileRepository : ExtraFileRepository, ISubtitleFileRepository + { + public SubtitleFileRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileService.cs b/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileService.cs new file mode 100644 index 000000000..fe8ba4f3b --- /dev/null +++ b/src/NzbDrone.Core/Extras/Subtitles/SubtitleFileService.cs @@ -0,0 +1,19 @@ +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Extras.Subtitles +{ + public interface ISubtitleFileService : IExtraFileService + { + } + + public class SubtitleFileService : ExtraFileService, ISubtitleFileService + { + public SubtitleFileService(IExtraFileRepository repository, ISeriesService seriesService, IDiskProvider diskProvider, Logger logger) + : base(repository, seriesService, diskProvider, logger) + { + } + } +} diff --git a/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs b/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs new file mode 100644 index 000000000..6361b7d84 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Extras.Subtitles +{ + public class SubtitleService : ExtraFileManager + { + private readonly ISubtitleFileService _subtitleFileService; + private readonly IDiskProvider _diskProvider; + private readonly Logger _logger; + + public SubtitleService(IConfigService configService, + IDiskTransferService diskTransferService, + ISubtitleFileService subtitleFileService, + IDiskProvider diskProvider, + Logger logger) + : base(configService, diskTransferService, subtitleFileService) + { + _subtitleFileService = subtitleFileService; + _diskProvider = diskProvider; + _logger = logger; + } + + public override int Order + { + get + { + return 1; + } + } + + public override IEnumerable CreateAfterSeriesScan(Series series, List episodeFiles) + { + return Enumerable.Empty(); + } + + public override IEnumerable CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) + { + return Enumerable.Empty(); + } + + public override IEnumerable CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder) + { + return Enumerable.Empty(); + } + + public override IEnumerable MoveFilesAfterRename(Series series, List episodeFiles) + { + // TODO: Remove + // We don't want to move files after rename yet. + + return Enumerable.Empty(); + + var subtitleFiles = _subtitleFileService.GetFilesBySeries(series.Id); + + var movedFiles = new List(); + + foreach (var episodeFile in episodeFiles) + { + var groupedExtraFilesForEpisodeFile = subtitleFiles.Where(m => m.EpisodeFileId == episodeFile.Id) + .GroupBy(s => s.Language + s.Extension).ToList(); + + foreach (var group in groupedExtraFilesForEpisodeFile) + { + var groupCount = group.Count(); + var copy = 1; + + if (groupCount > 1) + { + _logger.Warn("Multiple subtitle files found with the same language and extension for {0}", Path.Combine(series.Path, episodeFile.RelativePath)); + } + + foreach (var extraFile in group) + { + var existingFileName = Path.Combine(series.Path, extraFile.RelativePath); + var extension = GetExtension(extraFile, existingFileName, copy, groupCount > 1); + var newFileName = Path.ChangeExtension(Path.Combine(series.Path, episodeFile.RelativePath), extension); + + if (newFileName.PathNotEquals(existingFileName)) + { + try + { + _diskProvider.MoveFile(existingFileName, newFileName); + extraFile.RelativePath = series.Path.GetRelativePath(newFileName); + movedFiles.Add(extraFile); + } + catch (Exception ex) + { + _logger.Warn(ex, "Unable to move subtitle file: {0}", existingFileName); + } + } + + copy++; + } + } + } + + _subtitleFileService.Upsert(movedFiles); + + return movedFiles; + } + + public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) + { + if (SubtitleFileExtensions.Extensions.Contains(Path.GetExtension(path))) + { + var subtitleFile = ImportFile(series, episodeFile, path, extension, readOnly); + subtitleFile.Language = LanguageParser.ParseSubtitleLanguage(path); + + _subtitleFileService.Upsert(subtitleFile); + + return subtitleFile; + } + + return null; + } + + private string GetExtension(SubtitleFile extraFile, string existingFileName, int copy, bool multipleCopies = false) + { + var fileExtension = Path.GetExtension(existingFileName); + var extensionBuilder = new StringBuilder(); + + if (multipleCopies) + { + extensionBuilder.Append(copy); + extensionBuilder.Append("."); + } + + if (extraFile.Language != Language.Unknown) + { + extensionBuilder.Append(IsoLanguages.Get(extraFile.Language).TwoLetterCode); + extensionBuilder.Append("."); + } + + extensionBuilder.Append(fileExtension.TrimStart('.')); + + return extensionBuilder.ToString(); + } + } +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/DeleteBadMediaCovers.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/DeleteBadMediaCovers.cs index d1c944d6b..f1744abec 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/DeleteBadMediaCovers.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/DeleteBadMediaCovers.cs @@ -4,23 +4,27 @@ using System.Linq; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; -using NzbDrone.Core.Metadata.Files; +using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Housekeeping.Housekeepers { public class DeleteBadMediaCovers : IHousekeepingTask { + private readonly IMetadataFileService _metaFileService; private readonly ISeriesService _seriesService; - private readonly IMetadataFileService _metadataFileService; private readonly IDiskProvider _diskProvider; private readonly IConfigService _configService; private readonly Logger _logger; - public DeleteBadMediaCovers(ISeriesService seriesService, IMetadataFileService metadataFileService, IDiskProvider diskProvider, IConfigService configService, Logger logger) + public DeleteBadMediaCovers(IMetadataFileService metaFileService, + ISeriesService seriesService, + IDiskProvider diskProvider, + IConfigService configService, + Logger logger) { + _metaFileService = metaFileService; _seriesService = seriesService; - _metadataFileService = metadataFileService; _diskProvider = diskProvider; _configService = configService; _logger = logger; @@ -34,7 +38,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers foreach (var show in series) { - var images = _metadataFileService.GetFilesBySeries(show.Id) + var images = _metaFileService.GetFilesBySeries(show.Id) .Where(c => c.LastUpdated > new DateTime(2014, 12, 27) && c.RelativePath.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase)); foreach (var image in images) @@ -61,7 +65,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers private void DeleteMetadata(int id, string path) { - _metadataFileService.Delete(id); + _metaFileService.Delete(id); _diskProvider.DeleteFile(path); } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 2f270c70a..c636e60b7 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -11,6 +11,7 @@ using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Download; +using NzbDrone.Core.Extras; namespace NzbDrone.Core.MediaFiles.EpisodeImport @@ -24,18 +25,21 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport { private readonly IUpgradeMediaFiles _episodeFileUpgrader; private readonly IMediaFileService _mediaFileService; + private readonly IExtraService _extraService; private readonly IDiskProvider _diskProvider; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; public ImportApprovedEpisodes(IUpgradeMediaFiles episodeFileUpgrader, IMediaFileService mediaFileService, + IExtraService extraService, IDiskProvider diskProvider, IEventAggregator eventAggregator, Logger logger) { _episodeFileUpgrader = episodeFileUpgrader; _mediaFileService = mediaFileService; + _extraService = extraService; _diskProvider = diskProvider; _eventAggregator = eventAggregator; _logger = logger; @@ -98,9 +102,14 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport _mediaFileService.Add(episodeFile); importResults.Add(new ImportResult(importDecision)); + if (newDownload) + { + _extraService.ImportExtraFiles(localEpisode, episodeFile, downloadClientItem != null && downloadClientItem.IsReadOnly); + } + if (downloadClientItem != null) { - _eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, newDownload, downloadClientItem.DownloadClient, downloadClientItem.DownloadId)); + _eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, newDownload, downloadClientItem.DownloadClient, downloadClientItem.DownloadId, downloadClientItem.IsReadOnly)); } else { diff --git a/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs index 84275022a..518132857 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs @@ -1,5 +1,4 @@ -using System; -using NzbDrone.Common.Messaging; +using NzbDrone.Common.Messaging; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.MediaFiles.Events @@ -11,6 +10,7 @@ namespace NzbDrone.Core.MediaFiles.Events public bool NewDownload { get; private set; } public string DownloadClient { get; private set; } public string DownloadId { get; private set; } + public bool IsReadOnly { get; set; } public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, bool newDownload) { @@ -19,13 +19,14 @@ namespace NzbDrone.Core.MediaFiles.Events NewDownload = newDownload; } - public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, bool newDownload, string downloadClient, string downloadId) + public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, bool newDownload, string downloadClient, string downloadId, bool isReadOnly) { EpisodeInfo = episodeInfo; ImportedEpisode = importedEpisode; NewDownload = newDownload; DownloadClient = downloadClient; DownloadId = downloadId; + IsReadOnly = isReadOnly; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs b/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs deleted file mode 100644 index fb48ae442..000000000 --- a/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.MediaFiles.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Metadata.Files; -using NzbDrone.Core.Parser; - -namespace NzbDrone.Core.Metadata -{ - public class ExistingMetadataService : IHandle - { - private readonly IDiskProvider _diskProvider; - private readonly IMetadataFileService _metadataFileService; - private readonly IParsingService _parsingService; - private readonly Logger _logger; - private readonly List _consumers; - - public ExistingMetadataService(IDiskProvider diskProvider, - IEnumerable consumers, - IMetadataFileService metadataFileService, - IParsingService parsingService, - Logger logger) - { - _diskProvider = diskProvider; - _metadataFileService = metadataFileService; - _parsingService = parsingService; - _logger = logger; - _consumers = consumers.ToList(); - } - - public void Handle(SeriesScannedEvent message) - { - if (!_diskProvider.FolderExists(message.Series.Path)) return; - - _logger.Debug("Looking for existing metadata in {0}", message.Series.Path); - - var filesOnDisk = _diskProvider.GetFiles(message.Series.Path, SearchOption.AllDirectories); - - var possibleMetadataFiles = filesOnDisk.Where(c => !MediaFileExtensions.Extensions.Contains(Path.GetExtension(c).ToLower()) && - !c.StartsWith(Path.Combine(message.Series.Path, "EXTRAS"))).ToList(); - - var filteredFiles = _metadataFileService.FilterExistingFiles(possibleMetadataFiles, message.Series); - - var metadataFiles = new List(); - - foreach (var possibleMetadataFile in filteredFiles) - { - foreach (var consumer in _consumers) - { - var metadata = consumer.FindMetadataFile(message.Series, possibleMetadataFile); - - if (metadata == null) continue; - - if (metadata.Type == MetadataType.EpisodeImage || - metadata.Type == MetadataType.EpisodeMetadata) - { - var localEpisode = _parsingService.GetLocalEpisode(possibleMetadataFile, message.Series); - - if (localEpisode == null) - { - _logger.Debug("Unable to parse meta data file: {0}", possibleMetadataFile); - break; - } - - if (localEpisode.Episodes.Empty()) - { - _logger.Debug("Cannot find related episodes for: {0}", possibleMetadataFile); - break; - } - - if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1) - { - _logger.Debug("Metadata file: {0} does not match existing files.", possibleMetadataFile); - break; - } - - metadata.EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId; - } - - metadataFiles.Add(metadata); - } - } - - _metadataFileService.Upsert(metadataFiles); - } - } -} diff --git a/src/NzbDrone.Core/Metadata/Files/MetadataFileService.cs b/src/NzbDrone.Core/Metadata/Files/MetadataFileService.cs deleted file mode 100644 index 5aad2a1df..000000000 --- a/src/NzbDrone.Core/Metadata/Files/MetadataFileService.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using NLog; -using NzbDrone.Common; -using NzbDrone.Common.Disk; -using NzbDrone.Core.MediaFiles.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Tv.Events; - -namespace NzbDrone.Core.Metadata.Files -{ - public interface IMetadataFileService - { - List GetFilesBySeries(int seriesId); - List GetFilesByEpisodeFile(int episodeFileId); - MetadataFile FindByPath(string path); - List FilterExistingFiles(List files, Series series); - void Upsert(List metadataFiles); - void Delete(int id); - } - - public class MetadataFileService : IMetadataFileService, - IHandleAsync, - IHandleAsync, - IHandle - { - private readonly IMetadataFileRepository _repository; - private readonly ISeriesService _seriesService; - private readonly IDiskProvider _diskProvider; - private readonly Logger _logger; - - public MetadataFileService(IMetadataFileRepository repository, - ISeriesService seriesService, - IDiskProvider diskProvider, - Logger logger) - { - _repository = repository; - _seriesService = seriesService; - _diskProvider = diskProvider; - _logger = logger; - } - - public List GetFilesBySeries(int seriesId) - { - return _repository.GetFilesBySeries(seriesId); - } - - public List GetFilesByEpisodeFile(int episodeFileId) - { - return _repository.GetFilesByEpisodeFile(episodeFileId); - } - - public MetadataFile FindByPath(string path) - { - return _repository.FindByPath(path); - } - - public List FilterExistingFiles(List files, Series series) - { - var seriesFiles = GetFilesBySeries(series.Id).Select(f => Path.Combine(series.Path, f.RelativePath)).ToList(); - - if (!seriesFiles.Any()) return files; - - return files.Except(seriesFiles, PathEqualityComparer.Instance).ToList(); - } - - public void Upsert(List metadataFiles) - { - metadataFiles.ForEach(m => m.LastUpdated = DateTime.UtcNow); - - _repository.InsertMany(metadataFiles.Where(m => m.Id == 0).ToList()); - _repository.UpdateMany(metadataFiles.Where(m => m.Id > 0).ToList()); - } - - public void Delete(int id) - { - _repository.Delete(id); - } - - public void HandleAsync(SeriesDeletedEvent message) - { - _logger.Debug("Deleting Metadata from database for series: {0}", message.Series); - _repository.DeleteForSeries(message.Series.Id); - } - - public void HandleAsync(EpisodeFileDeletedEvent message) - { - var episodeFile = message.EpisodeFile; - var series = _seriesService.GetSeries(message.EpisodeFile.SeriesId); - - foreach (var metadata in _repository.GetFilesByEpisodeFile(episodeFile.Id)) - { - var path = Path.Combine(series.Path, metadata.RelativePath); - - if (_diskProvider.FileExists(path)) - { - _diskProvider.DeleteFile(path); - } - } - - _logger.Debug("Deleting Metadata from database for episode file: {0}", episodeFile); - _repository.DeleteForEpisodeFile(episodeFile.Id); - } - - public void Handle(MetadataFilesUpdated message) - { - Upsert(message.MetadataFiles); - } - } -} diff --git a/src/NzbDrone.Core/Metadata/Files/MetadataFilesUpdated.cs b/src/NzbDrone.Core/Metadata/Files/MetadataFilesUpdated.cs deleted file mode 100644 index 98427d7dd..000000000 --- a/src/NzbDrone.Core/Metadata/Files/MetadataFilesUpdated.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Common.Messaging; - -namespace NzbDrone.Core.Metadata.Files -{ - public class MetadataFilesUpdated : IEvent - { - public List MetadataFiles { get; set; } - - public MetadataFilesUpdated(List metadataFiles) - { - MetadataFiles = metadataFiles; - } - } -} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 8612ab78c..00b0c0df6 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -275,6 +275,7 @@ + Code @@ -487,6 +488,29 @@ + + + + + + + + + + + + + + + + + + + + + + + @@ -757,29 +781,25 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + @@ -827,6 +847,9 @@ + + + diff --git a/src/NzbDrone.Core/Parser/IsoLanguage.cs b/src/NzbDrone.Core/Parser/IsoLanguage.cs new file mode 100644 index 000000000..1bd198e50 --- /dev/null +++ b/src/NzbDrone.Core/Parser/IsoLanguage.cs @@ -0,0 +1,16 @@ +namespace NzbDrone.Core.Parser +{ + public class IsoLanguage + { + public string TwoLetterCode { get; set; } + public string ThreeLetterCode { get; set; } + public Language Language { get; set; } + + public IsoLanguage(string twoLetterCode, string threeLetterCode, Language language) + { + TwoLetterCode = twoLetterCode; + ThreeLetterCode = threeLetterCode; + Language = language; + } + } +} diff --git a/src/NzbDrone.Core/Parser/IsoLanguages.cs b/src/NzbDrone.Core/Parser/IsoLanguages.cs new file mode 100644 index 000000000..ddbbe74c2 --- /dev/null +++ b/src/NzbDrone.Core/Parser/IsoLanguages.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; + +namespace NzbDrone.Core.Parser +{ + public static class IsoLanguages + { + private static readonly HashSet All = new HashSet + { + new IsoLanguage("en", "eng", Language.English), + new IsoLanguage("fr", "fra", Language.French), + new IsoLanguage("es", "spa", Language.Spanish), + new IsoLanguage("de", "deu", Language.German), + new IsoLanguage("it", "ita", Language.Italian), + new IsoLanguage("da", "dan", Language.Danish), + new IsoLanguage("nl", "nld", Language.Dutch), + new IsoLanguage("ja", "jpn", Language.Japanese), +// new IsoLanguage("", "", Language.Cantonese), +// new IsoLanguage("", "", Language.Mandarin), + new IsoLanguage("ru", "rus", Language.Russian), + new IsoLanguage("pl", "pol", Language.Polish), + new IsoLanguage("vi", "vie", Language.Vietnamese), + new IsoLanguage("sv", "swe", Language.Swedish), + new IsoLanguage("no", "nor", Language.Norwegian), + new IsoLanguage("fi", "fin", Language.Finnish), + new IsoLanguage("tr", "tur", Language.Turkish), + new IsoLanguage("pt", "por", Language.Portuguese), +// new IsoLanguage("nl", "nld", Language.Flemish), + new IsoLanguage("el", "ell", Language.Greek), + new IsoLanguage("ko", "kor", Language.Korean), + new IsoLanguage("hu", "hun", Language.Hungarian) + }; + + public static IsoLanguage Find(string isoCode) + { + if (isoCode.Length == 2) + { + //Lookup ISO639-1 code + return All.SingleOrDefault(l => l.TwoLetterCode == isoCode); + } + else if (isoCode.Length == 3) + { + //Lookup ISO639-2T code + return All.SingleOrDefault(l => l.ThreeLetterCode == isoCode); + } + + return null; + } + + public static IsoLanguage Get(Language language) + { + return All.SingleOrDefault(l => l.Language == language); + } + } +} diff --git a/src/NzbDrone.Core/Parser/LanguageParser.cs b/src/NzbDrone.Core/Parser/LanguageParser.cs new file mode 100644 index 000000000..1eb52fe9e --- /dev/null +++ b/src/NzbDrone.Core/Parser/LanguageParser.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using NLog; +using NzbDrone.Common.Instrumentation; + +namespace NzbDrone.Core.Parser +{ + public static class LanguageParser + { + private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(LanguageParser)); + + private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?\b(?:ita|italian)\b)|(?german\b|videomann)|(?flemish)|(?greek)|(?(?:\W|_)(?:FR|VOSTFR)(?:\W|_))|(?\brus\b)|(?nl\W?subs?)|(?\b(?:HUNDUB|HUN)\b)", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly Regex SubtitleLanguageRegex = new Regex(".+?[-_. ](?[a-z]{2,3})$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static Language ParseLanguage(string title) + { + var lowerTitle = title.ToLower(); + + if (lowerTitle.Contains("english")) + return Language.English; + + if (lowerTitle.Contains("french")) + return Language.French; + + if (lowerTitle.Contains("spanish")) + return Language.Spanish; + + if (lowerTitle.Contains("danish")) + return Language.Danish; + + if (lowerTitle.Contains("dutch")) + return Language.Dutch; + + if (lowerTitle.Contains("japanese")) + return Language.Japanese; + + if (lowerTitle.Contains("cantonese")) + return Language.Cantonese; + + if (lowerTitle.Contains("mandarin")) + return Language.Mandarin; + + if (lowerTitle.Contains("korean")) + return Language.Korean; + + if (lowerTitle.Contains("russian")) + return Language.Russian; + + if (lowerTitle.Contains("polish")) + return Language.Polish; + + if (lowerTitle.Contains("vietnamese")) + return Language.Vietnamese; + + if (lowerTitle.Contains("swedish")) + return Language.Swedish; + + if (lowerTitle.Contains("norwegian")) + return Language.Norwegian; + + if (lowerTitle.Contains("nordic")) + return Language.Norwegian; + + if (lowerTitle.Contains("finnish")) + return Language.Finnish; + + if (lowerTitle.Contains("turkish")) + return Language.Turkish; + + if (lowerTitle.Contains("portuguese")) + return Language.Portuguese; + + if (lowerTitle.Contains("hungarian")) + return Language.Hungarian; + + var match = LanguageRegex.Match(title); + + if (match.Groups["italian"].Captures.Cast().Any()) + return Language.Italian; + + if (match.Groups["german"].Captures.Cast().Any()) + return Language.German; + + if (match.Groups["flemish"].Captures.Cast().Any()) + return Language.Flemish; + + if (match.Groups["greek"].Captures.Cast().Any()) + return Language.Greek; + + if (match.Groups["french"].Success) + return Language.French; + + if (match.Groups["russian"].Success) + return Language.Russian; + + if (match.Groups["dutch"].Success) + return Language.Dutch; + + if (match.Groups["hungarian"].Success) + return Language.Hungarian; + + return Language.English; + } + + public static Language ParseSubtitleLanguage(string fileName) + { + try + { + Logger.Debug("Parsing language from subtitlte file: {0}", fileName); + + var simpleFilename = Path.GetFileNameWithoutExtension(fileName); + var languageMatch = SubtitleLanguageRegex.Match(simpleFilename); + + if (languageMatch.Success) + { + var isoCode = languageMatch.Groups["iso_code"].Value; + var isoLanguage = IsoLanguages.Find(isoCode); + + return isoLanguage?.Language ?? Language.Unknown; + } + + Logger.Debug("Unable to parse langauge from subtitle file: {0}", fileName); + } + catch (Exception ex) + { + Logger.Debug("Failed parsing langauge from subtitle file: {0}", fileName); + } + + return Language.Unknown; + } + } +} diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 2a0eed87a..a326f5484 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -248,9 +248,6 @@ namespace NzbDrone.Core.Parser private static readonly Regex AnimeReleaseGroupRegex = new Regex(@"^(?:\[(?(?!\s).+?(?\b(?:ita|italian)\b)|(?german\b|videomann)|(?flemish)|(?greek)|(?(?:\W|_)(?:FR|VOSTFR)(?:\W|_))|(?\brus\b)|(?nl\W?subs?)|(?\b(?:HUNDUB|HUN)\b)", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex YearInTitleRegex = new Regex(@"^(?.+?)(?:\W|_)?(?<year>\d{4})", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -358,7 +355,7 @@ namespace NzbDrone.Core.Parser result.Special = true; } - result.Language = ParseLanguage(title); + result.Language = LanguageParser.ParseLanguage(title); Logger.Debug("Language parsed: {0}", result.Language); result.Quality = QualityParser.ParseQuality(title); @@ -493,97 +490,7 @@ namespace NzbDrone.Core.Parser return title; } - - public static Language ParseLanguage(string title) - { - var lowerTitle = title.ToLower(); - - if (lowerTitle.Contains("english")) - return Language.English; - - if (lowerTitle.Contains("french")) - return Language.French; - - if (lowerTitle.Contains("spanish")) - return Language.Spanish; - - if (lowerTitle.Contains("danish")) - return Language.Danish; - - if (lowerTitle.Contains("dutch")) - return Language.Dutch; - - if (lowerTitle.Contains("japanese")) - return Language.Japanese; - - if (lowerTitle.Contains("cantonese")) - return Language.Cantonese; - - if (lowerTitle.Contains("mandarin")) - return Language.Mandarin; - - if (lowerTitle.Contains("korean")) - return Language.Korean; - - if (lowerTitle.Contains("russian")) - return Language.Russian; - - if (lowerTitle.Contains("polish")) - return Language.Polish; - - if (lowerTitle.Contains("vietnamese")) - return Language.Vietnamese; - - if (lowerTitle.Contains("swedish")) - return Language.Swedish; - - if (lowerTitle.Contains("norwegian")) - return Language.Norwegian; - - if (lowerTitle.Contains("nordic")) - return Language.Norwegian; - - if (lowerTitle.Contains("finnish")) - return Language.Finnish; - - if (lowerTitle.Contains("turkish")) - return Language.Turkish; - - if (lowerTitle.Contains("portuguese")) - return Language.Portuguese; - - if (lowerTitle.Contains("hungarian")) - return Language.Hungarian; - - var match = LanguageRegex.Match(title); - - if (match.Groups["italian"].Captures.Cast<Capture>().Any()) - return Language.Italian; - - if (match.Groups["german"].Captures.Cast<Capture>().Any()) - return Language.German; - - if (match.Groups["flemish"].Captures.Cast<Capture>().Any()) - return Language.Flemish; - - if (match.Groups["greek"].Captures.Cast<Capture>().Any()) - return Language.Greek; - - if (match.Groups["french"].Success) - return Language.French; - - if (match.Groups["russian"].Success) - return Language.Russian; - - if (match.Groups["dutch"].Success) - return Language.Dutch; - - if (match.Groups["hungarian"].Success) - return Language.Hungarian; - - return Language.English; - } - + private static SeriesTitleInfo GetSeriesTitleInfo(string title) { var seriesTitleInfo = new SeriesTitleInfo(); diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 5b2cbfcd1..5e8096790 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -233,7 +233,7 @@ namespace NzbDrone.Core.Parser info.FullSeason = false; info.Quality = QualityParser.ParseQuality(title); info.ReleaseGroup = Parser.ParseReleaseGroup(title); - info.Language = Parser.ParseLanguage(title); + info.Language = LanguageParser.ParseLanguage(title); info.Special = true; _logger.Debug("Found special episode {0} for title '{1}'", info, title);