diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b50b61..2f0ae39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README-DOCKER.md b/README-DOCKER.md index 125d404..3c8c70d 100644 --- a/README-DOCKER.md +++ b/README-DOCKER.md @@ -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: diff --git a/client/src/app/add-new-torrent/add-new-torrent.component.html b/client/src/app/add-new-torrent/add-new-torrent.component.html index 1e82e1b..4ea4454 100644 --- a/client/src/app/add-new-torrent/add-new-torrent.component.html +++ b/client/src/app/add-new-torrent/add-new-torrent.component.html @@ -151,6 +151,17 @@ +
+ +
+ +
+

+ 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. +

+
+
diff --git a/client/src/app/add-new-torrent/add-new-torrent.component.ts b/client/src/app/add-new-torrent/add-new-torrent.component.ts index 6c52dff..f3491a0 100644 --- a/client/src/app/add-new-torrent/add-new-torrent.component.ts +++ b/client/src/app/add-new-torrent/add-new-torrent.component.ts @@ -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; diff --git a/client/src/app/models/torrent.model.ts b/client/src/app/models/torrent.model.ts index 9ac34be..7ef1646 100644 --- a/client/src/app/models/torrent.model.ts +++ b/client/src/app/models/torrent.model.ts @@ -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; diff --git a/client/src/app/torrent/torrent.component.html b/client/src/app/torrent/torrent.component.html index fc0521a..1c51e0a 100644 --- a/client/src/app/torrent/torrent.component.html +++ b/client/src/app/torrent/torrent.component.html @@ -116,6 +116,15 @@ } }
+
+ + {{ torrent.finishedActionDelay || 0 }} + @if (torrent.finishedActionDelay === 1) { + minute + } @else { + minutes + } +
{{ torrent.downloadMinSize }}MB diff --git a/server/RdtClient.Data/Data/TorrentData.cs b/server/RdtClient.Data/Data/TorrentData.cs index 5b74f63..e3484a1 100644 --- a/server/RdtClient.Data/Data/TorrentData.cs +++ b/server/RdtClient.Data/Data/TorrentData.cs @@ -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, diff --git a/server/RdtClient.Data/Migrations/20250706204358_AddFinishedActionDelay.Designer.cs b/server/RdtClient.Data/Migrations/20250706204358_AddFinishedActionDelay.Designer.cs new file mode 100644 index 0000000..626839f --- /dev/null +++ b/server/RdtClient.Data/Migrations/20250706204358_AddFinishedActionDelay.Designer.cs @@ -0,0 +1,482 @@ +// +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 + { + /// + 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("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("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", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("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", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Download", b => + { + b.Property("DownloadId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Completed") + .HasColumnType("TEXT"); + + b.Property("DownloadFinished") + .HasColumnType("TEXT"); + + b.Property("DownloadQueued") + .HasColumnType("TEXT"); + + b.Property("DownloadStarted") + .HasColumnType("TEXT"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Link") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RemoteId") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("TorrentId") + .HasColumnType("TEXT"); + + b.Property("UnpackingFinished") + .HasColumnType("TEXT"); + + b.Property("UnpackingQueued") + .HasColumnType("TEXT"); + + b.Property("UnpackingStarted") + .HasColumnType("TEXT"); + + b.HasKey("DownloadId"); + + b.HasIndex("TorrentId"); + + b.ToTable("Downloads"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Setting", b => + { + b.Property("SettingId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("SettingId"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("RdtClient.Data.Models.Data.Torrent", b => + { + b.Property("TorrentId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("ClientKind") + .HasColumnType("INTEGER"); + + b.Property("Completed") + .HasColumnType("TEXT"); + + b.Property("DeleteOnError") + .HasColumnType("INTEGER"); + + b.Property("DownloadAction") + .HasColumnType("INTEGER"); + + b.Property("DownloadClient") + .HasColumnType("INTEGER"); + + b.Property("DownloadManualFiles") + .HasColumnType("TEXT"); + + b.Property("DownloadMinSize") + .HasColumnType("INTEGER"); + + b.Property("DownloadRetryAttempts") + .HasColumnType("INTEGER"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("ExcludeRegex") + .HasColumnType("TEXT"); + + b.Property("FileOrMagnet") + .HasColumnType("TEXT"); + + b.Property("FilesSelected") + .HasColumnType("TEXT"); + + b.Property("FinishedAction") + .HasColumnType("INTEGER"); + + b.Property("FinishedActionDelay") + .HasColumnType("INTEGER"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("HostDownloadAction") + .HasColumnType("INTEGER"); + + b.Property("IncludeRegex") + .HasColumnType("TEXT"); + + b.Property("IsFile") + .HasColumnType("INTEGER"); + + b.Property("Lifetime") + .HasColumnType("INTEGER"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RdAdded") + .HasColumnType("TEXT"); + + b.Property("RdEnded") + .HasColumnType("TEXT"); + + b.Property("RdFiles") + .HasColumnType("TEXT"); + + b.Property("RdHost") + .HasColumnType("TEXT"); + + b.Property("RdId") + .HasColumnType("TEXT"); + + b.Property("RdName") + .HasColumnType("TEXT"); + + b.Property("RdProgress") + .HasColumnType("INTEGER"); + + b.Property("RdSeeders") + .HasColumnType("INTEGER"); + + b.Property("RdSize") + .HasColumnType("INTEGER"); + + b.Property("RdSpeed") + .HasColumnType("INTEGER"); + + b.Property("RdSplit") + .HasColumnType("INTEGER"); + + b.Property("RdStatus") + .HasColumnType("INTEGER"); + + b.Property("RdStatusRaw") + .HasColumnType("TEXT"); + + b.Property("Retry") + .HasColumnType("TEXT"); + + b.Property("RetryCount") + .HasColumnType("INTEGER"); + + b.Property("TorrentRetryAttempts") + .HasColumnType("INTEGER"); + + b.HasKey("TorrentId"); + + b.ToTable("Torrents"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", 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", 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 + } + } +} diff --git a/server/RdtClient.Data/Migrations/20250706204358_AddFinishedActionDelay.cs b/server/RdtClient.Data/Migrations/20250706204358_AddFinishedActionDelay.cs new file mode 100644 index 0000000..ba7192b --- /dev/null +++ b/server/RdtClient.Data/Migrations/20250706204358_AddFinishedActionDelay.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RdtClient.Data.Migrations +{ + /// + public partial class AddFinishedActionDelay : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FinishedActionDelay", + table: "Torrents", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FinishedActionDelay", + table: "Torrents"); + } + } +} diff --git a/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs b/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs index f9a2002..8dfc00b 100644 --- a/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs +++ b/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs @@ -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("FinishedAction") .HasColumnType("INTEGER"); + b.Property("FinishedActionDelay") + .HasColumnType("INTEGER"); + b.Property("Hash") .IsRequired() .HasColumnType("TEXT"); diff --git a/server/RdtClient.Data/Models/Data/Torrent.cs b/server/RdtClient.Data/Models/Data/Torrent.cs index 57efdbf..6c51b50 100644 --- a/server/RdtClient.Data/Models/Data/Torrent.cs +++ b/server/RdtClient.Data/Models/Data/Torrent.cs @@ -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; } diff --git a/server/RdtClient.Data/Models/Internal/DbSettings.cs b/server/RdtClient.Data/Models/Internal/DbSettings.cs index dae9217..453ab22 100644 --- a/server/RdtClient.Data/Models/Internal/DbSettings.cs +++ b/server/RdtClient.Data/Models/Internal/DbSettings.cs @@ -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 diff --git a/server/RdtClient.Data/RdtClient.Data.csproj b/server/RdtClient.Data/RdtClient.Data.csproj index 5172049..1df0e10 100644 --- a/server/RdtClient.Data/RdtClient.Data.csproj +++ b/server/RdtClient.Data/RdtClient.Data.csproj @@ -8,15 +8,15 @@ - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/server/RdtClient.Service.Test/RdtClient.Service.Test.csproj b/server/RdtClient.Service.Test/RdtClient.Service.Test.csproj index 66981cc..820bb4a 100644 --- a/server/RdtClient.Service.Test/RdtClient.Service.Test.csproj +++ b/server/RdtClient.Service.Test/RdtClient.Service.Test.csproj @@ -15,13 +15,13 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/server/RdtClient.Service.Test/Services/EnricherTest.cs b/server/RdtClient.Service.Test/Services/EnricherTest.cs index 95a36a8..48d38ae 100644 --- a/server/RdtClient.Service.Test/Services/EnricherTest.cs +++ b/server/RdtClient.Service.Test/Services/EnricherTest.cs @@ -13,7 +13,7 @@ public class EnricherTest : IDisposable public EnricherTest() { - _mockRepository = new MockRepository(MockBehavior.Strict); + _mockRepository = new(MockBehavior.Strict); _loggerMock = _mockRepository.Create>(MockBehavior.Loose); _trackerListGrabberMock = _mockRepository.Create(); } @@ -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() }; diff --git a/server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs b/server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs index ba369e2..4518c12 100644 --- a/server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs +++ b/server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs @@ -82,6 +82,7 @@ public class WatchFolderChecker(ILogger 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, diff --git a/server/RdtClient.Service/BackgroundServices/WebsocketsUpdater.cs b/server/RdtClient.Service/BackgroundServices/WebsocketsUpdater.cs index 37cf032..10237d4 100644 --- a/server/RdtClient.Service/BackgroundServices/WebsocketsUpdater.cs +++ b/server/RdtClient.Service/BackgroundServices/WebsocketsUpdater.cs @@ -14,7 +14,9 @@ public class WebsocketsUpdater(ILogger logger, IServiceProvid await Task.Delay(1000, stoppingToken); } - var remoteService = serviceProvider.GetRequiredService(); + var scope = serviceProvider.CreateScope(); + + var remoteService = scope.ServiceProvider.GetRequiredService(); while (!stoppingToken.IsCancellationRequested) { diff --git a/server/RdtClient.Service/DiConfig.cs b/server/RdtClient.Service/DiConfig.cs index abe8af7..8a53391 100644 --- a/server/RdtClient.Service/DiConfig.cs +++ b/server/RdtClient.Service/DiConfig.cs @@ -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); } diff --git a/server/RdtClient.Service/RdtClient.Service.csproj b/server/RdtClient.Service/RdtClient.Service.csproj index 72990be..21b33fa 100644 --- a/server/RdtClient.Service/RdtClient.Service.csproj +++ b/server/RdtClient.Service/RdtClient.Service.csproj @@ -12,20 +12,20 @@ - - - - + + + + - + - + - + - - + + diff --git a/server/RdtClient.Service/Services/Downloaders/BezzadDownloader.cs b/server/RdtClient.Service/Services/Downloaders/BezzadDownloader.cs index b62b925..cb2e112 100644 --- a/server/RdtClient.Service/Services/Downloaders/BezzadDownloader.cs +++ b/server/RdtClient.Service/Services/Downloaders/BezzadDownloader.cs @@ -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, diff --git a/server/RdtClient.Service/Services/Enricher.cs b/server/RdtClient.Service/Services/Enricher.cs index 64a2965..a79c67d 100644 --- a/server/RdtClient.Service/Services/Enricher.cs +++ b/server/RdtClient.Service/Services/Enricher.cs @@ -76,14 +76,14 @@ public sealed class Enricher(ILogger logger, ITrackerListGrabber track if (!paramDict.ContainsKey(key)) { - paramDict[key] = new List(); + paramDict[key] = []; } paramDict[key].AddRange(queryKVs.GetValues(key) ?? []); } var existingTrackers = paramDict.TryGetValue("tr", out var value) - ? new HashSet(value, StringComparer.OrdinalIgnoreCase) + ? new(value, StringComparer.OrdinalIgnoreCase) : new HashSet(StringComparer.OrdinalIgnoreCase); paramDict.Remove("tr"); diff --git a/server/RdtClient.Service/Services/QBittorrent.cs b/server/RdtClient.Service/Services/QBittorrent.cs index 03e7a4a..4cd1513 100644 --- a/server/RdtClient.Service/Services/QBittorrent.cs +++ b/server/RdtClient.Service/Services/QBittorrent.cs @@ -462,6 +462,7 @@ public class QBittorrent(ILogger 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 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, diff --git a/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs index 7b3890a..f5b2289 100644 --- a/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs +++ b/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs @@ -128,14 +128,18 @@ public class RealDebridTorrentClient(ILogger logger, IH public async Task 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 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; } diff --git a/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs index 29ae9b3..4994e72 100644 --- a/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs +++ b/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs @@ -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 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); diff --git a/server/RdtClient.Service/Services/TorrentRunner.cs b/server/RdtClient.Service/Services/TorrentRunner.cs index e7ba302..1679c28 100644 --- a/server/RdtClient.Service/Services/TorrentRunner.cs +++ b/server/RdtClient.Service/Services/TorrentRunner.cs @@ -217,16 +217,16 @@ public class TorrentRunner(ILogger 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 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 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 { diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs index 8d4be9b..cbd53c0 100644 --- a/server/RdtClient.Service/Services/Torrents.cs +++ b/server/RdtClient.Service/Services/Torrents.cs @@ -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, diff --git a/server/RdtClient.Service/Services/TrackerListGrabber.cs b/server/RdtClient.Service/Services/TrackerListGrabber.cs index 7e49b32..88b560f 100644 --- a/server/RdtClient.Service/Services/TrackerListGrabber.cs +++ b/server/RdtClient.Service/Services/TrackerListGrabber.cs @@ -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); diff --git a/server/RdtClient.Web/RdtClient.Web.csproj b/server/RdtClient.Web/RdtClient.Web.csproj index 1335563..10c5c95 100644 --- a/server/RdtClient.Web/RdtClient.Web.csproj +++ b/server/RdtClient.Web/RdtClient.Web.csproj @@ -30,16 +30,16 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + +