mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-21 10:01:27 -07:00
replace assetdelivery with thumbnails for rpc
This commit is contained in:
parent
055695e014
commit
fe8d85da58
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? _currentPresence;
|
||||||
private DiscordRPC.RichPresence? _originalPresence;
|
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;
|
private bool _visible = true;
|
||||||
|
|
||||||
public DiscordRichPresence(ActivityWatcher activityWatcher)
|
public DiscordRichPresence(ActivityWatcher activityWatcher)
|
||||||
@ -69,8 +75,107 @@ namespace Bloxstrap.Integrations
|
|||||||
}
|
}
|
||||||
else if (message.Command == "SetRichPresence")
|
else if (message.Command == "SetRichPresence")
|
||||||
{
|
{
|
||||||
|
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;
|
Models.BloxstrapRPC.RichPresence? presenceData;
|
||||||
|
|
||||||
|
Debug.Assert(_currentPresence is not null);
|
||||||
|
Debug.Assert(_originalPresence is not null);
|
||||||
|
|
||||||
|
if (_fetchThumbnailsToken != null)
|
||||||
|
{
|
||||||
|
_fetchThumbnailsToken.Cancel();
|
||||||
|
_fetchThumbnailsToken = null;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
presenceData = message.Data.Deserialize<Models.BloxstrapRPC.RichPresence>();
|
presenceData = message.Data.Deserialize<Models.BloxstrapRPC.RichPresence>();
|
||||||
@ -117,21 +222,39 @@ namespace Bloxstrap.Integrations
|
|||||||
else if (presenceData.TimestampEnd is not null)
|
else if (presenceData.TimestampEnd is not null)
|
||||||
_currentPresence.Timestamps.EndUnixMilliseconds = presenceData.TimestampEnd * 1000;
|
_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 is not null)
|
||||||
{
|
{
|
||||||
if (presenceData.SmallImage.Clear)
|
if (presenceData.SmallImage.Clear)
|
||||||
{
|
{
|
||||||
_currentPresence.Assets.SmallImageKey = "";
|
_currentPresence.Assets.SmallImageKey = "";
|
||||||
|
_smallImgBeingFetched = null;
|
||||||
}
|
}
|
||||||
else if (presenceData.SmallImage.Reset)
|
else if (presenceData.SmallImage.Reset)
|
||||||
{
|
{
|
||||||
_currentPresence.Assets.SmallImageText = _originalPresence.Assets.SmallImageText;
|
_currentPresence.Assets.SmallImageText = _originalPresence.Assets.SmallImageText;
|
||||||
_currentPresence.Assets.SmallImageKey = _originalPresence.Assets.SmallImageKey;
|
_currentPresence.Assets.SmallImageKey = _originalPresence.Assets.SmallImageKey;
|
||||||
|
_smallImgBeingFetched = null;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (presenceData.SmallImage.AssetId is not null)
|
if (presenceData.SmallImage.AssetId is not null)
|
||||||
_currentPresence.Assets.SmallImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.SmallImage.AssetId}";
|
{
|
||||||
|
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)
|
if (presenceData.SmallImage.HoverText is not null)
|
||||||
_currentPresence.Assets.SmallImageText = presenceData.SmallImage.HoverText;
|
_currentPresence.Assets.SmallImageText = presenceData.SmallImage.HoverText;
|
||||||
@ -143,25 +266,46 @@ namespace Bloxstrap.Integrations
|
|||||||
if (presenceData.LargeImage.Clear)
|
if (presenceData.LargeImage.Clear)
|
||||||
{
|
{
|
||||||
_currentPresence.Assets.LargeImageKey = "";
|
_currentPresence.Assets.LargeImageKey = "";
|
||||||
|
_largeImgBeingFetched = null;
|
||||||
}
|
}
|
||||||
else if (presenceData.LargeImage.Reset)
|
else if (presenceData.LargeImage.Reset)
|
||||||
{
|
{
|
||||||
_currentPresence.Assets.LargeImageText = _originalPresence.Assets.LargeImageText;
|
_currentPresence.Assets.LargeImageText = _originalPresence.Assets.LargeImageText;
|
||||||
_currentPresence.Assets.LargeImageKey = _originalPresence.Assets.LargeImageKey;
|
_currentPresence.Assets.LargeImageKey = _originalPresence.Assets.LargeImageKey;
|
||||||
|
_largeImgBeingFetched = null;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (presenceData.LargeImage.AssetId is not null)
|
if (presenceData.LargeImage.AssetId is not null)
|
||||||
_currentPresence.Assets.LargeImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.LargeImage.AssetId}";
|
{
|
||||||
|
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)
|
if (presenceData.LargeImage.HoverText is not null)
|
||||||
_currentPresence.Assets.LargeImageText = presenceData.LargeImage.HoverText;
|
_currentPresence.Assets.LargeImageText = presenceData.LargeImage.HoverText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (implicitUpdate)
|
if (smallImgFetch != null)
|
||||||
UpdatePresence();
|
_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)
|
public void SetVisibility(bool visible)
|
||||||
@ -225,13 +369,13 @@ namespace Bloxstrap.Integrations
|
|||||||
|
|
||||||
var universeDetails = activity.UniverseDetails!;
|
var universeDetails = activity.UniverseDetails!;
|
||||||
|
|
||||||
icon = universeDetails.Thumbnail.ImageUrl;
|
icon = universeDetails.Thumbnail.ImageUrl!;
|
||||||
|
|
||||||
if (App.Settings.Prop.ShowAccountOnRichPresence)
|
if (App.Settings.Prop.ShowAccountOnRichPresence)
|
||||||
{
|
{
|
||||||
var userDetails = await UserDetails.Fetch(activity.UserId);
|
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)"
|
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>
|
/// </summary>
|
||||||
public class ThumbnailResponse
|
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")]
|
[JsonPropertyName("targetId")]
|
||||||
public long TargetId { get; set; }
|
public long TargetId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Valid states:
|
||||||
|
/// - Error
|
||||||
|
/// - Completed
|
||||||
|
/// - InReview
|
||||||
|
/// - Pending
|
||||||
|
/// - Blocked
|
||||||
|
/// - TemporarilyUnavailable
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("state")]
|
[JsonPropertyName("state")]
|
||||||
public string State { get; set; } = null!;
|
public string State { get; set; } = null!;
|
||||||
|
|
||||||
[JsonPropertyName("imageUrl")]
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
101
Bloxstrap/Utility/Thumbnails.cs
Normal file
101
Bloxstrap/Utility/Thumbnails.cs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
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 != "Completed")
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, $"{response.TargetId} got \"{response.State}\"");
|
||||||
|
|
||||||
|
return response.ImageUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user