Slots redesign nad images moved to images.yml

This commit is contained in:
Kwoth
2021-11-03 14:22:51 +00:00
parent 65062306c6
commit d090aa23ee
21 changed files with 649 additions and 590 deletions

View File

@@ -1,141 +0,0 @@
using Newtonsoft.Json.Linq;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Serilog;
namespace NadekoBot.Services.Common
{
public class ImageLoader
{
private readonly HttpClient _http;
private readonly ConnectionMultiplexer _con;
public Func<string, RedisKey> GetKey { get; }
private IDatabase _db => _con.GetDatabase();
private readonly List<Task<KeyValuePair<RedisKey, RedisValue>>> uriTasks = new List<Task<KeyValuePair<RedisKey, RedisValue>>>();
public ImageLoader(HttpClient http, ConnectionMultiplexer con, Func<string, RedisKey> getKey)
{
_http = http;
_con = con;
GetKey = getKey;
}
private async Task<byte[]> GetImageData(Uri uri)
{
if (uri.IsFile)
{
try
{
var bytes = await File.ReadAllBytesAsync(uri.LocalPath);
return bytes;
}
catch (Exception ex)
{
Log.Warning(ex, "Failed reading image bytes");
return null;
}
}
else
{
return await _http.GetByteArrayAsync(uri);
}
}
async Task HandleJArray(JArray arr, string key)
{
var tasks = arr.Where(x => x.Type == JTokenType.String)
.Select(async x =>
{
try
{
return await GetImageData((Uri)x).ConfigureAwait(false);
}
catch
{
Log.Error("Error retreiving image for key {Key}: {Data}", key, x);
return null;
}
});
byte[][] vals = Array.Empty<byte[]>();
vals = await Task.WhenAll(tasks).ConfigureAwait(false);
if (vals.Any(x => x is null))
vals = vals.Where(x => x != null).ToArray();
await _db.KeyDeleteAsync(GetKey(key)).ConfigureAwait(false);
await _db.ListRightPushAsync(GetKey(key),
vals.Where(x => x != null)
.Select(x => (RedisValue)x)
.ToArray()).ConfigureAwait(false);
if (arr.Count != vals.Length)
{
Log.Information("{2}/{1} URIs for the key '{0}' have been loaded. Some of the supplied URIs are either unavailable or invalid.", key, arr.Count, vals.Count());
}
}
async Task<KeyValuePair<RedisKey, RedisValue>> HandleUri(Uri uri, string key)
{
try
{
RedisValue data = await GetImageData(uri).ConfigureAwait(false);
return new KeyValuePair<RedisKey, RedisValue>(GetKey(key), data);
}
catch
{
Log.Information("Setting '{0}' image failed. The URI you provided is either unavailable or invalid.", key.ToLowerInvariant());
return new KeyValuePair<RedisKey, RedisValue>("", "");
}
}
Task HandleJObject(JObject obj, string parent = "")
{
string GetParentString()
{
if (string.IsNullOrWhiteSpace(parent))
return "";
else
return parent + "_";
}
List<Task> tasks = new List<Task>();
Task t;
// go through all of the kvps in the object
foreach (var kvp in obj)
{
// if it's a JArray, resole it using jarray method which will
// return task<byte[][]> aka an array of all images' bytes
if (kvp.Value.Type == JTokenType.Array)
{
t = HandleJArray((JArray)kvp.Value, GetParentString() + kvp.Key);
tasks.Add(t);
}
else if (kvp.Value.Type == JTokenType.String)
{
var uriTask = HandleUri((Uri)kvp.Value, GetParentString() + kvp.Key);
uriTasks.Add(uriTask);
}
else if (kvp.Value.Type == JTokenType.Object)
{
t = HandleJObject((JObject)kvp.Value, GetParentString() + kvp.Key);
tasks.Add(t);
}
}
return Task.WhenAll(tasks);
}
public async Task LoadAsync(JObject obj)
{
await HandleJObject(obj).ConfigureAwait(false);
var results = await Task.WhenAll(uriTasks).ConfigureAwait(false);
await _db.StringSetAsync(results.Where(x => x.Key != "").ToArray()).ConfigureAwait(false);
}
}
}

