Initial jellyfin-multilang rewrite
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
using System.Net;
|
||||
|
||||
namespace Jellyfin.Plugin.Multilang.Services.Providers;
|
||||
|
||||
public enum ProviderFailureKind
|
||||
{
|
||||
Retryable,
|
||||
Terminal
|
||||
}
|
||||
|
||||
public sealed record ProviderHttpDiagnostics(
|
||||
ProviderHttpCount[] Counts,
|
||||
ProviderHttpRecent[] Recent);
|
||||
|
||||
public sealed record ProviderHttpCount(
|
||||
string Provider,
|
||||
string Outcome,
|
||||
int Count);
|
||||
|
||||
public sealed record ProviderHttpRecent(
|
||||
string Provider,
|
||||
string Url,
|
||||
string Outcome,
|
||||
int Attempt,
|
||||
long At);
|
||||
|
||||
public static class ProviderHttp
|
||||
{
|
||||
private static readonly object DiagnosticsLock = new();
|
||||
private static readonly Dictionary<string, int> Counts = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Queue<ProviderHttpRecent> Recent = new();
|
||||
|
||||
public static ProviderFailureKind Classify(HttpStatusCode code)
|
||||
=> code is HttpStatusCode.TooManyRequests or HttpStatusCode.RequestTimeout ||
|
||||
(int)code >= 500
|
||||
? ProviderFailureKind.Retryable
|
||||
: ProviderFailureKind.Terminal;
|
||||
|
||||
public static async Task<HttpResponseMessage> SendWithRetryAsync(
|
||||
HttpClient http,
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const int maxAttempts = 3;
|
||||
for (var attempt = 1; attempt <= maxAttempts; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await http.SendAsync(Clone(request), cancellationToken).ConfigureAwait(false);
|
||||
if (response.IsSuccessStatusCode || Classify(response.StatusCode) == ProviderFailureKind.Terminal || attempt == maxAttempts)
|
||||
{
|
||||
Record(request, "HTTP " + (int)response.StatusCode, attempt);
|
||||
return response;
|
||||
}
|
||||
|
||||
var delay = GetRetryDelay(response, attempt);
|
||||
response.Dispose();
|
||||
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (attempt >= maxAttempts)
|
||||
{
|
||||
Record(request, ex.GetType().Name, attempt);
|
||||
throw;
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(250 * attempt), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
throw new HttpRequestException("Provider request failed after retries.");
|
||||
}
|
||||
|
||||
public static ProviderHttpDiagnostics GetDiagnostics()
|
||||
{
|
||||
lock (DiagnosticsLock)
|
||||
{
|
||||
return new ProviderHttpDiagnostics(
|
||||
Counts
|
||||
.Select(kvp =>
|
||||
{
|
||||
var index = kvp.Key.IndexOf('|', StringComparison.Ordinal);
|
||||
var provider = index >= 0 ? kvp.Key[..index] : kvp.Key;
|
||||
var outcome = index >= 0 ? kvp.Key[(index + 1)..] : string.Empty;
|
||||
return new ProviderHttpCount(provider, outcome, kvp.Value);
|
||||
})
|
||||
.OrderBy(c => c.Provider)
|
||||
.ThenBy(c => c.Outcome)
|
||||
.ToArray(),
|
||||
Recent.Reverse().ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
private static TimeSpan GetRetryDelay(HttpResponseMessage response, int attempt)
|
||||
=> response.Headers.RetryAfter?.Delta ??
|
||||
(response.Headers.RetryAfter?.Date is { } date
|
||||
? date - DateTimeOffset.UtcNow
|
||||
: TimeSpan.FromMilliseconds(250 * attempt));
|
||||
|
||||
private static void Record(HttpRequestMessage request, string outcome, int attempt)
|
||||
{
|
||||
var provider = ProviderName(request.RequestUri);
|
||||
var key = provider + "|" + outcome;
|
||||
var recent = new ProviderHttpRecent(
|
||||
provider,
|
||||
SafeUrl(request.RequestUri),
|
||||
outcome,
|
||||
attempt,
|
||||
DateTimeOffset.UtcNow.ToUnixTimeSeconds());
|
||||
|
||||
lock (DiagnosticsLock)
|
||||
{
|
||||
Counts[key] = Counts.TryGetValue(key, out var count) ? count + 1 : 1;
|
||||
Recent.Enqueue(recent);
|
||||
while (Recent.Count > 50)
|
||||
Recent.Dequeue();
|
||||
}
|
||||
}
|
||||
|
||||
private static string ProviderName(Uri? uri)
|
||||
{
|
||||
var host = uri?.Host ?? string.Empty;
|
||||
if (host.Contains("themoviedb", StringComparison.OrdinalIgnoreCase) || host.Contains("tmdb", StringComparison.OrdinalIgnoreCase))
|
||||
return "TMDb";
|
||||
if (host.Contains("fanart", StringComparison.OrdinalIgnoreCase))
|
||||
return "Fanart.tv";
|
||||
return string.IsNullOrWhiteSpace(host) ? "unknown" : host;
|
||||
}
|
||||
|
||||
private static string SafeUrl(Uri? uri)
|
||||
=> uri is null ? string.Empty : uri.GetLeftPart(UriPartial.Path);
|
||||
|
||||
private static HttpRequestMessage Clone(HttpRequestMessage request)
|
||||
{
|
||||
var clone = new HttpRequestMessage(request.Method, request.RequestUri);
|
||||
foreach (var header in request.Headers)
|
||||
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user