From 0a7e85c3e245e49b9f4031066a3dd0eeeddd1267 Mon Sep 17 00:00:00 2001
From: omgbeez <251408589+omgbeez@users.noreply.github.com>
Date: Sat, 17 Jan 2026 12:22:10 -0500
Subject: [PATCH] Implement new rate-limiting framework for torrent clients
---
.../src/app/models/rate-limit-status.model.ts | 4 +
.../torrent-table.component.html | 7 ++
.../torrent-table/torrent-table.component.ts | 16 +++-
client/src/app/torrent.service.ts | 10 +++
.../Models/Internal/RateLimitException.cs | 6 ++
.../Models/Internal/RateLimitStatus.cs | 7 ++
.../Helpers/RateLimitHandlerTest.cs | 53 +++++++++++++
.../RdtClient.Service.Test.csproj | 1 +
.../BackgroundServices/ProviderUpdater.cs | 7 ++
server/RdtClient.Service/DiConfig.cs | 70 +++++++++++++++--
.../Helpers/RateLimitHandler.cs | 34 +++++++++
.../RdtClient.Service.csproj | 4 +-
.../Services/RemoteService.cs | 6 ++
.../TorrentClients/AllDebridTorrentClient.cs | 49 ++++++++----
.../TorrentClients/DebridLinkTorrentClient.cs | 25 ++++++-
.../TorrentClients/PremiumizeTorrentClient.cs | 49 ++++++++----
.../TorrentClients/RealDebridTorrentClient.cs | 29 +++++--
.../TorrentClients/TorBoxTorrentClient.cs | 60 +++++++++------
.../Services/TorrentRunner.cs | 75 ++++++++++++++-----
.../Controllers/TorrentsController.cs | 23 ++++++
20 files changed, 442 insertions(+), 93 deletions(-)
create mode 100644 client/src/app/models/rate-limit-status.model.ts
create mode 100644 server/RdtClient.Data/Models/Internal/RateLimitException.cs
create mode 100644 server/RdtClient.Data/Models/Internal/RateLimitStatus.cs
create mode 100644 server/RdtClient.Service.Test/Helpers/RateLimitHandlerTest.cs
create mode 100644 server/RdtClient.Service/Helpers/RateLimitHandler.cs
diff --git a/client/src/app/models/rate-limit-status.model.ts b/client/src/app/models/rate-limit-status.model.ts
new file mode 100644
index 0000000..c6009dc
--- /dev/null
+++ b/client/src/app/models/rate-limit-status.model.ts
@@ -0,0 +1,4 @@
+export interface RateLimitStatus {
+ nextDequeueTime: Date | null;
+ secondsRemaining: number;
+}
diff --git a/client/src/app/torrent-table/torrent-table.component.html b/client/src/app/torrent-table/torrent-table.component.html
index b4c0e79..d441002 100644
--- a/client/src/app/torrent-table/torrent-table.component.html
+++ b/client/src/app/torrent-table/torrent-table.component.html
@@ -13,6 +13,13 @@
Last check: {{ diskSpaceStatus.lastCheckTime | date: 'short' }}
}
+@if (rateLimitStatus?.nextDequeueTime) {
+
+ Debrid provider rate limit reached
+
+ New torrents will not be added until {{ rateLimitStatus.nextDequeueTime | date: 'medium' }}
+
+}
diff --git a/client/src/app/torrent-table/torrent-table.component.ts b/client/src/app/torrent-table/torrent-table.component.ts
index da7478f..720619b 100644
--- a/client/src/app/torrent-table/torrent-table.component.ts
+++ b/client/src/app/torrent-table/torrent-table.component.ts
@@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Torrent } from '../models/torrent.model';
import { DiskSpaceStatus } from '../models/disk-space-status.model';
+import { RateLimitStatus } from '../models/rate-limit-status.model';
import { TorrentService } from '../torrent.service';
import { forkJoin, Observable } from 'rxjs';
import { FormsModule } from '@angular/forms';
@@ -21,8 +22,8 @@ export class TorrentTableComponent implements OnInit {
public torrents: Torrent[] = [];
public selectedTorrents: string[] = [];
public error: string;
- public sortProperty = 'rdName';
- public sortDirection: 'asc' | 'desc' = 'asc';
+ public sortProperty = 'added';
+ public sortDirection: 'asc' | 'desc' = 'desc';
public isDeleteModalActive: boolean;
public deleteError: string;
@@ -50,6 +51,7 @@ export class TorrentTableComponent implements OnInit {
public updateSettingsTorrentLifetime: number;
public diskSpaceStatus: DiskSpaceStatus | null = null;
+ public rateLimitStatus: RateLimitStatus | null = null;
constructor(
private router: Router,
@@ -67,6 +69,16 @@ export class TorrentTableComponent implements OnInit {
this.diskSpaceStatus = status;
});
+ this.torrentService.getRateLimitStatus().subscribe({
+ next: (status) => {
+ this.rateLimitStatus = status;
+ },
+ });
+
+ this.torrentService.rateLimitStatus$.subscribe((status) => {
+ this.rateLimitStatus = status;
+ });
+
this.torrentService.update$.subscribe((result) => {
this.torrents = result;
});
diff --git a/client/src/app/torrent.service.ts b/client/src/app/torrent.service.ts
index 8ecd2f0..334e812 100644
--- a/client/src/app/torrent.service.ts
+++ b/client/src/app/torrent.service.ts
@@ -4,6 +4,7 @@ import * as signalR from '@microsoft/signalr';
import { Observable, Subject } from 'rxjs';
import { Torrent, TorrentFileAvailability } from './models/torrent.model';
import { DiskSpaceStatus } from './models/disk-space-status.model';
+import { RateLimitStatus } from './models/rate-limit-status.model';
import { APP_BASE_HREF } from '@angular/common';
@Injectable({
@@ -12,6 +13,7 @@ import { APP_BASE_HREF } from '@angular/common';
export class TorrentService {
public update$: Subject = new Subject();
public diskSpaceStatus$: Subject = new Subject();
+ public rateLimitStatus$: Subject = new Subject();
private connection: signalR.HubConnection;
@@ -40,6 +42,10 @@ export class TorrentService {
this.diskSpaceStatus$.next(status);
});
+ this.connection.on('rateLimitStatus', (status: any) => {
+ this.rateLimitStatus$.next(status);
+ });
+
this.connection.onreconnected(() => {
this.getDiskSpaceStatus().subscribe({
next: (status) => {
@@ -65,6 +71,10 @@ export class TorrentService {
return this.http.get(`${this.baseHref}Api/Torrents/DiskSpaceStatus`);
}
+ public getRateLimitStatus(): Observable {
+ return this.http.get(`${this.baseHref}Api/Torrents/RateLimitStatus`);
+ }
+
public uploadMagnet(magnetLink: string, torrent: Torrent): Observable {
return this.http.post(`${this.baseHref}Api/Torrents/UploadMagnet`, {
magnetLink,
diff --git a/server/RdtClient.Data/Models/Internal/RateLimitException.cs b/server/RdtClient.Data/Models/Internal/RateLimitException.cs
new file mode 100644
index 0000000..3564975
--- /dev/null
+++ b/server/RdtClient.Data/Models/Internal/RateLimitException.cs
@@ -0,0 +1,6 @@
+namespace RdtClient.Data.Models.Internal;
+
+public class RateLimitException(String message, TimeSpan retryAfter) : Exception(message)
+{
+ public TimeSpan RetryAfter { get; } = retryAfter;
+}
diff --git a/server/RdtClient.Data/Models/Internal/RateLimitStatus.cs b/server/RdtClient.Data/Models/Internal/RateLimitStatus.cs
new file mode 100644
index 0000000..dd4d7f1
--- /dev/null
+++ b/server/RdtClient.Data/Models/Internal/RateLimitStatus.cs
@@ -0,0 +1,7 @@
+namespace RdtClient.Data.Models.Internal;
+
+public class RateLimitStatus
+{
+ public DateTimeOffset? NextDequeueTime { get; set; }
+ public Double SecondsRemaining { get; set; }
+}
diff --git a/server/RdtClient.Service.Test/Helpers/RateLimitHandlerTest.cs b/server/RdtClient.Service.Test/Helpers/RateLimitHandlerTest.cs
new file mode 100644
index 0000000..3cc95a5
--- /dev/null
+++ b/server/RdtClient.Service.Test/Helpers/RateLimitHandlerTest.cs
@@ -0,0 +1,53 @@
+using System.Net;
+using RdtClient.Data.Models.Internal;
+using RdtClient.Service.Helpers;
+using Xunit;
+
+namespace RdtClient.Service.Test.Helpers;
+
+public class RateLimitHandlerTest
+{
+ [Fact]
+ public async Task SendAsync_ThrowsRateLimitException_On429WithRetryAfter()
+ {
+ // Arrange
+ var handler = new RateLimitHandler
+ {
+ InnerHandler = new MockHttpMessageHandler(HttpStatusCode.TooManyRequests, 3600)
+ };
+ var client = new HttpClient(handler);
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => client.GetAsync("http://example.com"));
+ Assert.Equal(TimeSpan.FromSeconds(3600), ex.RetryAfter);
+ Assert.Equal("TorBox rate limit exceeded", ex.Message);
+ }
+
+ [Fact]
+ public async Task SendAsync_ThrowsRateLimitException_On429WithoutRetryAfter()
+ {
+ // Arrange
+ var handler = new RateLimitHandler
+ {
+ InnerHandler = new MockHttpMessageHandler(HttpStatusCode.TooManyRequests, null)
+ };
+ var client = new HttpClient(handler);
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => client.GetAsync("http://example.com"));
+ Assert.Equal(TimeSpan.FromMinutes(2), ex.RetryAfter);
+ }
+
+ private class MockHttpMessageHandler(HttpStatusCode statusCode, Int32? retryAfterSeconds) : HttpMessageHandler
+ {
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ var response = new HttpResponseMessage(statusCode);
+ if (retryAfterSeconds.HasValue)
+ {
+ response.Headers.RetryAfter = new System.Net.Http.Headers.RetryConditionHeaderValue(TimeSpan.FromSeconds(retryAfterSeconds.Value));
+ }
+ return Task.FromResult(response);
+ }
+ }
+}
diff --git a/server/RdtClient.Service.Test/RdtClient.Service.Test.csproj b/server/RdtClient.Service.Test/RdtClient.Service.Test.csproj
index 3313d85..1a41e5b 100644
--- a/server/RdtClient.Service.Test/RdtClient.Service.Test.csproj
+++ b/server/RdtClient.Service.Test/RdtClient.Service.Test.csproj
@@ -20,6 +20,7 @@
+
all
diff --git a/server/RdtClient.Service/BackgroundServices/ProviderUpdater.cs b/server/RdtClient.Service/BackgroundServices/ProviderUpdater.cs
index a9a5eba..35b8ab4 100644
--- a/server/RdtClient.Service/BackgroundServices/ProviderUpdater.cs
+++ b/server/RdtClient.Service/BackgroundServices/ProviderUpdater.cs
@@ -2,6 +2,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RdtClient.Data.Enums;
+using RdtClient.Data.Models.Internal;
using RdtClient.Service.Services;
namespace RdtClient.Service.BackgroundServices;
@@ -19,6 +20,7 @@ public class ProviderUpdater(ILogger logger, IServiceProvider s
using var scope = serviceProvider.CreateScope();
var torrentService = scope.ServiceProvider.GetRequiredService();
+ var torrentRunner = scope.ServiceProvider.GetRequiredService();
logger.LogInformation("ProviderUpdater started.");
@@ -56,6 +58,11 @@ public class ProviderUpdater(ILogger logger, IServiceProvider s
logger.LogDebug("Finished updating torrent info from debrid provider, next update in {updateTime} seconds", updateTime);
}
}
+ catch (RateLimitException ex)
+ {
+ await torrentRunner.SetRateLimit(ex.RetryAfter, ex.Message);
+ _nextUpdate = DateTime.UtcNow.Add(ex.RetryAfter);
+ }
catch (Exception ex)
{
logger.LogError(ex, "Unexpected error occurred in ProviderUpdater: {ex.Message}", ex.Message);
diff --git a/server/RdtClient.Service/DiConfig.cs b/server/RdtClient.Service/DiConfig.cs
index c93f5e5..c9d2f27 100644
--- a/server/RdtClient.Service/DiConfig.cs
+++ b/server/RdtClient.Service/DiConfig.cs
@@ -4,8 +4,11 @@ using System.Reflection;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Polly;
-using Polly.Extensions.Http;
+using Polly.Timeout;
+using RateLimitHeaders.Polly;
+using RdtClient.Data.Models.Internal;
using RdtClient.Service.BackgroundServices;
+using RdtClient.Service.Helpers;
using RdtClient.Service.Middleware;
using RdtClient.Service.Services;
using RdtClient.Service.Services.TorrentClients;
@@ -16,6 +19,7 @@ namespace RdtClient.Service;
public static class DiConfig
{
public const String RD_CLIENT = "RdClient";
+ public const String TORBOX_CLIENT = "TorBoxClient";
public static readonly String UserAgent = $"rdt-client {Assembly.GetEntryAssembly()?.GetName().Version}";
public static void RegisterRdtServices(this IServiceCollection services)
@@ -58,11 +62,6 @@ public static class DiConfig
public static void RegisterHttpClients(this IServiceCollection services)
{
- var retryPolicy = HttpPolicyExtensions
- .HandleTransientHttpError()
- .OrResult(r => r.StatusCode == HttpStatusCode.TooManyRequests)
- .WaitAndRetryAsync(retryCount: 5, sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));
-
services.AddHttpClient();
services.ConfigureHttpClientDefaults(builder =>
{
@@ -72,7 +71,64 @@ public static class DiConfig
});
});
+ services.AddTransient();
+
services.AddHttpClient(RD_CLIENT)
- .AddPolicyHandler(retryPolicy);
+ .AddHttpMessageHandler()
+ .AddResilienceHandler("rd_client_handler", ConfigureResiliencePipeline);
+
+ services.AddHttpClient(TORBOX_CLIENT)
+ .AddHttpMessageHandler()
+ .AddResilienceHandler("torbox_client_handler", ConfigureResiliencePipeline);
+ }
+
+ private static void ConfigureResiliencePipeline(ResiliencePipelineBuilder builder)
+ {
+ builder.AddRateLimitHeaders(options =>
+ {
+ options.EnableProactiveThrottling = true;
+ });
+ builder.AddRetry(new()
+ {
+ ShouldHandle = args => args.Outcome switch
+ {
+ { Exception: HttpRequestException } => PredicateResult.True(),
+ { Result.StatusCode: HttpStatusCode.RequestTimeout } => PredicateResult.True(),
+ { Result.StatusCode: HttpStatusCode.TooManyRequests } => PredicateResult.True(),
+ { Result.StatusCode: >= HttpStatusCode.InternalServerError } => PredicateResult.True(),
+ _ => PredicateResult.False()
+ },
+ MaxRetryAttempts = 2,
+ BackoffType = DelayBackoffType.Exponential,
+ Delay = TimeSpan.FromSeconds(2),
+ UseJitter = true,
+ DelayGenerator = args =>
+ {
+ if (args.Outcome.Result is { StatusCode: HttpStatusCode.TooManyRequests } response)
+ {
+ var retryAfter = response.Headers.RetryAfter;
+ var delay = retryAfter?.Delta ?? (retryAfter?.Date.HasValue == true ? retryAfter.Date.Value - DateTimeOffset.UtcNow : TimeSpan.FromMinutes(2));
+
+ if (delay < TimeSpan.Zero)
+ {
+ delay = TimeSpan.FromMinutes(2);
+ }
+
+ if (delay >= TimeSpan.FromSeconds(Settings.Get.Provider.Timeout))
+ {
+ throw new RateLimitException("TorBox rate limit exceeded", delay);
+ }
+
+ return new ValueTask(delay);
+ }
+
+ return new ValueTask((TimeSpan?)null);
+ }
+ });
+
+ builder.AddTimeout(new TimeoutStrategyOptions
+ {
+ TimeoutGenerator = _ => new ValueTask(TimeSpan.FromSeconds(Settings.Get.Provider.Timeout))
+ });
}
}
diff --git a/server/RdtClient.Service/Helpers/RateLimitHandler.cs b/server/RdtClient.Service/Helpers/RateLimitHandler.cs
new file mode 100644
index 0000000..14a7742
--- /dev/null
+++ b/server/RdtClient.Service/Helpers/RateLimitHandler.cs
@@ -0,0 +1,34 @@
+using System.Net;
+using RdtClient.Data.Models.Internal;
+
+namespace RdtClient.Service.Helpers;
+
+public class RateLimitHandler : DelegatingHandler
+{
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var response = await base.SendAsync(request, cancellationToken);
+
+ if (response.StatusCode == HttpStatusCode.TooManyRequests)
+ {
+ var retryAfter = response.Headers.RetryAfter;
+ var delay = retryAfter?.Delta ?? (retryAfter?.Date.HasValue == true ? retryAfter.Date.Value - DateTimeOffset.UtcNow : TimeSpan.FromMinutes(2));
+
+ if (delay < TimeSpan.Zero)
+ {
+ delay = TimeSpan.FromMinutes(2);
+ }
+
+ throw new RateLimitException("TorBox rate limit exceeded", delay);
+ }
+
+ return response;
+ }
+ catch (TaskCanceledException ex) when (!cancellationToken.IsCancellationRequested)
+ {
+ throw new RateLimitException("TorBox rate limit exceeded (timeout)", TimeSpan.FromMinutes(2));
+ }
+ }
+}
diff --git a/server/RdtClient.Service/RdtClient.Service.csproj b/server/RdtClient.Service/RdtClient.Service.csproj
index 3d042d8..6822a09 100644
--- a/server/RdtClient.Service/RdtClient.Service.csproj
+++ b/server/RdtClient.Service/RdtClient.Service.csproj
@@ -18,6 +18,8 @@
+
+
@@ -25,7 +27,7 @@
-
+
diff --git a/server/RdtClient.Service/Services/RemoteService.cs b/server/RdtClient.Service/Services/RemoteService.cs
index 8eea62c..a930f09 100644
--- a/server/RdtClient.Service/Services/RemoteService.cs
+++ b/server/RdtClient.Service/Services/RemoteService.cs
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.SignalR;
+using RdtClient.Data.Models.Internal;
namespace RdtClient.Service.Services;
@@ -24,4 +25,9 @@ public class RemoteService(IHubContext hub, Torrents torrents)
{
await hub.Clients.All.SendCoreAsync("diskSpaceStatus", [status]);
}
+
+ public async Task UpdateRateLimitStatus(RateLimitStatus status)
+ {
+ await hub.Clients.All.SendCoreAsync("rateLimitStatus", [status]);
+ }
}
\ No newline at end of file
diff --git a/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs
index 31eacee..51905ea 100644
--- a/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs
+++ b/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient;
+using RdtClient.Data.Models.Internal;
using RdtClient.Service.Helpers;
using RdtClient.Data.Models.Data;
using File = AllDebridNET.File;
@@ -124,30 +125,46 @@ public class AllDebridTorrentClient(ILogger logger, IAll
public async Task AddMagnet(String magnetLink)
{
- var result = await allDebridNetClientFactory.GetClient().Magnet.UploadMagnetAsync(magnetLink);
-
- if (result?.Id == null)
+ try
{
- throw new("Unable to add magnet link");
+ var result = await allDebridNetClientFactory.GetClient().Magnet.UploadMagnetAsync(magnetLink);
+
+ if (result?.Id == null)
+ {
+ throw new("Unable to add magnet link");
+ }
+
+ var resultId = result.Id.ToString() ?? throw new($"Invalid responseID {result.Id}");
+
+ return resultId;
+ }
+ catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
+ ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
-
- var resultId = result.Id.ToString() ?? throw new($"Invalid responseID {result.Id}");
-
- return resultId;
}
public async Task AddFile(Byte[] bytes)
{
- var result = await allDebridNetClientFactory.GetClient().Magnet.UploadFileAsync(bytes);
-
- if (result?.Id == null)
+ try
{
- throw new("Unable to add torrent file");
+ var result = await allDebridNetClientFactory.GetClient().Magnet.UploadFileAsync(bytes);
+
+ if (result?.Id == null)
+ {
+ throw new("Unable to add torrent file");
+ }
+
+ var resultId = result.Id.ToString() ?? throw new($"Invalid responseID {result.Id}");
+
+ return resultId;
+ }
+ catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
+ ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
-
- var resultId = result.Id.ToString() ?? throw new($"Invalid responseID {result.Id}");
-
- return resultId;
}
public Task> GetAvailableFiles(String hash)
diff --git a/server/RdtClient.Service/Services/TorrentClients/DebridLinkTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/DebridLinkTorrentClient.cs
index 567b777..14b1574 100644
--- a/server/RdtClient.Service/Services/TorrentClients/DebridLinkTorrentClient.cs
+++ b/server/RdtClient.Service/Services/TorrentClients/DebridLinkTorrentClient.cs
@@ -4,6 +4,7 @@ using Newtonsoft.Json;
using DebridLinkFrNET;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient;
+using RdtClient.Data.Models.Internal;
using RdtClient.Service.Helpers;
using RdtClient.Data.Models.Data;
using Download = RdtClient.Data.Models.Data.Download;
@@ -120,16 +121,32 @@ public class DebridLinkClient(ILogger logger, IHttpClientFacto
public async Task AddMagnet(String magnetLink)
{
- var result = await GetClient().Seedbox.AddTorrentAsync(magnetLink);
+ try
+ {
+ var result = await GetClient().Seedbox.AddTorrentAsync(magnetLink);
- return result.Id ?? "";
+ return result.Id ?? "";
+ }
+ catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
+ ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
+ }
}
public async Task AddFile(Byte[] bytes)
{
- var result = await GetClient().Seedbox.AddTorrentByFileAsync(bytes);
+ try
+ {
+ var result = await GetClient().Seedbox.AddTorrentByFileAsync(bytes);
- return result.Id ?? "";
+ return result.Id ?? "";
+ }
+ catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
+ ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
+ }
}
public Task> GetAvailableFiles(String hash)
diff --git a/server/RdtClient.Service/Services/TorrentClients/PremiumizeTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/PremiumizeTorrentClient.cs
index 1c7014d..effae5e 100644
--- a/server/RdtClient.Service/Services/TorrentClients/PremiumizeTorrentClient.cs
+++ b/server/RdtClient.Service/Services/TorrentClients/PremiumizeTorrentClient.cs
@@ -4,6 +4,7 @@ using Newtonsoft.Json;
using PremiumizeNET;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient;
+using RdtClient.Data.Models.Internal;
using RdtClient.Service.Helpers;
using RdtClient.Data.Models.Data;
using Torrent = RdtClient.Data.Models.Data.Torrent;
@@ -91,30 +92,46 @@ public class PremiumizeTorrentClient(ILogger logger, IH
public async Task AddMagnet(String magnetLink)
{
- var result = await GetClient().Transfers.CreateAsync(magnetLink, "");
-
- if (result?.Id == null)
+ try
{
- throw new("Unable to add magnet link");
+ var result = await GetClient().Transfers.CreateAsync(magnetLink, "");
+
+ if (result?.Id == null)
+ {
+ throw new("Unable to add magnet link");
+ }
+
+ var resultId = result.Id ?? throw new($"Invalid responseID {result.Id}");
+
+ return resultId;
+ }
+ catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
+ ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
-
- var resultId = result.Id ?? throw new($"Invalid responseID {result.Id}");
-
- return resultId;
}
public async Task AddFile(Byte[] bytes)
{
- var result = await GetClient().Transfers.CreateAsync(bytes, "");
-
- if (result?.Id == null)
+ try
{
- throw new("Unable to add torrent file");
+ var result = await GetClient().Transfers.CreateAsync(bytes, "");
+
+ if (result?.Id == null)
+ {
+ throw new("Unable to add torrent file");
+ }
+
+ var resultId = result.Id ?? throw new($"Invalid responseID {result.Id}");
+
+ return resultId;
+ }
+ catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
+ ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
-
- var resultId = result.Id ?? throw new($"Invalid responseID {result.Id}");
-
- return resultId;
}
public Task> GetAvailableFiles(String hash)
diff --git a/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs
index f5b2289..1b5e101 100644
--- a/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs
+++ b/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs
@@ -5,6 +5,7 @@ using RDNET;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.TorrentClient;
+using RdtClient.Data.Models.Internal;
using RdtClient.Service.Helpers;
using Download = RdtClient.Data.Models.Data.Download;
using Torrent = RDNET.Torrent;
@@ -128,20 +129,36 @@ public class RealDebridTorrentClient(ILogger logger, IH
public async Task AddMagnet(String magnetLink)
{
- var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(Settings.Get.Provider.Timeout));
+ try
+ {
+ var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(Settings.Get.Provider.Timeout));
- var result = await GetClient().Torrents.AddMagnetAsync(magnetLink, timeoutCancellationToken.Token);
+ var result = await GetClient().Torrents.AddMagnetAsync(magnetLink, timeoutCancellationToken.Token);
- return result.Id;
+ return result.Id;
+ }
+ catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
+ ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
+ }
}
public async Task AddFile(Byte[] bytes)
{
- var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(Settings.Get.Provider.Timeout));
+ try
+ {
+ var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(Settings.Get.Provider.Timeout));
- var result = await GetClient().Torrents.AddFileAsync(bytes, timeoutCancellationToken.Token);
+ var result = await GetClient().Torrents.AddFileAsync(bytes, timeoutCancellationToken.Token);
- return result.Id;
+ return result.Id;
+ }
+ catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
+ ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
+ }
}
public Task> GetAvailableFiles(String hash)
diff --git a/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs
index 4994e72..7a2a012 100644
--- a/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs
+++ b/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs
@@ -5,6 +5,7 @@ using TorBoxNET;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Data.Models.Data;
+using RdtClient.Data.Models.Internal;
using RdtClient.Service.Helpers;
namespace RdtClient.Service.Services.TorrentClients;
@@ -23,10 +24,10 @@ public class TorBoxTorrentClient(ILogger logger, IHttpClien
throw new("TorBox API Key not set in the settings");
}
- var httpClient = httpClientFactory.CreateClient();
+ var httpClient = httpClientFactory.CreateClient(DiConfig.TORBOX_CLIENT);
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
- var torBoxNetClient = new TorBoxNetClient(null, httpClient, 5);
+ var torBoxNetClient = new TorBoxNetClient(null, httpClient, 1);
torBoxNetClient.UseApiAuthentication(apiKey);
// Get the server time to fix up the timezones on results
@@ -120,35 +121,48 @@ public class TorBoxTorrentClient(ILogger logger, IHttpClien
};
}
+ private async Task AddTorrentRetry(Func> action)
+ {
+ try
+ {
+ return await action(false);
+ }
+ catch (RateLimitException)
+ {
+ throw;
+ }
+ catch (Exception ex) when (ex.InnerException is RateLimitException rateLimitException)
+ {
+ throw rateLimitException;
+ }
+ catch (TorBoxException ex) when (ex.Error.Equals("active_limit", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
+ }
+ catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
+ }
+ }
+
public async Task AddMagnet(String magnetLink)
{
- var user = await GetClient().User.GetAsync(true);
-
- var result = await GetClient().Torrents.AddMagnetAsync(magnetLink, user.Data?.Settings?.SeedTorrents ?? 3, false);
-
- if (result.Error == "ACTIVE_LIMIT")
+ return await AddTorrentRetry(async asQueued =>
{
- var magnetLinkInfo = MonoTorrent.MagnetLink.Parse(magnetLink);
- return magnetLinkInfo.InfoHashes.V1!.ToHex().ToLowerInvariant();
- }
-
- return result.Data!.Hash!;
+ var user = await GetClient().User.GetAsync(true);
+ var result = await GetClient().Torrents.AddMagnetAsync(magnetLink, user.Data?.Settings?.SeedTorrents ?? 3, as_queued: asQueued);
+ return result.Data!.Hash!;
+ });
}
public async Task AddFile(Byte[] bytes)
{
- var user = await GetClient().User.GetAsync(true);
-
- var result = await GetClient().Torrents.AddFileAsync(bytes, user.Data?.Settings?.SeedTorrents ?? 3);
- if (result.Error == "ACTIVE_LIMIT")
+ return await AddTorrentRetry(async asQueued =>
{
- using var stream = new MemoryStream(bytes);
-
- var torrent = await MonoTorrent.Torrent.LoadAsync(stream);
- return torrent.InfoHashes.V1!.ToHex().ToLowerInvariant();
- }
-
- return result.Data!.Hash!;
+ var user = await GetClient().User.GetAsync(true);
+ var result = await GetClient().Torrents.AddFileAsync(bytes, user.Data?.Settings?.SeedTorrents ?? 3, as_queued: asQueued);
+ return result.Data!.Hash!;
+ });
}
public async Task> GetAvailableFiles(String hash)
diff --git a/server/RdtClient.Service/Services/TorrentRunner.cs b/server/RdtClient.Service/Services/TorrentRunner.cs
index 50e87ed..d5765b8 100644
--- a/server/RdtClient.Service/Services/TorrentRunner.cs
+++ b/server/RdtClient.Service/Services/TorrentRunner.cs
@@ -11,13 +11,15 @@ using System.Text.Json;
namespace RdtClient.Service.Services;
-public class TorrentRunner(ILogger logger, Torrents torrents, Downloads downloads)
+public class TorrentRunner(ILogger logger, Torrents torrents, Downloads downloads, RemoteService remoteService)
{
public static readonly ConcurrentDictionary ActiveDownloadClients = new();
public static readonly ConcurrentDictionary ActiveUnpackClients = new();
public static Boolean IsPausedForLowDiskSpace { get; set; }
+ public static DateTimeOffset NextDequeueTime { get; private set; } = DateTimeOffset.MinValue;
+
private readonly HttpClient _httpClient = new()
{
Timeout = TimeSpan.FromSeconds(10)
@@ -323,27 +325,51 @@ public class TorrentRunner(ILogger logger, Torrents torrents, Dow
if (torrentsToAddToProvider.Count != 0)
{
- var downloadingTorrentsCount = allTorrents.Count(m => m.RdStatus is not (TorrentStatus.Queued or TorrentStatus.Finished or TorrentStatus.Error));
-
- var maxParallelDownloads = Settings.Get.Provider.MaxParallelDownloads;
-
- logger.LogDebug("Currently downloading {downloadingTorrentCount}/{maxParallelDownloads} torrents, {queuedCount} queued.",
- downloadingTorrentsCount,
- maxParallelDownloads,
- torrentsToAddToProvider.Count);
-
- var dequeueCount = maxParallelDownloads == 0 ? torrentsToAddToProvider.Count : maxParallelDownloads - downloadingTorrentsCount;
-
- foreach (var torrent in torrentsToAddToProvider.Take(dequeueCount))
+ if (DateTimeOffset.Now < NextDequeueTime)
{
- try
+ logger.LogDebug($"Dequeuing torrents is paused until {NextDequeueTime}, {NextDequeueTime - DateTimeOffset.Now} remaining");
+ }
+ else
+ {
+ if (NextDequeueTime != DateTimeOffset.MinValue)
{
- await torrents.DequeueFromDebridQueue(torrent);
+ NextDequeueTime = DateTimeOffset.MinValue;
+
+ await remoteService.UpdateRateLimitStatus(new RateLimitStatus
+ {
+ NextDequeueTime = null,
+ SecondsRemaining = 0
+ });
}
- catch (Exception ex)
+
+ var downloadingTorrentsCount = allTorrents.Count(m => m.RdStatus is not (TorrentStatus.Queued or TorrentStatus.Finished or TorrentStatus.Error));
+
+ var maxParallelDownloads = Settings.Get.Provider.MaxParallelDownloads;
+
+ logger.LogDebug("Currently downloading {downloadingTorrentCount}/{maxParallelDownloads} torrents, {queuedCount} queued.",
+ downloadingTorrentsCount,
+ maxParallelDownloads,
+ torrentsToAddToProvider.Count);
+
+ var dequeueCount = maxParallelDownloads == 0 ? torrentsToAddToProvider.Count : maxParallelDownloads - downloadingTorrentsCount;
+
+ foreach (var torrent in torrentsToAddToProvider.Take(dequeueCount))
{
- await torrents.UpdateComplete(torrent.TorrentId, $"Could not add to provider: {ex.Message}", DateTimeOffset.Now, true);
- logger.LogWarning(ex, "Could not dequeue torrent {torrentId}", torrent.TorrentId);
+ try
+ {
+ await torrents.DequeueFromDebridQueue(torrent);
+ }
+ catch (RateLimitException ex)
+ {
+ await SetRateLimit(ex.RetryAfter, ex.Message);
+
+ break;
+ }
+ catch (Exception ex)
+ {
+ await torrents.UpdateComplete(torrent.TorrentId, $"Could not add to provider: {ex.Message}", DateTimeOffset.Now, true);
+ logger.LogWarning(ex, "Could not dequeue torrent {torrentId}", torrent.TorrentId);
+ }
}
}
}
@@ -698,6 +724,19 @@ public class TorrentRunner(ILogger logger, Torrents torrents, Dow
}
}
+ public async Task SetRateLimit(TimeSpan retryAfter, String message)
+ {
+ NextDequeueTime = DateTimeOffset.Now.Add(retryAfter);
+
+ Log($"Rate-limit reached, pausing dequeuing for {retryAfter.TotalMinutes} minutes (until {NextDequeueTime}): {message}");
+
+ await remoteService.UpdateRateLimitStatus(new RateLimitStatus
+ {
+ NextDequeueTime = NextDequeueTime,
+ SecondsRemaining = retryAfter.TotalSeconds
+ });
+ }
+
private void Log(String message, Download? download, Torrent? torrent)
{
if (download != null)
diff --git a/server/RdtClient.Web/Controllers/TorrentsController.cs b/server/RdtClient.Web/Controllers/TorrentsController.cs
index fd3a0dc..005d9ee 100644
--- a/server/RdtClient.Web/Controllers/TorrentsController.cs
+++ b/server/RdtClient.Web/Controllers/TorrentsController.cs
@@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MonoTorrent;
+using RdtClient.Data.Models.Internal;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers;
using RdtClient.Service.Services;
@@ -53,6 +54,28 @@ public class TorrentsController(ILogger logger, Torrents tor
return Ok(status);
}
+ [HttpGet]
+ [Route("RateLimitStatus")]
+ public ActionResult GetRateLimitStatus()
+ {
+ var nextDequeueTime = TorrentRunner.NextDequeueTime;
+
+ if (nextDequeueTime < DateTimeOffset.Now)
+ {
+ return Ok(new RateLimitStatus
+ {
+ NextDequeueTime = null,
+ SecondsRemaining = 0
+ });
+ }
+
+ return Ok(new RateLimitStatus
+ {
+ NextDequeueTime = nextDequeueTime,
+ SecondsRemaining = (nextDequeueTime - DateTimeOffset.Now).TotalSeconds
+ });
+ }
+
///
/// Used for debugging only. Force a tick.
///