📄 YouTubeSearchProvider.cs
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.Extensions.Options;
using MSearch.Domain;

namespace MSearch.SearchProviders.YouTube;

internal sealed class YouTubeSearchProvider(IOptions<YouTubeOptions> options, HttpClient httpClient) : ISearchProvider
{
    private readonly string apiKey = options.Value.ApiKey;

    public async IAsyncEnumerable<SearchResult> Search(
        SearchQuery query,
        [EnumeratorCancellation] CancellationToken cancellationToken
    )
    {
        var response = await httpClient.GetFromJsonAsync(
            $"https://www.googleapis.com/youtube/v3/search?key={apiKey}&part=snippet&q={Uri.EscapeDataString(query.Term)}",
            YouTubeJsonSerializerContext.Default.YouTubeSearchResponse,
            cancellationToken
        );
        if (response is null)
        {
            yield break;
        }
        foreach (var item in response.Items)
        {
            if (Map(item) is { } result)
            {
                yield return result;
            }
        }
    }

    private static SearchResult? Map(YouTubeItem item) =>
        Map(item.Id) is Uri url ? new(item.Snippet.Title, item.Snippet.Description, url) : null;

    private static Uri? Map(YouTubeId id) =>
        id switch
        {
            YouTubeId.Channel(var channelId) => new($"https://www.youtube.com/channel/{channelId}"),
            YouTubeId.Video(var videoId) => new($"https://www.youtube.com/video/{videoId}"),
            YouTubeId => null,
        };
}