diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs index fbbe4502b..88d843f16 100644 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs @@ -7,6 +7,7 @@ using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Core.Configuration; using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Indexers; @@ -60,6 +61,10 @@ public void SetUp() Mocker.GetMock() .Setup(s => s.GetSceneNames(It.IsAny(), It.IsAny>(), It.IsAny>())) .Returns(new List()); + + Mocker.GetMock() + .SetupGet(s => s.AnimeSeasonSearchFallback) + .Returns(AnimeSeasonSearchFallback.Always); } private void WithEpisode(int seasonNumber, int episodeNumber, int? sceneSeasonNumber, int? sceneEpisodeNumber, string airDate = null) diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 72ae79891..3e175fecc 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.Http.Proxy; using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.ImportLists; +using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.EpisodeImport; @@ -129,6 +130,13 @@ public int MinimumAge set { SetValue("MinimumAge", value); } } + public AnimeSeasonSearchFallback AnimeSeasonSearchFallback + { + get { return GetValueEnum("AnimeSeasonSearchFallback", AnimeSeasonSearchFallback.Always); } + + set { SetValue("AnimeSeasonSearchFallback", value); } + } + public ProperDownloadTypes DownloadPropersAndRepacks { get { return GetValueEnum("DownloadPropersAndRepacks", ProperDownloadTypes.PreferAndUpgrade); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 5ebb51b94..6b352950c 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using NzbDrone.Common.Http.Proxy; using NzbDrone.Core.ImportLists; +using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.Qualities; @@ -52,6 +53,9 @@ public interface IConfigService string ChmodFolder { get; set; } string ChownGroup { get; set; } + // Anime Season Search + AnimeSeasonSearchFallback AnimeSeasonSearchFallback { get; set; } + // Indexers int Retention { get; set; } int RssSyncInterval { get; set; } diff --git a/src/NzbDrone.Core/IndexerSearch/AnimeSeasonSearchFallback.cs b/src/NzbDrone.Core/IndexerSearch/AnimeSeasonSearchFallback.cs new file mode 100644 index 000000000..80ffd7934 --- /dev/null +++ b/src/NzbDrone.Core/IndexerSearch/AnimeSeasonSearchFallback.cs @@ -0,0 +1,10 @@ +namespace NzbDrone.Core.IndexerSearch +{ + public enum AnimeSeasonSearchFallback + { + Never = 0, + Always = 1, + FullSeasonAired = 2, + FullSeasonNotAired = 3 + } +} diff --git a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs index 3af963f78..6da06b308 100644 --- a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.DataAugmentation.Scene; +using NzbDrone.Core.Configuration; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Exceptions; using NzbDrone.Core.Indexers; @@ -32,6 +33,7 @@ public class ReleaseSearchService : ISearchForReleases private readonly ISeriesService _seriesService; private readonly IEpisodeService _episodeService; private readonly IMakeDownloadDecision _makeDownloadDecision; + private readonly IConfigService _configService; private readonly Logger _logger; public ReleaseSearchService(IIndexerFactory indexerFactory, @@ -39,6 +41,7 @@ public ReleaseSearchService(IIndexerFactory indexerFactory, ISeriesService seriesService, IEpisodeService episodeService, IMakeDownloadDecision makeDownloadDecision, + IConfigService configService, Logger logger) { _indexerFactory = indexerFactory; @@ -46,6 +49,7 @@ public ReleaseSearchService(IIndexerFactory indexerFactory, _seriesService = seriesService; _episodeService = episodeService; _makeDownloadDecision = makeDownloadDecision; + _configService = configService; _logger = logger; } @@ -421,9 +425,44 @@ private async Task> SearchAnimeSeason(Series series, List downloadDecisions.AddRange(decisions); } - foreach (var episode in episodesToSearch) + // Only skip per-episode fallback if we got approved season results. + // Indexers like AB return all results for a title regardless of season + // params, so raw result count alone is not reliable. + // For interactive search, always run per-episode so the user can see + // and pick individual releases regardless of whether a pack was found. + if (!interactiveSearch && downloadDecisions.Any(d => d.Approved)) { - downloadDecisions.AddRange(await SearchAnime(series, episode, monitoredOnly, userInvokedSearch, interactiveSearch, true)); + _logger.Debug("Season search returned approved results for {0}, skipping per-episode search for {1} episodes", series.Title, episodesToSearch.Count); + } + else + { + var fallbackSetting = _configService.AnimeSeasonSearchFallback; + var allEpisodesAired = episodesToSearch.All(e => e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(DateTime.UtcNow)); + + var shouldFallback = interactiveSearch || (fallbackSetting switch + { + AnimeSeasonSearchFallback.Never => false, + AnimeSeasonSearchFallback.Always => true, + AnimeSeasonSearchFallback.FullSeasonAired => allEpisodesAired, + AnimeSeasonSearchFallback.FullSeasonNotAired => !allEpisodesAired, + _ => !allEpisodesAired + }); + + if (shouldFallback) + { + _logger.Debug("No approved season results for {0}, falling back to per-episode search for {1} episodes (fallback: {2}, allAired: {3})", + series.Title, episodesToSearch.Count, fallbackSetting, allEpisodesAired); + + foreach (var episode in episodesToSearch) + { + downloadDecisions.AddRange(await SearchAnime(series, episode, monitoredOnly, userInvokedSearch, interactiveSearch, true)); + } + } + else + { + _logger.Debug("No approved season results for {0}, per-episode fallback skipped (fallback: {1}, allAired: {2})", + series.Title, fallbackSetting, allEpisodesAired); + } } return DeDupeDecisions(downloadDecisions); diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs index 4dd06eb2a..b0ca4e6f4 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs @@ -450,7 +450,7 @@ public virtual IndexerPageableRequestChain GetSearchRequests(AnimeSeasonSearchCr { var pageableRequests = new IndexerPageableRequestChain(); - if (SupportsSearch && Settings.AnimeStandardFormatSearch && searchCriteria.SeasonNumber > 0) + if (SupportsSearch && searchCriteria.SeasonNumber > 0) { AddTvIdPageableRequests(pageableRequests, Settings.AnimeCategories, diff --git a/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs index 92e9d6254..367fed877 100644 --- a/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs @@ -87,9 +87,9 @@ public virtual IndexerPageableRequestChain GetSearchRequests(AnimeSeasonSearchCr { var pageableRequests = new IndexerPageableRequestChain(); - foreach (var searchTitle in searchCriteria.SceneTitles.Select(PrepareQuery)) + if (searchCriteria.SeasonNumber > 0) { - if (Settings.AnimeStandardFormatSearch && searchCriteria.SeasonNumber > 0) + foreach (var searchTitle in searchCriteria.SceneTitles.Select(PrepareQuery)) { pageableRequests.Add(GetPagedRequests($"{searchTitle}+s{searchCriteria.SeasonNumber:00}")); } diff --git a/src/Sonarr.Api.V3/Config/IndexerConfigResource.cs b/src/Sonarr.Api.V3/Config/IndexerConfigResource.cs index 6082d18b1..2ac3ae900 100644 --- a/src/Sonarr.Api.V3/Config/IndexerConfigResource.cs +++ b/src/Sonarr.Api.V3/Config/IndexerConfigResource.cs @@ -1,4 +1,5 @@ using NzbDrone.Core.Configuration; +using NzbDrone.Core.IndexerSearch; using Sonarr.Http.REST; namespace Sonarr.Api.V3.Config @@ -9,6 +10,7 @@ public class IndexerConfigResource : RestResource public int Retention { get; set; } public int MaximumSize { get; set; } public int RssSyncInterval { get; set; } + public AnimeSeasonSearchFallback AnimeSeasonSearchFallback { get; set; } } public static class IndexerConfigResourceMapper @@ -20,7 +22,8 @@ public static IndexerConfigResource ToResource(IConfigService model) MinimumAge = model.MinimumAge, Retention = model.Retention, MaximumSize = model.MaximumSize, - RssSyncInterval = model.RssSyncInterval + RssSyncInterval = model.RssSyncInterval, + AnimeSeasonSearchFallback = model.AnimeSeasonSearchFallback }; } } diff --git a/src/Sonarr.Api.V5/Settings/IndexerSettingsResource.cs b/src/Sonarr.Api.V5/Settings/IndexerSettingsResource.cs index 3c1ffaac5..282a5657a 100644 --- a/src/Sonarr.Api.V5/Settings/IndexerSettingsResource.cs +++ b/src/Sonarr.Api.V5/Settings/IndexerSettingsResource.cs @@ -1,4 +1,5 @@ using NzbDrone.Core.Configuration; +using NzbDrone.Core.IndexerSearch; using Sonarr.Http.REST; namespace Sonarr.Api.V5.Settings @@ -9,6 +10,7 @@ public class IndexerSettingsResource : RestResource public int Retention { get; set; } public int MaximumSize { get; set; } public int RssSyncInterval { get; set; } + public AnimeSeasonSearchFallback AnimeSeasonSearchFallback { get; set; } } public static class IndexerConfigResourceMapper @@ -20,7 +22,8 @@ public static IndexerSettingsResource ToResource(IConfigService model) MinimumAge = model.MinimumAge, Retention = model.Retention, MaximumSize = model.MaximumSize, - RssSyncInterval = model.RssSyncInterval + RssSyncInterval = model.RssSyncInterval, + AnimeSeasonSearchFallback = model.AnimeSeasonSearchFallback }; } }