View File

@@ -1,52 +0,0 @@
using StackExchange.Redis;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace NadekoBot.Services.Common
{
public sealed class RedisImageArray : IReadOnlyList<byte[]>
{
public byte[] this[int index]
{
get
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
return _con.GetDatabase().ListGetByIndex(_key, index);
}
}
public int Count => _data.IsValueCreated
? _data.Value.Length
: (int)_con.GetDatabase().ListLength(_key);
private readonly ConnectionMultiplexer _con;
private readonly string _key;
private readonly Lazy<byte[][]> _data;
public RedisImageArray(string key, ConnectionMultiplexer con)
{
_con = con;
_key = key;
_data = new Lazy<byte[][]>(() => _con.GetDatabase().ListRange(_key).Select(x => (byte[])x).ToArray(), true);
}
public IEnumerator<byte[]> GetEnumerator()
{
var actualData = _data.Value;
for (int i = 0; i < actualData.Length; i++)
{
yield return actualData[i];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return _data.Value.GetEnumerator();
}
}
}

View File

@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace NadekoBot.Services
{
@@ -48,8 +49,11 @@ namespace NadekoBot.Services
}
RipFont = NotoSans.CreateFont(20, FontStyle.Bold);
DottyFont = FallBackFonts.First(x => x.Name == "dotty");
}
public FontFamily DottyFont { get; }
public FontFamily UniSans { get; }
public FontFamily NotoSans { get; }
//public FontFamily Emojis { get; }

View File

@@ -1,181 +0,0 @@
using NadekoBot.Common;
using NadekoBot.Services.Common;
using NadekoBot.Extensions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using NadekoBot.Common.ModuleBehaviors;
using Serilog;
namespace NadekoBot.Services
{
public sealed class RedisImagesCache : IImageCache, IReadyExecutor
{
private readonly ConnectionMultiplexer _con;
private readonly IBotCredentials _creds;
private readonly HttpClient _http;
private IDatabase _db => _con.GetDatabase();
private const string _basePath = "data/";
private const string _cardsPath = "data/images/cards";
public ImageUrls ImageUrls { get; private set; }
public IReadOnlyList<byte[]> Heads => GetByteArrayData(ImageKey.Coins_Heads);
public IReadOnlyList<byte[]> Tails => GetByteArrayData(ImageKey.Coins_Tails);
public IReadOnlyList<byte[]> Dice => GetByteArrayData(ImageKey.Dice);
public IReadOnlyList<byte[]> SlotEmojis => GetByteArrayData(ImageKey.Slots_Emojis);
public IReadOnlyList<byte[]> SlotNumbers => GetByteArrayData(ImageKey.Slots_Numbers);
public IReadOnlyList<byte[]> Currency => GetByteArrayData(ImageKey.Currency);
public byte[] SlotBackground => GetByteData(ImageKey.Slots_Bg);
public byte[] RategirlMatrix => GetByteData(ImageKey.Rategirl_Matrix);
public byte[] RategirlDot => GetByteData(ImageKey.Rategirl_Dot);
public byte[] XpBackground => GetByteData(ImageKey.Xp_Bg);
public byte[] Rip => GetByteData(ImageKey.Rip_Bg);
public byte[] RipOverlay => GetByteData(ImageKey.Rip_Overlay);
public byte[] GetCard(string key)
{
return _con.GetDatabase().StringGet(GetKey("card_" + key));
}
public enum ImageKey
{
Coins_Heads,
Coins_Tails,
Dice,
Slots_Bg,
Slots_Numbers,
Slots_Emojis,
Rategirl_Matrix,
Rategirl_Dot,
Xp_Bg,
Rip_Bg,
Rip_Overlay,
Currency,
}
public async Task OnReadyAsync()
{
if (await AllKeysExist())
return;
await Reload();
}
public RedisImagesCache(ConnectionMultiplexer con, IBotCredentials creds)
{
_con = con;
_creds = creds;
_http = new HttpClient();
ImageUrls = JsonConvert.DeserializeObject<ImageUrls>(
File.ReadAllText(Path.Combine(_basePath, "images.json")));
}
public async Task<bool> AllKeysExist()
{
try
{
var results = await Task.WhenAll(Enum.GetNames(typeof(ImageKey))
.Select(x => x.ToLowerInvariant())
.Select(x => _db.KeyExistsAsync(GetKey(x))))
.ConfigureAwait(false);
var cardsExist = await Task.WhenAll(GetAllCardNames()
.Select(x => "card_" + x)
.Select(x => _db.KeyExistsAsync(GetKey(x))))
.ConfigureAwait(false);
var num = results.Where(x => !x).Count();
return results.All(x => x) && cardsExist.All(x => x);
}
catch (Exception ex)
{
Log.Warning(ex, "Error checking for Image keys");
return false;
}
}
public async Task Reload()
{
try
{
var sw = Stopwatch.StartNew();
var obj = JObject.Parse(
File.ReadAllText(Path.Combine(_basePath, "images.json")));
ImageUrls = obj.ToObject<ImageUrls>();
var t = new ImageLoader(_http, _con, GetKey)
.LoadAsync(obj);
var loadCards = Task.Run(async () =>
{
await _db.StringSetAsync(Directory.GetFiles(_cardsPath)
.ToDictionary(
x => GetKey("card_" + Path.GetFileNameWithoutExtension(x)),
x => (RedisValue)File.ReadAllBytes(x)) // loads them and creates <name, bytes> pairs to store in redis
.ToArray())
.ConfigureAwait(false);
});
await Task.WhenAll(t, loadCards).ConfigureAwait(false);
sw.Stop();
Log.Information($"Images reloaded in {sw.Elapsed.TotalSeconds:F2}s");
}
catch (Exception ex)
{
Log.Error(ex, "Error reloading image service");
throw;
}
}
private IEnumerable<string> GetAllCardNames(bool showExtension = false)
{
return Directory.GetFiles(_cardsPath) // gets all cards from the cards folder
.Select(x => showExtension
? Path.GetFileName(x)
: Path.GetFileNameWithoutExtension(x)); // gets their names
}
public RedisKey GetKey(string key)
{
return $"{_creds.RedisKey()}_localimg_{key.ToLowerInvariant()}";
}
public byte[] GetByteData(string key)
{
return _db.StringGet(GetKey(key));
}
public byte[] GetByteData(ImageKey key) => GetByteData(key.ToString());
public RedisImageArray GetByteArrayData(string key)
{
return new RedisImageArray(GetKey(key), _con);
}
public RedisImageArray GetByteArrayData(ImageKey key) => GetByteArrayData(key.ToString());
}
}

