New: Add option to search for anime using standard episode numbers

Closes #4153
This commit is contained in:
Stéphane (Bakeneko) Dupont 2021-11-29 19:56:24 +01:00 committed by Mark McDowall
parent 05b1581b7d
commit cee17483d9
12 changed files with 282 additions and 27 deletions

View File

@ -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<FanzubRequestGenerator>
{
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<string>() { "Naruto Shippuuden" },
SeasonNumber = 1,
};
_animeSearchCriteria = new AnimeEpisodeSearchCriteria()
{
SceneTitles = new List<string>() { "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\"");
}
}
}

View File

@ -37,7 +37,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
_animeSearchCriteria = new AnimeEpisodeSearchCriteria()
{
SceneTitles = new List<string>() { "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()
{

View File

@ -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<NyaaRequestGenerator>
{
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<string>() { "Naruto Shippuuden" },
SeasonNumber = 1,
};
_animeSearchCriteria = new AnimeEpisodeSearchCriteria()
{
SceneTitles = new List<string>() { "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");
}
}
}

View File

@ -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})]";
}
}
}

View File

@ -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);
}

View File

@ -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<string> 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<string> 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<string> 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, "");

View File

@ -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));

View File

@ -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)));
}
}
}

View File

@ -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<int> 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

View File

@ -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}"));
}
}

View File

@ -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()

View File

@ -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()