diff --git a/.github/workflows/build-release.yaml b/.github/workflows/build-release.yaml
index d9000f0..0d08afa 100644
--- a/.github/workflows/build-release.yaml
+++ b/.github/workflows/build-release.yaml
@@ -2,7 +2,8 @@ name: Release
on:
push:
- branches: [main]
+ tags:
+ - v*
permissions:
id-token: write
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2babfed..a3a267a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,8 +5,24 @@ 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]
+
+## [v2.0.104] - 2025-04-12
+### Fixed
+- Update the version number
+
+## [v2.0.103] - 2025-04-12
+### Added
+- Button to select all options when deleting a torrent, thanks @EugeneKallis
+- Add setting to ignore update notifications. A notification will appear regardless of this setting if any GitHub Security Advisories are published in this repo.
### Changed
-- Download .zip of torrent files from TorBox when possible, thanks @asylumexp!
+- Download .zip of torrent files from TorBox when possible, thanks @asylumexp
+- Users of AllDebrid and RealDebrid will now have no files downloaded when all files are excluded by filters. Before, if all files were excluded, rdt-client would download all the files in the torrent.
+- Reduce number of calls to debrid provider API when no torrents need updating
+### Fixed
+- The dropdown navigation menu on mobile will now close when you navigate to another page
+- Long torrent names without spaces will now wrap across lines
+### Security
+- Require auth to change debrid api key
## [2.0.102] - 2025-03-07
### Changed
diff --git a/README.md b/README.md
index 7289574..56d361e 100644
--- a/README.md
+++ b/README.md
@@ -220,3 +220,14 @@ By default the application runs in the root of your hosted address (i.e. https:/
1. To stop: `docker stop rdtclient`
1. To remove: `docker rm rdtclient`
1. Or use `docker-build.bat`
+
+## Misc Install Notes
+
+### Rootless Podman, Linux Host, and CIFS Connections
+
+RDT Client read and write permission tests fail if the CIFS connection is not setup properly, despite permissions working inspection. In the Web GUI, it will report access denied, and in the log file you will see exceptions like this ([dotnet issue](https://github.com/dotnet/runtime/issues/42790)):
+```
+System.IO.IOException: Permission denied
+```
+The **nobrl** has to be specified in your CIFS connection - [man page](https://linux.die.net/man/8/mount.cifs).
+Example: ```Options=_netdev,credentials=/etc/samba/credentials/600file,rw,uid=SUBUID,gid=SBUGID,nobrl,file_mode=0770,dir_mode=0770,noperm```
diff --git a/client/src/app/torrent-table/torrent-table.component.html b/client/src/app/torrent-table/torrent-table.component.html
index b728bdc..536d57d 100644
--- a/client/src/app/torrent-table/torrent-table.component.html
+++ b/client/src/app/torrent-table/torrent-table.component.html
@@ -9,7 +9,7 @@
0 && selectedTorrents.length === torrents.length"
/>
@@ -107,19 +107,25 @@
-
+
Delete Torrents from client
-
+
Delete Torrents from provider
-
+
Delete local files
+
+
+
+
+ Select All
+
diff --git a/client/src/app/torrent-table/torrent-table.component.ts b/client/src/app/torrent-table/torrent-table.component.ts
index 3f17925..d188077 100644
--- a/client/src/app/torrent-table/torrent-table.component.ts
+++ b/client/src/app/torrent-table/torrent-table.component.ts
@@ -20,6 +20,7 @@ export class TorrentTableComponent implements OnInit {
public isDeleteModalActive: boolean;
public deleteError: string;
public deleting: boolean;
+ public deleteSelectAll: boolean;
public deleteData: boolean;
public deleteRdTorrent: boolean;
public deleteLocalFiles: boolean;
@@ -74,7 +75,7 @@ export class TorrentTableComponent implements OnInit {
return el.torrentId;
}
- public toggleSelectAll(event: any) {
+ public toggleDeleteSelectAll(event: any) {
this.selectedTorrents = [];
if (event.target.checked) {
@@ -247,4 +248,13 @@ export class TorrentTableComponent implements OnInit {
},
});
}
+ toggleDeleteSelectAllOptions() {
+ this.deleteData = this.deleteSelectAll;
+ this.deleteRdTorrent = this.deleteSelectAll;
+ this.deleteLocalFiles = this.deleteSelectAll;
+ }
+
+ updateDeleteSelectAll() {
+ this.deleteSelectAll = this.deleteData && this.deleteRdTorrent && this.deleteLocalFiles;
+ }
}
diff --git a/server/RdtClient.Data/Data/DownloadData.cs b/server/RdtClient.Data/Data/DownloadData.cs
index 9d141f9..3b9e303 100644
--- a/server/RdtClient.Data/Data/DownloadData.cs
+++ b/server/RdtClient.Data/Data/DownloadData.cs
@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
+using RdtClient.Data.Models.Data;
using Download = RdtClient.Data.Models.Data.Download;
namespace RdtClient.Data.Data;
@@ -29,13 +30,14 @@ public class DownloadData(DataContext dataContext)
.FirstOrDefaultAsync(m => m.TorrentId == torrentId && m.Path == path);
}
- public async Task
Add(Guid torrentId, String path)
+ public async Task Add(Guid torrentId, DownloadInfo downloadInfo)
{
var download = new Download
{
DownloadId = Guid.NewGuid(),
TorrentId = torrentId,
- Path = path,
+ FileName = downloadInfo.FileName,
+ Path = downloadInfo.RestrictedLink,
Added = DateTimeOffset.UtcNow,
DownloadQueued = DateTimeOffset.UtcNow,
RetryCount = 0
diff --git a/server/RdtClient.Data/Models/Data/Download.cs b/server/RdtClient.Data/Models/Data/Download.cs
index d078356..6720415 100644
--- a/server/RdtClient.Data/Models/Data/Download.cs
+++ b/server/RdtClient.Data/Models/Data/Download.cs
@@ -26,7 +26,7 @@ public class Download
public DateTimeOffset? Completed { get; set; }
public Int32 RetryCount { get; set; }
-
+
public String? Error { get; set; }
public String? RemoteId { get; set; }
@@ -41,4 +41,20 @@ public class Download
[NotMapped]
public Int64 Speed { get; set; }
+}
+
+///
+/// Used to create s
+///
+public class DownloadInfo
+{
+ ///
+ /// The name of the file. Should not include directory.
+ /// If the filename is not known, set tn null and `GetFileName` will be called with the unrestricted link.
+ ///
+ public required String? FileName;
+ ///
+ /// The restricted link to download this download. If the debrid serice in question does not have restricted links, use either a fake or the unrestricted link
+ ///
+ public required String RestrictedLink;
}
\ No newline at end of file
diff --git a/server/RdtClient.Data/Models/Internal/DbSettings.cs b/server/RdtClient.Data/Models/Internal/DbSettings.cs
index 5ad2a57..3295a1f 100644
--- a/server/RdtClient.Data/Models/Internal/DbSettings.cs
+++ b/server/RdtClient.Data/Models/Internal/DbSettings.cs
@@ -181,6 +181,13 @@ or
https://debrid-link.com/webapp/apikey ")]
public String ApiKey { get; set; } = "";
+ ///
+ /// API hostname to use for Real Debrid only
+ ///
+ [DisplayName("API Hostname (RD only)")]
+ [Description("Use this instead of the normal hostname for Real Debrid API requests. Only used by Real Debrid. Leave blank to use default.")]
+ public String? ApiHostname { get; set; }
+
[DisplayName("Automatically import and process torrents added to provider")]
[Description("When selected, import downloads that are not added through RealDebridClient but have been directly added to your debrid provider.")]
public Boolean AutoImport { get; set; } = false;
diff --git a/server/RdtClient.Service.Test/Services/TorrentClients/AllDebridTorrentClientTest.cs b/server/RdtClient.Service.Test/Services/TorrentClients/AllDebridTorrentClientTest.cs
index 140ee91..1ec1f88 100644
--- a/server/RdtClient.Service.Test/Services/TorrentClients/AllDebridTorrentClientTest.cs
+++ b/server/RdtClient.Service.Test/Services/TorrentClients/AllDebridTorrentClientTest.cs
@@ -405,7 +405,7 @@ public class AllDebridTorrentClientTest
}
[Fact]
- public async Task GetDownloadLinks_WhenTorrentRdIdNull_ReturnsNull()
+ public async Task GetDownloadInfos_WhenTorrentRdIdNull_ReturnsNull()
{
// Arrange
var mocks = new Mocks();
@@ -418,7 +418,7 @@ public class AllDebridTorrentClientTest
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act
- var result = await allDebridTorrentClient.GetDownloadLinks(torrent);
+ var result = await allDebridTorrentClient.GetDownloadInfos(torrent);
// Assert
Assert.Null(result);
@@ -426,7 +426,7 @@ public class AllDebridTorrentClientTest
}
[Fact]
- public async Task GetDownloadLinks_UsesFileFilter()
+ public async Task GetDownloadInfos_UsesFileFilter()
{
// Arrange
var mocks = new Mocks();
@@ -467,16 +467,16 @@ public class AllDebridTorrentClientTest
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act
- var result = await allDebridTorrentClient.GetDownloadLinks(torrent);
+ var result = await allDebridTorrentClient.GetDownloadInfos(torrent);
// Assert
Assert.NotNull(result);
Assert.Single(result);
- Assert.Contains("https://fake.url/file1.txt", result);
+ Assert.Single(result.Where(info => info.RestrictedLink == "https://fake.url/file1.txt"));
}
[Fact]
- public async Task GetDownloadLinks_WhenAllFilesExcluded_ReturnsEmptyList()
+ public async Task GetDownloadInfos_WhenAllFilesExcluded_ReturnsEmptyList()
{
// Arrange
var mocks = new Mocks();
@@ -501,14 +501,14 @@ public class AllDebridTorrentClientTest
DownloadLink = "https://fake.url/file-2.txt"
}
];
-
+
mocks.AllDebridClientMock.Setup(c => c.Magnet.FilesAsync(Int64.Parse(torrent.RdId), It.IsAny())).ReturnsAsync(files);
mocks.FileFilterMock.Setup(f => f.IsDownloadable(torrent, It.IsAny(), It.IsAny())).Returns(false);
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object);
// Act
- var result = await allDebridTorrentClient.GetDownloadLinks(torrent);
+ var result = await allDebridTorrentClient.GetDownloadInfos(torrent);
// Assert
Assert.NotNull(result);
diff --git a/server/RdtClient.Service/Services/Downloads.cs b/server/RdtClient.Service/Services/Downloads.cs
index 925c8e0..76677c9 100644
--- a/server/RdtClient.Service/Services/Downloads.cs
+++ b/server/RdtClient.Service/Services/Downloads.cs
@@ -1,4 +1,5 @@
using RdtClient.Data.Data;
+using RdtClient.Data.Models.Data;
using Download = RdtClient.Data.Models.Data.Download;
namespace RdtClient.Service.Services;
@@ -20,9 +21,9 @@ public class Downloads(DownloadData downloadData) : IDownloads
return await downloadData.Get(torrentId, path);
}
- public async Task Add(Guid torrentId, String path)
+ public async Task Add(Guid torrentId, DownloadInfo downloadInfo)
{
- return await downloadData.Add(torrentId, path);
+ return await downloadData.Add(torrentId, downloadInfo);
}
public async Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink)
diff --git a/server/RdtClient.Service/Services/IDownloads.cs b/server/RdtClient.Service/Services/IDownloads.cs
index d0550b9..b79f59a 100644
--- a/server/RdtClient.Service/Services/IDownloads.cs
+++ b/server/RdtClient.Service/Services/IDownloads.cs
@@ -7,7 +7,7 @@ public interface IDownloads
Task> GetForTorrent(Guid torrentId);
Task GetById(Guid downloadId);
Task Get(Guid torrentId, String path);
- Task Add(Guid torrentId, String path);
+ Task Add(Guid torrentId, DownloadInfo downloadInfo);
Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink);
Task UpdateFileName(Guid downloadId, String fileName);
Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime);
diff --git a/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs
index 6fd5388..b0f49d8 100644
--- a/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs
+++ b/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs
@@ -1,10 +1,10 @@
-using AllDebridNET;
+using System.Diagnostics;
+using AllDebridNET;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers;
-using System.Web;
using RdtClient.Data.Models.Data;
using File = AllDebridNET.File;
using Torrent = RdtClient.Data.Models.Data.Torrent;
@@ -155,9 +155,10 @@ public class AllDebridTorrentClient(ILogger logger, IAll
return Task.FromResult>([]);
}
- public Task SelectFiles(Torrent torrent)
+ ///
+ public Task SelectFiles(Torrent torrent)
{
- return Task.CompletedTask;
+ return Task.FromResult(torrent.Files.Count);
}
public async Task Delete(String torrentId)
@@ -245,34 +246,33 @@ public class AllDebridTorrentClient(ILogger logger, IAll
return torrent;
}
- public async Task?> GetDownloadLinks(Torrent torrent)
+ public async Task?> GetDownloadInfos(Torrent torrent)
{
if (torrent.RdId == null)
{
return null;
}
-
+
Log($"Getting download links", torrent);
-
+
var allFiles = await allDebridNetClientFactory.GetClient().Magnet.FilesAsync(Int64.Parse(torrent.RdId));
var files = GetFiles(allFiles);
-
- files = files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes)).ToList();
-
- return files.Where(m => m.DownloadLink != null).Select(m => m.DownloadLink!.ToString()).ToList();
+
+ return files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes) && f.DownloadLink != null).Select(f => new DownloadInfo
+ {
+ FileName = Path.GetFileName(f.Path),
+ RestrictedLink = f.DownloadLink!
+ }).ToList();
}
- public Task GetFileName(String downloadUrl)
+ ///
+ public Task GetFileName(Download download)
{
- if (String.IsNullOrWhiteSpace(downloadUrl))
- {
- return Task.FromResult("");
- }
-
- var uri = new Uri(downloadUrl);
-
- return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last()));
+ // FileName is set in GetDownlaadInfos
+ Debug.Assert(download.FileName != null);
+
+ return Task.FromResult(download.FileName);
}
private async Task GetInfo(String torrentId)
diff --git a/server/RdtClient.Service/Services/TorrentClients/DebridLinkTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/DebridLinkTorrentClient.cs
index a4c4026..567b777 100644
--- a/server/RdtClient.Service/Services/TorrentClients/DebridLinkTorrentClient.cs
+++ b/server/RdtClient.Service/Services/TorrentClients/DebridLinkTorrentClient.cs
@@ -1,12 +1,13 @@
-using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using DebridLinkFrNET;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers;
-using DebridLinkFrNET.Models;
-using System.Web;
+using RdtClient.Data.Models.Data;
using Download = RdtClient.Data.Models.Data.Download;
+using Torrent = DebridLinkFrNET.Models.Torrent;
namespace RdtClient.Service.Services.TorrentClients;
@@ -136,9 +137,10 @@ public class DebridLinkClient(ILogger logger, IHttpClientFacto
return Task.FromResult>([]);
}
- public Task SelectFiles(Data.Models.Data.Torrent torrent)
+ ///
+ public Task SelectFiles(Data.Models.Data.Torrent torrent)
{
- return Task.CompletedTask;
+ return Task.FromResult(torrent.Files.Count);
}
public async Task Delete(String torrentId)
@@ -234,7 +236,7 @@ public class DebridLinkClient(ILogger logger, IHttpClientFacto
return torrent;
}
- public async Task?> GetDownloadLinks(Data.Models.Data.Torrent torrent)
+ public async Task?> GetDownloadInfos(Data.Models.Data.Torrent torrent)
{
if (torrent.RdId == null)
{
@@ -248,19 +250,14 @@ public class DebridLinkClient(ILogger logger, IHttpClientFacto
return null;
}
- var downloadLinks = rdTorrent.Files?
- .Where(m => fileFilter.IsDownloadable(torrent, m.Path, m.Bytes) && m.DownloadLink != null)
- .Select(m => m.DownloadLink!)
- .ToList() ?? [];
-
- Log($"Found {downloadLinks.Count} links", torrent);
-
- foreach (var link in downloadLinks)
- {
- Log($"{link}", torrent);
- }
-
- return downloadLinks;
+ return rdTorrent.Files?
+ .Where(m => fileFilter.IsDownloadable(torrent, m.Path, m.Bytes) && m.DownloadLink != null)
+ .Select(m => new DownloadInfo
+ {
+ RestrictedLink = m.DownloadLink!,
+ FileName = Path.GetFileName(m.Path)
+ })
+ .ToList();
}
private async Task GetInfo(String torrentId)
@@ -280,16 +277,13 @@ public class DebridLinkClient(ILogger logger, IHttpClientFacto
logger.LogDebug(message);
}
- public Task GetFileName(String downloadUrl)
+ ///
+ public Task GetFileName(Download download)
{
- if (String.IsNullOrWhiteSpace(downloadUrl))
- {
- return Task.FromResult("");
- }
-
- var uri = new Uri(downloadUrl);
-
- return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last()));
+ // FileName is set in GetDownlaadInfos
+ Debug.Assert(download.FileName != null);
+
+ return Task.FromResult(download.FileName);
}
public static String? GetSymlinkPath(Data.Models.Data.Torrent torrent, Download download)
diff --git a/server/RdtClient.Service/Services/TorrentClients/ITorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/ITorrentClient.cs
index 38121ae..8cd0792 100644
--- a/server/RdtClient.Service/Services/TorrentClients/ITorrentClient.cs
+++ b/server/RdtClient.Service/Services/TorrentClients/ITorrentClient.cs
@@ -10,10 +10,24 @@ public interface ITorrentClient
Task AddMagnet(String magnetLink);
Task AddFile(Byte[] bytes);
Task> GetAvailableFiles(String hash);
- Task SelectFiles(Torrent torrent);
+ ///
+ /// Tell the debrid provider which files to download.
+ ///
+ ///
+ /// Not all providers support this feature.
+ ///
+ /// The torrent to select files for
+ /// Number of files selected
+ Task SelectFiles(Torrent torrent);
Task Delete(String torrentId);
Task Unrestrict(String link);
Task UpdateData(Torrent torrent, TorrentClientTorrent? torrentClientTorrent);
- Task?> GetDownloadLinks(Torrent torrent);
- Task GetFileName(String downloadUrl);
+ Task?> GetDownloadInfos(Torrent torrent);
+ ///
+ /// To be called only when . is not set by
+ ///
+ ///
+ /// The download to get the filename of
+ /// The filename of the download
+ Task GetFileName(Download download);
}
\ No newline at end of file
diff --git a/server/RdtClient.Service/Services/TorrentClients/PremiumizeTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/PremiumizeTorrentClient.cs
index f47dbba..1c7014d 100644
--- a/server/RdtClient.Service/Services/TorrentClients/PremiumizeTorrentClient.cs
+++ b/server/RdtClient.Service/Services/TorrentClients/PremiumizeTorrentClient.cs
@@ -1,10 +1,11 @@
-using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using PremiumizeNET;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers;
-using System.Web;
+using RdtClient.Data.Models.Data;
using Torrent = RdtClient.Data.Models.Data.Torrent;
namespace RdtClient.Service.Services.TorrentClients;
@@ -121,9 +122,12 @@ public class PremiumizeTorrentClient(ILogger logger, IH
return Task.FromResult>([]);
}
- public Task SelectFiles(Torrent torrent)
+ ///
+ public Task SelectFiles(Torrent torrent)
{
- return Task.CompletedTask;
+ // torrent.Files is not populated when this function is called
+ // by returning 1, we ensure no logic for "all files excluded" is followed
+ return Task.FromResult(1);
}
public async Task Delete(String id)
@@ -197,7 +201,7 @@ public class PremiumizeTorrentClient(ILogger logger, IH
return torrent;
}
- public async Task?> GetDownloadLinks(Torrent torrent)
+ public async Task?> GetDownloadInfos(Torrent torrent)
{
if (torrent.RdId == null)
{
@@ -212,7 +216,7 @@ public class PremiumizeTorrentClient(ILogger logger, IH
Log($"Found transfer {transfer.Name} ({transfer.Id})", torrent);
- var downloadLinks = await GetAllDownloadLinks(torrent, transfer.FolderId);
+ var downloadInfos = await GetAllDownloadInfos(torrent, transfer.FolderId);
if (!String.IsNullOrWhiteSpace(transfer.FileId))
{
@@ -225,27 +229,24 @@ public class PremiumizeTorrentClient(ILogger logger, IH
Log($"File {file.Name} ({file.Id}) does not contain a link", torrent);
}
- downloadLinks.Add(file.Link);
+ downloadInfos.Add(new () {RestrictedLink = file.Link, FileName = file.Name });
}
- foreach (var link in downloadLinks)
+ foreach (var info in downloadInfos)
{
- Log($"Found {link}", torrent);
+ Log($"Found {info.RestrictedLink}", torrent);
}
- return downloadLinks;
+ return downloadInfos;
}
- public Task GetFileName(String downloadUrl)
+ ///
+ public Task GetFileName(Download download)
{
- if (String.IsNullOrWhiteSpace(downloadUrl))
- {
- return Task.FromResult("");
- }
-
- var uri = new Uri(downloadUrl);
-
- return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last()));
+ // FileName is set in GetDownlaadInfos
+ Debug.Assert(download.FileName != null);
+
+ return Task.FromResult(download.FileName);
}
private async Task GetInfo(String id)
@@ -256,13 +257,12 @@ public class PremiumizeTorrentClient(ILogger logger, IH
return Map(result);
}
- private async Task> GetAllDownloadLinks(Torrent torrent, String folderId)
+ private async Task> GetAllDownloadInfos(Torrent torrent, String folderId)
{
- var downloadLinks = new List();
if (String.IsNullOrWhiteSpace(folderId))
{
- return downloadLinks;
+ return [];
}
var folder = await GetClient().Folder.ListAsync(folderId);
@@ -270,11 +270,13 @@ public class PremiumizeTorrentClient(ILogger logger, IH
if (folder.Content == null)
{
Log($"Found no items in folder {folder.Name} ({folderId})", torrent);
- return downloadLinks;
+ return [];
}
Log($"Found {folder.Content.Count} items in folder {folder.Name} ({folderId})", torrent);
+ var downloadInfos = new List();
+
foreach (var item in folder.Content)
{
if (item.Type == "file")
@@ -293,7 +295,7 @@ public class PremiumizeTorrentClient(ILogger logger, IH
Log($"Found item {item.Name} in folder {folder.Name} ({folderId})", torrent);
- downloadLinks.Add(item.Link);
+ downloadInfos.Add(new () { RestrictedLink = item.Link, FileName = item.Name});
}
else if (item.Type == "folder")
{
@@ -304,8 +306,8 @@ public class PremiumizeTorrentClient(ILogger logger, IH
}
Log($"Found subfolder {item.Name} in {folder.Name} ({folderId}), searching subfolder for files", torrent);
- var subDownloadLinks = await GetAllDownloadLinks(torrent, item.Id);
- downloadLinks.AddRange(subDownloadLinks);
+ var subDownloadLinks = await GetAllDownloadInfos(torrent, item.Id);
+ downloadInfos.AddRange(subDownloadLinks);
}
else
{
@@ -313,7 +315,7 @@ public class PremiumizeTorrentClient(ILogger logger, IH
}
}
- return downloadLinks;
+ return downloadInfos;
}
private void Log(String message, Torrent? torrent = null)
diff --git a/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs
index efff0a4..7b3890a 100644
--- a/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs
+++ b/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs
@@ -3,8 +3,11 @@ using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using RDNET;
using RdtClient.Data.Enums;
+using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers;
+using Download = RdtClient.Data.Models.Data.Download;
+using Torrent = RDNET.Torrent;
namespace RdtClient.Service.Services.TorrentClients;
@@ -26,7 +29,7 @@ public class RealDebridTorrentClient(ILogger logger, IH
var httpClient = httpClientFactory.CreateClient(DiConfig.RD_CLIENT);
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
- var rdtNetClient = new RdNetClient(null, httpClient, 5);
+ var rdtNetClient = new RdNetClient(null, httpClient, 5, Settings.Get.Provider.ApiHostname);
rdtNetClient.UseApiAuthentication(apiKey);
// Get the server time to fix up the timezones on results
@@ -142,7 +145,8 @@ public class RealDebridTorrentClient(ILogger logger, IH
return Task.FromResult>([]);
}
- public async Task SelectFiles(Data.Models.Data.Torrent torrent)
+ ///
+ public async Task SelectFiles(Data.Models.Data.Torrent torrent)
{
List files;
@@ -166,7 +170,14 @@ public class RealDebridTorrentClient(ILogger logger, IH
var fileIds = files.Select(m => m.Id.ToString()).ToArray();
+ if (fileIds.Length == 0)
+ {
+ return 0;
+ }
+
await GetClient().Torrents.SelectFilesAsync(torrent.RdId!, [.. fileIds]);
+
+ return fileIds.Length;
}
public async Task Delete(String torrentId)
@@ -265,7 +276,7 @@ public class RealDebridTorrentClient(ILogger logger, IH
return torrent;
}
- public async Task?> GetDownloadLinks(Data.Models.Data.Torrent torrent)
+ public async Task?> GetDownloadInfos(Data.Models.Data.Torrent torrent)
{
if (torrent.RdId == null)
{
@@ -279,7 +290,14 @@ public class RealDebridTorrentClient(ILogger logger, IH
return null;
}
- var downloadLinks = rdTorrent.Links.Where(m => !String.IsNullOrWhiteSpace(m)).ToList();
+ var downloadLinks = rdTorrent.Links
+ .Where(m => !String.IsNullOrWhiteSpace(m))
+ .Select(l => new DownloadInfo
+ {
+ RestrictedLink = l,
+ FileName = null
+ })
+ .ToList();
Log($"Found {downloadLinks.Count} links", torrent);
@@ -326,14 +344,15 @@ public class RealDebridTorrentClient(ILogger logger, IH
return null;
}
- public Task GetFileName(String downloadUrl)
+ ///
+ public Task GetFileName(Download download)
{
- if (String.IsNullOrWhiteSpace(downloadUrl))
+ if (String.IsNullOrWhiteSpace(download.Link))
{
return Task.FromResult("");
}
- var uri = new Uri(downloadUrl);
+ var uri = new Uri(download.Link);
return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last()));
}
diff --git a/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs
index 445f27e..98a4c12 100644
--- a/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs
+++ b/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs
@@ -1,10 +1,10 @@
using System.Reflection;
+using System.Diagnostics;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using TorBoxNET;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient;
-using System.Web;
using RdtClient.Data.Models.Data;
using RdtClient.Service.Helpers;
@@ -171,9 +171,10 @@ public class TorBoxTorrentClient(ILogger logger, IHttpClien
return [];
}
- public Task SelectFiles(Torrent torrent)
+ ///
+ public Task SelectFiles(Torrent torrent)
{
- return Task.CompletedTask;
+ return Task.FromResult(torrent.Files.Count);
}
public async Task Delete(String torrentId)
@@ -207,7 +208,7 @@ public class TorBoxTorrentClient(ILogger logger, IHttpClien
return torrent;
}
- var rdTorrent = await GetInfo(torrent.Hash) ?? throw new($"Resource not found");
+ var rdTorrent = torrentClientTorrent ?? await GetInfo(torrent.Hash) ?? throw new($"Resource not found");
if (!String.IsNullOrWhiteSpace(rdTorrent.Filename))
{
@@ -285,7 +286,7 @@ public class TorBoxTorrentClient(ILogger logger, IHttpClien
return torrent;
}
- public async Task?> GetDownloadLinks(Torrent torrent)
+ public async Task?> GetDownloadInfos(Torrent torrent)
{
var torrentId = await GetClient().Torrents.GetHashInfoAsync(torrent.Hash, skipCache: true);
var downloadableFiles = torrent.Files.Where(file => fileFilter.IsDownloadable(torrent, file.Path, file.Bytes)).ToList();
@@ -293,51 +294,36 @@ public class TorBoxTorrentClient(ILogger logger, IHttpClien
if (downloadableFiles.Count == torrent.Files.Count && torrent.DownloadClient != Data.Enums.DownloadClient.Symlink)
{
logger.LogDebug("Downloading files from TorBox as a zip.");
- return [$"https://torbox.app/fakedl/{torrentId?.Id}/zip"];
+
+ return
+ [
+ new DownloadInfo
+ {
+ RestrictedLink = $"https://torbox.app/fakedl/{torrentId?.Id}/zip",
+ FileName = $"{torrent.RdName}.zip"
+ }
+ ];
}
else
{
logger.LogDebug("Downloading files from TorBox individually.");
- return downloadableFiles.Select(file => $"https://torbox.app/fakedl/{torrentId?.Id}/{file.Id}").ToList();
+
+ return downloadableFiles.Select(file => new DownloadInfo
+ {
+ RestrictedLink = $"https://torbox.app/fakedl/{torrentId?.Id}/{file.Id}",
+ FileName = Path.GetFileName(file.Path)
+ })
+ .ToList();
}
}
- public async Task GetFileName(String downloadUrl)
+ ///
+ public Task GetFileName(Download download)
{
- if (String.IsNullOrWhiteSpace(downloadUrl))
- {
- return "";
- }
-
- var uri = new Uri(downloadUrl);
-
- using (HttpClient client = new())
- {
- client.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
- var request = new HttpRequestMessage(HttpMethod.Head, uri);
- var response = await client.SendAsync(request);
- if (response.Content.Headers.ContentDisposition != null)
- {
- var fileName = response.Content.Headers.ContentDisposition.FileName;
- if (!String.IsNullOrWhiteSpace(fileName))
- {
- return fileName.Trim('"');
- }
- }
- else
- {
- if (response.Content.Headers.ContentType?.MediaType == "application/zip")
- {
- return $"download-{new Random().Next(1, 10001)}.zip";
- }
- else
- {
- logger.LogDebug($"Failed to get filename for URI {downloadUrl}");
- }
- }
- }
-
- return HttpUtility.UrlDecode(uri.Segments.Last());
+ // FileName is set in GetDownlaadInfos
+ Debug.Assert(download.FileName != null);
+
+ return Task.FromResult(download.FileName);
}
private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset)
diff --git a/server/RdtClient.Service/Services/TorrentRunner.cs b/server/RdtClient.Service/Services/TorrentRunner.cs
index a7e7cb6..2d32031 100644
--- a/server/RdtClient.Service/Services/TorrentRunner.cs
+++ b/server/RdtClient.Service/Services/TorrentRunner.cs
@@ -362,8 +362,11 @@ public class TorrentRunner(ILogger logger, Torrents torrents, Dow
var downloadLink = await torrents.UnrestrictLink(download.DownloadId);
download.Link = downloadLink;
- var fileName = await torrents.RetrieveFileName(download.DownloadId);
- download.FileName = fileName;
+ if (download.FileName == null)
+ {
+ var fileName = await torrents.RetrieveFileName(download.DownloadId);
+ download.FileName = fileName;
+ }
}
}
catch (Exception ex)
diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs
index 9004cb3..3d1443d 100644
--- a/server/RdtClient.Service/Services/Torrents.cs
+++ b/server/RdtClient.Service/Services/Torrents.cs
@@ -223,7 +223,12 @@ public class Torrents(
return;
}
- await TorrentClient.SelectFiles(torrent);
+ var selected = await TorrentClient.SelectFiles(torrent);
+
+ if (selected == 0)
+ {
+ await MarkAllFilesExcluded(torrent);
+ }
}
public async Task CreateDownloads(Guid torrentId)
@@ -235,39 +240,48 @@ public class Torrents(
return;
}
- var downloadLinks = await TorrentClient.GetDownloadLinks(torrent);
+ var downloadInfos = await TorrentClient.GetDownloadInfos(torrent);
- if (downloadLinks == null)
+ if (downloadInfos == null)
{
return;
}
- if (downloadLinks.Count == 0)
+ if (downloadInfos.Count == 0)
{
- logger.LogInformation("All files excluded by filters (IncludeRegex: {includeRegex}, ExcludeRegex: {excludeRegex}, DownloadMinSize: {downloadMinSize} {torrentInfo}",
- torrent.IncludeRegex,
- torrent.ExcludeRegex,
- torrent.DownloadMinSize,
- torrent.ToLog());
-
- await torrentData.UpdateRetry(torrentId, null, torrent.TorrentRetryAttempts);
- await torrentData.UpdateComplete(torrentId, "All files excluded", DateTimeOffset.Now, false);
+ await MarkAllFilesExcluded(torrent);
return;
}
- foreach (var downloadLink in downloadLinks)
+ foreach (var downloadInfo in downloadInfos)
{
// Make sure downloads don't get added multiple times
- var downloadExists = await downloads.Get(torrent.TorrentId, downloadLink);
+ var downloadExists = await downloads.Get(torrent.TorrentId, downloadInfo.RestrictedLink);
- if (downloadExists == null && !String.IsNullOrWhiteSpace(downloadLink))
+ if (downloadExists == null && !String.IsNullOrWhiteSpace(downloadInfo.RestrictedLink))
{
- await downloads.Add(torrent.TorrentId, downloadLink);
+ await downloads.Add(torrent.TorrentId, downloadInfo);
}
}
}
+ ///
+ /// Logs a message to the console, sets the error on the torrent and ensures it is not retried.
+ ///
+ /// The torrent to mark as "All files excluded"
+ private async Task MarkAllFilesExcluded(Torrent torrent)
+ {
+ logger.LogInformation("All files excluded by filters (IncludeRegex: {includeRegex}, ExcludeRegex: {excludeRegex}, DownloadMinSize: {downloadMinSize}) {torrentInfo}",
+ torrent.IncludeRegex,
+ torrent.ExcludeRegex,
+ torrent.DownloadMinSize,
+ torrent.ToLog());
+
+ await torrentData.UpdateRetry(torrent.TorrentId, null, torrent.TorrentRetryAttempts);
+ await torrentData.UpdateComplete(torrent.TorrentId, "All files excluded", DateTimeOffset.Now, false);
+ }
+
public async Task Delete(Guid torrentId, Boolean deleteData, Boolean deleteRdTorrent, Boolean deleteLocalFiles)
{
var torrent = await GetById(torrentId);
@@ -390,13 +404,17 @@ public class Torrents(
return unrestrictedLink;
}
+ ///
+ /// To be called only when . is not set by
+ ///
+ ///
public async Task RetrieveFileName(Guid downloadId)
{
var download = await downloads.GetById(downloadId) ?? throw new($"Download with ID {downloadId} not found");
Log($"Retrieving filename for", download, download.Torrent);
- var fileName = await TorrentClient.GetFileName(download.Link!);
+ var fileName = await TorrentClient.GetFileName(download!);
await downloads.UpdateFileName(downloadId, fileName);
diff --git a/server/RdtClient.Web/RdtClient.Web.csproj b/server/RdtClient.Web/RdtClient.Web.csproj
index c32ccba..e62311f 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.102
+ 2.0.104
enable
enable
latest