Merge branch 'main' into chore/angular-v20

This commit is contained in:
Cucumberrbob 2025-07-29 19:29:05 +01:00
commit 48f00e0459
No known key found for this signature in database
GPG key ID: 2B935C47401C3614
28 changed files with 677 additions and 97 deletions

View file

@ -4,7 +4,22 @@ 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).
## [Unreleased]
## [Unreleased
## [2.0.115] - 2025-07-28
### Added
- Added setting to delay the finish action.
### Fixed
- Make sure the Real-Debrid provider times out when trying to add a new torrent.
## [2.0.114] - 2025-06-21
### Added
- Add Select All functionality to the delete dialog in individual torrent screen, thanks @mentalblank
- Add setting to add a list of trackers (from a URL) to every torrent and magnet that's added to rdt-client, thanks @mentalblank
### Changed
- The `User-Agent` header is now set on all requests to debrid providers' APIs.
## [2.0.113] - 2025-05-22
### Fixed

View file

@ -13,7 +13,7 @@ rdt-client is a web a web interface to manage your torrents on Real-Debrid. It s
Our images support multiple architectures such as `x86-64`, `arm64` and `armhf`. We utilise the docker manifest for multi-platform awareness. More information is available from docker [here](https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-2.md#manifest-list) and our announcement [here](https://blog.linuxserver.io/2019/02/21/the-lsio-pipeline-project/).
Simply pulling `rogerfar/rdt-client` should retrieve the correct image for your arch, but you can also pull specific arch images via tags.
Simply pulling `rogerfar/rdtclient` should retrieve the correct image for your arch, but you can also pull specific arch images via tags.
The architectures supported by this image are:

View file

@ -151,6 +151,17 @@
</div>
</div>
<div class="field">
<label class="label">Finished Action Delay (minutes)</label>
<div class="control is-fullwidth">
<input class="input" type="number" [(ngModel)]="finishedActionDelay" />
</div>
<p class="help">
When a torrent is finished downloading on the provider, perform this action. Use this setting if you only want
to add files to your debrid provider but not download them to the host.
</p>
</div>
<div class="field">
<label class="label">Category</label>
<div class="control">

View file

@ -26,6 +26,7 @@ export class AddNewTorrentComponent implements OnInit {
public hostDownloadAction: number = 0;
public downloadAction: number = 0;
public finishedAction: number = 0;
public finishedActionDelay: number = 0;
public downloadMinSize: number = 0;
public includeRegex: string = '';
public excludeRegex: string = '';
@ -72,6 +73,7 @@ export class AddNewTorrentComponent implements OnInit {
this.downloadAction =
settings.find((m) => m.key === 'Gui:Default:OnlyDownloadAvailableFiles')?.value === true ? 1 : 0;
this.finishedAction = settings.find((m) => m.key === 'Gui:Default:FinishedAction')?.value as number;
this.finishedActionDelay = settings.find((m) => m.key == 'Gui:Default:FinishedActionDelay')?.value as number;
this.downloadMinSize = settings.find((m) => m.key === 'Gui:Default:MinFileSize')?.value as number;
this.includeRegex = settings.find((m) => m.key === 'Gui:Default:IncludeRegex')?.value as string;
this.excludeRegex = settings.find((m) => m.key === 'Gui:Default:ExcludeRegex')?.value as string;
@ -138,6 +140,7 @@ export class AddNewTorrentComponent implements OnInit {
torrent.hostDownloadAction = this.hostDownloadAction;
torrent.downloadAction = this.downloadAction;
torrent.finishedAction = this.finishedAction;
torrent.finishedActionDelay = this.finishedActionDelay;
torrent.downloadMinSize = this.downloadMinSize;
torrent.includeRegex = this.includeRegex;
torrent.excludeRegex = this.excludeRegex;

View file

@ -8,6 +8,7 @@ export class Torrent {
public hostDownloadAction: number;
public downloadAction: number;
public finishedAction: number;
public finishedActionDelay: number;
public downloadMinSize: number;
public includeRegex: string;
public excludeRegex: string;

View file

@ -116,6 +116,15 @@
}
}
</div>
<div class="field">
<label class="label">Finished action delay</label>
{{ torrent.finishedActionDelay || 0 }}
@if (torrent.finishedActionDelay === 1) {
minute
} @else {
minutes
}
</div>
<div class="field">
<label class="label">Minimum file size to download</label>
{{ torrent.downloadMinSize }}MB

View file

@ -86,6 +86,7 @@ public class TorrentData(DataContext dataContext) : ITorrentData
Hash = hash.ToLower(),
Category = torrent.Category,
HostDownloadAction = torrent.HostDownloadAction,
FinishedActionDelay = torrent.FinishedActionDelay,
DownloadAction = torrent.DownloadAction,
FinishedAction = torrent.FinishedAction,
DownloadMinSize = torrent.DownloadMinSize,

View file

@ -0,0 +1,482 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using RdtClient.Data.Data;
#nullable disable
namespace RdtClient.Data.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20250706204358_AddFinishedActionDelay")]
partial class AddFinishedActionDelay
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.5");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
{
b.Property<Guid>("DownloadId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("DownloadStarted")
.HasColumnType("TEXT");
b.Property<string>("Error")
.HasColumnType("TEXT");
b.Property<string>("FileName")
.HasColumnType("TEXT");
b.Property<string>("Link")
.HasColumnType("TEXT");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("RemoteId")
.HasColumnType("TEXT");
b.Property<int>("RetryCount")
.HasColumnType("INTEGER");
b.Property<Guid>("TorrentId")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingFinished")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingQueued")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnpackingStarted")
.HasColumnType("TEXT");
b.HasKey("DownloadId");
b.HasIndex("TorrentId");
b.ToTable("Downloads");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Setting", b =>
{
b.Property<string>("SettingId")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("SettingId");
b.ToTable("Settings");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
{
b.Property<Guid>("TorrentId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Added")
.HasColumnType("TEXT");
b.Property<string>("Category")
.HasColumnType("TEXT");
b.Property<int?>("ClientKind")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("Completed")
.HasColumnType("TEXT");
b.Property<int>("DeleteOnError")
.HasColumnType("INTEGER");
b.Property<int>("DownloadAction")
.HasColumnType("INTEGER");
b.Property<int>("DownloadClient")
.HasColumnType("INTEGER");
b.Property<string>("DownloadManualFiles")
.HasColumnType("TEXT");
b.Property<int>("DownloadMinSize")
.HasColumnType("INTEGER");
b.Property<int>("DownloadRetryAttempts")
.HasColumnType("INTEGER");
b.Property<string>("Error")
.HasColumnType("TEXT");
b.Property<string>("ExcludeRegex")
.HasColumnType("TEXT");
b.Property<string>("FileOrMagnet")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("FilesSelected")
.HasColumnType("TEXT");
b.Property<int>("FinishedAction")
.HasColumnType("INTEGER");
b.Property<int>("FinishedActionDelay")
.HasColumnType("INTEGER");
b.Property<string>("Hash")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("HostDownloadAction")
.HasColumnType("INTEGER");
b.Property<string>("IncludeRegex")
.HasColumnType("TEXT");
b.Property<bool>("IsFile")
.HasColumnType("INTEGER");
b.Property<int>("Lifetime")
.HasColumnType("INTEGER");
b.Property<int?>("Priority")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("RdAdded")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("RdEnded")
.HasColumnType("TEXT");
b.Property<string>("RdFiles")
.HasColumnType("TEXT");
b.Property<string>("RdHost")
.HasColumnType("TEXT");
b.Property<string>("RdId")
.HasColumnType("TEXT");
b.Property<string>("RdName")
.HasColumnType("TEXT");
b.Property<long?>("RdProgress")
.HasColumnType("INTEGER");
b.Property<long?>("RdSeeders")
.HasColumnType("INTEGER");
b.Property<long?>("RdSize")
.HasColumnType("INTEGER");
b.Property<long?>("RdSpeed")
.HasColumnType("INTEGER");
b.Property<long?>("RdSplit")
.HasColumnType("INTEGER");
b.Property<int?>("RdStatus")
.HasColumnType("INTEGER");
b.Property<string>("RdStatusRaw")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Retry")
.HasColumnType("TEXT");
b.Property<int>("RetryCount")
.HasColumnType("INTEGER");
b.Property<int>("TorrentRetryAttempts")
.HasColumnType("INTEGER");
b.HasKey("TorrentId");
b.ToTable("Torrents");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b =>
{
b.HasOne("RdtClient.Data.Models.Data.Torrent", "Torrent")
.WithMany("Downloads")
.HasForeignKey("TorrentId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Torrent");
});
modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b =>
{
b.Navigation("Downloads");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace RdtClient.Data.Migrations
{
/// <inheritdoc />
public partial class AddFinishedActionDelay : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "FinishedActionDelay",
table: "Torrents",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FinishedActionDelay",
table: "Torrents");
}
}
}

View file

@ -15,7 +15,7 @@ namespace RdtClient.Data.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
modelBuilder.HasAnnotation("ProductVersion", "9.0.5");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
@ -332,6 +332,9 @@ namespace RdtClient.Data.Migrations
b.Property<int>("FinishedAction")
.HasColumnType("INTEGER");
b.Property<int>("FinishedActionDelay")
.HasColumnType("INTEGER");
b.Property<string>("Hash")
.IsRequired()
.HasColumnType("TEXT");

View file

@ -17,6 +17,7 @@ public class Torrent
public TorrentDownloadAction DownloadAction { get; set; }
public TorrentFinishedAction FinishedAction { get; set; }
public Int32 FinishedActionDelay { get; set; }
public TorrentHostDownloadAction HostDownloadAction { get; set; }
public Int32 DownloadMinSize { get; set; }
public String? IncludeRegex { get; set; }

View file

@ -269,6 +269,10 @@ public class DbSettingsDefaultsWithCategory : DbSettingsDefaults
[DisplayName("Post Download Action")]
[Description("When all files are downloaded from the provider to the host, perform this action. Does not apply when using the symlink downloader.")]
public TorrentFinishedAction FinishedAction { get; set; } = TorrentFinishedAction.RemoveAllTorrents;
[DisplayName("Finished Action Delay")]
[Description("When all files are downloaded from the provider to the host, wait this many minutes before performing the action above.")]
public Int32 FinishedActionDelay { get; set; } = 0;
}
public class DbSettingsDefaults

View file

@ -8,15 +8,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.5" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.5">
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.7" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog" Version="4.3.0" />
</ItemGroup>
</Project>

View file

@ -15,13 +15,13 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.0.14" />
<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="22.0.14" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.14" />
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.0.15" />
<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="22.0.15" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.15" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.0">
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View file

@ -13,7 +13,7 @@ public class EnricherTest : IDisposable
public EnricherTest()
{
_mockRepository = new MockRepository(MockBehavior.Strict);
_mockRepository = new(MockBehavior.Strict);
_loggerMock = _mockRepository.Create<ILogger<Enricher>>(MockBehavior.Loose);
_trackerListGrabberMock = _mockRepository.Create<ITrackerListGrabber>();
}
@ -67,7 +67,7 @@ public class EnricherTest : IDisposable
private static BEncodedDictionary CreateTorrentDictWithOnlyAnnounce(String announceUrl)
{
return new BEncodedDictionary
return new()
{
["announce"] = new BEncodedString(announceUrl),
["info"] = new BEncodedDictionary()
@ -76,7 +76,7 @@ public class EnricherTest : IDisposable
private static BEncodedDictionary CreateTorrentDictWithNoTrackers()
{
return new BEncodedDictionary
return new()
{
["info"] = new BEncodedDictionary()
};

View file

@ -82,6 +82,7 @@ public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProv
DownloadClient = Settings.Get.DownloadClient.Client,
Category = Settings.Get.Watch.Default.Category,
HostDownloadAction = Settings.Get.Watch.Default.HostDownloadAction,
FinishedActionDelay = Settings.Get.Watch.Default.FinishedActionDelay,
DownloadAction = Settings.Get.Watch.Default.OnlyDownloadAvailableFiles
? TorrentDownloadAction.DownloadAvailableFiles
: TorrentDownloadAction.DownloadAll,

View file

@ -14,7 +14,9 @@ public class WebsocketsUpdater(ILogger<WebsocketsUpdater> logger, IServiceProvid
await Task.Delay(1000, stoppingToken);
}
var remoteService = serviceProvider.GetRequiredService<RemoteService>();
var scope = serviceProvider.CreateScope();
var remoteService = scope.ServiceProvider.GetRequiredService<RemoteService>();
while (!stoppingToken.IsCancellationRequested)
{

View file

@ -1,5 +1,6 @@
using System.IO.Abstractions;
using System.Net;
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Polly;
@ -15,6 +16,7 @@ namespace RdtClient.Service;
public static class DiConfig
{
public const String RD_CLIENT = "RdClient";
public static readonly String UserAgent = $"rdt-client {Assembly.GetEntryAssembly()?.GetName().Version}";
public static void RegisterRdtServices(this IServiceCollection services)
{
@ -55,13 +57,20 @@ 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();
services.ConfigureHttpClientDefaults(builder =>
{
builder.ConfigureHttpClient(httpClient =>
{
httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent);
});
});
services.AddHttpClient(RD_CLIENT)
.AddPolicyHandler(retryPolicy);
}

View file

@ -12,20 +12,20 @@
<PackageReference Include="AllDebrid.NET" Version="1.0.18" />
<PackageReference Include="Aria2.NET" Version="1.0.6" />
<PackageReference Include="DebridLinkFr.NET" Version="1.0.4" />
<PackageReference Include="Downloader" Version="3.3.4" />
<PackageReference Include="Downloader.NET" Version="1.0.16" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.5.0" />
<PackageReference Include="Downloader" Version="4.0.2" />
<PackageReference Include="Downloader.NET" Version="1.0.18" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.7.0" />
<PackageReference Include="MonoTorrent" Version="3.0.2" />
<PackageReference Include="Polly" Version="8.5.2" />
<PackageReference Include="Polly" Version="8.6.2" />
<PackageReference Include="Premiumize.NET" Version="1.0.10" />
<PackageReference Include="RD.NET" Version="2.1.11" />
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SharpCompress" Version="0.39.0" />
<PackageReference Include="SharpCompress" Version="0.40.0" />
<PackageReference Include="Synology.Api.Client" Version="0.3.93" />
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.0.14" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.14" />
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="22.0.15" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.15" />
<PackageReference Include="TorBox.NET" Version="1.5.0" />
</ItemGroup>

View file

@ -32,7 +32,7 @@ public class BezzadDownloader : IDownloader
// For all options, see https://github.com/bezzad/Downloader
_downloadConfiguration = new()
{
MaxTryAgainOnFailover = 5,
MaxTryAgainOnFailure = 5,
RangeDownload = false,
ClearPackageOnCompletionWithFailure = true,
ReserveStorageSpaceBeforeStartingDownload = false,

View file

@ -76,14 +76,14 @@ public sealed class Enricher(ILogger<Enricher> logger, ITrackerListGrabber track
if (!paramDict.ContainsKey(key))
{
paramDict[key] = new List<String>();
paramDict[key] = [];
}
paramDict[key].AddRange(queryKVs.GetValues(key) ?? []);
}
var existingTrackers = paramDict.TryGetValue("tr", out var value)
? new HashSet<String>(value, StringComparer.OrdinalIgnoreCase)
? new(value, StringComparer.OrdinalIgnoreCase)
: new HashSet<String>(StringComparer.OrdinalIgnoreCase);
paramDict.Remove("tr");

View file

@ -462,6 +462,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
Category = category,
DownloadClient = Settings.Get.DownloadClient.Client,
HostDownloadAction = Settings.Get.Integrations.Default.HostDownloadAction,
FinishedActionDelay = Settings.Get.Integrations.Default.FinishedActionDelay,
DownloadAction = Settings.Get.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
FinishedAction = TorrentFinishedAction.None,
DownloadMinSize = Settings.Get.Integrations.Default.MinFileSize,
@ -486,6 +487,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
Category = category,
DownloadClient = Settings.Get.DownloadClient.Client,
HostDownloadAction = Settings.Get.Integrations.Default.HostDownloadAction,
FinishedActionDelay = Settings.Get.Integrations.Default.FinishedActionDelay,
DownloadAction = Settings.Get.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
FinishedAction = TorrentFinishedAction.None,
DownloadMinSize = Settings.Get.Integrations.Default.MinFileSize,

View file

@ -128,14 +128,18 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
public async Task<String> AddMagnet(String magnetLink)
{
var result = await GetClient().Torrents.AddMagnetAsync(magnetLink);
var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(Settings.Get.Provider.Timeout));
var result = await GetClient().Torrents.AddMagnetAsync(magnetLink, timeoutCancellationToken.Token);
return result.Id;
}
public async Task<String> AddFile(Byte[] bytes)
{
var result = await GetClient().Torrents.AddFileAsync(bytes);
var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(Settings.Get.Provider.Timeout));
var result = await GetClient().Torrents.AddFileAsync(bytes, timeoutCancellationToken.Token);
return result.Id;
}

View file

@ -1,4 +1,3 @@
using System.Reflection;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
@ -24,8 +23,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
throw new("TorBox API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient();
httpClient.DefaultRequestHeaders.Add("User-Agent", $"rdt-client {Assembly.GetEntryAssembly()?.GetName().Version}");
var httpClient = httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
var torBoxNetClient = new TorBoxNetClient(null, httpClient, 5);

View file

@ -217,16 +217,16 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
Log($"Unpack reported an error: {unpackClient.Error}", download, download.Torrent);
await downloads.UpdateError(downloadId, unpackClient.Error);
await downloads.UpdateCompleted(downloadId, DateTimeOffset.UtcNow);
}
else
{
Log($"Unpack finished successfully", download, download.Torrent);
await downloads.UpdateUnpackingFinished(downloadId, DateTimeOffset.UtcNow);
await downloads.UpdateCompleted(downloadId, DateTimeOffset.UtcNow);
}
await downloads.UpdateCompleted(downloadId, DateTimeOffset.UtcNow);
ActiveUnpackClients.TryRemove(downloadId, out _);
Log($"Removed from ActiveUnpackClients", download, download.Torrent);
@ -348,14 +348,64 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
allTorrents = await torrents.Get();
allTorrents = allTorrents.Where(m => m.Completed == null).ToList();
var completeTorrents = allTorrents.Where(m => m.Completed != null);
var torrentsToDelete = completeTorrents.Where(m => DateTimeOffset.UtcNow >= m.Completed?.AddMinutes(m.FinishedActionDelay) && m.Error == null);
if (allTorrents.Count > 0)
foreach (var torrent in torrentsToDelete)
{
if (torrent.DownloadClient == Data.Enums.DownloadClient.Symlink)
{
switch (torrent.FinishedAction)
{
case TorrentFinishedAction.RemoveAllTorrents:
Log($"Force setting FinishedAction to RemoveClient as download client is Symlink and FinishedAction is RemoveAllTorrents", torrent);
torrent.FinishedAction = TorrentFinishedAction.RemoveClient;
break;
case TorrentFinishedAction.RemoveRealDebrid:
Log($"Force setting FinishedAction to TorrentFinishedAction.None as download client is Symlink and FinishedAction is RemoveRealDebrid", torrent);
torrent.FinishedAction = TorrentFinishedAction.None;
break;
}
}
switch (torrent.FinishedAction)
{
case TorrentFinishedAction.RemoveAllTorrents:
Log($"Removing torrents from debrid provider and RDT-Client, no files", torrent);
await torrents.Delete(torrent.TorrentId, true, true, false);
break;
case TorrentFinishedAction.RemoveRealDebrid:
Log($"Removing torrents from debrid provider, no files", torrent);
await torrents.Delete(torrent.TorrentId, false, true, false);
break;
case TorrentFinishedAction.RemoveClient:
Log($"Removing torrents from client, no files", torrent);
await torrents.Delete(torrent.TorrentId, true, false, false);
break;
case TorrentFinishedAction.None:
Log($"Not removing torrents or files", torrent);
break;
default:
Log($"Invalid torrent FinishedAction {torrent.FinishedAction}", torrent);
break;
}
}
var incompleteTorrents = allTorrents.Where(m => m.Completed == null).ToList();
if (incompleteTorrents.Count > 0)
{
Log($"Processing {allTorrents.Count} torrents");
}
foreach (var torrent in allTorrents)
foreach (var torrent in incompleteTorrents)
{
try
{
@ -611,50 +661,6 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
{
logger.LogError(ex.Message, "Unable to run post process: {Message}", ex.Message);
}
if (torrent.DownloadClient == Data.Enums.DownloadClient.Symlink)
{
switch (torrent.FinishedAction)
{
case TorrentFinishedAction.RemoveAllTorrents:
Log($"Force setting FinishedAction to RemoveClient as download client is Symlink and FinishedAction is RemoveAllTorrents", torrent);
torrent.FinishedAction = TorrentFinishedAction.RemoveClient;
break;
case TorrentFinishedAction.RemoveRealDebrid:
Log($"Force setting FinishedAction to TorrentFinishedAction.None as download client is Symlink and FinishedAction is RemoveRealDebrid", torrent);
torrent.FinishedAction = TorrentFinishedAction.None;
break;
}
}
switch (torrent.FinishedAction)
{
case TorrentFinishedAction.RemoveAllTorrents:
Log($"Removing torrents from debrid provider and RDT-Client, no files", torrent);
await torrents.Delete(torrent.TorrentId, true, true, false);
break;
case TorrentFinishedAction.RemoveRealDebrid:
Log($"Removing torrents from debrid provider, no files", torrent);
await torrents.Delete(torrent.TorrentId, false, true, false);
break;
case TorrentFinishedAction.RemoveClient:
Log($"Removing torrents from client, no files", torrent);
await torrents.Delete(torrent.TorrentId, true, false, false);
break;
case TorrentFinishedAction.None:
Log($"Not removing torrents or files", torrent);
break;
default:
Log($"Invalid torrent FinishedAction {torrent.FinishedAction}", torrent);
break;
}
}
else
{

View file

@ -3,7 +3,6 @@ using System.IO.Abstractions;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Web;
using Microsoft.Extensions.Logging;
using MonoTorrent;
using RdtClient.Data.Data;
@ -514,6 +513,7 @@ public class Torrents(
DownloadClient = Settings.Get.DownloadClient.Client,
DownloadAction = Settings.Get.Provider.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
HostDownloadAction = Settings.Get.Provider.Default.HostDownloadAction,
FinishedActionDelay = Settings.Get.Provider.Default.FinishedActionDelay,
FinishedAction = Settings.Get.Provider.Default.FinishedAction,
DownloadMinSize = Settings.Get.Provider.Default.MinFileSize,
IncludeRegex = Settings.Get.Provider.Default.IncludeRegex,

View file

@ -1,4 +1,3 @@
using System.Net.Http.Headers;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System.Reflection;
@ -96,7 +95,7 @@ public class TrackerListGrabber(IHttpClientFactory httpClientFactory, IMemoryCac
{
logger.LogError(ex, "Unable to fetch tracker list.");
throw new Exception("Unable to fetch tracker list for enrichment.", ex);
throw new("Unable to fetch tracker list for enrichment.", ex);
}
finally
{
@ -116,7 +115,7 @@ public class TrackerListGrabber(IHttpClientFactory httpClientFactory, IMemoryCac
? $"v{version[..version.LastIndexOf('.')]}"
: "";
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("RdtClient", currentVersion));
httpClient.DefaultRequestHeaders.UserAgent.Add(new("RdtClient", currentVersion));
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
var token = cts.Token;
var response = await httpClient.GetAsync(trackerUri, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);

View file

@ -30,16 +30,16 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.5">
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="9.0.5" />
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="9.0.7" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
</ItemGroup>