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/),
|
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).
|
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
|
## [2.0.79] - 2024-06-03
|
||||||
### Changed
|
### Changed
|
||||||
- Fixed issue with qBittorrent progress sometimes throwing errors.
|
- Fixed issue with qBittorrent progress sometimes throwing errors.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "rdt-client",
|
"name": "rdt-client",
|
||||||
"version": "2.0.79",
|
"version": "2.0.80",
|
||||||
"description": "This is a web interface to manage your torrents on Real-Debrid.",
|
"description": "This is a web interface to manage your torrents on Real-Debrid.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,15 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.3" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="8.0.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="8.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.3">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentVersion = $"v{version[..version.LastIndexOf(".", StringComparison.Ordinal)]}";
|
CurrentVersion = $"v{version[..version.LastIndexOf('.')]}";
|
||||||
|
|
||||||
logger.LogInformation("UpdateChecker started, currently on version {CurrentVersion}.", CurrentVersion);
|
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.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Polly;
|
||||||
|
using Polly.Extensions.Http;
|
||||||
using RdtClient.Service.BackgroundServices;
|
using RdtClient.Service.BackgroundServices;
|
||||||
using RdtClient.Service.Handlers;
|
|
||||||
using RdtClient.Service.Middleware;
|
using RdtClient.Service.Middleware;
|
||||||
using RdtClient.Service.Services;
|
using RdtClient.Service.Services;
|
||||||
using RdtClient.Service.Services.TorrentClients;
|
using RdtClient.Service.Services.TorrentClients;
|
||||||
|
|
@ -38,9 +39,13 @@ public static class DiConfig
|
||||||
public static void RegisterHttpClients(this IServiceCollection services)
|
public static void RegisterHttpClients(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddHttpClient();
|
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)
|
services.AddHttpClient(RD_CLIENT)
|
||||||
.AddHttpMessageHandler(sp => new RateLimitingHandler(sp.GetRequiredService<ILogger<RateLimitingHandler>>(), 60, TimeSpan.FromSeconds(60)))
|
.AddPolicyHandler(retryPolicy);
|
||||||
.SetHandlerLifetime(Timeout.InfiniteTimeSpan);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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();
|
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];
|
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();
|
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];
|
var matchingTorrentFile = matchingTorrentFiles[0];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,7 @@ public class JsonModelBinder : IModelBinder
|
||||||
{
|
{
|
||||||
public Task BindModelAsync(ModelBindingContext bindingContext)
|
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||||
{
|
{
|
||||||
if (bindingContext == null)
|
ArgumentNullException.ThrowIfNull(bindingContext);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(bindingContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
|
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
|
||||||
if (valueProviderResult != ValueProviderResult.None)
|
if (valueProviderResult != ValueProviderResult.None)
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,17 @@ using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace RdtClient.Service.Middleware;
|
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('/')}/";
|
private readonly String _basePath = $"/{basePath.TrimStart('/').TrimEnd('/')}/";
|
||||||
|
|
||||||
public async Task InvokeAsync(HttpContext context)
|
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)
|
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 = ScriptRegex().Replace(responseBody, $"$1{_basePath}$2$3");
|
||||||
responseBody = Regex.Replace(responseBody, "(<link.*?href=\")(.*?)(\".*?>)", $"$1{_basePath}$2$3");
|
responseBody = LinkRegex().Replace(responseBody, $"$1{_basePath}$2$3");
|
||||||
|
|
||||||
context.Response.Headers.Remove("Content-Length");
|
context.Response.Headers.Remove("Content-Length");
|
||||||
await context.Response.WriteAsync(responseBody);
|
await context.Response.WriteAsync(responseBody);
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,17 @@
|
||||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
<PackageReference Include="AllDebrid.NET" Version="1.0.12" />
|
<PackageReference Include="AllDebrid.NET" Version="1.0.12" />
|
||||||
<PackageReference Include="Aria2.NET" Version="1.0.5" />
|
<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="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="MonoTorrent" Version="2.0.7" />
|
||||||
|
<PackageReference Include="Polly" Version="8.4.1" />
|
||||||
<PackageReference Include="Premiumize.NET" Version="1.0.4" />
|
<PackageReference Include="Premiumize.NET" Version="1.0.4" />
|
||||||
<PackageReference Include="RD.NET" Version="2.1.4" />
|
<PackageReference Include="RD.NET" Version="2.1.4" />
|
||||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.36.0" />
|
<PackageReference Include="SharpCompress" Version="0.37.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ public class SymlinkDownloader(String uri, String destinationPath, String path)
|
||||||
var filePath = new FileInfo(path);
|
var filePath = new FileInfo(path);
|
||||||
|
|
||||||
var rcloneMountPath = Settings.Get.DownloadClient.RcloneMountPath.TrimEnd(['\\', '/']);
|
var rcloneMountPath = Settings.Get.DownloadClient.RcloneMountPath.TrimEnd(['\\', '/']);
|
||||||
var searchSubDirectories = rcloneMountPath.EndsWith("*");
|
var searchSubDirectories = rcloneMountPath.EndsWith('*');
|
||||||
rcloneMountPath = rcloneMountPath.TrimEnd('*').TrimEnd(['\\', '/']);
|
rcloneMountPath = rcloneMountPath.TrimEnd('*').TrimEnd(['\\', '/']);
|
||||||
|
|
||||||
if (!Directory.Exists(rcloneMountPath))
|
if (!Directory.Exists(rcloneMountPath))
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ public class RdtHub : Hub
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<String, String> Users = new();
|
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()
|
public override async Task OnConnectedAsync()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ public class UnpackClient(Download download, String destinationPath)
|
||||||
|
|
||||||
var entries = archive.Entries
|
var entries = archive.Entries
|
||||||
.Where(entry => !entry.IsDirectory)
|
.Where(entry => !entry.IsDirectory)
|
||||||
.Select(m => m.Key)
|
.Select(m => m.Key!)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
archive.Dispose();
|
archive.Dispose();
|
||||||
|
|
|
||||||
|
|
@ -282,7 +282,7 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
|
||||||
return BadRequest();
|
return BadRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogDebug($"Delete {request.Hashes}");
|
logger.LogDebug("Delete {Hashes}", request.Hashes);
|
||||||
|
|
||||||
var hashes = request.Hashes.Split("|");
|
var hashes = request.Hashes.Split("|");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,18 +29,18 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="8.0.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="8.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.3">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="8.0.3" />
|
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="8.0.7" />
|
||||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
<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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue