diff --git a/CHANGELOG.md b/CHANGELOG.md index 2577c06..11f690b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.0.95] - 2025-01-19 +### Added +- Added the /api/v2/transfer/info qBittorrent endpoint. + +### Changed +- AllDebrid Symlink path fixes. + ## [2.0.94] - 2025-01-05 ### Changed - AllDebrid path fixes. diff --git a/Dockerfile b/Dockerfile index e210c88..98d7722 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,9 +10,9 @@ WORKDIR /appclient RUN apk add --no-cache git python3 py3-pip make g++ +COPY client ./client +COPY root ./root RUN \ - echo "**** Cloning Source Code ****" && \ - git clone https://github.com/rogerfar/rdt-client.git . && \ cd client && \ echo "**** Building Code ****" && \ npm ci && \ @@ -30,9 +30,8 @@ ENV BUILDPLATFORM=${BUILDPLATFORM:-linux/amd64} RUN mkdir /appserver WORKDIR /appserver +COPY server ./server RUN \ - echo "**** Cloning Source Code ****" && \ - git clone https://github.com/rogerfar/rdt-client.git . && \ echo "**** Building Source Code for $TARGETPLATFORM on $BUILDPLATFORM ****" && \ cd server && \ dotnet restore --no-cache RdtClient.sln && dotnet publish --no-restore -c Release -o out ; diff --git a/client/src/app/navbar/navbar.component.html b/client/src/app/navbar/navbar.component.html index c620099..7ea5f19 100644 --- a/client/src/app/navbar/navbar.component.html +++ b/client/src/app/navbar/navbar.component.html @@ -55,7 +55,7 @@ Profile Logout - Version 2.0.94 + Version 2.0.95 diff --git a/package.json b/package.json index 69cd004..0684e5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rdt-client", - "version": "2.0.94", + "version": "2.0.95", "description": "This is a web interface to manage your torrents on Real-Debrid.", "main": "index.js", "dependencies": { diff --git a/server/RdtClient.Data/Migrations/20250106112750_Torrent_Add_ClientKind.Designer.cs b/server/RdtClient.Data/Migrations/20250106112750_Torrent_Add_ClientKind.Designer.cs new file mode 100644 index 0000000..fd24225 --- /dev/null +++ b/server/RdtClient.Data/Migrations/20250106112750_Torrent_Add_ClientKind.Designer.cs @@ -0,0 +1,479 @@ +// +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("20250106112750_Torrent_Add_ClientKind")] + partial class Torrent_Add_ClientKind + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + 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("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/20250106112750_Torrent_Add_ClientKind.cs b/server/RdtClient.Data/Migrations/20250106112750_Torrent_Add_ClientKind.cs new file mode 100644 index 0000000..9cbc1b2 --- /dev/null +++ b/server/RdtClient.Data/Migrations/20250106112750_Torrent_Add_ClientKind.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RdtClient.Data.Migrations +{ + /// + public partial class Torrent_Add_ClientKind : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ClientKind", + table: "Torrents", + type: "INTEGER", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ClientKind", + table: "Torrents"); + } + } +} diff --git a/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs b/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs index ce27f40..f9a2002 100644 --- a/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs +++ b/server/RdtClient.Data/Migrations/DataContextModelSnapshot.cs @@ -293,6 +293,9 @@ namespace RdtClient.Data.Migrations b.Property("Category") .HasColumnType("TEXT"); + b.Property("ClientKind") + .HasColumnType("INTEGER"); + b.Property("Completed") .HasColumnType("TEXT"); diff --git a/server/RdtClient.Data/Models/Data/Torrent.cs b/server/RdtClient.Data/Models/Data/Torrent.cs index 3ffe5b3..f928e3e 100644 --- a/server/RdtClient.Data/Models/Data/Torrent.cs +++ b/server/RdtClient.Data/Models/Data/Torrent.cs @@ -44,6 +44,7 @@ public class Torrent [InverseProperty("Torrent")] public IList Downloads { get; set; } = []; + public TorrentClientKind? ClientKind { get; set; } public String? RdId { get; set; } public String? RdName { get; set; } public Int64? RdSize { get; set; } @@ -92,4 +93,12 @@ public class Torrent return DownloadManualFiles.Split(","); } } + + public enum TorrentClientKind + { + AllDebrid, + Premiumize, + RealDebrid, + TorBox + } } \ No newline at end of file diff --git a/server/RdtClient.Data/Models/QBittorrent/TransferInfo.cs b/server/RdtClient.Data/Models/QBittorrent/TransferInfo.cs new file mode 100644 index 0000000..8af9f38 --- /dev/null +++ b/server/RdtClient.Data/Models/QBittorrent/TransferInfo.cs @@ -0,0 +1,30 @@ +using System.Text.Json.Serialization; + +namespace RdtClient.Data.Models.QBittorrent; + +public class TransferInfo +{ + [JsonPropertyName("connection_status")] + public String ConnectionStatus { get; set; } = default!; + + [JsonPropertyName("dht_nodes")] + public Int64 DhtNodes { get; set; } + + [JsonPropertyName("dl_info_data")] + public Int64 DlInfoData { get; set; } + + [JsonPropertyName("dl_info_speed")] + public Int64 DlInfoSpeed { get; set; } + + [JsonPropertyName("dl_rate_limit")] + public Int64 DlRateLimit { get; set; } + + [JsonPropertyName("up_info_data")] + public Int64 UpInfoData { get; set; } + + [JsonPropertyName("up_info_speed")] + public Int64 UpInfoSpeed { get; set; } + + [JsonPropertyName("up_rate_limit")] + public Int64 UpRateLimit { get; set; } +} \ No newline at end of file diff --git a/server/RdtClient.Data/RdtClient.Data.csproj b/server/RdtClient.Data/RdtClient.Data.csproj index aebafe5..f21d4fd 100644 --- a/server/RdtClient.Data/RdtClient.Data.csproj +++ b/server/RdtClient.Data/RdtClient.Data.csproj @@ -8,11 +8,11 @@ - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/server/RdtClient.Service/Helpers/DownloadHelper.cs b/server/RdtClient.Service/Helpers/DownloadHelper.cs index 846b9be..0bc06ec 100644 --- a/server/RdtClient.Service/Helpers/DownloadHelper.cs +++ b/server/RdtClient.Service/Helpers/DownloadHelper.cs @@ -40,7 +40,6 @@ public static class DownloadHelper if (matchingTorrentFiles.Count > 0) { var matchingTorrentFile = matchingTorrentFiles[0]; - var subPath = Path.GetDirectoryName(matchingTorrentFile.Path); if (!String.IsNullOrWhiteSpace(subPath)) @@ -49,22 +48,23 @@ public static class DownloadHelper torrentPath = Path.Combine(torrentPath, subPath); } - else if (torrent.Files.Count == 1) - { - if (directory != fileName) - { - throw new($"Torrent path {torrentPath} does not match file name {fileName}. This is a requirement for single file torrents."); - } - - return torrentPath; - } } } else if (torrent.Files.Count == 1) { + // Debrid servers such as RealDebrid store single file torrents in a subfolder, but AllDebrid doesn't. + // We should replicate this behavior so that both folder structures are equal. + // See issue: https://github.com/rogerfar/rdt-client/issues/648 + if (torrent.ClientKind != Torrent.TorrentClientKind.AllDebrid) + { + torrentPath = Path.Combine(downloadPath, directory); + } + var torrentFile = torrent.Files[0]; var subPath = Path.GetDirectoryName(torrentFile.Path); - + + // What we think is a single file torrent may also be a folder with a single file in it. + // So make sure we handle that here. If this is not the case, torrentPath will be empty below. if (!String.IsNullOrWhiteSpace(subPath)) { subPath = subPath.Trim('/').Trim('\\'); diff --git a/server/RdtClient.Service/RdtClient.Service.csproj b/server/RdtClient.Service/RdtClient.Service.csproj index f2e1222..90ad249 100644 --- a/server/RdtClient.Service/RdtClient.Service.csproj +++ b/server/RdtClient.Service/RdtClient.Service.csproj @@ -11,17 +11,17 @@ - + - - + + - + - + diff --git a/server/RdtClient.Service/Services/DownloadClient.cs b/server/RdtClient.Service/Services/DownloadClient.cs index 049bde1..a6e2804 100644 --- a/server/RdtClient.Service/Services/DownloadClient.cs +++ b/server/RdtClient.Service/Services/DownloadClient.cs @@ -6,6 +6,9 @@ namespace RdtClient.Service.Services; public class DownloadClient(Download download, Torrent torrent, String destinationPath, String? category) { + private static Int64 _totalBytesDownloadedThisSession; + private static readonly Lock TotalBytesDownloadedLock = new(); + public IDownloader? Downloader; public Data.Enums.DownloadClient Type { get; private set; } @@ -18,6 +21,8 @@ public class DownloadClient(Download download, Torrent torrent, String destinati public Int64 BytesTotal { get; private set; } public Int64 BytesDone { get; private set; } + private Int64 LastBytesDone { get; set; } + public async Task Start() { BytesDone = 0; @@ -51,7 +56,7 @@ public class DownloadClient(Download download, Torrent torrent, String destinati Data.Enums.DownloadClient.Internal => new InternalDownloader(download.Link, filePath), Data.Enums.DownloadClient.Bezzad => new BezzadDownloader(download.Link, filePath), Data.Enums.DownloadClient.Aria2c => new Aria2cDownloader(download.RemoteId, download.Link, filePath, downloadPath, category), - Data.Enums.DownloadClient.Symlink => new SymlinkDownloader(download.Link, filePath, downloadPath), + Data.Enums.DownloadClient.Symlink => new SymlinkDownloader(download.Link, filePath, downloadPath, torrent.ClientKind), Data.Enums.DownloadClient.DownloadStation => await DownloadStationDownloader.Init(download.RemoteId, download.Link, filePath, downloadPath, category), _ => throw new($"Unknown download client {Type}") }; @@ -67,6 +72,12 @@ public class DownloadClient(Download download, Torrent torrent, String destinati Speed = args.Speed; BytesDone = args.BytesDone; BytesTotal = args.BytesTotal; + + var bytesAdded = BytesDone - LastBytesDone; + + LastBytesDone = BytesDone; + + AddToTotalBytesDownloadedThisSession(bytesAdded); }; var result = await Downloader.Download(); @@ -115,4 +126,20 @@ public class DownloadClient(Download download, Torrent torrent, String destinati } await Downloader.Resume(); } + + public static Int64 GetTotalBytesDownloadedThisSession() + { + lock (TotalBytesDownloadedLock) + { + return _totalBytesDownloadedThisSession; + } + } + + private static void AddToTotalBytesDownloadedThisSession(Int64 bytes) + { + lock (TotalBytesDownloadedLock) + { + _totalBytesDownloadedThisSession += bytes; + } + } } \ No newline at end of file diff --git a/server/RdtClient.Service/Services/Downloaders/SymlinkDownloader.cs b/server/RdtClient.Service/Services/Downloaders/SymlinkDownloader.cs index 9739d0f..74db7c0 100644 --- a/server/RdtClient.Service/Services/Downloaders/SymlinkDownloader.cs +++ b/server/RdtClient.Service/Services/Downloaders/SymlinkDownloader.cs @@ -1,9 +1,10 @@ -using RdtClient.Service.Helpers; +using RdtClient.Data.Models.Data; +using RdtClient.Service.Helpers; using Serilog; namespace RdtClient.Service.Services.Downloaders; -public class SymlinkDownloader(String uri, String destinationPath, String path) : IDownloader +public class SymlinkDownloader(String uri, String destinationPath, String path, Torrent.TorrentClientKind? clientKind) : IDownloader { public event EventHandler? DownloadComplete; public event EventHandler? DownloadProgress; @@ -16,7 +17,7 @@ public class SymlinkDownloader(String uri, String destinationPath, String path) public async Task Download() { - _logger.Debug($"Starting symlink resolving of {uri}, writing to path: {path}"); + _logger.Debug($"Starting symlink resolving of {path} (uri = {uri}), writing to path: {destinationPath}"); try { @@ -56,83 +57,113 @@ public class SymlinkDownloader(String uri, String destinationPath, String path) BytesTotal = 0, Speed = 0 }); - - var potentialFilePaths = new List(); - - var directoryInfo = new DirectoryInfo(searchPath); - while (directoryInfo.Parent != null) - { - potentialFilePaths.Add(directoryInfo.Name); - directoryInfo = directoryInfo.Parent; - - if (directoryInfo.FullName.TrimEnd(['\\', '/']) == rcloneMountPath) - { - break; - } - } - - potentialFilePaths.Add(fileName); - potentialFilePaths.Add(fileNameWithoutExtension); - // add an empty path so we can check for the new file in the base directory - potentialFilePaths.Add(""); - - potentialFilePaths = potentialFilePaths.Distinct().ToList(); - + String? file = null; + var shouldSearch = true; - for (var retryCount = 0; retryCount < MaxRetries; retryCount++) + // When resolving symlinks for AllDebrid, we know the exact file path, so we can skip the search. + if (clientKind == Torrent.TorrentClientKind.AllDebrid) { - DownloadProgress?.Invoke(this, - new() - { - BytesDone = retryCount, - BytesTotal = 10, - Speed = 1 - }); - - _logger.Debug($"Searching {rcloneMountPath} for {fileName} (attempt #{retryCount})..."); - - file = FindFile(rcloneMountPath, potentialFilePaths, fileName); - - if (file == null && searchSubDirectories) + var potentialFilePath = Path.Combine(rcloneMountPath, path); + + // Make sure the file exists before making any assumptions. + // If this somehow fails, fallback to the search below. + if (File.Exists(potentialFilePath)) { - var subDirectories = Directory.GetDirectories(rcloneMountPath, "*.*", SearchOption.TopDirectoryOnly); - - foreach (var subDirectory in subDirectories) - { - file = FindFile(Path.Combine(rcloneMountPath, subDirectory), potentialFilePaths, fileName); - - if (file != null) - { - break; - } - } - } - - if (file == null) - { - await Task.Delay(1000 * retryCount); + _logger.Debug($"Found file {path} at {potentialFilePath} using direct search"); + file = potentialFilePath; + shouldSearch = false; } else { - break; + // Log if the file wasn't found and continue searching. + _logger.Warning($"Expected file {path} to be at {potentialFilePath} but it wasn't found. Continuing search (this will probably fail)."); + } + } + + if (shouldSearch) + { + var potentialFilePaths = new List + { + searchPath + }; + + var directoryInfo = new DirectoryInfo(searchPath); + + while (directoryInfo.Parent != null) + { + potentialFilePaths.Add(directoryInfo.Name); + directoryInfo = directoryInfo.Parent; + + if (directoryInfo.FullName.TrimEnd(['\\', '/']) == rcloneMountPath) + { + break; + } + } + + potentialFilePaths.Add(fileName); + potentialFilePaths.Add(fileNameWithoutExtension); + + // add an empty path so we can check for the new file in the base directory + potentialFilePaths.Add(""); + + potentialFilePaths = potentialFilePaths.Distinct().ToList(); + + for (var retryCount = 0; retryCount < MaxRetries; retryCount++) + { + DownloadProgress?.Invoke(this, + new() + { + BytesDone = retryCount, + BytesTotal = 10, + Speed = 1 + }); + + _logger.Debug($"Searching {rcloneMountPath} for {fileName} (attempt #{retryCount})..."); + + file = FindFile(rcloneMountPath, potentialFilePaths, fileName); + + if (file == null && searchSubDirectories) + { + var subDirectories = Directory.GetDirectories(rcloneMountPath, "*.*", SearchOption.TopDirectoryOnly); + + foreach (var subDirectory in subDirectories) + { + file = FindFile(Path.Combine(rcloneMountPath, subDirectory), potentialFilePaths, fileName); + + if (file != null) + { + break; + } + } + } + + if (file == null) + { + await Task.Delay(1000 * retryCount); + } + else + { + break; + } } } if (file == null) { _logger.Debug($"Unable to find file in rclone mount. Folders available in {rcloneMountPath}: "); + try { var allFolders = FileHelper.GetDirectoryContents(rcloneMountPath); _logger.Debug(allFolders); } - catch(Exception ex) + catch (Exception ex) { _logger.Error(ex.Message); } - + throw new("Could not find file from rclone mount!"); } diff --git a/server/RdtClient.Service/Services/QBittorrent.cs b/server/RdtClient.Service/Services/QBittorrent.cs index e20f7c1..d066fab 100644 --- a/server/RdtClient.Service/Services/QBittorrent.cs +++ b/server/RdtClient.Service/Services/QBittorrent.cs @@ -208,7 +208,14 @@ public class QBittorrent(ILogger logger, Settings settings, Authent var torrentPath = downloadPath; if (!String.IsNullOrWhiteSpace(torrent.RdName)) { - torrentPath = Path.Combine(downloadPath, torrent.RdName) + Path.DirectorySeparatorChar; + // Alldebrid stores single file torrents at the root folder. + if (torrent.ClientKind == Torrent.TorrentClientKind.AllDebrid && torrent.Files.Count == 1) + { + torrentPath = Path.Combine(downloadPath, torrent.Files[0].Path); + } else + { + torrentPath = Path.Combine(downloadPath, torrent.RdName) + Path.DirectorySeparatorChar; + } } Int64 bytesDone = 0; @@ -629,7 +636,7 @@ public class QBittorrent(ILogger logger, Settings settings, Authent public async Task SyncMainData() { - var torrents = await TorrentInfo(); + var torrentInfo = await TorrentInfo(); var categories = await TorrentsCategories(); @@ -642,7 +649,7 @@ public class QBittorrent(ILogger logger, Settings settings, Authent Rid = 0, Tags = null, Trackers = new Dictionary>(), - Torrents = torrents.ToDictionary(m => m.Hash, m => m), + Torrents = torrentInfo.ToDictionary(m => m.Hash, m => m), ServerState = new() { DlInfoSpeed = activeDownloads, @@ -650,4 +657,17 @@ public class QBittorrent(ILogger logger, Settings settings, Authent } }; } + + public static TransferInfo TransferInfo() + { + var activeDownloads = TorrentRunner.ActiveDownloadClients.Sum(m => m.Value.Speed); + + return new() + { + ConnectionStatus = "connected", + DlInfoData = DownloadClient.GetTotalBytesDownloadedThisSession(), + DlInfoSpeed = activeDownloads, + DlRateLimit = Settings.Get.DownloadClient.MaxSpeed + }; + } } \ 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 6e9e178..4f6c0aa 100644 --- a/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs +++ b/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs @@ -174,6 +174,7 @@ public class AllDebridTorrentClient(ILogger logger, IHtt torrent.RdFiles = JsonConvert.SerializeObject(torrentClientTorrent.Files); } + torrent.ClientKind = Torrent.TorrentClientKind.AllDebrid; torrent.RdHost = torrentClientTorrent.Host; torrent.RdSplit = torrentClientTorrent.Split; torrent.RdProgress = torrentClientTorrent.Progress; diff --git a/server/RdtClient.Service/Services/TorrentClients/PremiumizeTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/PremiumizeTorrentClient.cs index 4a1745b..438fe29 100644 --- a/server/RdtClient.Service/Services/TorrentClients/PremiumizeTorrentClient.cs +++ b/server/RdtClient.Service/Services/TorrentClients/PremiumizeTorrentClient.cs @@ -162,6 +162,7 @@ public class PremiumizeTorrentClient(ILogger logger, IH torrent.RdFiles = JsonConvert.SerializeObject(torrentClientTorrent.Files); } + torrent.ClientKind = Torrent.TorrentClientKind.Premiumize; torrent.RdHost = torrentClientTorrent.Host; torrent.RdSplit = torrentClientTorrent.Split; torrent.RdProgress = torrentClientTorrent.Progress; diff --git a/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs index 42b06d2..29493fd 100644 --- a/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs +++ b/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs @@ -295,6 +295,7 @@ public class RealDebridTorrentClient(ILogger logger, IH torrent.RdFiles = JsonConvert.SerializeObject(torrentClientTorrent.Files); } + torrent.ClientKind = Data.Models.Data.Torrent.TorrentClientKind.RealDebrid; torrent.RdHost = torrentClientTorrent.Host; torrent.RdSplit = torrentClientTorrent.Split; torrent.RdProgress = torrentClientTorrent.Progress; diff --git a/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs index 8775072..45f562b 100644 --- a/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs +++ b/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs @@ -4,6 +4,7 @@ using TorBoxNET; using RdtClient.Data.Enums; using RdtClient.Data.Models.TorrentClient; using System.Web; +using RdtClient.Data.Models.Data; namespace RdtClient.Service.Services.TorrentClients; @@ -167,7 +168,7 @@ public class TorBoxTorrentClient(ILogger logger, IHttpClien return []; } - public Task SelectFiles(Data.Models.Data.Torrent torrent) + public Task SelectFiles(Torrent torrent) { return Task.CompletedTask; } @@ -194,7 +195,7 @@ public class TorBoxTorrentClient(ILogger logger, IHttpClien return result.Data!; } - public async Task UpdateData(Data.Models.Data.Torrent torrent, TorrentClientTorrent? torrentClientTorrent) + public async Task UpdateData(Torrent torrent, TorrentClientTorrent? torrentClientTorrent) { try { @@ -229,6 +230,7 @@ public class TorBoxTorrentClient(ILogger logger, IHttpClien torrent.RdFiles = JsonConvert.SerializeObject(rdTorrent.Files); } + torrent.ClientKind = Torrent.TorrentClientKind.TorBox; torrent.RdHost = rdTorrent.Host; torrent.RdSplit = rdTorrent.Split; torrent.RdProgress = rdTorrent.Progress; @@ -280,7 +282,7 @@ public class TorBoxTorrentClient(ILogger logger, IHttpClien return torrent; } - public async Task?> GetDownloadLinks(Data.Models.Data.Torrent torrent) + public async Task?> GetDownloadLinks(Torrent torrent) { var files = new List(); diff --git a/server/RdtClient.Web/Controllers/QBittorrentController.cs b/server/RdtClient.Web/Controllers/QBittorrentController.cs index fe162b2..118b89e 100644 --- a/server/RdtClient.Web/Controllers/QBittorrentController.cs +++ b/server/RdtClient.Web/Controllers/QBittorrentController.cs @@ -322,21 +322,22 @@ public class QBittorrentController(ILogger logger, QBitto foreach (var url in urls) { - try{ - if (url.StartsWith("magnet")) + try { - await qBittorrent.TorrentsAddMagnet(url.Trim(), request.Category, null); - } - else if (url.StartsWith("http")) - { - var httpClient = new HttpClient(); - var result = await httpClient.GetByteArrayAsync(url); - await qBittorrent.TorrentsAddFile(result, request.Category, null); - } - else - { - return BadRequest($"Invalid torrent link format {url}"); - } + if (url.StartsWith("magnet")) + { + await qBittorrent.TorrentsAddMagnet(url.Trim(), request.Category, null); + } + else if (url.StartsWith("http")) + { + var httpClient = new HttpClient(); + var result = await httpClient.GetByteArrayAsync(url); + await qBittorrent.TorrentsAddFile(result, request.Category, null); + } + else + { + return BadRequest($"Invalid torrent link format {url}"); + } } catch (RDNET.RealDebridException ex) { @@ -507,6 +508,22 @@ public class QBittorrentController(ILogger logger, QBitto { return await SyncMainData(); } + + [Authorize(Policy = "AuthSetting")] + [Route("transfer/info")] + [HttpGet] + public ActionResult TransferInfo() + { + return Ok(QBittorrent.TransferInfo()); + } + + [Authorize(Policy = "AuthSetting")] + [Route("transfer/info")] + [HttpPost] + public ActionResult TransferInfoPost() + { + return TransferInfo(); + } } public class QBAuthLoginRequest diff --git a/server/RdtClient.Web/RdtClient.Web.csproj b/server/RdtClient.Web/RdtClient.Web.csproj index a0eface..c4c2ce3 100644 --- a/server/RdtClient.Web/RdtClient.Web.csproj +++ b/server/RdtClient.Web/RdtClient.Web.csproj @@ -4,7 +4,7 @@ net9.0 Exe 94c24cba-f03f-4453-a671-3640b517c573 - 2.0.94 + 2.0.95 enable enable latest @@ -29,15 +29,15 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + +