diff --git a/src/NzbDrone.Core.Test/CustomFormats/CustomFormatsFixture.cs b/src/NzbDrone.Core.Test/CustomFormats/CustomFormatsTestHelpers.cs similarity index 91% rename from src/NzbDrone.Core.Test/CustomFormats/CustomFormatsFixture.cs rename to src/NzbDrone.Core.Test/CustomFormats/CustomFormatsTestHelpers.cs index bd1e8a815..a384a2b8e 100644 --- a/src/NzbDrone.Core.Test/CustomFormats/CustomFormatsFixture.cs +++ b/src/NzbDrone.Core.Test/CustomFormats/CustomFormatsTestHelpers.cs @@ -1,15 +1,13 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; -using NUnit.Framework; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Profiles; using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.CustomFormats { - [TestFixture] - public class CustomFormatsFixture : CoreTest + public class CustomFormatsTestHelpers : CoreTest { private static List _customFormats { get; set; } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/CustomFormatAllowedByProfileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/CustomFormatAllowedByProfileSpecificationFixture.cs index 84d3d3119..2c948c175 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/CustomFormatAllowedByProfileSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/CustomFormatAllowedByProfileSpecificationFixture.cs @@ -46,14 +46,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.DVD, new Revision(version: 2)) }, }; - CustomFormatsFixture.GivenCustomFormats(_format1, _format2); + CustomFormatsTestHelpers.GivenCustomFormats(_format1, _format2); } [Test] public void should_allow_if_format_score_greater_than_min() { _remoteEpisode.CustomFormats = new List { _format1 }; - _remoteEpisode.Series.QualityProfile.Value.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name); + _remoteEpisode.Series.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name); _remoteEpisode.CustomFormatScore = _remoteEpisode.Series.QualityProfile.Value.CalculateCustomFormatScore(_remoteEpisode.CustomFormats); Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); @@ -63,7 +63,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void should_deny_if_format_score_not_greater_than_min() { _remoteEpisode.CustomFormats = new List { _format2 }; - _remoteEpisode.Series.QualityProfile.Value.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name); + _remoteEpisode.Series.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name); _remoteEpisode.CustomFormatScore = _remoteEpisode.Series.QualityProfile.Value.CalculateCustomFormatScore(_remoteEpisode.CustomFormats); Console.WriteLine(_remoteEpisode.CustomFormatScore); @@ -76,7 +76,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void should_deny_if_format_score_not_greater_than_min_2() { _remoteEpisode.CustomFormats = new List { _format2, _format1 }; - _remoteEpisode.Series.QualityProfile.Value.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name); + _remoteEpisode.Series.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name); _remoteEpisode.CustomFormatScore = _remoteEpisode.Series.QualityProfile.Value.CalculateCustomFormatScore(_remoteEpisode.CustomFormats); Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); @@ -86,7 +86,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void should_allow_if_all_format_is_defined_in_profile() { _remoteEpisode.CustomFormats = new List { _format2, _format1 }; - _remoteEpisode.Series.QualityProfile.Value.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name, _format2.Name); + _remoteEpisode.Series.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name, _format2.Name); _remoteEpisode.CustomFormatScore = _remoteEpisode.Series.QualityProfile.Value.CalculateCustomFormatScore(_remoteEpisode.CustomFormats); Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); @@ -96,7 +96,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void should_deny_if_no_format_was_parsed_and_min_score_positive() { _remoteEpisode.CustomFormats = new List { }; - _remoteEpisode.Series.QualityProfile.Value.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name, _format2.Name); + _remoteEpisode.Series.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name, _format2.Name); _remoteEpisode.CustomFormatScore = _remoteEpisode.Series.QualityProfile.Value.CalculateCustomFormatScore(_remoteEpisode.CustomFormats); Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); @@ -106,7 +106,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void should_allow_if_no_format_was_parsed_min_score_is_zero() { _remoteEpisode.CustomFormats = new List { }; - _remoteEpisode.Series.QualityProfile.Value.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name, _format2.Name); + _remoteEpisode.Series.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name, _format2.Name); _remoteEpisode.Series.QualityProfile.Value.MinFormatScore = 0; _remoteEpisode.CustomFormatScore = _remoteEpisode.Series.QualityProfile.Value.CalculateCustomFormatScore(_remoteEpisode.CustomFormats); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs index dbb0e77ca..8bc95dc40 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs @@ -45,8 +45,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private void GivenProfile(QualityProfile profile) { - CustomFormatsFixture.GivenCustomFormats(); - profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems(); + CustomFormatsTestHelpers.GivenCustomFormats(); + profile.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(); profile.MinFormatScore = 0; _remoteMovie.Series.QualityProfile = profile; @@ -79,7 +79,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { _customFormat = new CustomFormat("My Format", new ResolutionSpecification { Value = (int)Resolution.R1080p }) { Id = 1 }; - CustomFormatsFixture.GivenCustomFormats(_customFormat); + CustomFormatsTestHelpers.GivenCustomFormats(_customFormat); } [Test] @@ -221,7 +221,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities(), MinFormatScore = 0, - FormatItems = CustomFormatsFixture.GetSampleFormatItems("My Format"), + FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems("My Format"), UpgradeAllowed = true }); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs index 1a1e73544..3552a5125 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs @@ -36,14 +36,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { Mocker.Resolve(); - CustomFormatsFixture.GivenCustomFormats(); + CustomFormatsTestHelpers.GivenCustomFormats(); _series = Builder.CreateNew() .With(e => e.QualityProfile = new QualityProfile { UpgradeAllowed = true, Items = Qualities.QualityFixture.GetDefaultQualities(), - FormatItems = CustomFormatsFixture.GetSampleFormatItems(), + FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(), MinFormatScore = 0 }) .Build(); @@ -74,7 +74,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .Build(); Mocker.GetMock() - .Setup(x => x.ParseCustomFormat(It.IsAny(), It.IsAny())) + .Setup(x => x.ParseCustomFormat(It.IsAny())) .Returns(new List()); } @@ -88,7 +88,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private void GivenQueueFormats(List formats) { Mocker.GetMock() - .Setup(x => x.ParseCustomFormat(It.IsAny(), It.IsAny())) + .Setup(x => x.ParseCustomFormat(It.IsAny())) .Returns(formats); } @@ -215,9 +215,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var lowFormat = new List { new CustomFormat("Bad Format", new ResolutionSpecification { Value = (int)Resolution.R1080p }) { Id = 2 } }; - CustomFormatsFixture.GivenCustomFormats(_remoteEpisode.CustomFormats.First(), lowFormat.First()); + CustomFormatsTestHelpers.GivenCustomFormats(_remoteEpisode.CustomFormats.First(), lowFormat.First()); - _series.QualityProfile.Value.FormatItems = CustomFormatsFixture.GetSampleFormatItems("My Format"); + _series.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems("My Format"); GivenQueueFormats(lowFormat); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/HistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/HistorySpecificationFixture.cs index 78b3c023f..15f05d0f8 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/HistorySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/HistorySpecificationFixture.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync Mocker.Resolve(); _upgradeHistory = Mocker.Resolve(); - CustomFormatsFixture.GivenCustomFormats(); + CustomFormatsTestHelpers.GivenCustomFormats(); var singleEpisodeList = new List { new Episode { Id = FIRST_EPISODE_ID, SeasonNumber = 12, EpisodeNumber = 3 } }; var doubleEpisodeList = new List @@ -56,7 +56,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync { UpgradeAllowed = true, Cutoff = Quality.Bluray1080p.Id, - FormatItems = CustomFormatsFixture.GetSampleFormatItems("None"), + FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems("None"), MinFormatScore = 0, Items = Qualities.QualityFixture.GetDefaultQualities() }) @@ -87,7 +87,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync .Returns(true); Mocker.GetMock() - .Setup(x => x.ParseCustomFormat(It.IsAny())) + .Setup(x => x.ParseCustomFormat(It.IsAny(), It.IsAny())) .Returns(new List()); } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs index f75915b69..2aa3cbc70 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs @@ -35,7 +35,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Mocker.Resolve(); _upgradeDisk = Mocker.Resolve(); - CustomFormatsFixture.GivenCustomFormats(); + CustomFormatsTestHelpers.GivenCustomFormats(); _firstFile = new EpisodeFile { Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 2)), DateAdded = DateTime.Now, Languages = new List { Language.English } }; _secondFile = new EpisodeFile { Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 2)), DateAdded = DateTime.Now, Languages = new List { Language.English } }; @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests UpgradeAllowed = true, Cutoff = Quality.Bluray1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities(), - FormatItems = CustomFormatsFixture.GetSampleFormatItems("None"), + FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems("None"), MinFormatScore = 0, }) .Build(); diff --git a/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs b/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs new file mode 100644 index 000000000..449f01bd7 --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Download.Aggregation.Aggregators; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.Download.Aggregation.Aggregators +{ + [TestFixture] + public class AggregateLanguagesFixture : CoreTest + { + private RemoteEpisode _remoteEpisode; + private Series _series; + private string _simpleReleaseTitle = "Series.Title.S01E01.xyz-RlsGroup"; + + [SetUp] + public void Setup() + { + var episodes = Builder.CreateListOfSize(1) + .BuildList(); + + _series = Builder.CreateNew() + .With(m => m.OriginalLanguage = Language.English) + .Build(); + + _remoteEpisode = Builder.CreateNew() + .With(l => l.ParsedEpisodeInfo = null) + .With(l => l.Episodes = episodes) + .With(l => l.Series = _series) + .Build(); + } + + private ParsedEpisodeInfo GetParsedEpisodeInfo(List languages, string releaseTitle, string releaseTokens = "") + { + return new ParsedEpisodeInfo + { + Languages = languages, + ReleaseTitle = releaseTitle, + ReleaseTokens = releaseTokens + }; + } + + [Test] + public void should_return_existing_language_if_episode_title_does_not_have_language() + { + _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List { Language.Original }, _simpleReleaseTitle); + + Subject.Aggregate(_remoteEpisode).Languages.Should().Contain(_series.OriginalLanguage); + } + + [Test] + public void should_return_parsed_language() + { + _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List { Language.French }, _simpleReleaseTitle); + + Subject.Aggregate(_remoteEpisode).Languages.Should().Equal(_remoteEpisode.ParsedEpisodeInfo.Languages); + } + + [Test] + public void should_exclude_language_that_is_part_of_episode_title_when_release_tokens_contains_episode_title() + { + var releaseTitle = "Series.Title.S01E01.Jimmy.The.Greek.xyz-RlsGroup"; + var releaseTokens = ".Jimmy.The.Greek.xyz-RlsGroup"; + + _remoteEpisode.Episodes.First().Title = "Jimmy The Greek"; + _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List { Language.Greek }, releaseTitle, releaseTokens); + + Subject.Aggregate(_remoteEpisode).Languages.Should().Equal(_series.OriginalLanguage); + } + + [Test] + public void should_remove_parsed_language_that_is_part_of_episode_title_when_release_tokens_contains_episode_title() + { + var releaseTitle = "Series.Title.S01E01.Jimmy.The.Greek.French.xyz-RlsGroup"; + var releaseTokens = ".Jimmy.The.Greek.French.xyz-RlsGroup"; + + _remoteEpisode.Episodes.First().Title = "Jimmy The Greek"; + _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List { Language.Greek, Language.French }, releaseTitle, releaseTokens); + + Subject.Aggregate(_remoteEpisode).Languages.Should().Equal(Language.French); + } + + [Test] + public void should_not_exclude_language_that_is_part_of_episode_title_when_release_tokens_does_not_contain_episode_title() + { + var releaseTitle = "Series.Title.S01E01.xyz-RlsGroup"; + var releaseTokens = ".xyz-RlsGroup"; + + _remoteEpisode.Episodes.First().Title = "Jimmy The Greek"; + _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List { Language.Greek }, releaseTitle, releaseTokens); + + Subject.Aggregate(_remoteEpisode).Languages.Should().Equal(Language.Greek); + } + + [Test] + public void should_use_reparse_language_after_determining_languages_that_are_in_episode_titles() + { + var releaseTitle = "Series.Title.S01E01.Jimmy.The.Greek.Greek.xyz-RlsGroup"; + var releaseTokens = ".Jimmy.The.Greek.Greek.xyz-RlsGroup"; + + _remoteEpisode.Episodes.First().Title = "Jimmy The Greek"; + _remoteEpisode.ParsedEpisodeInfo = GetParsedEpisodeInfo(new List { Language.Greek }, releaseTitle, releaseTokens); + + Subject.Aggregate(_remoteEpisode).Languages.Should().Equal(Language.Greek); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs index 0d95f207c..2290472d5 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs @@ -10,12 +10,9 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications; -using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles.Qualities; -using NzbDrone.Core.Profiles.Releases; using NzbDrone.Core.Qualities; -using NzbDrone.Core.Test.CustomFormats; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -272,7 +269,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications .Returns(new List()); Mocker.GetMock() - .Setup(s => s.ParseCustomFormat(It.IsAny(), It.IsAny())) + .Setup(s => s.ParseCustomFormat(It.IsAny())) .Returns(new List()); _localEpisode.Quality = new QualityModel(Quality.Bluray2160p); @@ -306,7 +303,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications .Returns(new List()); Mocker.GetMock() - .Setup(s => s.ParseCustomFormat(It.IsAny(), It.IsAny())) + .Setup(s => s.ParseCustomFormat(It.IsAny())) .Returns(new List()); _localEpisode.Quality = new QualityModel(Quality.Bluray1080p); @@ -384,7 +381,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications .Returns(new List()); Mocker.GetMock() - .Setup(s => s.ParseCustomFormat(It.IsAny(), It.IsAny())) + .Setup(s => s.ParseCustomFormat(It.IsAny())) .Returns(new List()); _localEpisode.Quality = new QualityModel(Quality.Bluray1080p); @@ -417,7 +414,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications .Returns(new List()); Mocker.GetMock() - .Setup(s => s.ParseCustomFormat(It.IsAny(), It.IsAny())) + .Setup(s => s.ParseCustomFormat(It.IsAny())) .Returns(new List()); _localEpisode.Quality = new QualityModel(Quality.Bluray1080p); diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs index ec673f294..043e4d36b 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs @@ -1,12 +1,11 @@ -using System; using System.Collections.Generic; using System.IO; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Blocklisting; +using NzbDrone.Core.Datastore.Migration; using NzbDrone.Core.History; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; @@ -14,28 +13,101 @@ namespace NzbDrone.Core.CustomFormats { public interface ICustomFormatCalculationService { - List ParseCustomFormat(ParsedEpisodeInfo episodeInfo, Series series); + List ParseCustomFormat(RemoteEpisode remoteEpisode); + List ParseCustomFormat(EpisodeFile episodeFile, Series series); List ParseCustomFormat(EpisodeFile episodeFile); - List ParseCustomFormat(Blocklist blocklist); - List ParseCustomFormat(EpisodeHistory history); + List ParseCustomFormat(Blocklist blocklist, Series series); + List ParseCustomFormat(EpisodeHistory history, Series series); } public class CustomFormatCalculationService : ICustomFormatCalculationService { private readonly ICustomFormatService _formatService; - private readonly IParsingService _parsingService; - private readonly ISeriesService _seriesService; - public CustomFormatCalculationService(ICustomFormatService formatService, - IParsingService parsingService, - ISeriesService seriesService) + public CustomFormatCalculationService(ICustomFormatService formatService) { _formatService = formatService; - _parsingService = parsingService; - _seriesService = seriesService; } - public static List ParseCustomFormat(ParsedEpisodeInfo episodeInfo, List allCustomFormats) + public List ParseCustomFormat(RemoteEpisode remoteEpisode) + { + var input = new CustomFormatInput + { + EpisodeInfo = remoteEpisode.ParsedEpisodeInfo, + Series = remoteEpisode.Series, + Size = remoteEpisode.Release.Size, + Languages = remoteEpisode.Languages + }; + + return ParseCustomFormat(input); + } + + public List ParseCustomFormat(EpisodeFile episodeFile, Series series) + { + return ParseCustomFormat(episodeFile, series, _formatService.All()); + } + + public List ParseCustomFormat(EpisodeFile episodeFile) + { + return ParseCustomFormat(episodeFile, episodeFile.Series.Value, _formatService.All()); + } + + public List ParseCustomFormat(Blocklist blocklist, Series series) + { + var parsed = Parser.Parser.ParseTitle(blocklist.SourceTitle); + + var episodeInfo = new ParsedEpisodeInfo + { + SeriesTitle = series.Title, + ReleaseTitle = parsed?.ReleaseTitle ?? blocklist.SourceTitle, + Quality = blocklist.Quality, + Languages = blocklist.Languages, + ReleaseGroup = parsed?.ReleaseGroup + }; + + var input = new CustomFormatInput + { + EpisodeInfo = episodeInfo, + Series = series, + Size = blocklist.Size ?? 0, + Languages = blocklist.Languages + }; + + return ParseCustomFormat(input); + } + + public List ParseCustomFormat(EpisodeHistory history, Series series) + { + var parsed = Parser.Parser.ParseTitle(history.SourceTitle); + + long.TryParse(history.Data.GetValueOrDefault("size"), out var size); + + var episodeInfo = new ParsedEpisodeInfo + { + SeriesTitle = series.Title, + ReleaseTitle = parsed?.ReleaseTitle ?? history.SourceTitle, + Quality = history.Quality, + Languages = history.Languages, + ReleaseGroup = parsed?.ReleaseGroup, + }; + + var input = new CustomFormatInput + { + EpisodeInfo = episodeInfo, + Series = series, + Size = size, + Languages = history.Languages + }; + + return ParseCustomFormat(input); + } + + private List ParseCustomFormat(CustomFormatInput input) + { + return ParseCustomFormat(input, _formatService.All()); + } + + private static List ParseCustomFormat(CustomFormatInput input, List allCustomFormats) { var matches = new List(); @@ -45,7 +117,7 @@ namespace NzbDrone.Core.CustomFormats .GroupBy(t => t.GetType()) .Select(g => new SpecificationMatchesGroup { - Matches = g.ToDictionary(t => t, t => t.IsSatisfiedBy(episodeInfo)) + Matches = g.ToDictionary(t => t, t => t.IsSatisfiedBy(input)) }) .ToList(); @@ -58,7 +130,7 @@ namespace NzbDrone.Core.CustomFormats return matches; } - public static List ParseCustomFormat(EpisodeFile episodeFile, List allCustomFormats) + private static List ParseCustomFormat(EpisodeFile episodeFile, Series series, List allCustomFormats) { var sceneName = string.Empty; if (episodeFile.SceneName.IsNotNullOrWhiteSpace()) @@ -74,81 +146,25 @@ namespace NzbDrone.Core.CustomFormats sceneName = Path.GetFileName(episodeFile.RelativePath); } - var info = new ParsedEpisodeInfo + var episodeInfo = new ParsedEpisodeInfo { SeriesTitle = episodeFile.Series.Value.Title, - ReleaseTitle = sceneName, + ReleaseTitle = sceneName, Quality = episodeFile.Quality, Languages = episodeFile.Languages, - ReleaseGroup = episodeFile.ReleaseGroup, - ExtraInfo = new Dictionary - { - { "Size", episodeFile.Size }, - { "Filename", Path.GetFileName(episodeFile.RelativePath) }, - { "OriginalLanguage", episodeFile.Series.Value.OriginalLanguage } - } + ReleaseGroup = episodeFile.ReleaseGroup }; - return ParseCustomFormat(info, allCustomFormats); - } - - public List ParseCustomFormat(ParsedEpisodeInfo episodeInfo, Series series) - { - if (series?.OriginalLanguage != null) + var input = new CustomFormatInput { - episodeInfo.ExtraInfo["OriginalLanguage"] = series.OriginalLanguage; - } - - return ParseCustomFormat(episodeInfo, _formatService.All()); - } - - public List ParseCustomFormat(EpisodeFile episodeFile) - { - return ParseCustomFormat(episodeFile, _formatService.All()); - } - - public List ParseCustomFormat(Blocklist blocklist) - { - var series = _seriesService.GetSeries(blocklist.SeriesId); - var parsed = Parser.Parser.ParseTitle(blocklist.SourceTitle); - - var info = new ParsedEpisodeInfo - { - SeriesTitle = series.Title, - ReleaseTitle = parsed?.ReleaseTitle ?? blocklist.SourceTitle, - Quality = blocklist.Quality, - Languages = blocklist.Languages, - ReleaseGroup = parsed?.ReleaseGroup, - ExtraInfo = new Dictionary - { - { "Size", blocklist.Size } - } - }; - - return ParseCustomFormat(info, series); - } - - public List ParseCustomFormat(EpisodeHistory history) - { - var series = _seriesService.GetSeries(history.SeriesId); - var parsed = Parser.Parser.ParseTitle(history.SourceTitle); - - long.TryParse(history.Data.GetValueOrDefault("size"), out var size); - - var info = new ParsedEpisodeInfo - { - SeriesTitle = series.Title, - ReleaseTitle = parsed?.ReleaseTitle ?? history.SourceTitle, - Quality = history.Quality, - Languages = history.Languages, - ReleaseGroup = parsed?.ReleaseGroup, - ExtraInfo = new Dictionary - { - { "Size", size } - } + EpisodeInfo = episodeInfo, + Series = series, + Size = episodeFile.Size, + Languages = episodeFile.Languages, + Filename = Path.GetFileName(episodeFile.RelativePath) }; - return ParseCustomFormat(info, series); + return ParseCustomFormat(input, allCustomFormats); } } } diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs new file mode 100644 index 000000000..ab035213a --- /dev/null +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.CustomFormats +{ + public class CustomFormatInput + { + public ParsedEpisodeInfo EpisodeInfo { get; set; } + public Series Series { get; set; } + public long Size { get; set; } + public List Languages { get; set; } + public string Filename { get; set; } + + public CustomFormatInput() + { + Languages = new List(); + } + + // public CustomFormatInput(ParsedEpisodeInfo episodeInfo, Series series) + // { + // EpisodeInfo = episodeInfo; + // Series = series; + // } + // + // public CustomFormatInput(ParsedEpisodeInfo episodeInfo, Series series, long size, List languages) + // { + // EpisodeInfo = episodeInfo; + // Series = series; + // Size = size; + // Languages = languages; + // } + // + // public CustomFormatInput(ParsedEpisodeInfo episodeInfo, Series series, long size, List languages, string filename) + // { + // EpisodeInfo = episodeInfo; + // Series = series; + // Size = size; + // Languages = languages; + // Filename = filename; + // } + } +} diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/CustomFormatSpecificationBase.cs b/src/NzbDrone.Core/CustomFormats/Specifications/CustomFormatSpecificationBase.cs index 5c0bf4d27..af168e883 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/CustomFormatSpecificationBase.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/CustomFormatSpecificationBase.cs @@ -1,4 +1,3 @@ -using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; namespace NzbDrone.Core.CustomFormats @@ -21,9 +20,9 @@ namespace NzbDrone.Core.CustomFormats public abstract NzbDroneValidationResult Validate(); - public bool IsSatisfiedBy(ParsedEpisodeInfo episodeInfo) + public bool IsSatisfiedBy(CustomFormatInput input) { - var match = IsSatisfiedByWithoutNegate(episodeInfo); + var match = IsSatisfiedByWithoutNegate(input); if (Negate) { match = !match; @@ -32,6 +31,6 @@ namespace NzbDrone.Core.CustomFormats return match; } - protected abstract bool IsSatisfiedByWithoutNegate(ParsedEpisodeInfo episodeInfo); + protected abstract bool IsSatisfiedByWithoutNegate(CustomFormatInput input); } } diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/ICustomFormatSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/ICustomFormatSpecification.cs index d2aabcd0a..1ccdff791 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/ICustomFormatSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/ICustomFormatSpecification.cs @@ -15,6 +15,6 @@ namespace NzbDrone.Core.CustomFormats NzbDroneValidationResult Validate(); ICustomFormatSpecification Clone(); - bool IsSatisfiedBy(ParsedEpisodeInfo episodeInfo); + bool IsSatisfiedBy(CustomFormatInput input); } } diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs index 42950a407..5f32ef24b 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/LanguageSpecification.cs @@ -2,7 +2,6 @@ using System.Linq; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Languages; -using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; namespace NzbDrone.Core.CustomFormats @@ -32,13 +31,13 @@ namespace NzbDrone.Core.CustomFormats [FieldDefinition(1, Label = "Language", Type = FieldType.Select, SelectOptions = typeof(LanguageFieldConverter))] public int Value { get; set; } - protected override bool IsSatisfiedByWithoutNegate(ParsedEpisodeInfo episodeInfo) + protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) { - var comparedLanguage = episodeInfo != null && Value == Language.Original.Id && episodeInfo.ExtraInfo.ContainsKey("OriginalLanguage") - ? (Language)episodeInfo.ExtraInfo["OriginalLanguage"] + var comparedLanguage = input.EpisodeInfo != null && Value == Language.Original.Id && input.Series.OriginalLanguage != Language.Unknown + ? input.Series.OriginalLanguage : (Language)Value; - return episodeInfo?.Languages?.Contains(comparedLanguage) ?? false; + return input.Languages?.Contains(comparedLanguage) ?? false; } public override NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/ReleaseGroupSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/ReleaseGroupSpecification.cs index 3c0066a43..58d443eed 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/ReleaseGroupSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/ReleaseGroupSpecification.cs @@ -1,5 +1,3 @@ -using NzbDrone.Core.Parser.Model; - namespace NzbDrone.Core.CustomFormats { public class ReleaseGroupSpecification : RegexSpecificationBase @@ -8,9 +6,9 @@ namespace NzbDrone.Core.CustomFormats public override string ImplementationName => "Release Group"; public override string InfoLink => "https://wiki.servarr.com/sonarr/settings#custom-formats-2"; - protected override bool IsSatisfiedByWithoutNegate(ParsedEpisodeInfo episodeInfo) + protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) { - return MatchString(episodeInfo?.ReleaseGroup); + return MatchString(input.EpisodeInfo?.ReleaseGroup); } } } diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/ReleaseTitleSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/ReleaseTitleSpecification.cs index c7f00baca..4610a9dc9 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/ReleaseTitleSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/ReleaseTitleSpecification.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Parser.Model; - namespace NzbDrone.Core.CustomFormats { public class ReleaseTitleSpecification : RegexSpecificationBase @@ -10,11 +6,9 @@ namespace NzbDrone.Core.CustomFormats public override string ImplementationName => "Release Title"; public override string InfoLink => "https://wiki.servarr.com/sonarr/settings#custom-formats-2"; - protected override bool IsSatisfiedByWithoutNegate(ParsedEpisodeInfo episodeInfo) + protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) { - var filename = (string)episodeInfo?.ExtraInfo?.GetValueOrDefault("Filename"); - - return MatchString(episodeInfo?.ReleaseTitle) || MatchString(filename); + return MatchString(input.EpisodeInfo?.ReleaseTitle) || MatchString(input.Filename); } } } diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/ResolutionSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/ResolutionSpecification.cs index c28a9f2b6..03729482c 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/ResolutionSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/ResolutionSpecification.cs @@ -24,9 +24,9 @@ namespace NzbDrone.Core.CustomFormats [FieldDefinition(1, Label = "Resolution", Type = FieldType.Select, SelectOptions = typeof(Resolution))] public int Value { get; set; } - protected override bool IsSatisfiedByWithoutNegate(ParsedEpisodeInfo episodeInfo) + protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) { - return (episodeInfo?.Quality?.Quality?.Resolution ?? (int)Resolution.Unknown) == Value; + return (input.EpisodeInfo?.Quality?.Quality?.Resolution ?? (int)Resolution.Unknown) == Value; } public override NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs index b0e3402bc..257904a30 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs @@ -1,8 +1,5 @@ -using System.Collections.Generic; using FluentValidation; -using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; -using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; namespace NzbDrone.Core.CustomFormats @@ -29,9 +26,9 @@ namespace NzbDrone.Core.CustomFormats [FieldDefinition(1, Label = "Maximum Size", HelpText = "Release must be less than or equal to this size", Unit = "GB", Type = FieldType.Number)] public double Max { get; set; } - protected override bool IsSatisfiedByWithoutNegate(ParsedEpisodeInfo episodeInfo) + protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) { - var size = (episodeInfo?.ExtraInfo?.GetValueOrDefault("Size", 0.0) as long?) ?? 0; + var size = input.Size; return size > Min.Gigabytes() && size <= Max.Gigabytes(); } diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/SourceSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/SourceSpecification.cs index 5c1fbcf3b..46824107a 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/SourceSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/SourceSpecification.cs @@ -24,9 +24,9 @@ namespace NzbDrone.Core.CustomFormats [FieldDefinition(1, Label = "Source", Type = FieldType.Select, SelectOptions = typeof(QualitySource))] public int Value { get; set; } - protected override bool IsSatisfiedByWithoutNegate(ParsedEpisodeInfo episodeInfo) + protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) { - return (episodeInfo?.Quality?.Quality?.Source ?? (int)QualitySource.Unknown) == (QualitySource)Value; + return (input.EpisodeInfo?.Quality?.Quality?.Source ?? (int)QualitySource.Unknown) == (QualitySource)Value; } public override NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index 5707bad98..84dad6f3d 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -88,19 +88,11 @@ namespace NzbDrone.Core.DecisionEngine } } - if (parsedEpisodeInfo != null && report.Size > 0) - { - parsedEpisodeInfo.ExtraInfo.Add("Size", report.Size); - } - if (parsedEpisodeInfo != null && !parsedEpisodeInfo.SeriesTitle.IsNullOrWhiteSpace()) { var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, report.TvdbId, report.TvRageId, searchCriteria); remoteEpisode.Release = report; - remoteEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(parsedEpisodeInfo, remoteEpisode.Series); - remoteEpisode.CustomFormatScore = remoteEpisode?.Series?.QualityProfile?.Value.CalculateCustomFormatScore(remoteEpisode.CustomFormats) ?? 0; - if (remoteEpisode.Series == null) { var reason = "Unknown Series"; @@ -120,6 +112,10 @@ namespace NzbDrone.Core.DecisionEngine else { _aggregationService.Augment(remoteEpisode); + + remoteEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(remoteEpisode); + remoteEpisode.CustomFormatScore = remoteEpisode?.Series?.QualityProfile?.Value.CalculateCustomFormatScore(remoteEpisode.CustomFormats) ?? 0; + remoteEpisode.DownloadAllowed = remoteEpisode.Episodes.Any(); decision = GetDecisionForReport(remoteEpisode, searchCriteria); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs index 4c82e7de6..67a0f32a2 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs @@ -52,7 +52,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications continue; } - var queuedItemCustomFormats = _formatService.ParseCustomFormat(remoteEpisode.ParsedEpisodeInfo, subject.Series); + var queuedItemCustomFormats = _formatService.ParseCustomFormat(remoteEpisode); _logger.Debug("Checking if existing release in queue meets cutoff. Queued: {0}", remoteEpisode.ParsedEpisodeInfo.Quality); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index 03b089327..4bb7a3f80 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -59,7 +59,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync continue; } - var customFormats = _formatService.ParseCustomFormat(mostRecent); + var customFormats = _formatService.ParseCustomFormat(mostRecent, subject.Series); // The series will be the same as the one in history since it's the same episode. // Instead of fetching the series from the DB reuse the known series. diff --git a/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs b/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs new file mode 100644 index 000000000..dde0e8097 --- /dev/null +++ b/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Download.Aggregation.Aggregators +{ + public class AggregateLanguages : IAggregateRemoteEpisode + { + private readonly Logger _logger; + + public AggregateLanguages(Logger logger) + { + _logger = logger; + } + + public RemoteEpisode Aggregate(RemoteEpisode remoteEpisode) + { + var parsedEpisodeInfo = remoteEpisode.ParsedEpisodeInfo; + var languages = parsedEpisodeInfo.Languages; + var series = remoteEpisode.Series; + var releaseTokens = parsedEpisodeInfo.ReleaseTokens ?? parsedEpisodeInfo.ReleaseTitle; + var normalizedReleaseTokens = Parser.Parser.NormalizeEpisodeTitle(releaseTokens); + var languagesToRemove = new List(); + + if (series == null) + { + _logger.Debug("Unable to aggregate languages, using parsed values: {0}", string.Join(", ", languages.ToList())); + + remoteEpisode.Languages = languages; + + return remoteEpisode; + } + + // Exclude any languages that are part of the episode title, if the episode title is in the release tokens (falls back to release title) + foreach (var episode in remoteEpisode.Episodes) + { + var episodeTitleLanguage = LanguageParser.ParseLanguages(episode.Title); + + if (!episodeTitleLanguage.Contains(Language.Unknown)) + { + var normalizedEpisodeTitle = Parser.Parser.NormalizeEpisodeTitle(episode.Title); + var episodeTitleIndex = normalizedReleaseTokens.IndexOf(normalizedEpisodeTitle, StringComparison.CurrentCultureIgnoreCase); + + if (episodeTitleIndex >= 0) + { + releaseTokens = releaseTokens.Remove(episodeTitleIndex, normalizedEpisodeTitle.Length); + languagesToRemove.AddRange(episodeTitleLanguage); + } + } + } + + // Remove any languages still in the title that would normally be removed + languagesToRemove = languagesToRemove.Except(LanguageParser.ParseLanguages(releaseTokens)).ToList(); + + // Remove all languages that aren't part of the updated releaseTokens + languages = languages.Except(languagesToRemove).ToList(); + + // Use series language as fallback if we couldn't parse a language + if (languages.Count == 0 || (languages.Count == 1 && languages.First() == Language.Unknown)) + { + languages = new List { series.OriginalLanguage }; + _logger.Debug("Language couldn't be parsed from release, fallback to series original language: {0}", series.OriginalLanguage.Name); + } + + if (languages.Contains(Language.Original)) + { + languages.Remove(Language.Original); + + if (!languages.Contains(series.OriginalLanguage)) + { + languages.Add(series.OriginalLanguage); + } + else + { + languages.Add(Language.Unknown); + } + } + + _logger.Debug("Selected languages: {0}", string.Join(", ", languages.ToList())); + + remoteEpisode.Languages = languages; + + return remoteEpisode; + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs b/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs index c6bbeb359..abbc62e6f 100644 --- a/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs +++ b/src/NzbDrone.Core/Download/Clients/Flood/Flood.cs @@ -57,7 +57,7 @@ namespace NzbDrone.Core.Download.Clients.Flood result.Add(remoteEpisode.ParsedEpisodeInfo.Quality.Quality.ToString()); break; case (int)AdditionalTags.Languages: - result.UnionWith(remoteEpisode.ParsedEpisodeInfo.Languages.ConvertAll(language => language.ToString())); + result.UnionWith(remoteEpisode.Languages.ConvertAll(language => language.ToString())); break; case (int)AdditionalTags.ReleaseGroup: result.Add(remoteEpisode.ParsedEpisodeInfo.ReleaseGroup); diff --git a/src/NzbDrone.Core/Download/IgnoredDownloadService.cs b/src/NzbDrone.Core/Download/IgnoredDownloadService.cs index 03acc180c..1d96f9cda 100644 --- a/src/NzbDrone.Core/Download/IgnoredDownloadService.cs +++ b/src/NzbDrone.Core/Download/IgnoredDownloadService.cs @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Download { SeriesId = series.Id, EpisodeIds = episodes.Select(e => e.Id).ToList(), - Languages = trackedDownload.RemoteEpisode.ParsedEpisodeInfo.Languages, + Languages = trackedDownload.RemoteEpisode.Languages, Quality = trackedDownload.RemoteEpisode.ParsedEpisodeInfo.Quality, SourceTitle = trackedDownload.DownloadItem.Title, DownloadClientInfo = trackedDownload.DownloadItem.DownloadClientInfo, diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs index a00efa1de..9b7f91431 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download.Aggregation; using NzbDrone.Core.Indexers; using NzbDrone.Core.Jobs; using NzbDrone.Core.Languages; @@ -44,7 +45,8 @@ namespace NzbDrone.Core.Download.Pending private readonly IDelayProfileService _delayProfileService; private readonly ITaskManager _taskManager; private readonly IConfigService _configService; - private readonly ICustomFormatCalculationService _customFormatCalculationService; + private readonly ICustomFormatCalculationService _formatCalculator; + private readonly IRemoteEpisodeAggregationService _aggregationService; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; @@ -55,7 +57,8 @@ namespace NzbDrone.Core.Download.Pending IDelayProfileService delayProfileService, ITaskManager taskManager, IConfigService configService, - ICustomFormatCalculationService customFormatCalculationService, + ICustomFormatCalculationService formatCalculator, + IRemoteEpisodeAggregationService aggregationService, IEventAggregator eventAggregator, Logger logger) { @@ -66,7 +69,8 @@ namespace NzbDrone.Core.Download.Pending _delayProfileService = delayProfileService; _taskManager = taskManager; _configService = configService; - _customFormatCalculationService = customFormatCalculationService; + _formatCalculator = formatCalculator; + _aggregationService = aggregationService; _eventAggregator = eventAggregator; _logger = logger; } @@ -158,6 +162,7 @@ namespace NzbDrone.Core.Download.Pending var nextRssSync = new Lazy(() => _taskManager.GetNextExecution(typeof(RssSyncCommand))); var pendingReleases = IncludeRemoteEpisodes(_repository.WithoutFallback()); + foreach (var pendingRelease in pendingReleases) { foreach (var episode in pendingRelease.RemoteEpisode.Episodes) @@ -185,7 +190,7 @@ namespace NzbDrone.Core.Download.Pending Id = GetQueueId(pendingRelease, episode), Series = pendingRelease.RemoteEpisode.Series, Episode = episode, - Languages = pendingRelease.RemoteEpisode.ParsedEpisodeInfo.Languages, + Languages = pendingRelease.RemoteEpisode.Languages, Quality = pendingRelease.RemoteEpisode.ParsedEpisodeInfo.Quality, Title = pendingRelease.Title, Size = pendingRelease.RemoteEpisode.Release.Size, @@ -330,7 +335,8 @@ namespace NzbDrone.Core.Download.Pending release.RemoteEpisode.Episodes = new List(); } - release.RemoteEpisode.CustomFormats = _customFormatCalculationService.ParseCustomFormat(release.RemoteEpisode.ParsedEpisodeInfo, release.RemoteEpisode.Series); + _aggregationService.Augment(release.RemoteEpisode); + release.RemoteEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(release.RemoteEpisode); result.Add(release); } diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index 8a6da2b46..75c7cf3a2 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -5,6 +5,7 @@ using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.Download.Aggregation; using NzbDrone.Core.Download.History; using NzbDrone.Core.History; using NzbDrone.Core.Messaging.Events; @@ -31,6 +32,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads private readonly IHistoryService _historyService; private readonly IEventAggregator _eventAggregator; private readonly IDownloadHistoryService _downloadHistoryService; + private readonly IRemoteEpisodeAggregationService _aggregationService; private readonly ICustomFormatCalculationService _formatCalculator; private readonly Logger _logger; private readonly ICached _cache; @@ -41,6 +43,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads ICustomFormatCalculationService formatCalculator, IEventAggregator eventAggregator, IDownloadHistoryService downloadHistoryService, + IRemoteEpisodeAggregationService aggregationService, Logger logger) { _parsingService = parsingService; @@ -48,6 +51,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads _formatCalculator = formatCalculator; _eventAggregator = eventAggregator; _downloadHistoryService = downloadHistoryService; + _aggregationService = aggregationService; _cache = cacheManager.GetCache(GetType()); _logger = logger; } @@ -111,6 +115,8 @@ namespace NzbDrone.Core.Download.TrackedDownloads if (parsedEpisodeInfo != null) { trackedDownload.RemoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0); + + _aggregationService.Augment(trackedDownload.RemoteEpisode); } var downloadHistory = _downloadHistoryService.GetLatestDownloadHistoryItem(downloadItem.DownloadId); @@ -147,7 +153,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads // Calculate custom formats if (trackedDownload.RemoteEpisode != null) { - trackedDownload.RemoteEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(parsedEpisodeInfo, trackedDownload.RemoteEpisode.Series); + trackedDownload.RemoteEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(trackedDownload.RemoteEpisode); } // Track it so it can be displayed in the queue even though we can't determine which series it is for @@ -206,6 +212,8 @@ namespace NzbDrone.Core.Download.TrackedDownloads var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title); trackedDownload.RemoteEpisode = parsedEpisodeInfo == null ? null : _parsingService.Map(parsedEpisodeInfo, 0, 0); + + _aggregationService.Augment(trackedDownload.RemoteEpisode); } private static TrackedDownloadState GetStateFromHistory(DownloadHistoryEventType eventType) diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 861a3be3c..fa1b6269d 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -148,7 +148,7 @@ namespace NzbDrone.Core.History SeriesId = episode.SeriesId, EpisodeId = episode.Id, DownloadId = message.DownloadId, - Languages = message.Episode.ParsedEpisodeInfo.Languages, + Languages = message.Episode.Languages, }; history.Data.Add("Indexer", message.Episode.Release.Indexer); diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 39abe33e1..205d7fbb5 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Organizer private readonly INamingConfigService _namingConfigService; private readonly IQualityDefinitionService _qualityDefinitionService; private readonly IUpdateMediaInfo _mediaInfoUpdater; - private readonly ICustomFormatService _formatService; + private readonly ICustomFormatCalculationService _formatCalculator; private readonly ICached _episodeFormatCache; private readonly ICached _absoluteEpisodeFormatCache; private readonly ICached _requiresEpisodeTitleCache; @@ -115,13 +115,13 @@ namespace NzbDrone.Core.Organizer IQualityDefinitionService qualityDefinitionService, ICacheManager cacheManager, IUpdateMediaInfo mediaInfoUpdater, - ICustomFormatService formatService, + ICustomFormatCalculationService formatCalculator, Logger logger) { _namingConfigService = namingConfigService; _qualityDefinitionService = qualityDefinitionService; _mediaInfoUpdater = mediaInfoUpdater; - _formatService = formatService; + _formatCalculator = formatCalculator; _episodeFormatCache = cacheManager.GetCache(GetType(), "episodeFormat"); _absoluteEpisodeFormatCache = cacheManager.GetCache(GetType(), "absoluteEpisodeFormat"); _requiresEpisodeTitleCache = cacheManager.GetCache(GetType(), "requiresEpisodeTitle"); @@ -691,7 +691,7 @@ namespace NzbDrone.Core.Organizer if (customFormats == null) { episodeFile.Series = series; - customFormats = CustomFormatCalculationService.ParseCustomFormat(episodeFile, _formatService.All()); + customFormats = _formatCalculator.ParseCustomFormat(episodeFile, series); } tokenHandlers["{Custom Formats}"] = m => string.Join(" ", customFormats.Where(x => x.IncludeCustomFormatWhenRenaming)); diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index b4f0e3100..35cf41cbe 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -30,9 +30,6 @@ namespace NzbDrone.Core.Parser.Model public string ReleaseTokens { get; set; } public int? DailyPart { get; set; } - [JsonIgnore] - public Dictionary ExtraInfo { get; set; } = new Dictionary(); - public ParsedEpisodeInfo() { EpisodeNumbers = new int[0]; diff --git a/src/NzbDrone.Core/Parser/Model/RemoteEpisode.cs b/src/NzbDrone.Core/Parser/Model/RemoteEpisode.cs index 4696cdbed..15f3c1fca 100644 --- a/src/NzbDrone.Core/Parser/Model/RemoteEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/RemoteEpisode.cs @@ -4,6 +4,7 @@ using System.Linq; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.Download.Clients; +using NzbDrone.Core.Languages; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Parser.Model @@ -23,11 +24,13 @@ namespace NzbDrone.Core.Parser.Model public List CustomFormats { get; set; } public int CustomFormatScore { get; set; } public SeriesMatchType SeriesMatchType { get; set; } + public List Languages { get; set; } public RemoteEpisode() { Episodes = new List(); CustomFormats = new List(); + Languages = new List(); } public bool IsRecentEpisode() diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 4deed93c4..afa8ef115 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -184,30 +184,9 @@ namespace NzbDrone.Core.Parser { remoteEpisode.Episodes = GetEpisodes(parsedEpisodeInfo, series, remoteEpisode.MappedSeasonNumber, sceneSource, searchCriteria); } - - parsedEpisodeInfo.ExtraInfo["OriginalLanguage"] = series.OriginalLanguage; - } - - // Use series language as fallback if we could't parse a language (more accurate than just using English) - if (parsedEpisodeInfo.Languages.Count <= 1 && parsedEpisodeInfo.Languages.First() == Language.Unknown && series != null) - { - parsedEpisodeInfo.Languages = new List { series.OriginalLanguage }; - _logger.Debug("Language couldn't be parsed from release, fallback to series original language: {0}", series.OriginalLanguage.Name); } - if (parsedEpisodeInfo.Languages.Contains(Language.Original)) - { - parsedEpisodeInfo.Languages.Remove(Language.Original); - - if (series != null && !parsedEpisodeInfo.Languages.Contains(series.OriginalLanguage)) - { - parsedEpisodeInfo.Languages.Add(series.OriginalLanguage); - } - else - { - parsedEpisodeInfo.Languages.Add(Language.Unknown); - } - } + remoteEpisode.Languages = parsedEpisodeInfo.Languages; if (remoteEpisode.Episodes == null) { diff --git a/src/Sonarr.Api.V3/Blocklist/BlocklistResource.cs b/src/Sonarr.Api.V3/Blocklist/BlocklistResource.cs index 7fa545550..71b119a54 100644 --- a/src/Sonarr.Api.V3/Blocklist/BlocklistResource.cs +++ b/src/Sonarr.Api.V3/Blocklist/BlocklistResource.cs @@ -44,7 +44,7 @@ namespace Sonarr.Api.V3.Blocklist SourceTitle = model.SourceTitle, Languages = model.Languages, Quality = model.Quality, - CustomFormats = formatCalculator.ParseCustomFormat(model).ToResource(), + CustomFormats = formatCalculator.ParseCustomFormat(model, model.Series).ToResource(), Date = model.Date, Protocol = model.Protocol, Indexer = model.Indexer, diff --git a/src/Sonarr.Api.V3/History/HistoryResource.cs b/src/Sonarr.Api.V3/History/HistoryResource.cs index b859b5769..b4d96e5d5 100644 --- a/src/Sonarr.Api.V3/History/HistoryResource.cs +++ b/src/Sonarr.Api.V3/History/HistoryResource.cs @@ -49,7 +49,7 @@ namespace Sonarr.Api.V3.History SourceTitle = model.SourceTitle, Languages = model.Languages, Quality = model.Quality, - CustomFormats = formatCalculator.ParseCustomFormat(model).ToResource(), + CustomFormats = formatCalculator.ParseCustomFormat(model, model.Series).ToResource(), // QualityCutoffNotMet Date = model.Date, diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs index c80324ce2..c45f0c9e7 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs @@ -104,7 +104,7 @@ namespace Sonarr.Api.V3.Indexers Title = releaseInfo.Title, FullSeason = parsedEpisodeInfo.FullSeason, SeasonNumber = parsedEpisodeInfo.SeasonNumber, - Languages = parsedEpisodeInfo.Languages, + Languages = remoteEpisode.Languages, AirDate = parsedEpisodeInfo.AirDate, SeriesTitle = parsedEpisodeInfo.SeriesTitle, EpisodeNumbers = parsedEpisodeInfo.EpisodeNumbers, diff --git a/src/Sonarr.Api.V3/Parse/ParseController.cs b/src/Sonarr.Api.V3/Parse/ParseController.cs index 8edc5feb9..84060d90f 100644 --- a/src/Sonarr.Api.V3/Parse/ParseController.cs +++ b/src/Sonarr.Api.V3/Parse/ParseController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Download.Aggregation; using NzbDrone.Core.Parser; using Sonarr.Api.V3.Episodes; using Sonarr.Api.V3.Series; @@ -11,10 +12,13 @@ namespace Sonarr.Api.V3.Parse public class ParseController : Controller { private readonly IParsingService _parsingService; + private readonly IRemoteEpisodeAggregationService _aggregationService; - public ParseController(IParsingService parsingService) + public ParseController(IParsingService parsingService, + IRemoteEpisodeAggregationService aggregationService) { _parsingService = parsingService; + _aggregationService = aggregationService; } [HttpGet] @@ -38,6 +42,8 @@ namespace Sonarr.Api.V3.Parse var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0); + _aggregationService.Augment(remoteEpisode); + if (remoteEpisode != null) { return new ParseResource