Merge pull request #746 from Cucumberrbob/refactor/get-download-info

refactor: use `GetDownloadInfo` - a richer version of `GetDownloadLinks`
This commit is contained in:
Roger Far 2025-04-13 07:37:57 -06:00 committed by GitHub
commit 2e028ed17c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 159 additions and 140 deletions

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

@ -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;
@ -245,34 +245,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;
@ -234,7 +235,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 +249,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 +276,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

@ -14,6 +14,12 @@ public interface ITorrentClient
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;
@ -197,7 +198,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 +213,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,34 +226,31 @@ 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 });
}
if (downloadLinks.Count == 0)
if (downloadInfos.Count == 0)
{
Log($"No download links found for transfer {transfer.Name} ({transfer.Id})", torrent);
return null;
}
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)
@ -263,13 +261,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);
@ -277,11 +274,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")
@ -300,7 +299,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")
{
@ -311,8 +310,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
{
@ -320,7 +319,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;
@ -265,7 +268,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 +282,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 +336,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;
@ -285,7 +285,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 +293,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

@ -235,14 +235,14 @@ 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,
@ -256,14 +256,14 @@ public class Torrents(
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);
}
}
}
@ -390,13 +390,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);