using EllieBot.Modules.Searches; using System.Net.Http.Json; namespace EllieBot.Modules.Music; public sealed class InvidiousYoutubeResolver : IYoutubeResolver { private readonly IHttpClientFactory _httpFactory; private readonly SearchesConfigService _sc; private readonly EllieRandom _rng; private string InvidiousApiUrl => _sc.Data.InvidiousInstances[_rng.Next(0, _sc.Data.InvidiousInstances.Count)]; public InvidiousYoutubeResolver(IHttpClientFactory httpFactory, SearchesConfigService sc) { _rng = new EllieRandom(); _httpFactory = httpFactory; _sc = sc; } public async Task<ITrackInfo?> ResolveByQueryAsync(string query) { using var http = _httpFactory.CreateClient(); var items = await http.GetFromJsonAsync<List<InvidiousSearchResponse>>( $"{InvidiousApiUrl}/api/v1/search" + $"?q={query}" + $"&type=video"); if (items is null || items.Count == 0) return null; var res = items.First(); return new InvTrackInfo() { Id = res.VideoId, Title = res.Title, Url = $"https://youtube.com/watch?v={res.VideoId}", Thumbnail = res.Thumbnails?.Select(x => x.Url).FirstOrDefault() ?? string.Empty, Duration = TimeSpan.FromSeconds(res.LengthSeconds), Platform = MusicPlatform.Youtube, StreamUrl = null, }; } public async Task<ITrackInfo?> ResolveByIdAsync(string id) => await InternalResolveByIdAsync(id); private async Task<InvTrackInfo?> InternalResolveByIdAsync(string id) { using var http = _httpFactory.CreateClient(); var res = await http.GetFromJsonAsync<InvidiousVideoResponse>( $"{InvidiousApiUrl}/api/v1/videos/{id}"); if (res is null) return null; return new InvTrackInfo() { Id = res.VideoId, Title = res.Title, Url = $"https://youtube.com/watch?v={res.VideoId}", Thumbnail = res.Thumbnails?.Select(x => x.Url).FirstOrDefault() ?? string.Empty, Duration = TimeSpan.FromSeconds(res.LengthSeconds), Platform = MusicPlatform.Youtube, StreamUrl = res.AdaptiveFormats.FirstOrDefault(x => x.AudioQuality == "AUDIO_QUALITY_HIGH")?.Url ?? res.AdaptiveFormats.FirstOrDefault(x => x.AudioQuality == "AUDIO_QUALITY_MEDIUM")?.Url ?? res.AdaptiveFormats.FirstOrDefault(x => x.AudioQuality == "AUDIO_QUALITY_LOW")?.Url }; } public async IAsyncEnumerable<ITrackInfo> ResolveTracksFromPlaylistAsync(string query) { using var http = _httpFactory.CreateClient(); var res = await http.GetFromJsonAsync<InvidiousPlaylistResponse>( $"{InvidiousApiUrl}/api/v1/search?type=video&q={query}"); if (res is null) yield break; foreach (var video in res.Videos) { yield return new InvTrackInfo() { Id = video.VideoId, Title = video.Title, Url = $"https://youtube.com/watch?v={video.VideoId}", Thumbnail = video.Thumbnails?.Select(x => x.Url).FirstOrDefault() ?? string.Empty, Duration = TimeSpan.FromSeconds(video.LengthSeconds), Platform = MusicPlatform.Youtube, StreamUrl = null }; } } public Task<ITrackInfo?> ResolveByQueryAsync(string query, bool tryExtractingId) => ResolveByQueryAsync(query); public async Task<string?> GetStreamUrl(string videoId) { var video = await InternalResolveByIdAsync(videoId); return video?.StreamUrl; } }