Add retry handler with polly to intercept 429 rate limit errors with Real-Debrid.
This commit is contained in:
parent
343d2d078c
commit
8dd0eec654
15 changed files with 59 additions and 93 deletions
|
|
@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.0.80] - 2024-07-13
|
||||
### Changed
|
||||
- Add rate limiter to retry requests that are rate limited from Real-Debrid.
|
||||
- Optimize calls to Real-Debrid API when torrents are finished and periodic updates.
|
||||
- Update to .NET 8.0.7
|
||||
|
||||
## [2.0.79] - 2024-06-03
|
||||
### Changed
|
||||
- Fixed issue with qBittorrent progress sometimes throwing errors.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "rdt-client",
|
||||
"version": "2.0.79",
|
||||
"version": "2.0.80",
|
||||
"description": "This is a web interface to manage your torrents on Real-Debrid.",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -8,15 +8,15 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.3">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
|
|||
return;
|
||||
}
|
||||
|
||||
CurrentVersion = $"v{version[..version.LastIndexOf(".", StringComparison.Ordinal)]}";
|
||||
CurrentVersion = $"v{version[..version.LastIndexOf('.')]}";
|
||||
|
||||
logger.LogInformation("UpdateChecker started, currently on version {CurrentVersion}.", CurrentVersion);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Polly;
|
||||
using Polly.Extensions.Http;
|
||||
using RdtClient.Service.BackgroundServices;
|
||||
using RdtClient.Service.Handlers;
|
||||
using RdtClient.Service.Middleware;
|
||||
using RdtClient.Service.Services;
|
||||
using RdtClient.Service.Services.TorrentClients;
|
||||
|
|
@ -38,9 +39,13 @@ public static class DiConfig
|
|||
public static void RegisterHttpClients(this IServiceCollection services)
|
||||
{
|
||||
services.AddHttpClient();
|
||||
|
||||
|
||||
var retryPolicy = HttpPolicyExtensions
|
||||
.HandleTransientHttpError()
|
||||
.OrResult(r => r.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
.WaitAndRetryAsync(retryCount: 5, sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));
|
||||
|
||||
services.AddHttpClient(RD_CLIENT)
|
||||
.AddHttpMessageHandler(sp => new RateLimitingHandler(sp.GetRequiredService<ILogger<RateLimitingHandler>>(), 60, TimeSpan.FromSeconds(60)))
|
||||
.SetHandlerLifetime(Timeout.InfiniteTimeSpan);
|
||||
.AddPolicyHandler(retryPolicy);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace RdtClient.Service.Handlers;
|
||||
|
||||
public class RateLimitingHandler : DelegatingHandler
|
||||
{
|
||||
private readonly ConcurrentQueue<DateTimeOffset> _callQueue = new();
|
||||
private readonly TimeSpan _requestWindow;
|
||||
private readonly ILogger<RateLimitingHandler> _logger;
|
||||
private readonly Int32 _requestsAllowedPerWindow;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RateLimitingHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="requestsAllowedPerWindow">The number of requests allowed per window.</param>
|
||||
/// <param name="requestWindow">The time to delay when the maximum number of requests has been reached (The window).</param>
|
||||
public RateLimitingHandler(ILogger<RateLimitingHandler> logger, Int32 requestsAllowedPerWindow, TimeSpan requestWindow)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(requestsAllowedPerWindow);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(requestWindow, TimeSpan.Zero);
|
||||
_logger = logger;
|
||||
_requestsAllowedPerWindow = requestsAllowedPerWindow;
|
||||
_requestWindow = requestWindow;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
_callQueue.Enqueue(now);
|
||||
while (_callQueue.TryPeek(out var oldCall) && now - oldCall > _requestWindow)
|
||||
{
|
||||
_logger.LogDebug("Removing oldest request from queue");
|
||||
_callQueue.TryDequeue(out _);
|
||||
}
|
||||
if (_callQueue.Count > _requestsAllowedPerWindow)
|
||||
{
|
||||
_logger.LogDebug("Delaying request");
|
||||
var earliestAllowedRequestTime = now.Add(-_requestWindow);
|
||||
var delayDuration = _callQueue.ElementAtOrDefault(_requestsAllowedPerWindow - 1) -
|
||||
earliestAllowedRequestTime;
|
||||
if (delayDuration > TimeSpan.Zero)
|
||||
{
|
||||
_logger.LogDebug("Delaying request for {DelayDuration}", delayDuration);
|
||||
await Task.Delay(delayDuration, cancellationToken);
|
||||
}
|
||||
}
|
||||
_logger.LogDebug("Sending request");
|
||||
return await base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ public static class DownloadHelper
|
|||
|
||||
var matchingTorrentFiles = torrent.Files.Where(m => m.Path.EndsWith(fileName)).Where(m => !String.IsNullOrWhiteSpace(m.Path)).ToList();
|
||||
|
||||
if (matchingTorrentFiles.Any())
|
||||
if (matchingTorrentFiles.Count > 0)
|
||||
{
|
||||
var matchingTorrentFile = matchingTorrentFiles[0];
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ public static class DownloadHelper
|
|||
|
||||
var matchingTorrentFiles = torrent.Files.Where(m => m.Path.EndsWith(fileName)).Where(m => !String.IsNullOrWhiteSpace(m.Path)).ToList();
|
||||
|
||||
if (matchingTorrentFiles.Any())
|
||||
if (matchingTorrentFiles.Count > 0)
|
||||
{
|
||||
var matchingTorrentFile = matchingTorrentFiles[0];
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,7 @@ public class JsonModelBinder : IModelBinder
|
|||
{
|
||||
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
{
|
||||
if (bindingContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(bindingContext));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(bindingContext);
|
||||
|
||||
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
|
||||
if (valueProviderResult != ValueProviderResult.None)
|
||||
|
|
|
|||
|
|
@ -3,8 +3,17 @@ using Microsoft.AspNetCore.Http;
|
|||
|
||||
namespace RdtClient.Service.Middleware;
|
||||
|
||||
public class BaseHrefMiddleware(RequestDelegate next, String basePath)
|
||||
public partial class BaseHrefMiddleware(RequestDelegate next, String basePath)
|
||||
{
|
||||
[GeneratedRegex(@"<base href=""/""")]
|
||||
private partial Regex BodyRegex();
|
||||
|
||||
[GeneratedRegex("(<script.*?src=\")(.*?)(\".*?</script>)")]
|
||||
private partial Regex ScriptRegex();
|
||||
|
||||
[GeneratedRegex("(<link.*?href=\")(.*?)(\".*?>)")]
|
||||
private partial Regex LinkRegex();
|
||||
|
||||
private readonly String _basePath = $"/{basePath.TrimStart('/').TrimEnd('/')}/";
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
|
|
@ -27,10 +36,10 @@ public class BaseHrefMiddleware(RequestDelegate next, String basePath)
|
|||
{
|
||||
if (context.Response.ContentType?.Contains("text/html") == true)
|
||||
{
|
||||
responseBody = Regex.Replace(responseBody, @"<base href=""/""", @$"<base href=""{_basePath}""");
|
||||
responseBody = BodyRegex().Replace(responseBody, @$"<base href=""{_basePath}""");
|
||||
|
||||
responseBody = Regex.Replace(responseBody, "(<script.*?src=\")(.*?)(\".*?</script>)", $"$1{_basePath}$2$3");
|
||||
responseBody = Regex.Replace(responseBody, "(<link.*?href=\")(.*?)(\".*?>)", $"$1{_basePath}$2$3");
|
||||
responseBody = ScriptRegex().Replace(responseBody, $"$1{_basePath}$2$3");
|
||||
responseBody = LinkRegex().Replace(responseBody, $"$1{_basePath}$2$3");
|
||||
|
||||
context.Response.Headers.Remove("Content-Length");
|
||||
await context.Response.WriteAsync(responseBody);
|
||||
|
|
|
|||
|
|
@ -11,14 +11,17 @@
|
|||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="AllDebrid.NET" Version="1.0.12" />
|
||||
<PackageReference Include="Aria2.NET" Version="1.0.5" />
|
||||
<PackageReference Include="Downloader" Version="3.0.6" />
|
||||
<PackageReference Include="Downloader" Version="3.1.2" />
|
||||
<PackageReference Include="Downloader.NET" Version="1.0.12" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.7.0" />
|
||||
<PackageReference Include="MonoTorrent" Version="2.0.7" />
|
||||
<PackageReference Include="Polly" Version="8.4.1" />
|
||||
<PackageReference Include="Premiumize.NET" Version="1.0.4" />
|
||||
<PackageReference Include="RD.NET" Version="2.1.4" />
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.36.0" />
|
||||
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.37.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ public class SymlinkDownloader(String uri, String destinationPath, String path)
|
|||
var filePath = new FileInfo(path);
|
||||
|
||||
var rcloneMountPath = Settings.Get.DownloadClient.RcloneMountPath.TrimEnd(['\\', '/']);
|
||||
var searchSubDirectories = rcloneMountPath.EndsWith("*");
|
||||
var searchSubDirectories = rcloneMountPath.EndsWith('*');
|
||||
rcloneMountPath = rcloneMountPath.TrimEnd('*').TrimEnd(['\\', '/']);
|
||||
|
||||
if (!Directory.Exists(rcloneMountPath))
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ public class RdtHub : Hub
|
|||
{
|
||||
private static readonly ConcurrentDictionary<String, String> Users = new();
|
||||
|
||||
public static Boolean HasConnections => Users.Any();
|
||||
public static Boolean HasConnections => !Users.IsEmpty;
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ public class UnpackClient(Download download, String destinationPath)
|
|||
|
||||
var entries = archive.Entries
|
||||
.Where(entry => !entry.IsDirectory)
|
||||
.Select(m => m.Key)
|
||||
.Select(m => m.Key!)
|
||||
.ToList();
|
||||
|
||||
archive.Dispose();
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
|
|||
return BadRequest();
|
||||
}
|
||||
|
||||
logger.LogDebug($"Delete {request.Hashes}");
|
||||
logger.LogDebug("Delete {Hashes}", request.Hashes);
|
||||
|
||||
var hashes = request.Hashes.Split("|");
|
||||
|
||||
|
|
|
|||
|
|
@ -29,18 +29,18 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.3">
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="8.0.3" />
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="8.0.7" />
|
||||
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
Loading…
Reference in a new issue