This commit is contained in:
Oscar 2026-04-25 02:02:40 +00:00 committed by GitHub
commit aeb31aa8aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 79 additions and 7 deletions

View file

@ -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<ISceneMappingService>()
.Setup(s => s.GetSceneNames(It.IsAny<int>(), It.IsAny<List<int>>(), It.IsAny<List<int>>()))
.Returns(new List<string>());
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.AnimeSeasonSearchFallback)
.Returns(AnimeSeasonSearchFallback.Always);
}
private void WithEpisode(int seasonNumber, int episodeNumber, int? sceneSeasonNumber, int? sceneEpisodeNumber, string airDate = null)

View file

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

View file

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

View file

@ -0,0 +1,10 @@
namespace NzbDrone.Core.IndexerSearch
{
public enum AnimeSeasonSearchFallback
{
Never = 0,
Always = 1,
FullSeasonAired = 2,
FullSeasonNotAired = 3
}
}

View file

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

View file

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

View file

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

View file

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

View file

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