Skip to content

Commit c4792f8

Browse files
Kevin Heneveldclaude
authored andcommitted
fix(search): apply RelevanceFilter in DownloadService and LibraryController auto-pick flows too
The previous commit wired the context-aware RelevanceFilter into AutomaticSearchService only. The two other auto-pick callers (DownloadService.SearchAndDownloadAsync via the manual "search and download" endpoint, and LibraryController.ProcessAudiobookForSearchAsync via the bulk-search-all endpoint) bypassed the filter and went straight from search to scoring — so they could still pick an irrelevant result when the indexer returned only weak matches. DownloadService injects SearchResultFilterPipeline via its primary constructor; LibraryController resolves it from the existing scope factory to avoid widening its constructor. Both call pipeline.ApplyFilters(searchResults, audiobook: audiobook) before ScoreSearchResults, matching the AutomaticSearchService pattern. Manual search (no audiobook context) keeps the old behavior — the filter fails open without context. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent fdb44cb commit c4792f8

2 files changed

Lines changed: 44 additions & 1 deletion

File tree

listenarr.api/Controllers/LibraryController.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3195,6 +3195,30 @@ private async Task<int> ProcessAudiobookForSearchAsync(
31953195
return 0;
31963196
}
31973197

3198+
// Context-aware filter pass — reject results whose title isn't relevant to
3199+
// this specific audiobook before scoring picks one to download. This is the
3200+
// bulk-search-all endpoint, an auto-pick flow like AutomaticSearchService.
3201+
try
3202+
{
3203+
using var filterScope = _scopeFactory.CreateScope();
3204+
var filterPipeline = filterScope.ServiceProvider.GetRequiredService<Listenarr.Application.Search.Filters.SearchResultFilterPipeline>();
3205+
var preFilterCount = searchResults.Count;
3206+
searchResults = filterPipeline.ApplyFilters(searchResults, logFilteredResults: true, audiobook: audiobook);
3207+
if (searchResults.Count < preFilterCount)
3208+
{
3209+
_logger.LogInformation("Filtered {Removed} of {Total} raw results for audiobook '{Title}' via context-aware pipeline", preFilterCount - searchResults.Count, preFilterCount, LogRedaction.SanitizeText(audiobook.Title));
3210+
}
3211+
if (!searchResults.Any())
3212+
{
3213+
_logger.LogInformation("All search results filtered for audiobook '{Title}'", LogRedaction.SanitizeText(audiobook.Title));
3214+
return 0;
3215+
}
3216+
}
3217+
catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException)
3218+
{
3219+
_logger.LogDebug(ex, "SearchResultFilterPipeline unavailable; skipping context-aware filtering for audiobook {Id}", audiobook.Id);
3220+
}
3221+
31983222
// Score results against quality profile
31993223
var scoredResults = await qualityProfileService.ScoreSearchResults(searchResults, audiobook.QualityProfile!);
32003224

listenarr.application/Downloads/DownloadService.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
using Listenarr.Application.Interfaces;
2626
using Listenarr.Domain.Models;
2727
using Listenarr.Application.Interfaces.Repositories;
28+
using Listenarr.Application.Search.Filters;
2829
using Microsoft.Extensions.Logging;
2930
using Listenarr.Application.Security;
3031

@@ -44,7 +45,8 @@ public class DownloadService(
4445
IDownloadQueueService downloadQueueService,
4546
INotificationService notificationService,
4647
IHubBroadcaster hubBroadcaster,
47-
IDownloadHistoryService downloadHistoryService) : IDownloadService
48+
IDownloadHistoryService downloadHistoryService,
49+
SearchResultFilterPipeline filterPipeline) : IDownloadService
4850
{
4951
// Cache expiration constants
5052
private const int QueueCacheExpirationSeconds = 10;
@@ -206,6 +208,23 @@ public async Task<SearchAndDownloadResult> SearchAndDownloadAsync(int audiobookI
206208
};
207209
}
208210

211+
// Context-aware filter pass — reject results whose title isn't relevant to
212+
// this specific audiobook before scoring picks one to download.
213+
var preFilterCount = searchResults.Count;
214+
searchResults = filterPipeline.ApplyFilters(searchResults, logFilteredResults: true, audiobook: audiobook);
215+
if (searchResults.Count < preFilterCount)
216+
{
217+
logger.LogInformation("Filtered {Removed} of {Total} raw results for audiobook '{Title}' via context-aware pipeline", preFilterCount - searchResults.Count, preFilterCount, LogRedaction.SanitizeText(audiobook.Title));
218+
}
219+
if (!searchResults.Any())
220+
{
221+
return new SearchAndDownloadResult
222+
{
223+
Success = false,
224+
Message = "All results filtered as irrelevant"
225+
};
226+
}
227+
209228
// Score results against quality profile
210229
var scoredResults = await qualityProfileService.ScoreSearchResults(searchResults, audiobook.QualityProfile);
211230

0 commit comments

Comments
 (0)