📄 SpotifySearchProvider.cs
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.Extensions.Logging;
using MSearch.Domain;
using SpotifyAPI.Web;

namespace MSearch.SearchProviders.Spotify;

internal sealed class SpotifySearchProvider(ILogger<SpotifySearchProvider> logger, ISpotifyClient spotifyClient)
    : ISearchProvider
{
    public async IAsyncEnumerable<SearchResult> Search(
        SearchQuery query,
        [EnumeratorCancellation] CancellationToken cancellationToken
    )
    {
        var response = await spotifyClient.Search.Item(
            new(SearchRequest.Types.All, query.Term) { Limit = 3 },
            cancellationToken
        );

        if (response.Tracks.Items is not null)
        {
            foreach (var track in response.Tracks.Items)
            {
                if (TryMap(track) is not { } result)
                {
                    logger.FailedMappingTrack(track);
                    continue;
                }
                yield return result;
            }
        }

        if (response.Artists.Items is not null)
        {
            foreach (var artist in response.Artists.Items)
            {
                if (TryMap(artist) is not { } result)
                {
                    logger.FailedMappingArtist(artist);
                    continue;
                }
                yield return result;
            }
        }

        if (response.Albums.Items is not null)
        {
            foreach (var album in response.Albums.Items)
            {
                if (TryMap(album) is not { } result)
                {
                    logger.FailedMappingAlbum(album);
                    continue;
                }
                yield return result;
            }
        }
    }

    private static SearchResult? TryMap(FullTrack track) =>
        GetSpotifyUrl(track.ExternalUrls) is Uri spotifyUrl ? new(track.Name, Summary: null, spotifyUrl) : null;

    private static SearchResult? TryMap(FullArtist artist) =>
        GetSpotifyUrl(artist.ExternalUrls) is Uri spotifyUrl ? new(artist.Name, Summary: null, spotifyUrl) : null;

    private static SearchResult? TryMap(SimpleAlbum album) =>
        GetSpotifyUrl(album.ExternalUrls) is Uri spotifyUrl ? new(album.Name, Summary: null, spotifyUrl) : null;

    private static Uri? GetSpotifyUrl(IReadOnlyDictionary<string, string> externalUrls) =>
        externalUrls.GetValueOrDefault("spotify") is string spotifyUrl ? new(spotifyUrl) : null;
}

internal static partial class LoggerExtensions
{
    [LoggerMessage(LogLevel.Warning, "Failed mapping track {Track}")]
    public static partial void FailedMappingTrack(this ILogger logger, FullTrack track);

    [LoggerMessage(LogLevel.Warning, "Failed mapping artist {Artist}")]
    public static partial void FailedMappingArtist(this ILogger logger, FullArtist artist);

    [LoggerMessage(LogLevel.Warning, "Failed mapping album {Album}")]
    public static partial void FailedMappingAlbum(this ILogger logger, SimpleAlbum album);
}