mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-04-27 12:02:51 -04:00
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using allstarr.Controllers;
|
||||||
|
|
||||||
|
namespace allstarr.Tests;
|
||||||
|
|
||||||
|
public class JellyfinControllerSearchLimitTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null, 20, true, 20, 20, 20)]
|
||||||
|
[InlineData("MusicAlbum", 20, true, 0, 20, 0)]
|
||||||
|
[InlineData("Audio", 20, true, 20, 0, 0)]
|
||||||
|
[InlineData("MusicArtist", 20, true, 0, 0, 20)]
|
||||||
|
[InlineData("Playlist", 20, true, 0, 20, 0)]
|
||||||
|
[InlineData("Playlist", 20, false, 0, 0, 0)]
|
||||||
|
[InlineData("Audio,MusicArtist", 15, true, 15, 0, 15)]
|
||||||
|
[InlineData("BoxSet", 10, true, 0, 0, 0)]
|
||||||
|
public void GetExternalSearchLimits_UsesRequestedItemTypes(
|
||||||
|
string? includeItemTypes,
|
||||||
|
int limit,
|
||||||
|
bool includePlaylistsAsAlbums,
|
||||||
|
int expectedSongLimit,
|
||||||
|
int expectedAlbumLimit,
|
||||||
|
int expectedArtistLimit)
|
||||||
|
{
|
||||||
|
var requestedTypes = string.IsNullOrWhiteSpace(includeItemTypes)
|
||||||
|
? null
|
||||||
|
: includeItemTypes.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
|
||||||
|
var method = typeof(JellyfinController).GetMethod(
|
||||||
|
"GetExternalSearchLimits",
|
||||||
|
BindingFlags.Static | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
Assert.NotNull(method);
|
||||||
|
|
||||||
|
var result = ((int SongLimit, int AlbumLimit, int ArtistLimit))method!.Invoke(
|
||||||
|
null,
|
||||||
|
new object?[] { requestedTypes, limit, includePlaylistsAsAlbums })!;
|
||||||
|
|
||||||
|
Assert.Equal(expectedSongLimit, result.SongLimit);
|
||||||
|
Assert.Equal(expectedAlbumLimit, result.AlbumLimit);
|
||||||
|
Assert.Equal(expectedArtistLimit, result.ArtistLimit);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -299,6 +299,65 @@ public class SquidWTFMetadataServiceTests
|
|||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SearchAllAsync_WithZeroLimits_SkipsUnusedBuckets()
|
||||||
|
{
|
||||||
|
var requestKinds = new List<string>();
|
||||||
|
var handler = new StubHttpMessageHandler(request =>
|
||||||
|
{
|
||||||
|
var trackQuery = GetQueryParameter(request.RequestUri!, "s");
|
||||||
|
var albumQuery = GetQueryParameter(request.RequestUri!, "al");
|
||||||
|
var artistQuery = GetQueryParameter(request.RequestUri!, "a");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(trackQuery))
|
||||||
|
{
|
||||||
|
requestKinds.Add("song");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent(CreateTrackSearchResponse(CreateTrackPayload(1, "Song", "USRC12345678")))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(albumQuery))
|
||||||
|
{
|
||||||
|
requestKinds.Add("album");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent(CreateAlbumSearchResponse())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(artistQuery))
|
||||||
|
{
|
||||||
|
requestKinds.Add("artist");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent(CreateArtistSearchResponse())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException($"Unexpected request URI: {request.RequestUri}");
|
||||||
|
});
|
||||||
|
|
||||||
|
var httpClient = new HttpClient(handler);
|
||||||
|
_mockHttpClientFactory.Setup(f => f.CreateClient(It.IsAny<string>())).Returns(httpClient);
|
||||||
|
|
||||||
|
var service = new SquidWTFMetadataService(
|
||||||
|
_mockHttpClientFactory.Object,
|
||||||
|
_subsonicSettings,
|
||||||
|
_squidwtfSettings,
|
||||||
|
_mockLogger.Object,
|
||||||
|
_mockCache.Object,
|
||||||
|
new List<string> { "https://test1.example.com" });
|
||||||
|
|
||||||
|
var result = await service.SearchAllAsync("OK Computer", 0, 5, 0);
|
||||||
|
|
||||||
|
Assert.Empty(result.Songs);
|
||||||
|
Assert.Single(result.Albums);
|
||||||
|
Assert.Empty(result.Artists);
|
||||||
|
Assert.Equal(new[] { "album" }, requestKinds);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ExplicitFilter_RespectsSettings()
|
public void ExplicitFilter_RespectsSettings()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using allstarr.Models.Domain;
|
||||||
using allstarr.Models.Search;
|
using allstarr.Models.Search;
|
||||||
using allstarr.Models.Subsonic;
|
using allstarr.Models.Subsonic;
|
||||||
using allstarr.Services.Common;
|
using allstarr.Services.Common;
|
||||||
@@ -304,6 +305,7 @@ public partial class JellyfinController
|
|||||||
|
|
||||||
// Run local and external searches in parallel
|
// Run local and external searches in parallel
|
||||||
var itemTypes = ParseItemTypes(includeItemTypes);
|
var itemTypes = ParseItemTypes(includeItemTypes);
|
||||||
|
var externalSearchLimits = GetExternalSearchLimits(itemTypes, limit, includePlaylistsAsAlbums: true);
|
||||||
var jellyfinTask = GetLocalSearchResultForCurrentRequest(
|
var jellyfinTask = GetLocalSearchResultForCurrentRequest(
|
||||||
cleanQuery,
|
cleanQuery,
|
||||||
includeItemTypes,
|
includeItemTypes,
|
||||||
@@ -312,12 +314,29 @@ public partial class JellyfinController
|
|||||||
recursive,
|
recursive,
|
||||||
userId);
|
userId);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"SEARCH TRACE: external limits for query '{Query}' => songs={SongLimit}, albums={AlbumLimit}, artists={ArtistLimit}",
|
||||||
|
cleanQuery,
|
||||||
|
externalSearchLimits.SongLimit,
|
||||||
|
externalSearchLimits.AlbumLimit,
|
||||||
|
externalSearchLimits.ArtistLimit);
|
||||||
|
|
||||||
// Use parallel metadata service if available (races providers), otherwise use primary
|
// Use parallel metadata service if available (races providers), otherwise use primary
|
||||||
var externalTask = favoritesOnlyRequest
|
var externalTask = favoritesOnlyRequest
|
||||||
? Task.FromResult(new SearchResult())
|
? Task.FromResult(new SearchResult())
|
||||||
: _parallelMetadataService != null
|
: _parallelMetadataService != null
|
||||||
? _parallelMetadataService.SearchAllAsync(cleanQuery, limit, limit, limit, HttpContext.RequestAborted)
|
? _parallelMetadataService.SearchAllAsync(
|
||||||
: _metadataService.SearchAllAsync(cleanQuery, limit, limit, limit, HttpContext.RequestAborted);
|
cleanQuery,
|
||||||
|
externalSearchLimits.SongLimit,
|
||||||
|
externalSearchLimits.AlbumLimit,
|
||||||
|
externalSearchLimits.ArtistLimit,
|
||||||
|
HttpContext.RequestAborted)
|
||||||
|
: _metadataService.SearchAllAsync(
|
||||||
|
cleanQuery,
|
||||||
|
externalSearchLimits.SongLimit,
|
||||||
|
externalSearchLimits.AlbumLimit,
|
||||||
|
externalSearchLimits.ArtistLimit,
|
||||||
|
HttpContext.RequestAborted);
|
||||||
|
|
||||||
var playlistTask = favoritesOnlyRequest || !_settings.EnableExternalPlaylists
|
var playlistTask = favoritesOnlyRequest || !_settings.EnableExternalPlaylists
|
||||||
? Task.FromResult(new List<ExternalPlaylist>())
|
? Task.FromResult(new List<ExternalPlaylist>())
|
||||||
@@ -672,11 +691,36 @@ public partial class JellyfinController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var cleanQuery = searchTerm.Trim().Trim('"');
|
var cleanQuery = searchTerm.Trim().Trim('"');
|
||||||
|
var requestedTypes = ParseItemTypes(includeItemTypes);
|
||||||
|
var externalSearchLimits = GetExternalSearchLimits(requestedTypes, limit, includePlaylistsAsAlbums: false);
|
||||||
|
var includesSongs = requestedTypes == null || requestedTypes.Length == 0 ||
|
||||||
|
requestedTypes.Contains("Audio", StringComparer.OrdinalIgnoreCase);
|
||||||
|
var includesAlbums = requestedTypes == null || requestedTypes.Length == 0 ||
|
||||||
|
requestedTypes.Contains("MusicAlbum", StringComparer.OrdinalIgnoreCase);
|
||||||
|
var includesArtists = requestedTypes == null || requestedTypes.Length == 0 ||
|
||||||
|
requestedTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"SEARCH TRACE: hint limits for query '{Query}' => songs={SongLimit}, albums={AlbumLimit}, artists={ArtistLimit}",
|
||||||
|
cleanQuery,
|
||||||
|
externalSearchLimits.SongLimit,
|
||||||
|
externalSearchLimits.AlbumLimit,
|
||||||
|
externalSearchLimits.ArtistLimit);
|
||||||
|
|
||||||
// Use parallel metadata service if available (races providers), otherwise use primary
|
// Use parallel metadata service if available (races providers), otherwise use primary
|
||||||
var externalTask = _parallelMetadataService != null
|
var externalTask = _parallelMetadataService != null
|
||||||
? _parallelMetadataService.SearchAllAsync(cleanQuery, limit, limit, limit, HttpContext.RequestAborted)
|
? _parallelMetadataService.SearchAllAsync(
|
||||||
: _metadataService.SearchAllAsync(cleanQuery, limit, limit, limit, HttpContext.RequestAborted);
|
cleanQuery,
|
||||||
|
externalSearchLimits.SongLimit,
|
||||||
|
externalSearchLimits.AlbumLimit,
|
||||||
|
externalSearchLimits.ArtistLimit,
|
||||||
|
HttpContext.RequestAborted)
|
||||||
|
: _metadataService.SearchAllAsync(
|
||||||
|
cleanQuery,
|
||||||
|
externalSearchLimits.SongLimit,
|
||||||
|
externalSearchLimits.AlbumLimit,
|
||||||
|
externalSearchLimits.ArtistLimit,
|
||||||
|
HttpContext.RequestAborted);
|
||||||
|
|
||||||
// Run searches in parallel (local Jellyfin hints + external providers)
|
// Run searches in parallel (local Jellyfin hints + external providers)
|
||||||
var jellyfinTask = GetLocalSearchHintsResultForCurrentRequest(cleanQuery, userId);
|
var jellyfinTask = GetLocalSearchHintsResultForCurrentRequest(cleanQuery, userId);
|
||||||
@@ -689,9 +733,15 @@ public partial class JellyfinController
|
|||||||
var (localSongs, localAlbums, localArtists) = _modelMapper.ParseSearchHintsResponse(jellyfinResult);
|
var (localSongs, localAlbums, localArtists) = _modelMapper.ParseSearchHintsResponse(jellyfinResult);
|
||||||
|
|
||||||
// NO deduplication - merge all results and take top matches
|
// NO deduplication - merge all results and take top matches
|
||||||
var allSongs = localSongs.Concat(externalResult.Songs).Take(limit).ToList();
|
var allSongs = includesSongs
|
||||||
var allAlbums = localAlbums.Concat(externalResult.Albums).Take(limit).ToList();
|
? localSongs.Concat(externalResult.Songs).Take(limit).ToList()
|
||||||
var allArtists = localArtists.Concat(externalResult.Artists).Take(limit).ToList();
|
: new List<Song>();
|
||||||
|
var allAlbums = includesAlbums
|
||||||
|
? localAlbums.Concat(externalResult.Albums).Take(limit).ToList()
|
||||||
|
: new List<Album>();
|
||||||
|
var allArtists = includesArtists
|
||||||
|
? localArtists.Concat(externalResult.Artists).Take(limit).ToList()
|
||||||
|
: new List<Artist>();
|
||||||
|
|
||||||
return _responseBuilder.CreateSearchHintsResponse(
|
return _responseBuilder.CreateSearchHintsResponse(
|
||||||
allSongs.Take(limit).ToList(),
|
allSongs.Take(limit).ToList(),
|
||||||
@@ -742,6 +792,33 @@ public partial class JellyfinController
|
|||||||
return string.Equals(Request.Query["IsFavorite"].ToString(), "true", StringComparison.OrdinalIgnoreCase);
|
return string.Equals(Request.Query["IsFavorite"].ToString(), "true", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static (int SongLimit, int AlbumLimit, int ArtistLimit) GetExternalSearchLimits(
|
||||||
|
string[]? requestedTypes,
|
||||||
|
int limit,
|
||||||
|
bool includePlaylistsAsAlbums)
|
||||||
|
{
|
||||||
|
if (limit <= 0)
|
||||||
|
{
|
||||||
|
return (0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestedTypes == null || requestedTypes.Length == 0)
|
||||||
|
{
|
||||||
|
return (limit, limit, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
var includeSongs = requestedTypes.Contains("Audio", StringComparer.OrdinalIgnoreCase);
|
||||||
|
var includeAlbums = requestedTypes.Contains("MusicAlbum", StringComparer.OrdinalIgnoreCase) ||
|
||||||
|
(includePlaylistsAsAlbums &&
|
||||||
|
requestedTypes.Contains("Playlist", StringComparer.OrdinalIgnoreCase));
|
||||||
|
var includeArtists = requestedTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
return (
|
||||||
|
includeSongs ? limit : 0,
|
||||||
|
includeAlbums ? limit : 0,
|
||||||
|
includeArtists ? limit : 0);
|
||||||
|
}
|
||||||
|
|
||||||
private static IActionResult CreateEmptyItemsResponse(int startIndex)
|
private static IActionResult CreateEmptyItemsResponse(int startIndex)
|
||||||
{
|
{
|
||||||
return new JsonResult(new
|
return new JsonResult(new
|
||||||
|
|||||||
@@ -135,10 +135,15 @@ public class DeezerMetadataService : TrackParserBase, IMusicMetadataService
|
|||||||
|
|
||||||
public async Task<SearchResult> SearchAllAsync(string query, int songLimit = 20, int albumLimit = 20, int artistLimit = 20, CancellationToken cancellationToken = default)
|
public async Task<SearchResult> SearchAllAsync(string query, int songLimit = 20, int albumLimit = 20, int artistLimit = 20, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
// Execute searches in parallel
|
var songsTask = songLimit > 0
|
||||||
var songsTask = SearchSongsAsync(query, songLimit, cancellationToken);
|
? SearchSongsAsync(query, songLimit, cancellationToken)
|
||||||
var albumsTask = SearchAlbumsAsync(query, albumLimit, cancellationToken);
|
: Task.FromResult(new List<Song>());
|
||||||
var artistsTask = SearchArtistsAsync(query, artistLimit, cancellationToken);
|
var albumsTask = albumLimit > 0
|
||||||
|
? SearchAlbumsAsync(query, albumLimit, cancellationToken)
|
||||||
|
: Task.FromResult(new List<Album>());
|
||||||
|
var artistsTask = artistLimit > 0
|
||||||
|
? SearchArtistsAsync(query, artistLimit, cancellationToken)
|
||||||
|
: Task.FromResult(new List<Artist>());
|
||||||
|
|
||||||
await Task.WhenAll(songsTask, albumsTask, artistsTask);
|
await Task.WhenAll(songsTask, albumsTask, artistsTask);
|
||||||
|
|
||||||
|
|||||||
@@ -160,9 +160,15 @@ public class QobuzMetadataService : TrackParserBase, IMusicMetadataService
|
|||||||
|
|
||||||
public async Task<SearchResult> SearchAllAsync(string query, int songLimit = 20, int albumLimit = 20, int artistLimit = 20, CancellationToken cancellationToken = default)
|
public async Task<SearchResult> SearchAllAsync(string query, int songLimit = 20, int albumLimit = 20, int artistLimit = 20, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var songsTask = SearchSongsAsync(query, songLimit, cancellationToken);
|
var songsTask = songLimit > 0
|
||||||
var albumsTask = SearchAlbumsAsync(query, albumLimit, cancellationToken);
|
? SearchSongsAsync(query, songLimit, cancellationToken)
|
||||||
var artistsTask = SearchArtistsAsync(query, artistLimit, cancellationToken);
|
: Task.FromResult(new List<Song>());
|
||||||
|
var albumsTask = albumLimit > 0
|
||||||
|
? SearchAlbumsAsync(query, albumLimit, cancellationToken)
|
||||||
|
: Task.FromResult(new List<Album>());
|
||||||
|
var artistsTask = artistLimit > 0
|
||||||
|
? SearchArtistsAsync(query, artistLimit, cancellationToken)
|
||||||
|
: Task.FromResult(new List<Artist>());
|
||||||
|
|
||||||
await Task.WhenAll(songsTask, albumsTask, artistsTask);
|
await Task.WhenAll(songsTask, albumsTask, artistsTask);
|
||||||
|
|
||||||
|
|||||||
@@ -498,14 +498,19 @@ public class SquidWTFMetadataService : TrackParserBase, IMusicMetadataService
|
|||||||
|
|
||||||
public async Task<SearchResult> SearchAllAsync(string query, int songLimit = 20, int albumLimit = 20, int artistLimit = 20, CancellationToken cancellationToken = default)
|
public async Task<SearchResult> SearchAllAsync(string query, int songLimit = 20, int albumLimit = 20, int artistLimit = 20, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
// Execute searches in parallel
|
var songsTask = songLimit > 0
|
||||||
var songsTask = SearchSongsAsync(query, songLimit, cancellationToken);
|
? SearchSongsAsync(query, songLimit, cancellationToken)
|
||||||
var albumsTask = SearchAlbumsAsync(query, albumLimit, cancellationToken);
|
: Task.FromResult(new List<Song>());
|
||||||
var artistsTask = SearchArtistsAsync(query, artistLimit, cancellationToken);
|
var albumsTask = albumLimit > 0
|
||||||
|
? SearchAlbumsAsync(query, albumLimit, cancellationToken)
|
||||||
|
: Task.FromResult(new List<Album>());
|
||||||
|
var artistsTask = artistLimit > 0
|
||||||
|
? SearchArtistsAsync(query, artistLimit, cancellationToken)
|
||||||
|
: Task.FromResult(new List<Artist>());
|
||||||
|
|
||||||
await Task.WhenAll(songsTask, albumsTask, artistsTask);
|
await Task.WhenAll(songsTask, albumsTask, artistsTask);
|
||||||
|
|
||||||
var temp = new SearchResult
|
var temp = new SearchResult
|
||||||
{
|
{
|
||||||
Songs = await songsTask,
|
Songs = await songsTask,
|
||||||
Albums = await albumsTask,
|
Albums = await albumsTask,
|
||||||
|
|||||||
Reference in New Issue
Block a user