View File

@@ -0,0 +1,13 @@
using System;
namespace NadekoBot.Services
{
public static class RedisImageExtensions
{
private const string OldCdnUrl = "nadeko-pictures.nyc3.digitaloceanspaces.com";
private const string NewCdnUrl = "cdn.nadeko.bot";
public static Uri ToNewCdn(this Uri uri)
=> new(uri.ToString().Replace(OldCdnUrl, NewCdnUrl));
}
}

View File

@@ -0,0 +1,312 @@
using NadekoBot.Common;
using NadekoBot.Extensions;
using Newtonsoft.Json;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Common.Yml;
using Serilog;
namespace NadekoBot.Services
{
public sealed class RedisImagesCache : IImageCache, IReadyExecutor
{
private readonly ConnectionMultiplexer _con;
private readonly IBotCredentials _creds;
private readonly HttpClient _http;
private readonly string _imagesPath;
private IDatabase _db => _con.GetDatabase();
private const string _basePath = "data/";
private const string _cardsPath = "data/images/cards";
public ImageUrls ImageUrls { get; private set; }
public enum ImageKeys
{
CoinHeads,
CoinTails,
Dice,
SlotBg,
SlotEmojis,
SlotNumbers,
Currency,
RategirlMatrix,
RategirlDot,
RipOverlay,
RipBg,
XpBg
}
public IReadOnlyList<byte[]> Heads
=> GetByteArrayData(ImageKeys.CoinHeads);
public IReadOnlyList<byte[]> Tails
=> GetByteArrayData(ImageKeys.CoinTails);
public IReadOnlyList<byte[]> Dice
=> GetByteArrayData(ImageKeys.Dice);
public IReadOnlyList<byte[]> SlotEmojis
=> GetByteArrayData(ImageKeys.SlotEmojis);
public IReadOnlyList<byte[]> SlotNumbers
=> GetByteArrayData(ImageKeys.SlotNumbers);
public IReadOnlyList<byte[]> Currency
=> GetByteArrayData(ImageKeys.Currency);
public byte[] SlotBackground
=> GetByteData(ImageKeys.SlotBg);
public byte[] RategirlMatrix
=> GetByteData(ImageKeys.RategirlMatrix);
public byte[] RategirlDot
=> GetByteData(ImageKeys.RategirlDot);
public byte[] XpBackground
=> GetByteData(ImageKeys.XpBg);
public byte[] Rip
=> GetByteData(ImageKeys.RipBg);
public byte[] RipOverlay
=> GetByteData(ImageKeys.RipOverlay);
public byte[] GetCard(string key)
{
// since cards are always local for now, don't cache them
return File.ReadAllBytes(Path.Join(_cardsPath, key + ".jpg"));
}
public async Task OnReadyAsync()
{
if (await AllKeysExist())
return;
await Reload();
}
public RedisImagesCache(ConnectionMultiplexer con, IBotCredentials creds)
{
_con = con;
_creds = creds;
_http = new HttpClient();
_imagesPath = Path.Combine(_basePath, "images.yml");
Migrate();
ImageUrls = Yaml.Deserializer.Deserialize<ImageUrls>(File.ReadAllText(_imagesPath));
}
private void Migrate()
{
// migrate to yml
if (File.Exists(Path.Combine(_basePath, "images.json")))
{
var oldFilePath = Path.Combine(_basePath, "images.json");
var backupFilePath = Path.Combine(_basePath, "images.json.backup");
var oldData = JsonConvert.DeserializeObject<OldImageUrls>(
File.ReadAllText(oldFilePath));
if (oldData is not null)
{
var newData = new ImageUrls()
{
Coins = new ImageUrls.CoinData()
{
Heads = oldData.Coins.Heads.Length == 1 &&
oldData.Coins.Heads[0].ToString() == "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/heads.png"
? new[] { new Uri("https://cdn.nadeko.bot/coins/heads3.png") }
: oldData.Coins.Heads,
Tails = oldData.Coins.Tails.Length == 1 &&
oldData.Coins.Tails[0].ToString() == "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/tails.png"
? new[] { new Uri("https://cdn.nadeko.bot/coins/tails3.png") }
: oldData.Coins.Tails,
},
Dice = oldData.Dice.Map(x => x.ToNewCdn()),
Currency = oldData.Currency.Map(x => x.ToNewCdn()),
Rategirl = new ImageUrls.RategirlData()
{
Dot = oldData.Rategirl.Dot.ToNewCdn(),
Matrix = oldData.Rategirl.Matrix.ToNewCdn()
},
Rip = new ImageUrls.RipData()
{
Bg = oldData.Rip.Bg.ToNewCdn(),
Overlay = oldData.Rip.Overlay.ToNewCdn(),
},
Slots = new ImageUrls.SlotData()
{
Bg = new Uri("https://cdn.nadeko.bot/slots/slots_bg.png"),
Emojis = new[]
{
"https://cdn.nadeko.bot/slots/0.png",
"https://cdn.nadeko.bot/slots/1.png",
"https://cdn.nadeko.bot/slots/2.png",
"https://cdn.nadeko.bot/slots/3.png",
"https://cdn.nadeko.bot/slots/4.png",
"https://cdn.nadeko.bot/slots/5.png"
}.Map(x => new Uri(x)),
Numbers = new[]
{
"https://cdn.nadeko.bot/other/slots/numbers/0.png",
"https://cdn.nadeko.bot/other/slots/numbers/1.png",
"https://cdn.nadeko.bot/other/slots/numbers/2.png",
"https://cdn.nadeko.bot/other/slots/numbers/3.png",
"https://cdn.nadeko.bot/other/slots/numbers/4.png",
"https://cdn.nadeko.bot/other/slots/numbers/5.png",
"https://cdn.nadeko.bot/other/slots/numbers/6.png",
"https://cdn.nadeko.bot/other/slots/numbers/7.png",
"https://cdn.nadeko.bot/other/slots/numbers/8.png",
"https://cdn.nadeko.bot/other/slots/numbers/9.png"
}.Map(x => new Uri(x)),
},
Xp = new ImageUrls.XpData()
{
Bg = oldData.Xp.Bg.ToNewCdn(),
},
Version = 2,
};
File.Move(oldFilePath, backupFilePath, true);
File.WriteAllText(_imagesPath, Yaml.Serializer.Serialize(newData));
}
}
}
public async Task Reload()
{
ImageUrls = Yaml.Deserializer.Deserialize<ImageUrls>(await File.ReadAllTextAsync(_imagesPath));
foreach (var key in GetAllKeys())
{
switch (key)
{
case ImageKeys.CoinHeads:
await Load(key, ImageUrls.Coins.Heads);
break;
case ImageKeys.CoinTails:
await Load(key, ImageUrls.Coins.Tails);
break;
case ImageKeys.Dice:
await Load(key, ImageUrls.Dice);
break;
case ImageKeys.SlotBg:
await Load(key, ImageUrls.Slots.Bg);
break;
case ImageKeys.SlotEmojis:
await Load(key, ImageUrls.Slots.Emojis);
break;
case ImageKeys.SlotNumbers:
await Load(key, ImageUrls.Slots.Numbers);
break;
case ImageKeys.Currency:
await Load(key, ImageUrls.Currency);
break;
case ImageKeys.RategirlMatrix:
await Load(key, ImageUrls.Rategirl.Matrix);
break;
case ImageKeys.RategirlDot:
await Load(key, ImageUrls.Rategirl.Dot);
break;
case ImageKeys.RipOverlay:
await Load(key, ImageUrls.Rip.Overlay);
break;
case ImageKeys.RipBg:
await Load(key, ImageUrls.Rip.Bg);
break;
case ImageKeys.XpBg:
await Load(key, ImageUrls.Xp.Bg);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
private async Task Load(ImageKeys key, Uri uri)
{
var data = await GetImageData(uri);
if (data is null)
return;
await _db.StringSetAsync(GetRedisKey(key), data);
}
private async Task Load(ImageKeys key, Uri[] uris)
{
await _db.KeyDeleteAsync(GetRedisKey(key));
var imageData = await Task.WhenAll(uris.Select(GetImageData));
var vals = imageData
.Where(x => x is not null)
.Select(x => (RedisValue)x)
.ToArray();
await _db.ListRightPushAsync(GetRedisKey(key), vals);
if (uris.Length != vals.Length)
{
Log.Information("{Loaded}/{Max} URIs for the key '{ImageKey}' have been loaded.\n" +
"Some of the supplied URIs are either unavailable or invalid.",
vals.Length, uris.Length, key);
}
}
private async Task<byte[]> GetImageData(Uri uri)
{
if (uri.IsFile)
{
try
{
var bytes = await File.ReadAllBytesAsync(uri.LocalPath);
return bytes;
}
catch (Exception ex)
{
Log.Warning(ex, "Failed reading image bytes from uri: {Uri}", uri.ToString());
return null;
}
}
try
{
return await _http.GetByteArrayAsync(uri);
}
catch (Exception ex)
{
Log.Warning(ex, "Image url you provided is not a valid image: {Uri}", uri.ToString());
return null;
}
}
private async Task<bool> AllKeysExist()
{
var tasks = await Task.WhenAll(GetAllKeys()
.Select(x => _db.KeyExistsAsync(GetRedisKey(x))));
return tasks.All(exist => exist);
}
private IEnumerable<ImageKeys> GetAllKeys() =>
Enum.GetValues<ImageKeys>();
private byte[][] GetByteArrayData(ImageKeys key)
=> _db.ListRange(GetRedisKey(key)).Map(x => (byte[])x);
private byte[] GetByteData(ImageKeys key)
=> _db.StringGet(GetRedisKey(key));
private RedisKey GetRedisKey(ImageKeys key)
=> _creds.RedisKey() + "_image_" + key;
}
}