mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-10 15:25:42 -07:00
Replace AssetDelivery API with Thumbnails API for Discord RPC images (#4947)
* replace assetdelivery with thumbnails for rpc * update GetThumbnailUrlAsync logging * fix build error
This commit is contained in:
parent
055695e014
commit
c1842c0443
81
Bloxstrap/Extensions/HttpClientEx.cs
Normal file
81
Bloxstrap/Extensions/HttpClientEx.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bloxstrap.Extensions
|
||||
{
|
||||
internal static class HttpClientEx
|
||||
{
|
||||
public static async Task<HttpResponseMessage> GetWithRetriesAsync(this HttpClient client, string url, int retries, CancellationToken token)
|
||||
{
|
||||
HttpResponseMessage response = null!;
|
||||
|
||||
for (int i = 1; i <= retries; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
response = await client.GetAsync(url, token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
App.Logger.WriteException("HttpClientEx::GetWithRetriesAsync", ex);
|
||||
|
||||
if (i == retries)
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public static async Task<HttpResponseMessage> PostWithRetriesAsync(this HttpClient client, string url, HttpContent? content, int retries, CancellationToken token)
|
||||
{
|
||||
HttpResponseMessage response = null!;
|
||||
|
||||
for (int i = 1; i <= retries; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
response = await client.PostAsync(url, content, token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
App.Logger.WriteException("HttpClientEx::PostWithRetriesAsync", ex);
|
||||
|
||||
if (i == retries)
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public static async Task<T?> GetFromJsonWithRetriesAsync<T>(this HttpClient client, string url, int retries, CancellationToken token) where T : class
|
||||
{
|
||||
HttpResponseMessage response = await GetWithRetriesAsync(client, url, retries, token);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using var stream = await response.Content.ReadAsStreamAsync(token);
|
||||
return await JsonSerializer.DeserializeAsync<T>(stream, cancellationToken: token);
|
||||
}
|
||||
|
||||
public static async Task<T?> PostFromJsonWithRetriesAsync<T>(this HttpClient client, string url, HttpContent? content, int retries, CancellationToken token) where T : class
|
||||
{
|
||||
HttpResponseMessage response = await PostWithRetriesAsync(client, url, content, retries, token);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using var stream = await response.Content.ReadAsStreamAsync(token);
|
||||
return await JsonSerializer.DeserializeAsync<T>(stream, cancellationToken: token);
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,12 @@ namespace Bloxstrap.Integrations
|
||||
private DiscordRPC.RichPresence? _currentPresence;
|
||||
private DiscordRPC.RichPresence? _originalPresence;
|
||||
|
||||
private FixedSizeList<ThumbnailCacheEntry> _thumbnailCache = new FixedSizeList<ThumbnailCacheEntry>(20);
|
||||
|
||||
private ulong? _smallImgBeingFetched = null;
|
||||
private ulong? _largeImgBeingFetched = null;
|
||||
private CancellationTokenSource? _fetchThumbnailsToken;
|
||||
|
||||
private bool _visible = true;
|
||||
|
||||
public DiscordRichPresence(ActivityWatcher activityWatcher)
|
||||
@ -69,101 +75,239 @@ namespace Bloxstrap.Integrations
|
||||
}
|
||||
else if (message.Command == "SetRichPresence")
|
||||
{
|
||||
Models.BloxstrapRPC.RichPresence? presenceData;
|
||||
|
||||
try
|
||||
{
|
||||
presenceData = message.Data.Deserialize<Models.BloxstrapRPC.RichPresence>();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (presenceData is null)
|
||||
{
|
||||
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (presenceData.Details is not null)
|
||||
{
|
||||
if (presenceData.Details.Length > 128)
|
||||
App.Logger.WriteLine(LOG_IDENT, $"Details cannot be longer than 128 characters");
|
||||
else if (presenceData.Details == "<reset>")
|
||||
_currentPresence.Details = _originalPresence.Details;
|
||||
else
|
||||
_currentPresence.Details = presenceData.Details;
|
||||
}
|
||||
|
||||
if (presenceData.State is not null)
|
||||
{
|
||||
if (presenceData.State.Length > 128)
|
||||
App.Logger.WriteLine(LOG_IDENT, $"State cannot be longer than 128 characters");
|
||||
else if (presenceData.State == "<reset>")
|
||||
_currentPresence.State = _originalPresence.State;
|
||||
else
|
||||
_currentPresence.State = presenceData.State;
|
||||
}
|
||||
|
||||
if (presenceData.TimestampStart == 0)
|
||||
_currentPresence.Timestamps.Start = null;
|
||||
else if (presenceData.TimestampStart is not null)
|
||||
_currentPresence.Timestamps.StartUnixMilliseconds = presenceData.TimestampStart * 1000;
|
||||
|
||||
if (presenceData.TimestampEnd == 0)
|
||||
_currentPresence.Timestamps.End = null;
|
||||
else if (presenceData.TimestampEnd is not null)
|
||||
_currentPresence.Timestamps.EndUnixMilliseconds = presenceData.TimestampEnd * 1000;
|
||||
|
||||
if (presenceData.SmallImage is not null)
|
||||
{
|
||||
if (presenceData.SmallImage.Clear)
|
||||
{
|
||||
_currentPresence.Assets.SmallImageKey = "";
|
||||
}
|
||||
else if (presenceData.SmallImage.Reset)
|
||||
{
|
||||
_currentPresence.Assets.SmallImageText = _originalPresence.Assets.SmallImageText;
|
||||
_currentPresence.Assets.SmallImageKey = _originalPresence.Assets.SmallImageKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (presenceData.SmallImage.AssetId is not null)
|
||||
_currentPresence.Assets.SmallImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.SmallImage.AssetId}";
|
||||
|
||||
if (presenceData.SmallImage.HoverText is not null)
|
||||
_currentPresence.Assets.SmallImageText = presenceData.SmallImage.HoverText;
|
||||
}
|
||||
}
|
||||
|
||||
if (presenceData.LargeImage is not null)
|
||||
{
|
||||
if (presenceData.LargeImage.Clear)
|
||||
{
|
||||
_currentPresence.Assets.LargeImageKey = "";
|
||||
}
|
||||
else if (presenceData.LargeImage.Reset)
|
||||
{
|
||||
_currentPresence.Assets.LargeImageText = _originalPresence.Assets.LargeImageText;
|
||||
_currentPresence.Assets.LargeImageKey = _originalPresence.Assets.LargeImageKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (presenceData.LargeImage.AssetId is not null)
|
||||
_currentPresence.Assets.LargeImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.LargeImage.AssetId}";
|
||||
|
||||
if (presenceData.LargeImage.HoverText is not null)
|
||||
_currentPresence.Assets.LargeImageText = presenceData.LargeImage.HoverText;
|
||||
}
|
||||
}
|
||||
ProcessSetRichPresence(message, implicitUpdate);
|
||||
}
|
||||
|
||||
if (implicitUpdate)
|
||||
UpdatePresence();
|
||||
}
|
||||
|
||||
private void AddToThumbnailCache(ulong id, string? url)
|
||||
{
|
||||
if (url != null)
|
||||
_thumbnailCache.Add(new ThumbnailCacheEntry { Id = id, Url = url });
|
||||
}
|
||||
|
||||
private async Task UpdatePresenceIconsAsync(ulong? smallImg, ulong? largeImg, bool implicitUpdate, CancellationToken token)
|
||||
{
|
||||
Debug.Assert(smallImg != null || largeImg != null);
|
||||
|
||||
if (smallImg != null && largeImg != null)
|
||||
{
|
||||
string?[] urls = await Thumbnails.GetThumbnailUrlsAsync(new List<ThumbnailRequest>
|
||||
{
|
||||
new ThumbnailRequest
|
||||
{
|
||||
TargetId = (ulong)smallImg,
|
||||
Type = "Asset",
|
||||
Size = "512x512",
|
||||
IsCircular = false
|
||||
},
|
||||
new ThumbnailRequest
|
||||
{
|
||||
TargetId = (ulong)largeImg,
|
||||
Type = "Asset",
|
||||
Size = "512x512",
|
||||
IsCircular = false
|
||||
}
|
||||
}, token);
|
||||
|
||||
string? smallUrl = urls[0];
|
||||
string? largeUrl = urls[1];
|
||||
|
||||
AddToThumbnailCache((ulong)smallImg, smallUrl);
|
||||
AddToThumbnailCache((ulong)largeImg, largeUrl);
|
||||
|
||||
if (_currentPresence != null)
|
||||
{
|
||||
_currentPresence.Assets.SmallImageKey = smallUrl;
|
||||
_currentPresence.Assets.LargeImageKey = largeUrl;
|
||||
}
|
||||
}
|
||||
else if (smallImg != null)
|
||||
{
|
||||
string? url = await Thumbnails.GetThumbnailUrlAsync(new ThumbnailRequest
|
||||
{
|
||||
TargetId = (ulong)smallImg,
|
||||
Type = "Asset",
|
||||
Size = "512x512",
|
||||
IsCircular = false
|
||||
}, token);
|
||||
|
||||
AddToThumbnailCache((ulong)smallImg, url);
|
||||
|
||||
if (_currentPresence != null)
|
||||
_currentPresence.Assets.SmallImageKey = url;
|
||||
}
|
||||
else if (largeImg != null)
|
||||
{
|
||||
string? url = await Thumbnails.GetThumbnailUrlAsync(new ThumbnailRequest
|
||||
{
|
||||
TargetId = (ulong)largeImg,
|
||||
Type = "Asset",
|
||||
Size = "512x512",
|
||||
IsCircular = false
|
||||
}, token);
|
||||
|
||||
AddToThumbnailCache((ulong)largeImg, url);
|
||||
|
||||
if (_currentPresence != null)
|
||||
_currentPresence.Assets.LargeImageKey = url;
|
||||
}
|
||||
|
||||
_smallImgBeingFetched = null;
|
||||
_largeImgBeingFetched = null;
|
||||
|
||||
if (implicitUpdate)
|
||||
UpdatePresence();
|
||||
}
|
||||
|
||||
private void ProcessSetRichPresence(Message message, bool implicitUpdate)
|
||||
{
|
||||
const string LOG_IDENT = "DiscordRichPresence::ProcessSetRichPresence";
|
||||
Models.BloxstrapRPC.RichPresence? presenceData;
|
||||
|
||||
Debug.Assert(_currentPresence is not null);
|
||||
Debug.Assert(_originalPresence is not null);
|
||||
|
||||
if (_fetchThumbnailsToken != null)
|
||||
{
|
||||
_fetchThumbnailsToken.Cancel();
|
||||
_fetchThumbnailsToken = null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
presenceData = message.Data.Deserialize<Models.BloxstrapRPC.RichPresence>();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (presenceData is null)
|
||||
{
|
||||
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (presenceData.Details is not null)
|
||||
{
|
||||
if (presenceData.Details.Length > 128)
|
||||
App.Logger.WriteLine(LOG_IDENT, $"Details cannot be longer than 128 characters");
|
||||
else if (presenceData.Details == "<reset>")
|
||||
_currentPresence.Details = _originalPresence.Details;
|
||||
else
|
||||
_currentPresence.Details = presenceData.Details;
|
||||
}
|
||||
|
||||
if (presenceData.State is not null)
|
||||
{
|
||||
if (presenceData.State.Length > 128)
|
||||
App.Logger.WriteLine(LOG_IDENT, $"State cannot be longer than 128 characters");
|
||||
else if (presenceData.State == "<reset>")
|
||||
_currentPresence.State = _originalPresence.State;
|
||||
else
|
||||
_currentPresence.State = presenceData.State;
|
||||
}
|
||||
|
||||
if (presenceData.TimestampStart == 0)
|
||||
_currentPresence.Timestamps.Start = null;
|
||||
else if (presenceData.TimestampStart is not null)
|
||||
_currentPresence.Timestamps.StartUnixMilliseconds = presenceData.TimestampStart * 1000;
|
||||
|
||||
if (presenceData.TimestampEnd == 0)
|
||||
_currentPresence.Timestamps.End = null;
|
||||
else if (presenceData.TimestampEnd is not null)
|
||||
_currentPresence.Timestamps.EndUnixMilliseconds = presenceData.TimestampEnd * 1000;
|
||||
|
||||
// set these to start fetching
|
||||
ulong? smallImgFetch = null;
|
||||
ulong? largeImgFetch = null;
|
||||
|
||||
if (presenceData.SmallImage is not null)
|
||||
{
|
||||
if (presenceData.SmallImage.Clear)
|
||||
{
|
||||
_currentPresence.Assets.SmallImageKey = "";
|
||||
_smallImgBeingFetched = null;
|
||||
}
|
||||
else if (presenceData.SmallImage.Reset)
|
||||
{
|
||||
_currentPresence.Assets.SmallImageText = _originalPresence.Assets.SmallImageText;
|
||||
_currentPresence.Assets.SmallImageKey = _originalPresence.Assets.SmallImageKey;
|
||||
_smallImgBeingFetched = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (presenceData.SmallImage.AssetId is not null)
|
||||
{
|
||||
ThumbnailCacheEntry? entry = _thumbnailCache.FirstOrDefault(x => x.Id == presenceData.SmallImage.AssetId);
|
||||
|
||||
if (entry == null)
|
||||
{
|
||||
smallImgFetch = presenceData.SmallImage.AssetId;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentPresence.Assets.SmallImageKey = entry.Url;
|
||||
_smallImgBeingFetched = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (presenceData.SmallImage.HoverText is not null)
|
||||
_currentPresence.Assets.SmallImageText = presenceData.SmallImage.HoverText;
|
||||
}
|
||||
}
|
||||
|
||||
if (presenceData.LargeImage is not null)
|
||||
{
|
||||
if (presenceData.LargeImage.Clear)
|
||||
{
|
||||
_currentPresence.Assets.LargeImageKey = "";
|
||||
_largeImgBeingFetched = null;
|
||||
}
|
||||
else if (presenceData.LargeImage.Reset)
|
||||
{
|
||||
_currentPresence.Assets.LargeImageText = _originalPresence.Assets.LargeImageText;
|
||||
_currentPresence.Assets.LargeImageKey = _originalPresence.Assets.LargeImageKey;
|
||||
_largeImgBeingFetched = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (presenceData.LargeImage.AssetId is not null)
|
||||
{
|
||||
ThumbnailCacheEntry? entry = _thumbnailCache.FirstOrDefault(x => x.Id == presenceData.LargeImage.AssetId);
|
||||
|
||||
if (entry == null)
|
||||
{
|
||||
largeImgFetch = presenceData.LargeImage.AssetId;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentPresence.Assets.LargeImageKey = entry.Url;
|
||||
_largeImgBeingFetched = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (presenceData.LargeImage.HoverText is not null)
|
||||
_currentPresence.Assets.LargeImageText = presenceData.LargeImage.HoverText;
|
||||
}
|
||||
}
|
||||
|
||||
if (smallImgFetch != null)
|
||||
_smallImgBeingFetched = smallImgFetch;
|
||||
if (largeImgFetch != null)
|
||||
_largeImgBeingFetched = largeImgFetch;
|
||||
|
||||
if (_smallImgBeingFetched != null || _largeImgBeingFetched != null)
|
||||
{
|
||||
_fetchThumbnailsToken = new CancellationTokenSource();
|
||||
Task.Run(() => UpdatePresenceIconsAsync(_smallImgBeingFetched, _largeImgBeingFetched, implicitUpdate, _fetchThumbnailsToken.Token));
|
||||
}
|
||||
}
|
||||
|
||||
public void SetVisibility(bool visible)
|
||||
{
|
||||
App.Logger.WriteLine("DiscordRichPresence::SetVisibility", $"Setting presence visibility ({visible})");
|
||||
@ -225,13 +369,13 @@ namespace Bloxstrap.Integrations
|
||||
|
||||
var universeDetails = activity.UniverseDetails!;
|
||||
|
||||
icon = universeDetails.Thumbnail.ImageUrl;
|
||||
icon = universeDetails.Thumbnail.ImageUrl!;
|
||||
|
||||
if (App.Settings.Prop.ShowAccountOnRichPresence)
|
||||
{
|
||||
var userDetails = await UserDetails.Fetch(activity.UserId);
|
||||
|
||||
smallImage = userDetails.Thumbnail.ImageUrl;
|
||||
smallImage = userDetails.Thumbnail.ImageUrl!;
|
||||
smallImageText = $"Playing on {userDetails.Data.DisplayName} (@{userDetails.Data.Name})"; // i.e. "axell (@Axelan_se)"
|
||||
}
|
||||
|
||||
|
8
Bloxstrap/Models/APIs/Roblox/ThumbnailBatchResponse.cs
Normal file
8
Bloxstrap/Models/APIs/Roblox/ThumbnailBatchResponse.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Bloxstrap.Models.APIs.Roblox
|
||||
{
|
||||
internal class ThumbnailBatchResponse
|
||||
{
|
||||
[JsonPropertyName("data")]
|
||||
public ThumbnailResponse[] Data { get; set; } = Array.Empty<ThumbnailResponse>();
|
||||
}
|
||||
}
|
34
Bloxstrap/Models/APIs/Roblox/ThumbnailRequest.cs
Normal file
34
Bloxstrap/Models/APIs/Roblox/ThumbnailRequest.cs
Normal file
@ -0,0 +1,34 @@
|
||||
namespace Bloxstrap.Models.APIs.Roblox
|
||||
{
|
||||
internal class ThumbnailRequest
|
||||
{
|
||||
[JsonPropertyName("requestId")]
|
||||
public string? RequestId { get; set; }
|
||||
|
||||
[JsonPropertyName("targetId")]
|
||||
public ulong TargetId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// TODO: make this an enum
|
||||
/// List of valid types can be found at https://thumbnails.roblox.com//docs/index.html
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = "Avatar";
|
||||
|
||||
/// <summary>
|
||||
/// List of valid sizes can be found at https://thumbnails.roblox.com//docs/index.html
|
||||
/// </summary>
|
||||
[JsonPropertyName("size")]
|
||||
public string Size { get; set; } = "30x30";
|
||||
|
||||
/// <summary>
|
||||
/// TODO: make this an enum
|
||||
/// List of valid types can be found at https://thumbnails.roblox.com//docs/index.html
|
||||
/// </summary>
|
||||
[JsonPropertyName("format")]
|
||||
public string Format { get; set; } = "Png";
|
||||
|
||||
[JsonPropertyName("isCircular")]
|
||||
public bool IsCircular { get; set; } = true;
|
||||
}
|
||||
}
|
@ -5,13 +5,31 @@
|
||||
/// </summary>
|
||||
public class ThumbnailResponse
|
||||
{
|
||||
[JsonPropertyName("requestId")]
|
||||
public string RequestId { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("errorCode")]
|
||||
public int ErrorCode { get; set; } = 0;
|
||||
|
||||
[JsonPropertyName("errorMessage")]
|
||||
public string? ErrorMessage { get; set; } = null;
|
||||
|
||||
[JsonPropertyName("targetId")]
|
||||
public long TargetId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Valid states:
|
||||
/// - Error
|
||||
/// - Completed
|
||||
/// - InReview
|
||||
/// - Pending
|
||||
/// - Blocked
|
||||
/// - TemporarilyUnavailable
|
||||
/// </summary>
|
||||
[JsonPropertyName("state")]
|
||||
public string State { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("imageUrl")]
|
||||
public string ImageUrl { get; set; } = null!;
|
||||
public string? ImageUrl { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
|
8
Bloxstrap/Models/ThumbnailCacheEntry.cs
Normal file
8
Bloxstrap/Models/ThumbnailCacheEntry.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Bloxstrap.Models
|
||||
{
|
||||
internal class ThumbnailCacheEntry
|
||||
{
|
||||
public ulong Id { get; set; }
|
||||
public string Url { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
25
Bloxstrap/Utility/FixedCapacityList.cs
Normal file
25
Bloxstrap/Utility/FixedCapacityList.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bloxstrap.Utility
|
||||
{
|
||||
internal class FixedSizeList<T> : List<T>
|
||||
{
|
||||
public int MaxSize { get; }
|
||||
|
||||
public FixedSizeList(int size)
|
||||
{
|
||||
MaxSize = size;
|
||||
}
|
||||
|
||||
public new void Add(T item)
|
||||
{
|
||||
if (Count >= MaxSize)
|
||||
RemoveAt(Count - 1);
|
||||
base.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
103
Bloxstrap/Utility/Thumbnails.cs
Normal file
103
Bloxstrap/Utility/Thumbnails.cs
Normal file
@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bloxstrap.Utility
|
||||
{
|
||||
internal static class Thumbnails
|
||||
{
|
||||
// TODO: remove requests from list once they're finished or failed
|
||||
/// <remarks>
|
||||
/// Returned array may contain null values
|
||||
/// </remarks>
|
||||
public static async Task<string?[]> GetThumbnailUrlsAsync(List<ThumbnailRequest> requests, CancellationToken token)
|
||||
{
|
||||
const string LOG_IDENT = "Thumbnails::GetThumbnailUrlsAsync";
|
||||
const int RETRIES = 5;
|
||||
const int RETRY_TIME_INCREMENT = 500; // ms
|
||||
|
||||
string?[] urls = new string?[requests.Count];
|
||||
|
||||
// assign unique request ids to each request
|
||||
for (int i = 0; i < requests.Count; i++)
|
||||
requests[i].RequestId = i.ToString();
|
||||
|
||||
var payload = new StringContent(JsonSerializer.Serialize(requests));
|
||||
|
||||
ThumbnailResponse[] response = null!;
|
||||
|
||||
for (int i = 1; i <= RETRIES; i++)
|
||||
{
|
||||
var json = await App.HttpClient.PostFromJsonWithRetriesAsync<ThumbnailBatchResponse>("https://thumbnails.roblox.com/v1/batch", payload, 3, token);
|
||||
if (json == null)
|
||||
throw new InvalidHTTPResponseException("Deserialised ThumbnailBatchResponse is null");
|
||||
|
||||
response = json.Data;
|
||||
|
||||
bool finished = response.All(x => x.State != "Pending");
|
||||
if (finished)
|
||||
break;
|
||||
|
||||
if (i == RETRIES)
|
||||
App.Logger.WriteLine(LOG_IDENT, "Ran out of retries");
|
||||
else
|
||||
await Task.Delay(RETRY_TIME_INCREMENT * i, token);
|
||||
}
|
||||
|
||||
foreach (var item in response)
|
||||
{
|
||||
if (item.State == "Pending")
|
||||
App.Logger.WriteLine(LOG_IDENT, $"{item.TargetId} is still pending");
|
||||
else if (item.State == "Error")
|
||||
App.Logger.WriteLine(LOG_IDENT, $"{item.TargetId} got error code {item.ErrorCode} ({item.ErrorMessage})");
|
||||
else if (item.State != "Completed")
|
||||
App.Logger.WriteLine(LOG_IDENT, $"{item.TargetId} got \"{item.State}\"");
|
||||
|
||||
urls[int.Parse(item.RequestId)] = item.ImageUrl;
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
public static async Task<string?> GetThumbnailUrlAsync(ThumbnailRequest request, CancellationToken token)
|
||||
{
|
||||
const string LOG_IDENT = "Thumbnails::GetThumbnailUrlAsync";
|
||||
const int RETRIES = 5;
|
||||
const int RETRY_TIME_INCREMENT = 500; // ms
|
||||
|
||||
request.RequestId = "0";
|
||||
|
||||
var payload = new StringContent(JsonSerializer.Serialize(new ThumbnailRequest[] { request }));
|
||||
|
||||
ThumbnailResponse response = null!;
|
||||
|
||||
for (int i = 1; i <= RETRIES; i++)
|
||||
{
|
||||
var json = await App.HttpClient.PostFromJsonWithRetriesAsync<ThumbnailBatchResponse>("https://thumbnails.roblox.com/v1/batch", payload, 3, token);
|
||||
if (json == null)
|
||||
throw new InvalidHTTPResponseException("Deserialised ThumbnailBatchResponse is null");
|
||||
|
||||
response = json.Data[0];
|
||||
|
||||
if (response.State != "Pending")
|
||||
break;
|
||||
|
||||
if (i == RETRIES)
|
||||
App.Logger.WriteLine(LOG_IDENT, "Ran out of retries");
|
||||
else
|
||||
await Task.Delay(RETRY_TIME_INCREMENT * i, token);
|
||||
}
|
||||
|
||||
if (response.State == "Pending")
|
||||
App.Logger.WriteLine(LOG_IDENT, $"{response.TargetId} is still pending");
|
||||
else if (response.State == "Error")
|
||||
App.Logger.WriteLine(LOG_IDENT, $"{response.TargetId} got error code {response.ErrorCode} ({response.ErrorMessage})");
|
||||
else if (response.State != "Completed")
|
||||
App.Logger.WriteLine(LOG_IDENT, $"{response.TargetId} got \"{response.State}\"");
|
||||
|
||||
return response.ImageUrl;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user