diff --git a/src/NzbDrone.Core.Test/IndexerTests/FanzubTests/FanzubRequestGeneratorFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/FanzubTests/FanzubRequestGeneratorFixture.cs new file mode 100644 index 000000000..b13e972fd --- /dev/null +++ b/src/NzbDrone.Core.Test/IndexerTests/FanzubTests/FanzubRequestGeneratorFixture.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Indexers.Fanzub; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.IndexerTests.FanzubTests +{ + public class FanzubRequestGeneratorFixture : CoreTest + { + private SeasonSearchCriteria _seasonSearchCriteria; + private AnimeEpisodeSearchCriteria _animeSearchCriteria; + + [SetUp] + public void SetUp() + { + Subject.Settings = new FanzubSettings() + { + BaseUrl = "http://127.0.0.1:1234/", + }; + + _seasonSearchCriteria = new SeasonSearchCriteria() + { + SceneTitles = new List() { "Naruto Shippuuden" }, + SeasonNumber = 1, + }; + + _animeSearchCriteria = new AnimeEpisodeSearchCriteria() + { + SceneTitles = new List() { "Naruto Shippuuden" }, + AbsoluteEpisodeNumber = 9, + SeasonNumber = 1, + EpisodeNumber = 9 + }; + } + + [Test] + public void should_not_search_season() + { + var results = Subject.GetSearchRequests(_seasonSearchCriteria); + + results.GetAllTiers().Should().HaveCount(0); + } + + [Test] + public void should_search_season() + { + Subject.Settings.AnimeStandardFormatSearch = true; + var results = Subject.GetSearchRequests(_seasonSearchCriteria); + + results.GetAllTiers().Should().HaveCount(1); + + var page = results.GetAllTiers().First().First(); + + page.Url.FullUri.Should().Contain("q=\"Naruto+Shippuuden%20S01\"|\"Naruto+Shippuuden%20-%20S01\""); + } + + [Test] + public void should_use_only_absolute_numbering_for_anime_search() + { + var results = Subject.GetSearchRequests(_animeSearchCriteria); + + results.GetAllTiers().Should().HaveCount(1); + + var page = results.GetAllTiers().First().First(); + + page.Url.FullUri.Should().Contain("q=\"Naruto+Shippuuden%2009\"|\"Naruto+Shippuuden%20-%2009\""); + } + + [Test] + public void should_also_use_standard_numbering_for_anime_search() + { + Subject.Settings.AnimeStandardFormatSearch = true; + var results = Subject.GetSearchRequests(_animeSearchCriteria); + + results.GetAllTiers().Should().HaveCount(1); + + var page = results.GetAllTiers().First().First(); + + page.Url.FullUri.Should().Contain("q=\"Naruto+Shippuuden%2009\"|\"Naruto+Shippuuden%20-%2009\"|\"Naruto+Shippuuden%20S01E09\"|\"Naruto+Shippuuden%20-%20S01E09\""); + } + } +} diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs index 53e8f7561..c111487a0 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabRequestGeneratorFixture.cs @@ -37,7 +37,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests _animeSearchCriteria = new AnimeEpisodeSearchCriteria() { SceneTitles = new List() { "Monkey+Island" }, - AbsoluteEpisodeNumber = 100 + AbsoluteEpisodeNumber = 100, + SeasonNumber = 5, + EpisodeNumber = 4 }; _capabilities = new NewznabCapabilities(); @@ -123,6 +125,31 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests pages.Count.Should().BeLessThan(500); } + [Test] + public void should_use_only_absolute_numbering_for_anime_search() + { + var results = Subject.GetSearchRequests(_animeSearchCriteria); + + results.GetAllTiers().Should().HaveCount(1); + + var page = results.GetAllTiers().First().First(); + + page.Url.FullUri.Should().Contain("q=Monkey%20Island+100"); + } + + [Test] + public void should_also_use_standard_numbering_for_anime_search() + { + Subject.Settings.AnimeStandardFormatSearch = true; + var results = Subject.GetSearchRequests(_animeSearchCriteria); + + results.GetTier(0).Should().HaveCount(2); + var pages = results.GetTier(0).Take(2).Select(t => t.First()).ToList(); + + pages[0].Url.FullUri.Should().Contain("q=Monkey%20Island+100"); + pages[1].Url.FullUri.Should().Contain("q=Monkey%20Island+s05e04"); + } + [Test] public void should_not_search_by_rid_if_not_supported() { diff --git a/src/NzbDrone.Core.Test/IndexerTests/NyaaTests/NyaaRequestGeneratorFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NyaaTests/NyaaRequestGeneratorFixture.cs new file mode 100644 index 000000000..cca362dc8 --- /dev/null +++ b/src/NzbDrone.Core.Test/IndexerTests/NyaaTests/NyaaRequestGeneratorFixture.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Indexers.Nyaa; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.IndexerTests.NyaaTests +{ + public class NyaaRequestGeneratorFixture : CoreTest + { + private SeasonSearchCriteria _seasonSearchCriteria; + private AnimeEpisodeSearchCriteria _animeSearchCriteria; + + [SetUp] + public void SetUp() + { + Subject.Settings = new NyaaSettings() + { + BaseUrl = "http://127.0.0.1:1234/", + }; + + _seasonSearchCriteria = new SeasonSearchCriteria() + { + SceneTitles = new List() { "Naruto Shippuuden" }, + SeasonNumber = 1, + }; + + _animeSearchCriteria = new AnimeEpisodeSearchCriteria() + { + SceneTitles = new List() { "Naruto Shippuuden" }, + AbsoluteEpisodeNumber = 9, + SeasonNumber = 1, + EpisodeNumber = 9 + }; + } + + [Test] + public void should_not_search_season() + { + var results = Subject.GetSearchRequests(_seasonSearchCriteria); + + results.GetAllTiers().Should().HaveCount(0); + } + + [Test] + public void should_search_season() + { + Subject.Settings.AnimeStandardFormatSearch = true; + var results = Subject.GetSearchRequests(_seasonSearchCriteria); + + results.GetAllTiers().Should().HaveCount(1); + + var page = results.GetAllTiers().First().First(); + + page.Url.FullUri.Should().Contain("term=Naruto+Shippuuden+s01"); + } + + [Test] + public void should_use_only_absolute_numbering_for_anime_search() + { + var results = Subject.GetSearchRequests(_animeSearchCriteria); + + results.GetTier(0).Should().HaveCount(2); + var pages = results.GetTier(0).Take(2).Select(t => t.First()).ToList(); + + pages[0].Url.FullUri.Should().Contain("term=Naruto+Shippuuden+9"); + pages[1].Url.FullUri.Should().Contain("term=Naruto+Shippuuden+09"); + } + + [Test] + public void should_also_use_standard_numbering_for_anime_search() + { + Subject.Settings.AnimeStandardFormatSearch = true; + var results = Subject.GetSearchRequests(_animeSearchCriteria); + + results.GetTier(0).Should().HaveCount(3); + var pages = results.GetTier(0).Take(3).Select(t => t.First()).ToList(); + + pages[0].Url.FullUri.Should().Contain("term=Naruto+Shippuuden+9"); + pages[1].Url.FullUri.Should().Contain("term=Naruto+Shippuuden+09"); + pages[2].Url.FullUri.Should().Contain("term=Naruto+Shippuuden+s01e09"); + } + } +} diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/AnimeEpisodeSearchCriteria.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/AnimeEpisodeSearchCriteria.cs index 297d36558..50fe0c867 100644 --- a/src/NzbDrone.Core/IndexerSearch/Definitions/AnimeEpisodeSearchCriteria.cs +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/AnimeEpisodeSearchCriteria.cs @@ -3,11 +3,13 @@ public class AnimeEpisodeSearchCriteria : SearchCriteriaBase { public int AbsoluteEpisodeNumber { get; set; } + public int EpisodeNumber { get; set; } + public int SeasonNumber { get; set; } public bool IsSeasonSearch { get; set; } public override string ToString() { - return string.Format("[{0} : {1:00}]", Series.Title, AbsoluteEpisodeNumber); + return $"[{Series.Title} : S{SeasonNumber:00}E{EpisodeNumber:00} ({AbsoluteEpisodeNumber:00})]"; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs index 6a07c7e39..8159afc81 100644 --- a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs @@ -349,19 +349,9 @@ namespace NzbDrone.Core.IndexerSearch searchSpec.IsSeasonSearch = isSeasonSearch; - if (episode.SceneAbsoluteEpisodeNumber.HasValue) - { - searchSpec.AbsoluteEpisodeNumber = episode.SceneAbsoluteEpisodeNumber.Value; - } - else if (episode.AbsoluteEpisodeNumber.HasValue) - { - searchSpec.AbsoluteEpisodeNumber = episode.AbsoluteEpisodeNumber.Value; - } - else - { - _logger.Error($"Can not search for {series.Title} - S{episode.SeasonNumber:00}E{episode.EpisodeNumber:00} it does not have an absolute episode number"); - throw new SearchFailedException("Absolute episode number is missing"); - } + searchSpec.SeasonNumber = episode.SceneSeasonNumber ?? episode.SeasonNumber; + searchSpec.EpisodeNumber = episode.SceneEpisodeNumber ?? episode.EpisodeNumber; + searchSpec.AbsoluteEpisodeNumber = episode.SceneAbsoluteEpisodeNumber ?? episode.AbsoluteEpisodeNumber ?? 0; return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); } diff --git a/src/NzbDrone.Core/Indexers/Fanzub/FanzubRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Fanzub/FanzubRequestGenerator.cs index 7c8ce1feb..831649c55 100644 --- a/src/NzbDrone.Core/Indexers/Fanzub/FanzubRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Fanzub/FanzubRequestGenerator.cs @@ -36,7 +36,15 @@ namespace NzbDrone.Core.Indexers.Fanzub public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria) { - return new IndexerPageableRequestChain(); + var pageableRequests = new IndexerPageableRequestChain(); + + if (Settings.AnimeStandardFormatSearch && searchCriteria.SeasonNumber > 0) + { + var searchTitles = searchCriteria.CleanSceneTitles.SelectMany(v => GetSeasonSearchStrings(v, searchCriteria.SeasonNumber)).ToList(); + pageableRequests.Add(GetPagedRequests(string.Join("|", searchTitles))); + } + + return pageableRequests; } public virtual IndexerPageableRequestChain GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria) @@ -55,6 +63,11 @@ namespace NzbDrone.Core.Indexers.Fanzub var searchTitles = searchCriteria.CleanSceneTitles.SelectMany(v => GetTitleSearchStrings(v, searchCriteria.AbsoluteEpisodeNumber)).ToList(); + if (Settings.AnimeStandardFormatSearch && searchCriteria.SeasonNumber > 0 && searchCriteria.EpisodeNumber > 0) + { + searchTitles.AddRange(searchCriteria.CleanSceneTitles.SelectMany(v => GetTitleSearchStrings(v, searchCriteria.SeasonNumber, searchCriteria.EpisodeNumber)).ToList()); + } + pageableRequests.Add(GetPagedRequests(string.Join("|", searchTitles))); return pageableRequests; @@ -78,6 +91,13 @@ namespace NzbDrone.Core.Indexers.Fanzub yield return new IndexerRequest(url.ToString(), HttpAccept.Rss); } + private IEnumerable GetSeasonSearchStrings(string title, int seasonNumber) + { + var formats = new[] { "{0}%20S{1:00}", "{0}%20-%20S{1:00}" }; + + return formats.Select(s => "\"" + string.Format(s, CleanTitle(title), seasonNumber) + "\""); + } + private IEnumerable GetTitleSearchStrings(string title, int absoluteEpisodeNumber) { var formats = new[] { "{0}%20{1:00}", "{0}%20-%20{1:00}" }; @@ -85,6 +105,13 @@ namespace NzbDrone.Core.Indexers.Fanzub return formats.Select(s => "\"" + string.Format(s, CleanTitle(title), absoluteEpisodeNumber) + "\""); } + private IEnumerable GetTitleSearchStrings(string title, int seasonNumber, int episodeNumber) + { + var formats = new[] { "{0}%20S{1:00}E{2:00}", "{0}%20-%20S{1:00}E{2:00}" }; + + return formats.Select(s => "\"" + string.Format(s, CleanTitle(title), seasonNumber, episodeNumber) + "\""); + } + private string CleanTitle(string title) { return RemoveCharactersRegex.Replace(title, ""); diff --git a/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs b/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs index fd07365fd..be5191140 100644 --- a/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs +++ b/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs @@ -25,6 +25,9 @@ namespace NzbDrone.Core.Indexers.Fanzub [FieldDefinition(0, Label = "Rss URL", HelpText = "Enter to URL to an Fanzub compatible RSS feed")] public string BaseUrl { get; set; } + [FieldDefinition(1, Label = "Anime Standard Format Search", Type = FieldType.Checkbox, HelpText = "Also search for anime using the standard numbering")] + public bool AnimeStandardFormatSearch { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs index 620195e34..23579375f 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs @@ -321,6 +321,15 @@ namespace NzbDrone.Core.Indexers.Newznab string.Format("&q={0}+{1:00}", NewsnabifyTitle(queryTitle), searchCriteria.AbsoluteEpisodeNumber))); + + if (Settings.AnimeStandardFormatSearch && searchCriteria.SeasonNumber > 0 && searchCriteria.EpisodeNumber > 0) + { + pageableRequests.Add(GetPagedRequests(MaxPages, Settings.AnimeCategories, "search", + string.Format("&q={0}+s{1:00}e{2:00}", + NewsnabifyTitle(queryTitle), + searchCriteria.SeasonNumber, + searchCriteria.EpisodeNumber))); + } } } diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs index ddb49d00f..699e0cc94 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs @@ -79,7 +79,10 @@ namespace NzbDrone.Core.Indexers.Newznab [FieldDefinition(4, Label = "Anime Categories", Type = FieldType.Select, SelectOptionsProviderAction = "newznabCategories", HelpText = "Drop down list, leave blank to disable anime")] public IEnumerable AnimeCategories { get; set; } - [FieldDefinition(5, Label = "Additional Parameters", HelpText = "Additional Newznab parameters", Advanced = true)] + [FieldDefinition(5, Label = "Anime Standard Format Search", Type = FieldType.Checkbox, HelpText = "Also search for anime using the standard numbering")] + public bool AnimeStandardFormatSearch { get; set; } + + [FieldDefinition(6, Label = "Additional Parameters", HelpText = "Additional Newznab parameters", Advanced = true)] public string AdditionalParameters { get; set; } // Field 6 is used by TorznabSettings MinimumSeeders diff --git a/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs index 8b8e3c93d..292843053 100644 --- a/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs @@ -33,7 +33,19 @@ namespace NzbDrone.Core.Indexers.Nyaa public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria) { - return new IndexerPageableRequestChain(); + var pageableRequests = new IndexerPageableRequestChain(); + + if (Settings.AnimeStandardFormatSearch && searchCriteria.SeasonNumber > 0) + { + foreach (var queryTitle in searchCriteria.SceneTitles) + { + var searchTitle = PrepareQuery(queryTitle); + + pageableRequests.Add(GetPagedRequests(MaxPages, $"{searchTitle}+s{searchCriteria.SeasonNumber:00}")); + } + } + + return pageableRequests; } public virtual IndexerPageableRequestChain GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria) @@ -54,11 +66,19 @@ namespace NzbDrone.Core.Indexers.Nyaa { var searchTitle = PrepareQuery(queryTitle); - pageableRequests.Add(GetPagedRequests(MaxPages, $"{searchTitle}+{searchCriteria.AbsoluteEpisodeNumber:0}")); - - if (searchCriteria.AbsoluteEpisodeNumber < 10) + if (searchCriteria.AbsoluteEpisodeNumber > 0) { - pageableRequests.Add(GetPagedRequests(MaxPages, $"{searchTitle}+{searchCriteria.AbsoluteEpisodeNumber:00}")); + pageableRequests.Add(GetPagedRequests(MaxPages, $"{searchTitle}+{searchCriteria.AbsoluteEpisodeNumber:0}")); + + if (searchCriteria.AbsoluteEpisodeNumber < 10) + { + pageableRequests.Add(GetPagedRequests(MaxPages, $"{searchTitle}+{searchCriteria.AbsoluteEpisodeNumber:00}")); + } + } + + if (Settings.AnimeStandardFormatSearch && searchCriteria.SeasonNumber > 0 && searchCriteria.EpisodeNumber > 0) + { + pageableRequests.Add(GetPagedRequests(MaxPages, $"{searchTitle}+s{searchCriteria.SeasonNumber:00}e{searchCriteria.EpisodeNumber:00}")); } } diff --git a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs index 3c3cffa14..f5bc2bd5a 100644 --- a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs +++ b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs @@ -30,13 +30,16 @@ namespace NzbDrone.Core.Indexers.Nyaa [FieldDefinition(0, Label = "Website URL")] public string BaseUrl { get; set; } - [FieldDefinition(1, Label = "Additional Parameters", Advanced = true, HelpText = "Please note if you change the category you will have to add required/restricted rules about the subgroups to avoid foreign language releases.")] + [FieldDefinition(1, Label = "Anime Standard Format Search", Type = FieldType.Checkbox, HelpText = "Also search for anime using the standard numbering")] + public bool AnimeStandardFormatSearch { get; set; } + + [FieldDefinition(2, Label = "Additional Parameters", Advanced = true, HelpText = "Please note if you change the category you will have to add required/restricted rules about the subgroups to avoid foreign language releases.")] public string AdditionalParameters { get; set; } - [FieldDefinition(2, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(3, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] public int MinimumSeeders { get; set; } - [FieldDefinition(3)] + [FieldDefinition(4)] public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings(); public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs index 476ba1932..b7784a497 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs @@ -57,10 +57,10 @@ namespace NzbDrone.Core.Indexers.Torznab MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; } - [FieldDefinition(6, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] + [FieldDefinition(7, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] public int MinimumSeeders { get; set; } - [FieldDefinition(7)] + [FieldDefinition(8)] public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings(); public override NzbDroneValidationResult Validate()