using System.IO; using System.Net.Http; using CompatBot.ThumbScrapper; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; namespace CompatBot.Database.Providers; internal static class ThumbnailProvider { private static readonly HttpClient HttpClient = HttpClientFactory.Create(); private static readonly PsnClient.Client PsnClient = new(); private static readonly MemoryCache ColorCache = new(new MemoryCacheOptions{ ExpirationScanFrequency = TimeSpan.FromDays(1) }); public static async ValueTask GetThumbnailUrlAsync(this DiscordClient client, string? productCode) { if (string.IsNullOrEmpty(productCode)) return null; productCode = productCode.ToUpperInvariant(); var tmdbInfo = await PsnClient.GetTitleMetaAsync(productCode, Config.Cts.Token).ConfigureAwait(false); if (tmdbInfo is { Icon.Url: string tmdbIconUrl }) return tmdbIconUrl; await using (var db = await ThumbnailDb.OpenWriteAsync().ConfigureAwait(false)) { //todo: add search task if not found if (await db.Thumbnail .AsNoTracking() .FirstOrDefaultAsync(t => t.ProductCode == productCode) .ConfigureAwait(false) is { EmbeddableUrl: { Length: > 0 } embeddableUrl } ) { return embeddableUrl; } } await using var wdb = await ThumbnailDb.OpenWriteAsync().ConfigureAwait(false); var thumb = await wdb.Thumbnail.FirstOrDefaultAsync(t => t.ProductCode == productCode).ConfigureAwait(false); if (string.IsNullOrEmpty(thumb?.Url) || !ScrapeStateProvider.IsFresh(thumb.Timestamp)) { var gameTdbCoverUrl = await GameTdbScraper.GetThumbAsync(productCode).ConfigureAwait(false); if (!string.IsNullOrEmpty(gameTdbCoverUrl)) { if (thumb is null) thumb = (await wdb.Thumbnail.AddAsync(new() {ProductCode = productCode, Url = gameTdbCoverUrl}).ConfigureAwait(false)).Entity; else thumb.Url = gameTdbCoverUrl; thumb.Timestamp = DateTime.UtcNow.Ticks; await wdb.SaveChangesAsync().ConfigureAwait(false); } } if (string.IsNullOrEmpty(thumb?.Url)) return null; var contentName = thumb.ContentId ?? thumb.ProductCode; var (embedUrl, _) = await GetEmbeddableUrlAsync(client, contentName, thumb.Url).ConfigureAwait(false); if (embedUrl is null) return null; thumb.EmbeddableUrl = embedUrl; await wdb.SaveChangesAsync().ConfigureAwait(false); return embedUrl; } public static async ValueTask GetTitleNameAsync(string? productCode, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(productCode)) return null; productCode = productCode.ToUpperInvariant(); await using (var db = await ThumbnailDb.OpenReadAsync().ConfigureAwait(false)) { if (await db.Thumbnail .AsNoTracking() .FirstOrDefaultAsync(t => t.ProductCode == productCode, cancellationToken: cancellationToken) .ConfigureAwait(false) is { Name: { Length: > 0 } result } ) { return result; } } await using var wdb = await ThumbnailDb.OpenWriteAsync().ConfigureAwait(false); var thumb = await wdb.Thumbnail.FirstOrDefaultAsync( t => t.ProductCode == productCode, cancellationToken: cancellationToken ).ConfigureAwait(false); var title = (await PsnClient.GetTitleMetaAsync(productCode, cancellationToken).ConfigureAwait(false))?.Name; try { if (!string.IsNullOrEmpty(title)) { if (thumb is null) await wdb.Thumbnail.AddAsync(new() { ProductCode = productCode, Name = title, }, cancellationToken).ConfigureAwait(false); else thumb.Name = title; await wdb.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } } catch (Exception e) { Config.Log.Warn(e); } return title; } public static async ValueTask<(string? url, DiscordColor color)> GetThumbnailUrlWithColorAsync(DiscordClient client, string contentId, DiscordColor defaultColor, string? url = null) { if (string.IsNullOrEmpty(contentId)) throw new ArgumentException("ContentID can't be empty", nameof(contentId)); contentId = contentId.ToUpperInvariant(); await using var wdb = await ThumbnailDb.OpenWriteAsync().ConfigureAwait(false); //todo: fix this if it's ever get re-used var info = await wdb.Thumbnail.FirstOrDefaultAsync(ti => ti.ContentId == contentId, Config.Cts.Token).ConfigureAwait(false); info ??= new() {Url = url}; if (info.Url is null) return (null, defaultColor); DiscordColor? analyzedColor = null; if (string.IsNullOrEmpty(info.EmbeddableUrl)) { var (embedUrl, image) = await GetEmbeddableUrlAsync(client, contentId, info.Url).ConfigureAwait(false); if (embedUrl is string eUrl) { info.EmbeddableUrl = eUrl; if (image is byte[] jpg) { Config.Log.Trace("Getting dominant color for " + eUrl); await using var stream = Config.MemoryStreamManager.GetStream(jpg); analyzedColor = await ColorGetter.GetDominantColorAsync(stream, defaultColor).ConfigureAwait(false); if (analyzedColor.HasValue && analyzedColor.Value.Value != defaultColor.Value) info.EmbedColor = analyzedColor.Value.Value; } await wdb.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); } } if (!info.EmbedColor.HasValue && !analyzedColor.HasValue || info.EmbedColor.HasValue && info.EmbedColor.Value == defaultColor.Value) { var c = await GetImageColorAsync(info.EmbeddableUrl, defaultColor).ConfigureAwait(false); if (c.HasValue && c.Value.Value != defaultColor.Value) { info.EmbedColor = c.Value.Value; await wdb.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); } } var color = info.EmbedColor.HasValue ? new(info.EmbedColor.Value) : defaultColor; return (info.EmbeddableUrl, color); } public static async ValueTask<(string? url, byte[]? image)> GetEmbeddableUrlAsync(DiscordClient client, string contentId, string url) { try { if (!string.IsNullOrEmpty(Path.GetExtension(url))) return (url, null); await using var imgStream = await HttpClient.GetStreamAsync(url).ConfigureAwait(false); await using var memStream = Config.MemoryStreamManager.GetStream(); await imgStream.CopyToAsync(memStream).ConfigureAwait(false); // minimum jpg size is 119 bytes, png is 67 bytes if (memStream.Length < 64) return (null, null); memStream.Seek(0, SeekOrigin.Begin); var spam = await client.GetChannelAsync(Config.ThumbnailSpamId).ConfigureAwait(false); var message = await spam.SendMessageAsync(new DiscordMessageBuilder().AddFile(contentId + ".jpg", memStream).WithContent(contentId)).ConfigureAwait(false); url = message.Attachments[0].Url; return (url, memStream.ToArray()); } catch (Exception e) { Config.Log.Warn(e); } return (null, null); } public static async ValueTask GetImageColorAsync(string? url, DiscordColor? defaultColor = null) { try { if (string.IsNullOrEmpty(url)) return null; if (ColorCache.TryGetValue(url, out DiscordColor? result)) return result; await using var imgStream = await HttpClient.GetStreamAsync(url).ConfigureAwait(false); await using var memStream = Config.MemoryStreamManager.GetStream(); await imgStream.CopyToAsync(memStream).ConfigureAwait(false); // minimum jpg size is 119 bytes, png is 67 bytes if (memStream.Length < 64) return null; memStream.Seek(0, SeekOrigin.Begin); Config.Log.Trace("Getting dominant color for " + url); result = await ColorGetter.GetDominantColorAsync(memStream, defaultColor).ConfigureAwait(false); ColorCache.Set(url, result, TimeSpan.FromHours(1)); return result; } catch (Exception e) { Config.Log.Warn(e); } return null; } }