From 3ec84f74b14076bca94b6a4bbd8af9d78d2bdad0 Mon Sep 17 00:00:00 2001 From: Matheus Fillipe Date: Wed, 19 Aug 2026 17:53:42 +0200 Subject: [PATCH] retry search with title only when title and author finds nothing --- .../Downloads/Queue/SearchQueryFallbacks.cs | 37 +++++++++++ .../Downloads/Submission/DownloadService.cs | 10 ++- .../Search/AutomaticSearchService.cs | 10 +++ .../Queue/SearchQueryFallbacksTests.cs | 65 +++++++++++++++++++ 4 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 listenarr.application/Downloads/Queue/SearchQueryFallbacks.cs create mode 100644 tests/Features/Application/Downloads/Queue/SearchQueryFallbacksTests.cs diff --git a/listenarr.application/Downloads/Queue/SearchQueryFallbacks.cs b/listenarr.application/Downloads/Queue/SearchQueryFallbacks.cs new file mode 100644 index 000000000..4a76280d8 --- /dev/null +++ b/listenarr.application/Downloads/Queue/SearchQueryFallbacks.cs @@ -0,0 +1,37 @@ +/* + * Listenarr - Audiobook Management System + * Copyright (C) 2024-2026 Listenarr Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +using System.Text.RegularExpressions; + +namespace Listenarr.Application.Downloads.Queue +{ + public static partial class SearchQueryFallbacks + { + [GeneratedRegex(@"\s*[\(\[][^\)\]]*[\)\]]", RegexOptions.None, matchTimeoutMilliseconds: 1000)] + private static partial Regex EditionSuffix(); + + public static IReadOnlyList Expand(string primaryQuery, string? title) + { + var candidates = new List { primaryQuery }; + + if (!string.IsNullOrWhiteSpace(title)) + { + candidates.Add(title); + candidates.Add(EditionSuffix().Replace(title, string.Empty)); + } + + return candidates + .Select(c => c?.Trim() ?? string.Empty) + .Where(c => !string.IsNullOrEmpty(c)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } + } +} diff --git a/listenarr.application/Downloads/Submission/DownloadService.cs b/listenarr.application/Downloads/Submission/DownloadService.cs index 5746081d0..56fddcddc 100644 --- a/listenarr.application/Downloads/Submission/DownloadService.cs +++ b/listenarr.application/Downloads/Submission/DownloadService.cs @@ -147,7 +147,15 @@ public async Task SearchAndDownloadAsync(int audiobookI // Search using the working search service. This is an automatic search (triggered // by the background/manual 'search-and-download' endpoint), so set isAutomaticSearch // to true to ensure only indexers are queried (no Amazon/Audible scraping). - var searchResults = await searchService.SearchAsync(searchQuery, isAutomaticSearch: true); + List? searchResults = null; + foreach (var fallbackQuery in SearchQueryFallbacks.Expand(searchQuery, audiobook.Title)) + { + searchResults = await searchService.SearchAsync(fallbackQuery, isAutomaticSearch: true); + if (searchResults != null && searchResults.Any()) + { + break; + } + } if (searchResults == null || !searchResults.Any()) { diff --git a/listenarr.infrastructure/HostedServices/Search/AutomaticSearchService.cs b/listenarr.infrastructure/HostedServices/Search/AutomaticSearchService.cs index 0c160ca18..8fa8ea687 100644 --- a/listenarr.infrastructure/HostedServices/Search/AutomaticSearchService.cs +++ b/listenarr.infrastructure/HostedServices/Search/AutomaticSearchService.cs @@ -180,6 +180,16 @@ private async Task ProcessAudiobookAsync( // Search for results var searchResults = await searchService.SearchAsync(searchQuery, isAutomaticSearch: true); + foreach (var candidate in SearchQueryFallbacks.Expand(searchQuery, audiobook.Title)) + { + if (searchResults.Count > 0) + { + break; + } + + searchQuery = candidate; + searchResults = await searchService.SearchAsync(candidate, isAutomaticSearch: true); + } _logger.LogInformation("Found {Count} raw search results for audiobook '{Title}'", searchResults.Count, audiobook.Title); // Broadcast detailed debug info about the raw search results to help diagnose automatic search failures diff --git a/tests/Features/Application/Downloads/Queue/SearchQueryFallbacksTests.cs b/tests/Features/Application/Downloads/Queue/SearchQueryFallbacksTests.cs new file mode 100644 index 000000000..1f598736e --- /dev/null +++ b/tests/Features/Application/Downloads/Queue/SearchQueryFallbacksTests.cs @@ -0,0 +1,65 @@ +/* + * Listenarr - Audiobook Management System + * Copyright (C) 2024-2026 Listenarr Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Listenarr.Tests.Features.Application.Downloads.Queue +{ + [Trait("Area", "Search")] + public class SearchQueryFallbacksTests + { + [Fact] + public void Expand_KeepsCombinedQueryFirst() + { + var queries = SearchQueryFallbacks.Expand("Red Rising Pierce Brown", "Red Rising"); + + Assert.Equal("Red Rising Pierce Brown", queries[0]); + } + + [Fact] + public void Expand_FallsBackToTitleOnly() + { + var queries = SearchQueryFallbacks.Expand("Project Hail Mary Andy Weir", "Project Hail Mary"); + + Assert.Contains("Project Hail Mary", queries); + } + + [Fact] + public void Expand_FallsBackToTitleWithoutEditionSuffix() + { + var queries = SearchQueryFallbacks.Expand( + "Red Rising (Part 1 of 2) (Dramatized Adaptation) Pierce Brown", + "Red Rising (Part 1 of 2) (Dramatized Adaptation)"); + + Assert.Equal("Red Rising", queries[^1]); + } + + [Fact] + public void Expand_DoesNotRepeatIdenticalQueries() + { + var queries = SearchQueryFallbacks.Expand("Dune", "Dune"); + + Assert.Single(queries); + } + + [Fact] + public void Expand_IgnoresMissingTitle() + { + var queries = SearchQueryFallbacks.Expand("Dune Frank Herbert", null); + + Assert.Equal(new[] { "Dune Frank Herbert" }, queries); + } + } +}