Merge branch 'main' into fix/pm-all-files-excluded

This commit is contained in:
Cucumberrbob 2025-04-13 14:52:09 +01:00
commit 478cc0e714
No known key found for this signature in database
GPG key ID: 2B935C47401C3614
20 changed files with 274 additions and 168 deletions

View file

@ -2,7 +2,8 @@ name: Release
on:
push:
branches: [main]
tags:
- v*
permissions:
id-token: write

View file

@ -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

View file

@ -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```

View file

@ -9,7 +9,7 @@
<th>
<input
type="checkbox"
(click)="toggleSelectAll($event)"
(click)="toggleDeleteSelectAll($event)"
[checked]="selectedTorrents.length > 0 && selectedTorrents.length === torrents.length"
/>
</th>
@ -107,19 +107,25 @@
<label class="label"></label>
<div class="control">
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteData" />
<input type="checkbox" [(ngModel)]="deleteData" (change)="updateDeleteSelectAll()" />
Delete Torrents from client
</label>
<br />
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteRdTorrent" />
<input type="checkbox" [(ngModel)]="deleteRdTorrent" (change)="updateDeleteSelectAll()" />
Delete Torrents from provider
</label>
<br />
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteLocalFiles" />
<input type="checkbox" [(ngModel)]="deleteLocalFiles" (change)="updateDeleteSelectAll()" />
Delete local files
</label>
<br />
<br />
<label class="checkbox">
<input type="checkbox" [(ngModel)]="deleteSelectAll" (change)="toggleDeleteSelectAllOptions()" />
Select All
</label>
</div>
</div>
<div class="notification is-primary">

View file

@ -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;
}
}

View file

@ -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<Download> Add(Guid torrentId, String path)
public async Task<Download> 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

View file

@ -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; }
}
/// <summary>
/// Used to create <see cref="Download"/>s
/// </summary>
public class DownloadInfo
{
/// <summary>
/// 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.
/// </summary>
public required String? FileName;
/// <summary>
/// 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
/// </summary>
public required String RestrictedLink;
}

View file

@ -181,6 +181,13 @@ or
<a href=""https://debrid-link.com/webapp/apikey"" target=""_blank"" rel=""noopener"">https://debrid-link.com/webapp/apikey</a>")]
public String ApiKey { get; set; } = "";
/// <summary>
/// API hostname to use <b>for Real Debrid only</b>
/// </summary>
[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;

View file

@ -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<CancellationToken>())).ReturnsAsync(files);
mocks.FileFilterMock.Setup(f => f.IsDownloadable(torrent, It.IsAny<String>(), It.IsAny<Int64>())).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);

View file

@ -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<Download> Add(Guid torrentId, String path)
public async Task<Download> 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)

View file

@ -7,7 +7,7 @@ public interface IDownloads
Task<List<Download>> GetForTorrent(Guid torrentId);
Task<Download?> GetById(Guid downloadId);
Task<Download?> Get(Guid torrentId, String path);
Task<Download> Add(Guid torrentId, String path);
Task<Download> Add(Guid torrentId, DownloadInfo downloadInfo);
Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink);
Task UpdateFileName(Guid downloadId, String fileName);
Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime);

View file

@ -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<AllDebridTorrentClient> logger, IAll
return Task.FromResult<IList<TorrentClientAvailableFile>>([]);
}
public Task SelectFiles(Torrent torrent)
/// <inheritdoc />
public Task<Int32?> SelectFiles(Torrent torrent)
{
return Task.CompletedTask;
return Task.FromResult<Int32?>(torrent.Files.Count);
}
public async Task Delete(String torrentId)
@ -245,34 +246,33 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
return torrent;
}
public async Task<IList<String>?> GetDownloadLinks(Torrent torrent)
public async Task<IList<DownloadInfo>?> 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<String> GetFileName(String downloadUrl)
/// <inheritdoc />
public Task<String> 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<TorrentClientTorrent> GetInfo(String torrentId)

View file

@ -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<DebridLinkClient> logger, IHttpClientFacto
return Task.FromResult<IList<TorrentClientAvailableFile>>([]);
}
public Task SelectFiles(Data.Models.Data.Torrent torrent)
/// <inheritdoc />
public Task<Int32?> SelectFiles(Data.Models.Data.Torrent torrent)
{
return Task.CompletedTask;
return Task.FromResult<Int32?>(torrent.Files.Count);
}
public async Task Delete(String torrentId)
@ -234,7 +236,7 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
return torrent;
}
public async Task<IList<String>?> GetDownloadLinks(Data.Models.Data.Torrent torrent)
public async Task<IList<DownloadInfo>?> GetDownloadInfos(Data.Models.Data.Torrent torrent)
{
if (torrent.RdId == null)
{
@ -248,19 +250,14 @@ public class DebridLinkClient(ILogger<DebridLinkClient> 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<TorrentClientTorrent> GetInfo(String torrentId)
@ -280,16 +277,13 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
logger.LogDebug(message);
}
public Task<String> GetFileName(String downloadUrl)
/// <inheritdoc />
public Task<String> 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)

View file

@ -10,10 +10,24 @@ public interface ITorrentClient
Task<String> AddMagnet(String magnetLink);
Task<String> AddFile(Byte[] bytes);
Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash);
Task SelectFiles(Torrent torrent);
/// <summary>
/// Tell the debrid provider which files to download.
/// </summary>
/// <remark>
/// Not all providers support this feature.
/// </remark>
/// <param name="torrent">The torrent to select files for</param>
/// <returns>Number of files selected</returns>
Task<Int32?> SelectFiles(Torrent torrent);
Task Delete(String torrentId);
Task<String> Unrestrict(String link);
Task<Torrent> UpdateData(Torrent torrent, TorrentClientTorrent? torrentClientTorrent);
Task<IList<String>?> GetDownloadLinks(Torrent torrent);
Task<String> GetFileName(String downloadUrl);
Task<IList<DownloadInfo>?> GetDownloadInfos(Torrent torrent);
/// <summary>
/// To be called only when <see cref="Data.Models.Data.Download" />.<see cref="Data.Models.Data.Download.FileName" /> is not set by
/// <see cref="GetDownloadInfos" />
/// </summary>
/// <param name="download">The download to get the filename of</param>
/// <returns>The filename of the download</returns>
Task<String> GetFileName(Download download);
}

View file

@ -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<PremiumizeTorrentClient> logger, IH
return Task.FromResult<IList<TorrentClientAvailableFile>>([]);
}
public Task SelectFiles(Torrent torrent)
/// <inheritdoc />
public Task<Int32?> 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<Int32?>(1);
}
public async Task Delete(String id)
@ -197,7 +201,7 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
return torrent;
}
public async Task<IList<String>?> GetDownloadLinks(Torrent torrent)
public async Task<IList<DownloadInfo>?> GetDownloadInfos(Torrent torrent)
{
if (torrent.RdId == null)
{
@ -212,7 +216,7 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> 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<PremiumizeTorrentClient> 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<String> GetFileName(String downloadUrl)
/// <inheritdoc />
public Task<String> 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<TorrentClientTorrent> GetInfo(String id)
@ -256,13 +257,12 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
return Map(result);
}
private async Task<List<String>> GetAllDownloadLinks(Torrent torrent, String folderId)
private async Task<List<DownloadInfo>> GetAllDownloadInfos(Torrent torrent, String folderId)
{
var downloadLinks = new List<String>();
if (String.IsNullOrWhiteSpace(folderId))
{
return downloadLinks;
return [];
}
var folder = await GetClient().Folder.ListAsync(folderId);
@ -270,11 +270,13 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> 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<DownloadInfo>();
foreach (var item in folder.Content)
{
if (item.Type == "file")
@ -293,7 +295,7 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> 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<PremiumizeTorrentClient> 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<PremiumizeTorrentClient> logger, IH
}
}
return downloadLinks;
return downloadInfos;
}
private void Log(String message, Torrent? torrent = null)

View file

@ -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<RealDebridTorrentClient> 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<RealDebridTorrentClient> logger, IH
return Task.FromResult<IList<TorrentClientAvailableFile>>([]);
}
public async Task SelectFiles(Data.Models.Data.Torrent torrent)
/// <inheritdoc />
public async Task<Int32?> SelectFiles(Data.Models.Data.Torrent torrent)
{
List<TorrentClientFile> files;
@ -166,7 +170,14 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> 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<RealDebridTorrentClient> logger, IH
return torrent;
}
public async Task<IList<String>?> GetDownloadLinks(Data.Models.Data.Torrent torrent)
public async Task<IList<DownloadInfo>?> GetDownloadInfos(Data.Models.Data.Torrent torrent)
{
if (torrent.RdId == null)
{
@ -279,7 +290,14 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> 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<RealDebridTorrentClient> logger, IH
return null;
}
public Task<String> GetFileName(String downloadUrl)
/// <inheritdoc />
public Task<String> 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()));
}

View file

@ -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<TorBoxTorrentClient> logger, IHttpClien
return [];
}
public Task SelectFiles(Torrent torrent)
/// <inheritdoc />
public Task<Int32?> SelectFiles(Torrent torrent)
{
return Task.CompletedTask;
return Task.FromResult<Int32?>(torrent.Files.Count);
}
public async Task Delete(String torrentId)
@ -207,7 +208,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> 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<TorBoxTorrentClient> logger, IHttpClien
return torrent;
}
public async Task<IList<String>?> GetDownloadLinks(Torrent torrent)
public async Task<IList<DownloadInfo>?> 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<TorBoxTorrentClient> 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<String> GetFileName(String downloadUrl)
/// <inheritdoc />
public Task<String> 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)

View file

@ -362,8 +362,11 @@ public class TorrentRunner(ILogger<TorrentRunner> 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)

View file

@ -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);
}
}
}
/// <summary>
/// Logs a message to the console, sets the error on the torrent and ensures it is not retried.
/// </summary>
/// <param name="torrent">The torrent to mark as "All files excluded"</param>
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;
}
/// <summary>
/// To be called only when <see cref="Data.Models.Data.Download" />.<see cref="Data.Models.Data.Download.FileName" /> is not set by
/// <see cref="ITorrentClient.GetDownloadInfos" />
/// </summary>
public async Task<String> 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);

View file

@ -4,7 +4,7 @@
<TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType>
<UserSecretsId>94c24cba-f03f-4453-a671-3640b517c573</UserSecretsId>
<Version>2.0.102</Version>
<Version>2.0.104</Version>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>