mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-12 02:08:27 -04:00
Global usings and file scoped namespaces
This commit is contained in:
@@ -1,213 +1,208 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace NadekoBot.Modules.Music
|
||||
namespace NadekoBot.Modules.Music;
|
||||
|
||||
public sealed class RedisTrackCacher : ITrackCacher
|
||||
{
|
||||
public sealed class RedisTrackCacher : ITrackCacher
|
||||
private readonly ConnectionMultiplexer _multiplexer;
|
||||
|
||||
public RedisTrackCacher(ConnectionMultiplexer multiplexer)
|
||||
{
|
||||
private readonly ConnectionMultiplexer _multiplexer;
|
||||
|
||||
public RedisTrackCacher(ConnectionMultiplexer multiplexer)
|
||||
{
|
||||
_multiplexer = multiplexer;
|
||||
}
|
||||
_multiplexer = multiplexer;
|
||||
}
|
||||
|
||||
public async Task<string?> GetOrCreateStreamLink(
|
||||
string id,
|
||||
MusicPlatform platform,
|
||||
Func<Task<(string StreamUrl, TimeSpan Expiry)>> streamUrlFactory
|
||||
)
|
||||
public async Task<string?> GetOrCreateStreamLink(
|
||||
string id,
|
||||
MusicPlatform platform,
|
||||
Func<Task<(string StreamUrl, TimeSpan Expiry)>> streamUrlFactory
|
||||
)
|
||||
{
|
||||
var trackStreamKey = CreateStreamKey(id, platform);
|
||||
|
||||
var value = await GetStreamFromCacheInternalAsync(trackStreamKey);
|
||||
|
||||
// if there is no cached value
|
||||
if (value == default)
|
||||
{
|
||||
var trackStreamKey = CreateStreamKey(id, platform);
|
||||
|
||||
var value = await GetStreamFromCacheInternalAsync(trackStreamKey);
|
||||
|
||||
// if there is no cached value
|
||||
if (value == default)
|
||||
{
|
||||
// otherwise retrieve and cache a new value, and run this method again
|
||||
var success = await CreateAndCacheStreamUrlAsync(trackStreamKey, streamUrlFactory);
|
||||
if (!success)
|
||||
return null;
|
||||
// otherwise retrieve and cache a new value, and run this method again
|
||||
var success = await CreateAndCacheStreamUrlAsync(trackStreamKey, streamUrlFactory);
|
||||
if (!success)
|
||||
return null;
|
||||
|
||||
return await GetOrCreateStreamLink(id, platform, streamUrlFactory);
|
||||
}
|
||||
|
||||
// cache new one for future use
|
||||
_ = Task.Run(() => CreateAndCacheStreamUrlAsync(trackStreamKey, streamUrlFactory));
|
||||
|
||||
return value;
|
||||
return await GetOrCreateStreamLink(id, platform, streamUrlFactory);
|
||||
}
|
||||
|
||||
// cache new one for future use
|
||||
_ = Task.Run(() => CreateAndCacheStreamUrlAsync(trackStreamKey, streamUrlFactory));
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string CreateStreamKey(string id, MusicPlatform platform)
|
||||
=> $"track:stream:{platform}:{id}";
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string CreateStreamKey(string id, MusicPlatform platform)
|
||||
=> $"track:stream:{platform}:{id}";
|
||||
|
||||
private async Task<bool> CreateAndCacheStreamUrlAsync(
|
||||
string trackStreamKey,
|
||||
Func<Task<(string StreamUrl, TimeSpan Expiry)>> factory)
|
||||
private async Task<bool> CreateAndCacheStreamUrlAsync(
|
||||
string trackStreamKey,
|
||||
Func<Task<(string StreamUrl, TimeSpan Expiry)>> factory)
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = await factory();
|
||||
if (data == default)
|
||||
return false;
|
||||
|
||||
await CacheStreamUrlInternalAsync(trackStreamKey, data.StreamUrl, data.Expiry);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error resolving stream link for {TrackCacheKey}", trackStreamKey);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Task CacheStreamUrlAsync(string id, MusicPlatform platform, string url, TimeSpan expiry)
|
||||
=> CacheStreamUrlInternalAsync(CreateStreamKey(id, platform), url, expiry);
|
||||
|
||||
private async Task CacheStreamUrlInternalAsync(string trackStreamKey, string url, TimeSpan expiry)
|
||||
{
|
||||
// keys need to be expired after an hour
|
||||
// to make sure client doesn't get an expired stream url
|
||||
// to achieve this, track keys will be just pointers to real data
|
||||
// but that data will expire
|
||||
|
||||
var db = _multiplexer.GetDatabase();
|
||||
var dataKey = $"entry:{Guid.NewGuid()}:{trackStreamKey}";
|
||||
await db.StringSetAsync(dataKey, url, expiry: expiry);
|
||||
await db.ListRightPushAsync(trackStreamKey, dataKey);
|
||||
}
|
||||
|
||||
private async Task<string?> GetStreamFromCacheInternalAsync(string trackStreamKey)
|
||||
{
|
||||
// Job of the method which retrieves keys is to pop the elements
|
||||
// from the list of cached trackurls until it finds a non-expired key
|
||||
|
||||
var db = _multiplexer.GetDatabase();
|
||||
while(true)
|
||||
{
|
||||
string? dataKey = await db.ListLeftPopAsync(trackStreamKey);
|
||||
if (dataKey == default)
|
||||
return null;
|
||||
|
||||
var streamUrl = await db.StringGetAsync(dataKey);
|
||||
if (streamUrl == default)
|
||||
continue;
|
||||
|
||||
return streamUrl;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string CreateCachedDataKey(string id, MusicPlatform platform)
|
||||
=> $"track:data:{platform}:{id}";
|
||||
|
||||
public Task CacheTrackDataAsync(ICachableTrackData data)
|
||||
{
|
||||
var db = _multiplexer.GetDatabase();
|
||||
|
||||
var trackDataKey = CreateCachedDataKey(data.Id, data.Platform);
|
||||
var dataString = JsonSerializer.Serialize((object)data);
|
||||
// cache for 1 day
|
||||
return db.StringSetAsync(trackDataKey, dataString, expiry: TimeSpan.FromDays(1));
|
||||
}
|
||||
|
||||
public async Task<ICachableTrackData?> GetCachedDataByIdAsync(string id, MusicPlatform platform)
|
||||
{
|
||||
var db = _multiplexer.GetDatabase();
|
||||
|
||||
var trackDataKey = CreateCachedDataKey(id, platform);
|
||||
var data = await db.StringGetAsync(trackDataKey);
|
||||
var data = await factory();
|
||||
if (data == default)
|
||||
return null;
|
||||
return false;
|
||||
|
||||
return JsonSerializer.Deserialize<CachableTrackData>(data);
|
||||
await CacheStreamUrlInternalAsync(trackStreamKey, data.StreamUrl, data.Expiry);
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string CreateCachedQueryDataKey(string query, MusicPlatform platform)
|
||||
=> $"track:query_to_id:{platform}:{query}";
|
||||
public async Task<ICachableTrackData?> GetCachedDataByQueryAsync(string query, MusicPlatform platform)
|
||||
catch (Exception ex)
|
||||
{
|
||||
query = Uri.EscapeDataString(query.Trim());
|
||||
|
||||
var db = _multiplexer.GetDatabase();
|
||||
var queryDataKey = CreateCachedQueryDataKey(query, platform);
|
||||
|
||||
var trackId = await db.StringGetAsync(queryDataKey);
|
||||
if (trackId == default)
|
||||
return null;
|
||||
|
||||
return await GetCachedDataByIdAsync(trackId, platform);
|
||||
}
|
||||
|
||||
public async Task CacheTrackDataByQueryAsync(string query, ICachableTrackData data)
|
||||
{
|
||||
query = Uri.EscapeDataString(query.Trim());
|
||||
|
||||
// first cache the data
|
||||
await CacheTrackDataAsync(data);
|
||||
|
||||
// then map the query to cached data's id
|
||||
var db = _multiplexer.GetDatabase();
|
||||
|
||||
var queryDataKey = CreateCachedQueryDataKey(query, data.Platform);
|
||||
await db.StringSetAsync(queryDataKey, data.Id, TimeSpan.FromDays(7));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string CreateCachedPlaylistKey(string playlistId, MusicPlatform platform)
|
||||
=> $"playlist:{platform}:{playlistId}";
|
||||
public async Task<IReadOnlyCollection<string>> GetPlaylistTrackIdsAsync(string playlistId, MusicPlatform platform)
|
||||
{
|
||||
var db = _multiplexer.GetDatabase();
|
||||
var key = CreateCachedPlaylistKey(playlistId, platform);
|
||||
var vals = await db.ListRangeAsync(key);
|
||||
if (vals == default || vals.Length == 0)
|
||||
return Array.Empty<string>();
|
||||
|
||||
return vals.Select(x => x.ToString()).ToList();
|
||||
}
|
||||
|
||||
public async Task CachePlaylistTrackIdsAsync(string playlistId, MusicPlatform platform, IEnumerable<string> ids)
|
||||
{
|
||||
var db = _multiplexer.GetDatabase();
|
||||
var key = CreateCachedPlaylistKey(playlistId, platform);
|
||||
await db.ListRightPushAsync(key, ids.Select(x => (RedisValue) x).ToArray());
|
||||
await db.KeyExpireAsync(key, TimeSpan.FromDays(7));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string CreateCachedPlaylistQueryKey(string query, MusicPlatform platform)
|
||||
=> $"playlist:query:{platform}:{query}";
|
||||
public Task CachePlaylistIdByQueryAsync(string query, MusicPlatform platform, string playlistId)
|
||||
{
|
||||
query = Uri.EscapeDataString(query.Trim());
|
||||
var key = CreateCachedPlaylistQueryKey(query, platform);
|
||||
var db = _multiplexer.GetDatabase();
|
||||
return db.StringSetAsync(key, playlistId, TimeSpan.FromDays(7));
|
||||
}
|
||||
|
||||
public async Task<string?> GetPlaylistIdByQueryAsync(string query, MusicPlatform platform)
|
||||
{
|
||||
query = Uri.EscapeDataString(query.Trim());
|
||||
var key = CreateCachedPlaylistQueryKey(query, platform);
|
||||
|
||||
var val = await _multiplexer.GetDatabase().StringGetAsync(key);
|
||||
if (val == default)
|
||||
return null;
|
||||
|
||||
return val;
|
||||
Log.Error(ex, "Error resolving stream link for {TrackCacheKey}", trackStreamKey);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Task CacheStreamUrlAsync(string id, MusicPlatform platform, string url, TimeSpan expiry)
|
||||
=> CacheStreamUrlInternalAsync(CreateStreamKey(id, platform), url, expiry);
|
||||
|
||||
private async Task CacheStreamUrlInternalAsync(string trackStreamKey, string url, TimeSpan expiry)
|
||||
{
|
||||
// keys need to be expired after an hour
|
||||
// to make sure client doesn't get an expired stream url
|
||||
// to achieve this, track keys will be just pointers to real data
|
||||
// but that data will expire
|
||||
|
||||
var db = _multiplexer.GetDatabase();
|
||||
var dataKey = $"entry:{Guid.NewGuid()}:{trackStreamKey}";
|
||||
await db.StringSetAsync(dataKey, url, expiry: expiry);
|
||||
await db.ListRightPushAsync(trackStreamKey, dataKey);
|
||||
}
|
||||
|
||||
private async Task<string?> GetStreamFromCacheInternalAsync(string trackStreamKey)
|
||||
{
|
||||
// Job of the method which retrieves keys is to pop the elements
|
||||
// from the list of cached trackurls until it finds a non-expired key
|
||||
|
||||
var db = _multiplexer.GetDatabase();
|
||||
while(true)
|
||||
{
|
||||
string? dataKey = await db.ListLeftPopAsync(trackStreamKey);
|
||||
if (dataKey == default)
|
||||
return null;
|
||||
|
||||
var streamUrl = await db.StringGetAsync(dataKey);
|
||||
if (streamUrl == default)
|
||||
continue;
|
||||
|
||||
return streamUrl;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string CreateCachedDataKey(string id, MusicPlatform platform)
|
||||
=> $"track:data:{platform}:{id}";
|
||||
|
||||
public Task CacheTrackDataAsync(ICachableTrackData data)
|
||||
{
|
||||
var db = _multiplexer.GetDatabase();
|
||||
|
||||
var trackDataKey = CreateCachedDataKey(data.Id, data.Platform);
|
||||
var dataString = JsonSerializer.Serialize((object)data);
|
||||
// cache for 1 day
|
||||
return db.StringSetAsync(trackDataKey, dataString, expiry: TimeSpan.FromDays(1));
|
||||
}
|
||||
|
||||
public async Task<ICachableTrackData?> GetCachedDataByIdAsync(string id, MusicPlatform platform)
|
||||
{
|
||||
var db = _multiplexer.GetDatabase();
|
||||
|
||||
var trackDataKey = CreateCachedDataKey(id, platform);
|
||||
var data = await db.StringGetAsync(trackDataKey);
|
||||
if (data == default)
|
||||
return null;
|
||||
|
||||
return JsonSerializer.Deserialize<CachableTrackData>(data);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string CreateCachedQueryDataKey(string query, MusicPlatform platform)
|
||||
=> $"track:query_to_id:{platform}:{query}";
|
||||
public async Task<ICachableTrackData?> GetCachedDataByQueryAsync(string query, MusicPlatform platform)
|
||||
{
|
||||
query = Uri.EscapeDataString(query.Trim());
|
||||
|
||||
var db = _multiplexer.GetDatabase();
|
||||
var queryDataKey = CreateCachedQueryDataKey(query, platform);
|
||||
|
||||
var trackId = await db.StringGetAsync(queryDataKey);
|
||||
if (trackId == default)
|
||||
return null;
|
||||
|
||||
return await GetCachedDataByIdAsync(trackId, platform);
|
||||
}
|
||||
|
||||
public async Task CacheTrackDataByQueryAsync(string query, ICachableTrackData data)
|
||||
{
|
||||
query = Uri.EscapeDataString(query.Trim());
|
||||
|
||||
// first cache the data
|
||||
await CacheTrackDataAsync(data);
|
||||
|
||||
// then map the query to cached data's id
|
||||
var db = _multiplexer.GetDatabase();
|
||||
|
||||
var queryDataKey = CreateCachedQueryDataKey(query, data.Platform);
|
||||
await db.StringSetAsync(queryDataKey, data.Id, TimeSpan.FromDays(7));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string CreateCachedPlaylistKey(string playlistId, MusicPlatform platform)
|
||||
=> $"playlist:{platform}:{playlistId}";
|
||||
public async Task<IReadOnlyCollection<string>> GetPlaylistTrackIdsAsync(string playlistId, MusicPlatform platform)
|
||||
{
|
||||
var db = _multiplexer.GetDatabase();
|
||||
var key = CreateCachedPlaylistKey(playlistId, platform);
|
||||
var vals = await db.ListRangeAsync(key);
|
||||
if (vals == default || vals.Length == 0)
|
||||
return Array.Empty<string>();
|
||||
|
||||
return vals.Select(x => x.ToString()).ToList();
|
||||
}
|
||||
|
||||
public async Task CachePlaylistTrackIdsAsync(string playlistId, MusicPlatform platform, IEnumerable<string> ids)
|
||||
{
|
||||
var db = _multiplexer.GetDatabase();
|
||||
var key = CreateCachedPlaylistKey(playlistId, platform);
|
||||
await db.ListRightPushAsync(key, ids.Select(x => (RedisValue) x).ToArray());
|
||||
await db.KeyExpireAsync(key, TimeSpan.FromDays(7));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string CreateCachedPlaylistQueryKey(string query, MusicPlatform platform)
|
||||
=> $"playlist:query:{platform}:{query}";
|
||||
public Task CachePlaylistIdByQueryAsync(string query, MusicPlatform platform, string playlistId)
|
||||
{
|
||||
query = Uri.EscapeDataString(query.Trim());
|
||||
var key = CreateCachedPlaylistQueryKey(query, platform);
|
||||
var db = _multiplexer.GetDatabase();
|
||||
return db.StringSetAsync(key, playlistId, TimeSpan.FromDays(7));
|
||||
}
|
||||
|
||||
public async Task<string?> GetPlaylistIdByQueryAsync(string query, MusicPlatform platform)
|
||||
{
|
||||
query = Uri.EscapeDataString(query.Trim());
|
||||
var key = CreateCachedPlaylistQueryKey(query, platform);
|
||||
|
||||
var val = await _multiplexer.GetDatabase().StringGetAsync(key);
|
||||
if (val == default)
|
||||
return null;
|
||||
|
||||
return val;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user