feat(backend): search albums on YouTube Music

Two read-only endpoints: the album search and the album detail with its
tracklist. Both go through the same single-flight cache as the song search,
and the detail one especially benefits since it costs two upstream calls, the
browse id lookup and the album itself, for metadata that never changes.
Paginator handling is now shared between the song and album searches.
This commit is contained in:
2026-07-20 20:36:28 +02:00
parent dd15f885cb
commit 07e1daf2ce
5 changed files with 237 additions and 15 deletions
@@ -0,0 +1,21 @@
using ErrorOr;
using Mediator;
using Musify.Application.Contracts;
namespace Musify.Application.YouTube
{
public record GetYouTubeAlbumQuery(string AlbumId)
: IQuery<ErrorOr<YouTubeAlbumDetail>>;
public class GetYouTubeAlbumQueryHandler(IYouTubeMusicService youTubeMusicService)
: IQueryHandler<GetYouTubeAlbumQuery, ErrorOr<YouTubeAlbumDetail>>
{
public async ValueTask<ErrorOr<YouTubeAlbumDetail>> Handle(GetYouTubeAlbumQuery request, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(request.AlbumId))
return Error.Validation(description: "The album id must not be empty.");
return await youTubeMusicService.GetAlbumAsync(request.AlbumId, cancellationToken);
}
}
}
@@ -0,0 +1,21 @@
using ErrorOr;
using Mediator;
using Musify.Application.Contracts;
namespace Musify.Application.YouTube
{
public record SearchYouTubeAlbumsQuery(string Query, string ContinuationToken)
: IQuery<ErrorOr<YouTubeAlbumSearchResult>>;
public class SearchYouTubeAlbumsQueryHandler(IYouTubeMusicService youTubeMusicService)
: IQueryHandler<SearchYouTubeAlbumsQuery, ErrorOr<YouTubeAlbumSearchResult>>
{
public async ValueTask<ErrorOr<YouTubeAlbumSearchResult>> Handle(SearchYouTubeAlbumsQuery request, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(request.Query))
return Error.Validation(description: "The search query must not be empty.");
return await youTubeMusicService.SearchAlbumsAsync(request.Query, request.ContinuationToken, cancellationToken);
}
}
}