#nullable disable using Google; using Google.Apis.Services; using Google.Apis.Urlshortener.v1; using Google.Apis.YouTube.v3; using Newtonsoft.Json.Linq; using System.Net; using System.Text.RegularExpressions; using System.Xml; namespace EllieBot.Services; public sealed partial class GoogleApiService : IGoogleApiService, IEService { private static readonly Regex _plRegex = new(@"(?:youtu\.be\/|list=)(?[\da-zA-Z\-_]*)", RegexOptions.Compiled); private readonly YouTubeService _yt; private readonly UrlshortenerService _sh; //private readonly Regex YtVideoIdRegex = new Regex(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?[a-zA-Z0-9_-]{6,11})", RegexOptions.Compiled); private readonly IBotCredsProvider _creds; private readonly IHttpClientFactory _httpFactory; public GoogleApiService(IBotCredsProvider creds, IHttpClientFactory factory) : this() { _creds = creds; _httpFactory = factory; var bcs = new BaseClientService.Initializer { ApplicationName = "Ellie Bot", ApiKey = _creds.GetCreds().GoogleApiKey }; _yt = new(bcs); _sh = new(bcs); } public async Task> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1) { if (string.IsNullOrWhiteSpace(keywords)) throw new ArgumentNullException(nameof(keywords)); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count); var match = _plRegex.Match(keywords); if (match.Length > 1) return new[] { match.Groups["id"].Value }; var query = _yt.Search.List("snippet"); query.MaxResults = count; query.Type = "playlist"; query.Q = keywords; return (await query.ExecuteAsync()).Items.Select(i => i.Id.PlaylistId); } public async Task> GetRelatedVideosAsync(string id, int count = 2, string user = null) { if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id)); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count); var query = _yt.Search.List("snippet"); query.MaxResults = count; query.Q = id; // query.RelatedToVideoId = id; query.Type = "video"; query.QuotaUser = user; // bad workaround as there's no replacement for related video querying right now. // Query youtube with the id of the video, take a second video in the results // skip the first one as that's probably the same video. return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId).Skip(1); } public async Task> GetVideoLinksByKeywordAsync(string keywords, int count = 1) { if (string.IsNullOrWhiteSpace(keywords)) throw new ArgumentNullException(nameof(keywords)); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count); var query = _yt.Search.List("snippet"); query.MaxResults = count; query.Q = keywords; query.Type = "video"; query.SafeSearch = SearchResource.ListRequest.SafeSearchEnum.Strict; return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId); } public async Task> GetVideoInfosByKeywordAsync( string keywords, int count = 1) { if (string.IsNullOrWhiteSpace(keywords)) throw new ArgumentNullException(nameof(keywords)); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count); var query = _yt.Search.List("snippet"); query.MaxResults = count; query.Q = keywords; query.Type = "video"; return (await query.ExecuteAsync()).Items.Select(i => (i.Snippet.Title.TrimTo(50), i.Id.VideoId, "https://www.youtube.com/watch?v=" + i.Id.VideoId, i.Snippet.Thumbnails.High.Url)); } public Task ShortenUrl(Uri url) => ShortenUrl(url.ToString()); public async Task ShortenUrl(string url) { if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException(nameof(url)); if (string.IsNullOrWhiteSpace(_creds.GetCreds().GoogleApiKey)) return url; try { var response = await _sh.Url.Insert(new() { LongUrl = url }) .ExecuteAsync(); return response.Id; } catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.Forbidden) { return url; } catch (Exception ex) { Log.Warning(ex, "Error shortening URL"); return url; } } public async Task> GetPlaylistTracksAsync(string playlistId, int count = 50) { if (string.IsNullOrWhiteSpace(playlistId)) throw new ArgumentNullException(nameof(playlistId)); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count); string nextPageToken = null; var toReturn = new List(count); do { var toGet = count > 50 ? 50 : count; count -= toGet; var query = _yt.PlaylistItems.List("contentDetails"); query.MaxResults = toGet; query.PlaylistId = playlistId; query.PageToken = nextPageToken; var data = await query.ExecuteAsync(); toReturn.AddRange(data.Items.Select(i => i.ContentDetails.VideoId)); nextPageToken = data.NextPageToken; } while (count > 0 && !string.IsNullOrWhiteSpace(nextPageToken)); return toReturn; } public async Task> GetVideoDurationsAsync(IEnumerable videoIds) { var videoIdsList = videoIds as List ?? videoIds.ToList(); var toReturn = new Dictionary(); if (!videoIdsList.Any()) return toReturn; var remaining = videoIdsList.Count; do { var toGet = remaining > 50 ? 50 : remaining; remaining -= toGet; var q = _yt.Videos.List("contentDetails"); q.Id = string.Join(",", videoIdsList.Take(toGet)); videoIdsList = videoIdsList.Skip(toGet).ToList(); var items = (await q.ExecuteAsync()).Items; foreach (var i in items) toReturn.Add(i.Id, XmlConvert.ToTimeSpan(i.ContentDetails.Duration)); } while (remaining > 0); return toReturn; } public async Task Translate(string sourceText, string sourceLanguage, string targetLanguage) { string text; if (!Languages.ContainsKey(sourceLanguage) || !Languages.ContainsKey(targetLanguage)) throw new ArgumentException(nameof(sourceLanguage) + "/" + nameof(targetLanguage)); var url = new Uri(string.Format( "https://translate.googleapis.com/translate_a/single?client=gtx&sl={0}&tl={1}&dt=t&q={2}", ConvertToLanguageCode(sourceLanguage), ConvertToLanguageCode(targetLanguage), WebUtility.UrlEncode(sourceText))); using (var http = _httpFactory.CreateClient()) { http.DefaultRequestHeaders.Add("user-agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"); text = await http.GetStringAsync(url); } return string.Concat(JArray.Parse(text)[0].Select(x => x[0])); } private string ConvertToLanguageCode(string language) { Languages.TryGetValue(language, out var mode); return mode; } }