From b8f0d79dee94f5e2c60809f0294d42ddd46ca269 Mon Sep 17 00:00:00 2001 From: ScrubN <72096833+ScrubN@users.noreply.github.com> Date: Sun, 16 Jun 2024 20:56:37 -0400 Subject: [PATCH] Refactor TwitchHelper.GetImage --- TwitchDownloaderCore/Tools/HighlightIcons.cs | 6 +- TwitchDownloaderCore/TwitchHelper.cs | 112 +++++++++---------- 2 files changed, 59 insertions(+), 59 deletions(-) diff --git a/TwitchDownloaderCore/Tools/HighlightIcons.cs b/TwitchDownloaderCore/Tools/HighlightIcons.cs index aee70c39..1a986769 100644 --- a/TwitchDownloaderCore/Tools/HighlightIcons.cs +++ b/TwitchDownloaderCore/Tools/HighlightIcons.cs @@ -56,7 +56,7 @@ public sealed class HighlightIcons : IDisposable private SKImage _watchStreakIcon; private SKImage _charityDonationIcon; - private readonly string _cachePath; + private readonly DirectoryInfo _cacheDir; private readonly SKColor _purple; private readonly bool _offline; private readonly double _fontSize; @@ -66,7 +66,7 @@ public sealed class HighlightIcons : IDisposable public HighlightIcons(ChatRenderOptions renderOptions, SKColor iconPurple, SKPaint outlinePaint) { - _cachePath = Path.Combine(renderOptions.TempFolder, "icons"); + _cacheDir = new DirectoryInfo(Path.Combine(renderOptions.TempFolder, "icons")); _purple = iconPurple; _offline = renderOptions.Offline; _fontSize = renderOptions.FontSize; @@ -184,7 +184,7 @@ private SKImage GenerateGiftedManyIcon() return SKImage.FromBitmap(offlineBitmap); } - var taskIconBytes = TwitchHelper.GetImage(_cachePath, GIFTED_MANY_ICON_URL, "gift-illus", "3", "png"); + var taskIconBytes = TwitchHelper.GetImage(_cacheDir, GIFTED_MANY_ICON_URL, "gift-illus", 3, "png", StubTaskProgress.Instance); taskIconBytes.Wait(); using var ms = new MemoryStream(taskIconBytes.Result); // Illustration is 72x72 using var codec = SKCodec.Create(ms); diff --git a/TwitchDownloaderCore/TwitchHelper.cs b/TwitchDownloaderCore/TwitchHelper.cs index 14aad3e5..7ec093d8 100644 --- a/TwitchDownloaderCore/TwitchHelper.cs +++ b/TwitchDownloaderCore/TwitchHelper.cs @@ -1,7 +1,6 @@ using SkiaSharp; using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; using System.Linq; @@ -9,6 +8,7 @@ using System.Net.Http; using System.Net.Http.Json; using System.Runtime.InteropServices; +using System.Security; using System.Text; using System.Text.RegularExpressions; using System.Threading; @@ -389,9 +389,9 @@ public static async Task> GetThirdPartyEmotes(List co return returnList; } - string bttvFolder = Path.Combine(cacheFolder, "bttv"); - string ffzFolder = Path.Combine(cacheFolder, "ffz"); - string stvFolder = Path.Combine(cacheFolder, "stv"); + DirectoryInfo bttvFolder = new DirectoryInfo(Path.Combine(cacheFolder, "bttv")); + DirectoryInfo ffzFolder = new DirectoryInfo(Path.Combine(cacheFolder, "ffz")); + DirectoryInfo stvFolder = new DirectoryInfo(Path.Combine(cacheFolder, "stv")); EmoteResponse emoteDataResponse = await GetThirdPartyEmotesMetadata(streamerId, bttv, ffz, stv, allowUnlistedEmotes, logger, cancellationToken); @@ -434,10 +434,10 @@ public static async Task> GetThirdPartyEmotes(List co return returnList; static async Task FetchEmoteImages(IReadOnlyCollection comments, IEnumerable emoteResponse, ICollection returnList, - ICollection alreadyAdded, string cacheFolder, ITaskLogger logger, CancellationToken cancellationToken) + ICollection alreadyAdded, DirectoryInfo cacheFolder, ITaskLogger logger, CancellationToken cancellationToken) { - if (!Directory.Exists(cacheFolder)) - CreateDirectory(cacheFolder); + if (!cacheFolder.Exists) + cacheFolder = CreateDirectory(cacheFolder.FullName); IEnumerable emoteResponseQuery; if (comments.Count == 0) @@ -457,7 +457,7 @@ where comments.Any(comment => Regex.IsMatch(comment.message.body, pattern)) { try { - var imageData = await GetImage(cacheFolder, emote.ImageUrl.Replace("[scale]", "2"), emote.Id, "2", emote.ImageType, cancellationToken); + var imageData = await GetImage(cacheFolder, emote.ImageUrl.Replace("[scale]", "2"), emote.Id, 2, emote.ImageType, logger, cancellationToken); var newEmote = new TwitchEmote(imageData, EmoteProvider.ThirdParty, 2, emote.Id, emote.Code); newEmote.IsZeroWidth = emote.IsZeroWidth; @@ -478,9 +478,9 @@ public static async Task> GetEmotes(List comments, st List alreadyAdded = new List(); List failedEmotes = new List(); - string emoteFolder = Path.Combine(cacheFolder, "emotes"); - if (!Directory.Exists(emoteFolder)) - TwitchHelper.CreateDirectory(emoteFolder); + DirectoryInfo emoteFolder = new DirectoryInfo(Path.Combine(cacheFolder, "emotes")); + if (!emoteFolder.Exists) + emoteFolder = CreateDirectory(emoteFolder.FullName); // Load our embedded emotes if (embeddedData?.firstParty != null) @@ -518,7 +518,7 @@ public static async Task> GetEmotes(List comments, st { try { - byte[] bytes = await GetImage(emoteFolder, $"https://static-cdn.jtvnw.net/emoticons/v2/{id}/default/dark/2.0", id, "2", "png", cancellationToken); + byte[] bytes = await GetImage(emoteFolder, $"https://static-cdn.jtvnw.net/emoticons/v2/{id}/default/dark/2.0", id, 2, "png", logger, cancellationToken); TwitchEmote newEmote = new TwitchEmote(bytes, EmoteProvider.FirstParty, 2, id, id); alreadyAdded.Add(id); returnList.Add(newEmote); @@ -638,9 +638,9 @@ public static async Task> GetChatBadges(List comments, List badgesData = await GetChatBadgesData(comments, streamerId, cancellationToken); - string badgeFolder = Path.Combine(cacheFolder, "badges"); - if (!Directory.Exists(badgeFolder)) - TwitchHelper.CreateDirectory(badgeFolder); + DirectoryInfo badgeFolder = new DirectoryInfo(Path.Combine(cacheFolder, "badges")); + if (!badgeFolder.Exists) + badgeFolder = CreateDirectory(badgeFolder.FullName); foreach(var badge in badgesData) { @@ -654,7 +654,7 @@ public static async Task> GetChatBadges(List comments, foreach (var (version, data) in badge.versions) { string id = data.url.Split('/')[^2]; - byte[] bytes = await GetImage(badgeFolder, data.url, id, "2", "png", cancellationToken); + byte[] bytes = await GetImage(badgeFolder, data.url, id, 2, "png", logger, cancellationToken); versions.Add(version, new ChatBadgeData { title = data.title, @@ -805,9 +805,9 @@ public static async Task> GetBits(List comments, strin cheerResponseMessage.EnsureSuccessStatusCode(); var cheerResponse = await cheerResponseMessage.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); - string bitFolder = Path.Combine(cacheFolder, "bits"); - if (!Directory.Exists(bitFolder)) - TwitchHelper.CreateDirectory(bitFolder); + DirectoryInfo bitFolder = new DirectoryInfo(Path.Combine(cacheFolder, "bits")); + if (!bitFolder.Exists) + bitFolder = CreateDirectory(bitFolder.FullName); if (cheerResponse?.data != null) { @@ -849,7 +849,8 @@ where comments { int minBits = tier.bits; string url = templateURL.Replace("PREFIX", node.prefix.ToLower()).Replace("BACKGROUND", "dark").Replace("ANIMATION", "animated").Replace("TIER", tier.bits.ToString()).Replace("SCALE.EXTENSION", "2.gif"); - TwitchEmote emote = new TwitchEmote(await GetImage(bitFolder, url, node.id + tier.bits, "2", "gif", cancellationToken), EmoteProvider.FirstParty, 2, prefix + minBits, prefix + minBits); + var bytes = await GetImage(bitFolder, url, node.id + tier.bits, 2, "gif", logger, cancellationToken); + TwitchEmote emote = new TwitchEmote(bytes, EmoteProvider.FirstParty, 2, prefix + minBits, prefix + minBits); tierList.Add(new KeyValuePair(minBits, emote)); } returnList.Add(newEmote); @@ -1011,65 +1012,64 @@ public static async Task GetUserInfo(IEnumerable id return await response.Content.ReadFromJsonAsync(); } - public static async Task GetImage(string cachePath, string imageUrl, string imageId, string imageScale, string imageType, CancellationToken cancellationToken = new()) + public static async Task GetImage(DirectoryInfo cacheDir, string imageUrl, string imageId, int imageScale, string imageType, ITaskLogger logger, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - byte[] imageBytes = null; + cacheDir.Refresh(); + if (!cacheDir.Exists) + { + CreateDirectory(cacheDir.FullName); + cacheDir.Refresh(); + } - if (!Directory.Exists(cachePath)) - CreateDirectory(cachePath); + byte[] imageBytes; - string filePath = Path.Combine(cachePath!, imageId + "_" + imageScale + "." + imageType); - if (File.Exists(filePath)) + var filePath = Path.Combine(cacheDir.FullName, $"{imageId}_{imageScale}.{imageType}"); + var file = new FileInfo(filePath); + + if (file.Exists) { try { - await using FileStream stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); - byte[] bytes = new byte[stream.Length]; - stream.Seek(0, SeekOrigin.Begin); - _ = await stream.ReadAsync(bytes, cancellationToken); + await using var fs = file.Open(FileMode.Open, FileAccess.Read, FileShare.Read); + imageBytes = new byte[fs.Length]; + _ = await fs.ReadAsync(imageBytes, cancellationToken); - //Check if image file is not corrupt - if (bytes.Length > 0) + if (imageBytes.Length > 0) { - using SKImage image = SKImage.FromEncodedData(bytes); - if (image != null) + using var ms = new MemoryStream(imageBytes); + using var codec = SKCodec.Create(ms, out var result); + + if (codec is not null) { - imageBytes = bytes; - } - else - { - //Try to delete the corrupted image - try - { - await stream.DisposeAsync(); - File.Delete(filePath); - } - catch { } + return imageBytes; } + + logger.LogVerbose($"Failed to decode {imageId} from cache: {result}"); } + + // Delete the corrupted image + file.Delete(); } - catch (IOException) + catch (Exception e) when (e is IOException or SecurityException) { - //File being written to by parallel process? Maybe. Can just fallback to HTTP request. + // File being written to by parallel process? Maybe. Can just fallback to HTTP request. + logger.LogVerbose($"Failed to read from or delete {file.Name}: {e.Message}"); } } - // If fetching from cache failed - if (imageBytes != null) - return imageBytes; - - // Fallback to HTTP request imageBytes = await httpClient.GetByteArrayAsync(imageUrl, cancellationToken); - //Let's save this image to the cache try { - await using var stream = File.Open(filePath, FileMode.Create, FileAccess.Write, FileShare.Read); - await stream.WriteAsync(imageBytes, cancellationToken); + await using var fs = file.Open(FileMode.Create, FileAccess.Write, FileShare.Read); + await fs.WriteAsync(imageBytes, cancellationToken); + } + catch (Exception e) + { + logger.LogVerbose($"Failed to open or write to {file.Name}: {e.Message}"); } - catch { } return imageBytes; }