Global formatting.

This commit is contained in:
Roger Far 2026-02-11 19:44:49 -07:00
parent 24321a55c9
commit a9648248f4
91 changed files with 990 additions and 849 deletions

View file

@ -9,25 +9,25 @@ public class DownloadData(DataContext dataContext)
public async Task<List<Download>> GetForTorrent(Guid torrentId) public async Task<List<Download>> GetForTorrent(Guid torrentId)
{ {
return await dataContext.Downloads return await dataContext.Downloads
.AsNoTracking() .AsNoTracking()
.Where(m => m.TorrentId == torrentId) .Where(m => m.TorrentId == torrentId)
.ToListAsync(); .ToListAsync();
} }
public async Task<Download?> GetById(Guid downloadId) public async Task<Download?> GetById(Guid downloadId)
{ {
return await dataContext.Downloads return await dataContext.Downloads
.Include(m => m.Torrent) .Include(m => m.Torrent)
.AsNoTracking() .AsNoTracking()
.FirstOrDefaultAsync(m => m.DownloadId == downloadId); .FirstOrDefaultAsync(m => m.DownloadId == downloadId);
} }
public async Task<Download?> Get(Guid torrentId, String path) public async Task<Download?> Get(Guid torrentId, String path)
{ {
return await dataContext.Downloads return await dataContext.Downloads
.Include(m => m.Torrent) .Include(m => m.Torrent)
.AsNoTracking() .AsNoTracking()
.FirstOrDefaultAsync(m => m.TorrentId == torrentId && m.Path == path); .FirstOrDefaultAsync(m => m.TorrentId == torrentId && m.Path == path);
} }
public async Task<Download> Add(Guid torrentId, DownloadInfo downloadInfo) public async Task<Download> Add(Guid torrentId, DownloadInfo downloadInfo)
@ -55,7 +55,7 @@ public class DownloadData(DataContext dataContext)
public async Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink) public async Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink)
{ {
var dbDownload = await dataContext.Downloads var dbDownload = await dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId); .FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null) if (dbDownload == null)
{ {
@ -72,7 +72,7 @@ public class DownloadData(DataContext dataContext)
public async Task UpdateFileName(Guid downloadId, String fileName) public async Task UpdateFileName(Guid downloadId, String fileName)
{ {
var dbDownload = await dataContext.Downloads var dbDownload = await dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId); .FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null) if (dbDownload == null)
{ {
@ -89,7 +89,7 @@ public class DownloadData(DataContext dataContext)
public async Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime) public async Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime)
{ {
var dbDownload = await dataContext.Downloads var dbDownload = await dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId); .FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null) if (dbDownload == null)
{ {
@ -106,7 +106,7 @@ public class DownloadData(DataContext dataContext)
public async Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime) public async Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime)
{ {
var dbDownload = await dataContext.Downloads var dbDownload = await dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId); .FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null) if (dbDownload == null)
{ {
@ -123,7 +123,7 @@ public class DownloadData(DataContext dataContext)
public async Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime) public async Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime)
{ {
var dbDownload = await dataContext.Downloads var dbDownload = await dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId); .FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null) if (dbDownload == null)
{ {
@ -140,7 +140,7 @@ public class DownloadData(DataContext dataContext)
public async Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime) public async Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime)
{ {
var dbDownload = await dataContext.Downloads var dbDownload = await dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId); .FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null) if (dbDownload == null)
{ {
@ -157,7 +157,7 @@ public class DownloadData(DataContext dataContext)
public async Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime) public async Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime)
{ {
var dbDownload = await dataContext.Downloads var dbDownload = await dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId); .FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null) if (dbDownload == null)
{ {
@ -174,7 +174,7 @@ public class DownloadData(DataContext dataContext)
public async Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime) public async Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime)
{ {
var dbDownload = await dataContext.Downloads var dbDownload = await dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId); .FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null) if (dbDownload == null)
{ {
@ -191,7 +191,7 @@ public class DownloadData(DataContext dataContext)
public async Task UpdateError(Guid downloadId, String? error) public async Task UpdateError(Guid downloadId, String? error)
{ {
var dbDownload = await dataContext.Downloads var dbDownload = await dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId); .FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null) if (dbDownload == null)
{ {
@ -208,7 +208,7 @@ public class DownloadData(DataContext dataContext)
public async Task UpdateRetryCount(Guid downloadId, Int32 retryCount) public async Task UpdateRetryCount(Guid downloadId, Int32 retryCount)
{ {
var dbDownload = await dataContext.Downloads var dbDownload = await dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId); .FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null) if (dbDownload == null)
{ {
@ -225,7 +225,7 @@ public class DownloadData(DataContext dataContext)
public async Task UpdateRemoteId(Guid downloadId, String remoteId) public async Task UpdateRemoteId(Guid downloadId, String remoteId)
{ {
var dbDownload = await dataContext.Downloads var dbDownload = await dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId); .FirstOrDefaultAsync(m => m.DownloadId == downloadId);
if (dbDownload == null) if (dbDownload == null)
{ {
@ -240,8 +240,8 @@ public class DownloadData(DataContext dataContext)
public async Task DeleteForTorrent(Guid torrentId) public async Task DeleteForTorrent(Guid torrentId)
{ {
var downloads = await dataContext.Downloads var downloads = await dataContext.Downloads
.Where(m => m.TorrentId == torrentId) .Where(m => m.TorrentId == torrentId)
.ToListAsync(); .ToListAsync();
dataContext.Downloads.RemoveRange(downloads); dataContext.Downloads.RemoveRange(downloads);
@ -253,7 +253,7 @@ public class DownloadData(DataContext dataContext)
public async Task Reset(Guid downloadId) public async Task Reset(Guid downloadId)
{ {
var dbDownload = await dataContext.Downloads var dbDownload = await dataContext.Downloads
.FirstOrDefaultAsync(m => m.DownloadId == downloadId) .FirstOrDefaultAsync(m => m.DownloadId == downloadId)
?? throw new($"Cannot find download with ID {downloadId}"); ?? throw new($"Cannot find download with ID {downloadId}");
dbDownload.RetryCount = 0; dbDownload.RetryCount = 0;

View file

@ -67,11 +67,14 @@ public class SettingData(DataContext dataContext, ILogger<SettingData> logger)
{ {
var dbSettings = await dataContext.Settings.AsNoTracking().ToListAsync(); var dbSettings = await dataContext.Settings.AsNoTracking().ToListAsync();
var expectedSettings = GetSettings(Get, null).Where(m => m.Type != "Object").Select(m => new Setting var expectedSettings = GetSettings(Get, null)
{ .Where(m => m.Type != "Object")
SettingId = m.Key, .Select(m => new Setting
Value = m.Value?.ToString() {
}).ToList(); SettingId = m.Key,
Value = m.Value?.ToString()
})
.ToList();
var newSettings = expectedSettings.Where(m => dbSettings.All(p => p.SettingId != m.SettingId)).ToList(); var newSettings = expectedSettings.Where(m => dbSettings.All(p => p.SettingId != m.SettingId)).ToList();

View file

@ -17,9 +17,9 @@ public class TorrentData(DataContext dataContext) : ITorrentData
try try
{ {
_torrentCache ??= await dataContext.Torrents _torrentCache ??= await dataContext.Torrents
.AsNoTracking() .AsNoTracking()
.Include(m => m.Downloads) .Include(m => m.Downloads)
.ToListAsync(); .ToListAsync();
return [.. _torrentCache.OrderBy(m => m.Priority ?? 9999).ThenBy(m => m.Added)]; return [.. _torrentCache.OrderBy(m => m.Priority ?? 9999).ThenBy(m => m.Added)];
} }
@ -32,9 +32,9 @@ public class TorrentData(DataContext dataContext) : ITorrentData
public async Task<Torrent?> GetById(Guid torrentId) public async Task<Torrent?> GetById(Guid torrentId)
{ {
var dbTorrent = await dataContext.Torrents var dbTorrent = await dataContext.Torrents
.AsNoTracking() .AsNoTracking()
.Include(m => m.Downloads) .Include(m => m.Downloads)
.FirstOrDefaultAsync(m => m.TorrentId == torrentId); .FirstOrDefaultAsync(m => m.TorrentId == torrentId);
if (dbTorrent == null) if (dbTorrent == null)
{ {
@ -54,9 +54,9 @@ public class TorrentData(DataContext dataContext) : ITorrentData
hash = hash.ToLower(); hash = hash.ToLower();
var dbTorrent = await dataContext.Torrents var dbTorrent = await dataContext.Torrents
.AsNoTracking() .AsNoTracking()
.Include(m => m.Downloads) .Include(m => m.Downloads)
.FirstOrDefaultAsync(m => m.Hash == hash); .FirstOrDefaultAsync(m => m.Hash == hash);
if (dbTorrent == null) if (dbTorrent == null)
{ {

View file

@ -14,5 +14,5 @@ public enum DownloadClient
Symlink, Symlink,
[Description("Synology DownloadStation")] [Description("Synology DownloadStation")]
DownloadStation, DownloadStation
} }

View file

@ -8,5 +8,5 @@ public enum TorrentHostDownloadAction
DownloadAll = 0, DownloadAll = 0,
[Description("Don't download any files to host")] [Description("Don't download any files to host")]
DownloadNone = 1, DownloadNone = 1
} }

View file

@ -44,17 +44,19 @@ public class Download
} }
/// <summary> /// <summary>
/// Used to create <see cref="Download"/>s /// Used to create <see cref="Download" />s
/// </summary> /// </summary>
public class DownloadInfo public class DownloadInfo
{ {
/// <summary> /// <summary>
/// The name of the file. Should not include directory. /// 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. /// If the filename is not known, set tn null and `GetFileName` will be called with the unrestricted link.
/// </summary> /// </summary>
public required String? FileName; public required String? FileName;
/// <summary> /// <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 /// 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> /// </summary>
public required String RestrictedLink; public required String RestrictedLink;
} }

View file

@ -17,7 +17,7 @@ public class Torrent
public TorrentDownloadAction DownloadAction { get; set; } public TorrentDownloadAction DownloadAction { get; set; }
public TorrentFinishedAction FinishedAction { get; set; } public TorrentFinishedAction FinishedAction { get; set; }
public Int32 FinishedActionDelay { get; set; } public Int32 FinishedActionDelay { get; set; }
public TorrentHostDownloadAction HostDownloadAction { get; set; } public TorrentHostDownloadAction HostDownloadAction { get; set; }
public Int32 DownloadMinSize { get; set; } public Int32 DownloadMinSize { get; set; }
public String? IncludeRegex { get; set; } public String? IncludeRegex { get; set; }

View file

@ -1,5 +1,5 @@
using RdtClient.Data.Enums; using System.ComponentModel;
using System.ComponentModel; using RdtClient.Data.Enums;
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
@ -155,6 +155,7 @@ http://127.0.0.1:6800/jsonrpc.")]
[DisplayName("Synology DownloadStation Username")] [DisplayName("Synology DownloadStation Username")]
[Description("The username to use when connecting to the Synology DownloadStation.")] [Description("The username to use when connecting to the Synology DownloadStation.")]
public String? DownloadStationUsername { get; set; } = null; public String? DownloadStationUsername { get; set; } = null;
[DisplayName("Synology DownloadStation Password")] [DisplayName("Synology DownloadStation Password")]
[Description("The password to use when connecting to the Synology DownloadStation.")] [Description("The password to use when connecting to the Synology DownloadStation.")]
public String? DownloadStationPassword { get; set; } = null; public String? DownloadStationPassword { get; set; } = null;
@ -202,7 +203,7 @@ or
public String ApiKey { get; set; } = ""; public String ApiKey { get; set; } = "";
/// <summary> /// <summary>
/// API hostname to use <b>for Real Debrid only</b> /// API hostname to use <b>for Real Debrid only</b>
/// </summary> /// </summary>
[DisplayName("API Hostname (RD 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.")] [Description("Use this instead of the normal hostname for Real Debrid API requests. Only used by Real Debrid. Leave blank to use default.")]

View file

@ -0,0 +1,14 @@
using System.Diagnostics.CodeAnalysis;
[assembly:
SuppressMessage("Usage",
"xUnit1045:Avoid using TheoryData type arguments that might not be serializable",
Justification = "It is serializable.",
Scope = "NamespaceAndDescendants",
Target = "N:RdtClient.Service.Test")]
[assembly:
SuppressMessage("Performance",
"SYSLIB1045:Convert to 'GeneratedRegexAttribute'.",
Justification = "We don't care for unit tests.",
Scope = "NamespaceAndDescendants",
Target = "N:RdtClient.Service.Test")]

View file

@ -28,10 +28,11 @@ public class DownloadableFileFilterTest
} }
[Theory] [Theory]
// downloadMinSize is in MB, fileSize is in B // downloadMinSize is in MB, fileSize is in B
[InlineData(100, 20 * 1024 * 1024)] [InlineData(100, 20 * 1024 * 1024)]
[InlineData(2, 2 * 1024 * 1024)] [InlineData(2, 2 * 1024 * 1024)]
[InlineData(2, 2 * (1000 * 1000 + 1))] // mostly to show we use 1024 not 1000 for conversion [InlineData(2, 2 * ((1000 * 1000) + 1))] // mostly to show we use 1024 not 1000 for conversion
public void IsDownloadable_WhenDownloadMinSizeSpecified_AndDownloadBelowSize_ReturnsFalse(Int32 downloadMinSize, Int64 fileSize) public void IsDownloadable_WhenDownloadMinSizeSpecified_AndDownloadBelowSize_ReturnsFalse(Int32 downloadMinSize, Int64 fileSize)
{ {
// Arrange // Arrange
@ -54,7 +55,7 @@ public class DownloadableFileFilterTest
[Theory] [Theory]
[InlineData(100, 110 * 1024 * 1024)] [InlineData(100, 110 * 1024 * 1024)]
[InlineData(2, 2 * 1024 * 1024 + 1)] [InlineData(2, (2 * 1024 * 1024) + 1)]
public void IsDownloadable_WhenDownloadMinSizeSpecified_AndDownloadAboveSize_ReturnsTrue(Int32 downloadMinSize, Int64 fileSize) public void IsDownloadable_WhenDownloadMinSizeSpecified_AndDownloadAboveSize_ReturnsTrue(Int32 downloadMinSize, Int64 fileSize)
{ {
// Arrange // Arrange
@ -202,9 +203,8 @@ public class DownloadableFileFilterTest
} }
[Theory] [Theory]
[InlineData(10, "file", 10 * 1024 * 1024 + 1, "no-match.txt")] [InlineData(10, "file", (10 * 1024 * 1024) + 1, "no-match.txt")]
public void IsDownloadable_WhenBothDownloadMinSizeAndIncludeRegexSpecified_AndDownloadAboveSizeAndDoesNotMatchRegex_ReturnsFalse( public void IsDownloadable_WhenBothDownloadMinSizeAndIncludeRegexSpecified_AndDownloadAboveSizeAndDoesNotMatchRegex_ReturnsFalse(Int32 minSize,
Int32 minSize,
String includeRegex, String includeRegex,
Int64 fileSize, Int64 fileSize,
String filePath) String filePath)
@ -229,9 +229,8 @@ public class DownloadableFileFilterTest
} }
[Theory] [Theory]
[InlineData(10, "file", 10 * 1024 * 1024 - 1, "file.txt")] [InlineData(10, "file", (10 * 1024 * 1024) - 1, "file.txt")]
public void IsDownloadable_WhenBothDownloadMinSizeAndIncludeRegexSpecified_AndDownloadBelowSizeAndMatchesRegex_ReturnsFalse( public void IsDownloadable_WhenBothDownloadMinSizeAndIncludeRegexSpecified_AndDownloadBelowSizeAndMatchesRegex_ReturnsFalse(Int32 minSize,
Int32 minSize,
String includeRegex, String includeRegex,
Int64 fileSize, Int64 fileSize,
String filePath) String filePath)
@ -254,6 +253,7 @@ public class DownloadableFileFilterTest
// Assert // Assert
Assert.False(result); Assert.False(result);
} }
private class Mocks private class Mocks
{ {
public readonly Mock<ILogger<DownloadableFileFilter>> LoggerMock = new(); public readonly Mock<ILogger<DownloadableFileFilter>> LoggerMock = new();

View file

@ -7,7 +7,7 @@ using Synology.Api.Client.Apis.DownloadStation.Task.Models;
namespace RdtClient.Service.Test.Services.Downloaders; namespace RdtClient.Service.Test.Services.Downloaders;
class Mocks internal class Mocks
{ {
public readonly String Gid; public readonly String Gid;
public readonly Mock<ISynologyClient> SynologyClientMock = new(); public readonly Mock<ISynologyClient> SynologyClientMock = new();
@ -22,7 +22,7 @@ class Mocks
} }
} }
class FakeDelayProvider : IDelayProvider internal class FakeDelayProvider : IDelayProvider
{ {
public Task Delay(Int32 milliseconds) public Task Delay(Int32 milliseconds)
{ {

View file

@ -1,14 +1,18 @@
using System.Web;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MonoTorrent.BEncoding;
using Moq; using Moq;
using RdtClient.Service.Services; using RdtClient.Service.Services;
using MonoTorrent.BEncoding;
namespace RdtClient.Service.Test.Services; namespace RdtClient.Service.Test.Services;
public class EnricherTest : IDisposable public class EnricherTest : IDisposable
{ {
private readonly MockRepository _mockRepository; private const String TestMagnetLink =
"magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567&dn=TestFile&tr=http%3A%2F%2Ftracker1.com%2Fannounce&tr=http%3A%2F%2Ftracker2.com%2Fannounce";
private readonly Mock<ILogger<Enricher>> _loggerMock; private readonly Mock<ILogger<Enricher>> _loggerMock;
private readonly MockRepository _mockRepository;
private readonly Mock<ITrackerListGrabber> _trackerListGrabberMock; private readonly Mock<ITrackerListGrabber> _trackerListGrabberMock;
public EnricherTest() public EnricherTest()
@ -23,9 +27,6 @@ public class EnricherTest : IDisposable
_mockRepository.VerifyAll(); _mockRepository.VerifyAll();
} }
private const String TestMagnetLink =
"magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567&dn=TestFile&tr=http%3A%2F%2Ftracker1.com%2Fannounce&tr=http%3A%2F%2Ftracker2.com%2Fannounce";
// Helper methods for creating BEncodedDictionary objects for torrents // Helper methods for creating BEncodedDictionary objects for torrents
private static BEncodedDictionary CreateStandardTorrentDict(String announceUrl, List<String>? announceListTier = null) private static BEncodedDictionary CreateStandardTorrentDict(String announceUrl, List<String>? announceListTier = null)
{ {
@ -216,7 +217,7 @@ public class EnricherTest : IDisposable
var result = await enricher.EnrichMagnetLink(magnetLink); var result = await enricher.EnrichMagnetLink(magnetLink);
// Assert // Assert
var queryParams = System.Web.HttpUtility.ParseQueryString(new Uri(result).Query); var queryParams = HttpUtility.ParseQueryString(new Uri(result).Query);
Assert.Equal("urn:btih:HASH", queryParams["xt"]); Assert.Equal("urn:btih:HASH", queryParams["xt"]);
Assert.Equal("MyFile", queryParams["dn"]); Assert.Equal("MyFile", queryParams["dn"]);
} }
@ -239,7 +240,7 @@ public class EnricherTest : IDisposable
var result = await enricher.EnrichMagnetLink(magnetLink); var result = await enricher.EnrichMagnetLink(magnetLink);
// Assert // Assert
var queryParams = System.Web.HttpUtility.ParseQueryString(new Uri(result).Query); var queryParams = HttpUtility.ParseQueryString(new Uri(result).Query);
var trValues = queryParams.GetValues("tr")?.ToList() ?? new List<String>(); var trValues = queryParams.GetValues("tr")?.ToList() ?? new List<String>();
Assert.Contains("udp://existing", trValues); Assert.Contains("udp://existing", trValues);

View file

@ -522,8 +522,8 @@ public class AllDebridTorrentClientTest
{ {
public readonly Mock<IAllDebridNetClientFactory> AllDebridClientFactoryMock; public readonly Mock<IAllDebridNetClientFactory> AllDebridClientFactoryMock;
public readonly Mock<IAllDebridNETClient> AllDebridClientMock; public readonly Mock<IAllDebridNETClient> AllDebridClientMock;
public readonly Mock<ILogger<AllDebridTorrentClient>> LoggerMock;
public readonly Mock<IDownloadableFileFilter> FileFilterMock; public readonly Mock<IDownloadableFileFilter> FileFilterMock;
public readonly Mock<ILogger<AllDebridTorrentClient>> LoggerMock;
public Mocks() public Mocks()
{ {

View file

@ -13,14 +13,14 @@ using TorrentsService = RdtClient.Service.Services.Torrents;
namespace RdtClient.Service.Test.Services; namespace RdtClient.Service.Test.Services;
class Mocks internal class Mocks
{ {
public readonly Mock<IDownloads> DownloadsMock;
public readonly Mock<IEnricher> EnricherMock;
public readonly Mock<IProcessFactory> ProcessFactoryMock; public readonly Mock<IProcessFactory> ProcessFactoryMock;
public readonly Mock<IProcess> ProcessMock; public readonly Mock<IProcess> ProcessMock;
public readonly Mock<ILogger<TorrentsService>> TorrentsLoggerMock;
public readonly Mock<IDownloads> DownloadsMock;
public readonly Mock<ITorrentData> TorrentDataMock; public readonly Mock<ITorrentData> TorrentDataMock;
public readonly Mock<IEnricher> EnricherMock; public readonly Mock<ILogger<TorrentsService>> TorrentsLoggerMock;
public Mocks() public Mocks()
{ {
@ -63,8 +63,7 @@ public class TorrentsTest
return new() return new()
{ {
{ {
torrent, torrent, downloads
downloads
} }
}; };
} }
@ -96,7 +95,7 @@ public class TorrentsTest
{ {
{ {
filePath, new("Test file") filePath, new("Test file")
}, }
}); });
var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object, var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object,
@ -162,7 +161,7 @@ public class TorrentsTest
{ {
{ {
filePath, new("Test file") filePath, new("Test file")
}, }
}); });
var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object, var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object,
@ -210,7 +209,7 @@ public class TorrentsTest
{ {
{ {
filePath, new("Test file") filePath, new("Test file")
}, }
}); });
var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object, var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object,
@ -277,7 +276,7 @@ public class TorrentsTest
{ {
{ {
filePath, new("Test file") filePath, new("Test file")
}, }
}); });
var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object, var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object,

View file

@ -10,8 +10,8 @@ namespace RdtClient.Service.BackgroundServices;
public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider serviceProvider) : BackgroundService public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider serviceProvider) : BackgroundService
{ {
private Boolean _isPausedForLowDiskSpace;
private static DiskSpaceStatus? _lastStatus; private static DiskSpaceStatus? _lastStatus;
private Boolean _isPausedForLowDiskSpace;
public static DiskSpaceStatus? GetCurrentStatus() public static DiskSpaceStatus? GetCurrentStatus()
{ {
@ -39,10 +39,12 @@ public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider
if (minimumFreeSpaceGB <= 0) if (minimumFreeSpaceGB <= 0)
{ {
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
continue; continue;
} }
var intervalMinutes = Settings.Get.DownloadClient.DiskSpaceCheckIntervalMinutes; var intervalMinutes = Settings.Get.DownloadClient.DiskSpaceCheckIntervalMinutes;
if (intervalMinutes < 1) if (intervalMinutes < 1)
{ {
intervalMinutes = 1; intervalMinutes = 1;
@ -55,6 +57,7 @@ public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider
{ {
logger.LogWarning($"Download path does not exist: {downloadPath}"); logger.LogWarning($"Download path does not exist: {downloadPath}");
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
continue; continue;
} }
@ -67,13 +70,14 @@ public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider
} }
var shouldPause = availableSpaceGB > 0 && availableSpaceGB < minimumFreeSpaceGB; var shouldPause = availableSpaceGB > 0 && availableSpaceGB < minimumFreeSpaceGB;
var shouldResume = availableSpaceGB >= (minimumFreeSpaceGB * 2); var shouldResume = availableSpaceGB >= minimumFreeSpaceGB * 2;
if (shouldPause && !_isPausedForLowDiskSpace) if (shouldPause && !_isPausedForLowDiskSpace)
{ {
logger.LogWarning($"Pausing Bezzad downloads: {availableSpaceGB} GB available, threshold is {minimumFreeSpaceGB} GB"); logger.LogWarning($"Pausing Bezzad downloads: {availableSpaceGB} GB available, threshold is {minimumFreeSpaceGB} GB");
var pausedCount = 0; var pausedCount = 0;
foreach (var download in TorrentRunner.ActiveDownloadClients) foreach (var download in TorrentRunner.ActiveDownloadClients)
{ {
if (download.Value.Type == DownloadClient.Bezzad) if (download.Value.Type == DownloadClient.Bezzad)
@ -96,6 +100,7 @@ public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider
ThresholdGB = minimumFreeSpaceGB, ThresholdGB = minimumFreeSpaceGB,
LastCheckTime = DateTimeOffset.UtcNow LastCheckTime = DateTimeOffset.UtcNow
}; };
_lastStatus = status; _lastStatus = status;
await remoteService.UpdateDiskSpaceStatus(status); await remoteService.UpdateDiskSpaceStatus(status);
} }
@ -110,6 +115,7 @@ public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider
ThresholdGB = minimumFreeSpaceGB, ThresholdGB = minimumFreeSpaceGB,
LastCheckTime = DateTimeOffset.UtcNow LastCheckTime = DateTimeOffset.UtcNow
}; };
_lastStatus = status; _lastStatus = status;
await remoteService.UpdateDiskSpaceStatus(status); await remoteService.UpdateDiskSpaceStatus(status);
} }
@ -118,6 +124,7 @@ public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider
logger.LogInformation($"Resuming Bezzad downloads: {availableSpaceGB} GB available, resume threshold is {minimumFreeSpaceGB * 2} GB"); logger.LogInformation($"Resuming Bezzad downloads: {availableSpaceGB} GB available, resume threshold is {minimumFreeSpaceGB * 2} GB");
var resumedCount = 0; var resumedCount = 0;
foreach (var download in TorrentRunner.ActiveDownloadClients) foreach (var download in TorrentRunner.ActiveDownloadClients)
{ {
if (download.Value.Type == DownloadClient.Bezzad) if (download.Value.Type == DownloadClient.Bezzad)
@ -140,6 +147,7 @@ public class DiskSpaceMonitor(ILogger<DiskSpaceMonitor> logger, IServiceProvider
ThresholdGB = minimumFreeSpaceGB, ThresholdGB = minimumFreeSpaceGB,
LastCheckTime = DateTimeOffset.UtcNow LastCheckTime = DateTimeOffset.UtcNow
}; };
_lastStatus = status; _lastStatus = status;
await remoteService.UpdateDiskSpaceStatus(status); await remoteService.UpdateDiskSpaceStatus(status);
} }

View file

@ -6,7 +6,6 @@ using Microsoft.Extensions.Logging;
using RdtClient.Data.Data; using RdtClient.Data.Data;
using RdtClient.Service.Services; using RdtClient.Service.Services;
namespace RdtClient.Service.BackgroundServices; namespace RdtClient.Service.BackgroundServices;
public class Startup(IServiceProvider serviceProvider) : IHostedService public class Startup(IServiceProvider serviceProvider) : IHostedService
@ -32,5 +31,8 @@ public class Startup(IServiceProvider serviceProvider) : IHostedService
Ready = true; Ready = true;
} }
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
} }

View file

@ -7,13 +7,12 @@ namespace RdtClient.Service.BackgroundServices;
public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
{ {
private static readonly List<String> KnownGhsaIds = [];
public static String? CurrentVersion { get; private set; } public static String? CurrentVersion { get; private set; }
public static String? LatestVersion { get; private set; } public static String? LatestVersion { get; private set; }
public static Boolean? IsInsecure { get; private set; } public static Boolean? IsInsecure { get; private set; }
private static readonly List<String> KnownGhsaIds = [];
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
while (!Startup.Ready) while (!Startup.Ready)
@ -45,6 +44,7 @@ public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
if (latestRelease == null) if (latestRelease == null)
{ {
logger.LogWarning($"Unable to find latest version on GitHub"); logger.LogWarning($"Unable to find latest version on GitHub");
return; return;
} }
@ -62,6 +62,7 @@ public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
if (unseenGhsaIds == null) if (unseenGhsaIds == null)
{ {
logger.LogWarning($"Unable to find security advisories on GitHub"); logger.LogWarning($"Unable to find security advisories on GitHub");
return; return;
} }
@ -80,11 +81,11 @@ public class UpdateChecker(ILogger<UpdateChecker> logger) : BackgroundService
private static async Task<T?> GitHubRequest<T>(String endpoint, CancellationToken cancellationToken) private static async Task<T?> GitHubRequest<T>(String endpoint, CancellationToken cancellationToken)
{ {
var httpClient = new HttpClient(); var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.UserAgent.Add(new("RdtClient", CurrentVersion)); httpClient.DefaultRequestHeaders.UserAgent.Add(new("RdtClient", CurrentVersion));
var response = await httpClient.GetStringAsync($"https://api.github.com{endpoint}", cancellationToken); var response = await httpClient.GetStringAsync($"https://api.github.com{endpoint}", cancellationToken);
return JsonConvert.DeserializeObject<T>(response); return JsonConvert.DeserializeObject<T>(response);
} }
} }

View file

@ -147,6 +147,7 @@ public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProv
fileInfo.Name, fileInfo.Name,
errorStorePath); errorStorePath);
} }
File.Move(torrentFile, processedPath); File.Move(torrentFile, processedPath);
} }
} }
@ -169,6 +170,7 @@ public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProv
{ {
return true; return true;
} }
return false; return false;
} }
} }

View file

@ -61,9 +61,10 @@ public static class DiConfig
var retryPolicy = HttpPolicyExtensions var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError() .HandleTransientHttpError()
.OrResult(r => r.StatusCode == HttpStatusCode.TooManyRequests) .OrResult(r => r.StatusCode == HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(retryCount: 5, sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))); .WaitAndRetryAsync(5, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));
services.AddHttpClient(); services.AddHttpClient();
services.ConfigureHttpClientDefaults(builder => services.ConfigureHttpClientDefaults(builder =>
{ {
builder.ConfigureHttpClient(httpClient => builder.ConfigureHttpClient(httpClient =>

View file

@ -5,5 +5,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "Bug in Serilog", Scope = "NamespaceAndDescendants", Target = "N:RdtClient.Service")] [assembly:
SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "Bug in Serilog", Scope = "NamespaceAndDescendants", Target = "N:RdtClient.Service")]
[assembly: SuppressMessage("Style", "IDE0305:Simplify collection initialization", Justification = "Code style", Scope = "NamespaceAndDescendants", Target = "N:RdtClient.Service")] [assembly: SuppressMessage("Style", "IDE0305:Simplify collection initialization", Justification = "Code style", Scope = "NamespaceAndDescendants", Target = "N:RdtClient.Service")]

View file

@ -1,6 +1,6 @@
using System.IO.Abstractions; using System.IO.Abstractions;
using RdtClient.Data.Models.Data;
using System.Web; using System.Web;
using RdtClient.Data.Models.Data;
namespace RdtClient.Service.Helpers; namespace RdtClient.Service.Helpers;

View file

@ -85,6 +85,7 @@ public static class FileHelper
{ {
var stringBuilder = new StringBuilder(); var stringBuilder = new StringBuilder();
GetDirectoryContents(path, stringBuilder, ""); GetDirectoryContents(path, stringBuilder, "");
return stringBuilder.ToString(); return stringBuilder.ToString();
} }
@ -93,6 +94,7 @@ public static class FileHelper
var directoryInfo = new DirectoryInfo(path); var directoryInfo = new DirectoryInfo(path);
var directories = directoryInfo.GetDirectories(); var directories = directoryInfo.GetDirectories();
foreach (var directory in directories) foreach (var directory in directories)
{ {
stringBuilder.AppendLine($"{indent}{directory.Name}"); stringBuilder.AppendLine($"{indent}{directory.Name}");
@ -100,6 +102,7 @@ public static class FileHelper
} }
var files = directoryInfo.GetFiles(); var files = directoryInfo.GetFiles();
foreach (var file in files) foreach (var file in files)
{ {
stringBuilder.AppendLine($"{indent}{file.Name}"); stringBuilder.AppendLine($"{indent}{file.Name}");
@ -112,13 +115,16 @@ public static class FileHelper
{ {
return 0; return 0;
} }
try try
{ {
if (!Directory.Exists(path)) if (!Directory.Exists(path))
{ {
return 0; return 0;
} }
var driveInfo = new DriveInfo(path); var driveInfo = new DriveInfo(path);
return driveInfo.AvailableFreeSpace / (1024 * 1024 * 1024); return driveInfo.AvailableFreeSpace / (1024 * 1024 * 1024);
} }
catch catch

View file

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Newtonsoft.Json;
namespace RdtClient.Service.Helpers; namespace RdtClient.Service.Helpers;
@ -9,15 +10,18 @@ public class JsonModelBinder : IModelBinder
ArgumentNullException.ThrowIfNull(bindingContext); ArgumentNullException.ThrowIfNull(bindingContext);
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult != ValueProviderResult.None) if (valueProviderResult != ValueProviderResult.None)
{ {
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
var valueAsString = valueProviderResult.FirstValue ?? ""; var valueAsString = valueProviderResult.FirstValue ?? "";
var result = Newtonsoft.Json.JsonConvert.DeserializeObject(valueAsString, bindingContext.ModelType); var result = JsonConvert.DeserializeObject(valueAsString, bindingContext.ModelType);
if (result != null) if (result != null)
{ {
bindingContext.Result = ModelBindingResult.Success(result); bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View file

@ -16,7 +16,7 @@ public static class Logger
fileName = HttpUtility.UrlDecode(fileName); fileName = HttpUtility.UrlDecode(fileName);
} }
var done = (Int32)((Double)download.BytesDone / download.BytesTotal * 100); var done = (Int32)(((Double)download.BytesDone / download.BytesTotal) * 100);
if (done < 0) if (done < 0)
{ {

View file

@ -6,7 +6,6 @@ namespace RdtClient.Service.Middleware;
public class AuthSettingRequirement : IAuthorizationRequirement public class AuthSettingRequirement : IAuthorizationRequirement
{ {
} }
public class AuthSettingHandler : AuthorizationHandler<AuthSettingRequirement> public class AuthSettingHandler : AuthorizationHandler<AuthSettingRequirement>

View file

@ -6,7 +6,7 @@ namespace RdtClient.Service.Middleware;
public class AuthorizeMiddleware(RequestDelegate next) public class AuthorizeMiddleware(RequestDelegate next)
{ {
/// <summary> /// <summary>
/// Return a 403 instead of a 401, it's quirk that QBittorrent has. /// Return a 403 instead of a 401, it's quirk that QBittorrent has.
/// </summary> /// </summary>
/// <param name="context"></param> /// <param name="context"></param>
/// <returns></returns> /// <returns></returns>
@ -14,9 +14,9 @@ public class AuthorizeMiddleware(RequestDelegate next)
{ {
await next(context); await next(context);
if (context.Response.StatusCode == (Int32) HttpStatusCode.Unauthorized) if (context.Response.StatusCode == (Int32)HttpStatusCode.Unauthorized)
{ {
context.Response.StatusCode = (Int32) HttpStatusCode.Forbidden; context.Response.StatusCode = (Int32)HttpStatusCode.Forbidden;
} }
} }
} }

View file

@ -5,6 +5,8 @@ namespace RdtClient.Service.Middleware;
public partial class BaseHrefMiddleware(RequestDelegate next, String basePath) public partial class BaseHrefMiddleware(RequestDelegate next, String basePath)
{ {
private readonly String _basePath = $"/{basePath.TrimStart('/').TrimEnd('/')}/";
[GeneratedRegex(@"<base href=""/""")] [GeneratedRegex(@"<base href=""/""")]
private partial Regex BodyRegex(); private partial Regex BodyRegex();
@ -14,8 +16,6 @@ public partial class BaseHrefMiddleware(RequestDelegate next, String basePath)
[GeneratedRegex("(<link.*?href=\")(.*?)(\".*?>)")] [GeneratedRegex("(<link.*?href=\")(.*?)(\".*?>)")]
private partial Regex LinkRegex(); private partial Regex LinkRegex();
private readonly String _basePath = $"/{basePath.TrimStart('/').TrimEnd('/')}/";
public async Task InvokeAsync(HttpContext context) public async Task InvokeAsync(HttpContext context)
{ {
var originalBody = context.Response.Body; var originalBody = context.Response.Body;

View file

@ -13,10 +13,11 @@ public static class ExceptionMiddlewareExtensions
{ {
appError.Run(async context => appError.Run(async context =>
{ {
context.Response.StatusCode = (Int32) HttpStatusCode.InternalServerError; context.Response.StatusCode = (Int32)HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/json"; context.Response.ContentType = "application/json";
var contextFeature = context.Features.Get<IExceptionHandlerFeature>(); var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
if (contextFeature != null) if (contextFeature != null)
{ {
await context.Response.WriteAsync(contextFeature.Error.Message); await context.Response.WriteAsync(contextFeature.Error.Message);

View file

@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Http; using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Text;
namespace RdtClient.Service.Middleware; namespace RdtClient.Service.Middleware;
@ -43,7 +43,7 @@ public class RequestLoggingMiddleware(RequestDelegate next, ILoggerFactory logge
{ {
request.EnableBuffering(); request.EnableBuffering();
using var reader = new StreamReader(request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true); using var reader = new StreamReader(request.Body, Encoding.UTF8, false, leaveOpen: true);
var body = await reader.ReadToEndAsync(); var body = await reader.ReadToEndAsync();
request.Body.Position = 0; request.Body.Position = 0;

View file

@ -117,6 +117,7 @@ public class DownloadClient(Download download, Torrent torrent, String destinati
{ {
return; return;
} }
await Downloader.Cancel(); await Downloader.Cancel();
} }
@ -126,6 +127,7 @@ public class DownloadClient(Download download, Torrent torrent, String destinati
{ {
return; return;
} }
await Downloader.Pause(); await Downloader.Pause();
} }
@ -135,6 +137,7 @@ public class DownloadClient(Download download, Torrent torrent, String destinati
{ {
return; return;
} }
await Downloader.Resume(); await Downloader.Resume();
} }

View file

@ -5,17 +5,14 @@ namespace RdtClient.Service.Services.Downloaders;
public class Aria2cDownloader : IDownloader public class Aria2cDownloader : IDownloader
{ {
public event EventHandler<DownloadCompleteEventArgs>? DownloadComplete;
public event EventHandler<DownloadProgressEventArgs>? DownloadProgress;
private const Int32 RetryCount = 5; private const Int32 RetryCount = 5;
private readonly Aria2NetClient _aria2NetClient; private readonly Aria2NetClient _aria2NetClient;
private readonly String _filePath;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly String _uri;
private readonly String _filePath;
private readonly String _remotePath; private readonly String _remotePath;
private readonly String _uri;
private String? _gid; private String? _gid;
@ -50,6 +47,9 @@ public class Aria2cDownloader : IDownloader
_aria2NetClient = new(Settings.Get.DownloadClient.Aria2cUrl, Settings.Get.DownloadClient.Aria2cSecret, httpClient, 10); _aria2NetClient = new(Settings.Get.DownloadClient.Aria2cUrl, Settings.Get.DownloadClient.Aria2cSecret, httpClient, 10);
} }
public event EventHandler<DownloadCompleteEventArgs>? DownloadComplete;
public event EventHandler<DownloadProgressEventArgs>? DownloadProgress;
public async Task<String> Download() public async Task<String> Download()
{ {
var path = Path.GetDirectoryName(_remotePath) ?? throw new($"Invalid file path {_filePath}"); var path = Path.GetDirectoryName(_remotePath) ?? throw new($"Invalid file path {_filePath}");
@ -68,7 +68,8 @@ public class Aria2cDownloader : IDownloader
} }
var retryCount = 0; var retryCount = 0;
while(true)
while (true)
{ {
try try
{ {
@ -177,20 +178,25 @@ public class Aria2cDownloader : IDownloader
if (download == null) if (download == null)
{ {
DownloadComplete?.Invoke(this, new() DownloadComplete?.Invoke(this,
{ new()
Error = $"Download was not found in Aria2" {
}); Error = $"Download was not found in Aria2"
});
return; return;
} }
if (!String.IsNullOrWhiteSpace(download.ErrorMessage) || download.Status == "error") if (!String.IsNullOrWhiteSpace(download.ErrorMessage) || download.Status == "error")
{ {
await Remove(); await Remove();
DownloadComplete?.Invoke(this, new()
{ DownloadComplete?.Invoke(this,
Error = $"{download.ErrorCode}: {download.ErrorMessage}" new()
}); {
Error = $"{download.ErrorCode}: {download.ErrorMessage}"
});
return; return;
} }
@ -201,42 +207,49 @@ public class Aria2cDownloader : IDownloader
await Remove(); await Remove();
var retryCount = 0; var retryCount = 0;
while (true) while (true)
{ {
DownloadProgress?.Invoke(this, new() DownloadProgress?.Invoke(this,
{ new()
BytesDone = download.CompletedLength, {
BytesTotal = download.TotalLength, BytesDone = download.CompletedLength,
Speed = download.DownloadSpeed BytesTotal = download.TotalLength,
}); Speed = download.DownloadSpeed
});
if (retryCount >= 10) if (retryCount >= 10)
{ {
DownloadComplete?.Invoke(this, new() DownloadComplete?.Invoke(this,
{ new()
Error = $"File not found at {_filePath} (on aria2 {_remotePath})" {
}); Error = $"File not found at {_filePath} (on aria2 {_remotePath})"
});
break; break;
} }
if (File.Exists(_filePath)) if (File.Exists(_filePath))
{ {
DownloadComplete?.Invoke(this, new()); DownloadComplete?.Invoke(this, new());
break; break;
} }
await Task.Delay(1000 * retryCount); await Task.Delay(1000 * retryCount);
retryCount++; retryCount++;
} }
return; return;
} }
DownloadProgress?.Invoke(this, new() DownloadProgress?.Invoke(this,
{ new()
BytesDone = download.CompletedLength, {
BytesTotal = download.TotalLength, BytesDone = download.CompletedLength,
Speed = download.DownloadSpeed BytesTotal = download.TotalLength,
}); Speed = download.DownloadSpeed
});
} }
private async Task Remove() private async Task Remove()

View file

@ -6,16 +6,14 @@ namespace RdtClient.Service.Services.Downloaders;
public class BezzadDownloader : IDownloader public class BezzadDownloader : IDownloader
{ {
public event EventHandler<DownloadCompleteEventArgs>? DownloadComplete;
public event EventHandler<DownloadProgressEventArgs>? DownloadProgress;
private readonly DownloadService _downloadService;
private readonly DownloadConfiguration _downloadConfiguration; private readonly DownloadConfiguration _downloadConfiguration;
private readonly DownloadService _downloadService;
private readonly String _filePath; private readonly String _filePath;
private readonly String _uri;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly String _uri;
private Boolean _finished; private Boolean _finished;
@ -65,12 +63,12 @@ public class BezzadDownloader : IDownloader
} }
DownloadProgress.Invoke(this, DownloadProgress.Invoke(this,
new() new()
{ {
Speed = (Int64)args.BytesPerSecondSpeed, Speed = (Int64)args.BytesPerSecondSpeed,
BytesDone = args.ReceivedBytesSize, BytesDone = args.ReceivedBytesSize,
BytesTotal = args.TotalBytesToReceive BytesTotal = args.TotalBytesToReceive
}); });
}; };
_downloadService.DownloadFileCompleted += (_, args) => _downloadService.DownloadFileCompleted += (_, args) =>
@ -96,6 +94,9 @@ public class BezzadDownloader : IDownloader
}; };
} }
public event EventHandler<DownloadCompleteEventArgs>? DownloadComplete;
public event EventHandler<DownloadProgressEventArgs>? DownloadProgress;
public Task<String> Download() public Task<String> Download()
{ {
_logger.Debug($"Starting download of {_uri}, writing to path: {_filePath}"); _logger.Debug($"Starting download of {_uri}, writing to path: {_filePath}");
@ -123,6 +124,7 @@ public class BezzadDownloader : IDownloader
{ {
_logger.Debug($"Pausing download {_uri}"); _logger.Debug($"Pausing download {_uri}");
_downloadService.Pause(); _downloadService.Pause();
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -130,6 +132,7 @@ public class BezzadDownloader : IDownloader
{ {
_logger.Debug($"Resuming download {_uri}"); _logger.Debug($"Resuming download {_uri}");
_downloadService.Resume(); _downloadService.Resume();
return Task.CompletedTask; return Task.CompletedTask;
} }

View file

@ -5,7 +5,7 @@ using Synology.Api.Client.Apis.DownloadStation.Task.Models;
namespace RdtClient.Service.Services.Downloaders; namespace RdtClient.Service.Services.Downloaders;
class DelayProvider : IDelayProvider internal class DelayProvider : IDelayProvider
{ {
public Task Delay(Int32 delay) public Task Delay(Int32 delay)
{ {
@ -15,22 +15,25 @@ class DelayProvider : IDelayProvider
public class DownloadStationDownloader : IDownloader public class DownloadStationDownloader : IDownloader
{ {
public event EventHandler<DownloadCompleteEventArgs>? DownloadComplete;
public event EventHandler<DownloadProgressEventArgs>? DownloadProgress;
private const Int32 RetryCount = 5; private const Int32 RetryCount = 5;
private readonly IDelayProvider _delayProvider;
private readonly ISynologyClient _synologyClient; private readonly String _filePath;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly String _filePath;
private readonly String _uri;
private readonly String? _remotePath; private readonly String? _remotePath;
private readonly IDelayProvider _delayProvider;
private readonly ISynologyClient _synologyClient;
private readonly String _uri;
private String? _gid; private String? _gid;
public DownloadStationDownloader(String? gid, String uri, String? remotePath, String filePath, String downloadPath, ISynologyClient synologyClient, IDelayProvider? delayProvider = null) public DownloadStationDownloader(String? gid,
String uri,
String? remotePath,
String filePath,
String downloadPath,
ISynologyClient synologyClient,
IDelayProvider? delayProvider = null)
{ {
_logger = Log.ForContext<DownloadStationDownloader>(); _logger = Log.ForContext<DownloadStationDownloader>();
_logger.Debug($"Instantiated new DownloadStation Downloader for URI {uri} to filePath {filePath} and downloadPath {downloadPath} and GID {gid}"); _logger.Debug($"Instantiated new DownloadStation Downloader for URI {uri} to filePath {filePath} and downloadPath {downloadPath} and GID {gid}");
@ -43,48 +46,8 @@ public class DownloadStationDownloader : IDownloader
_delayProvider = delayProvider ?? new DelayProvider(); _delayProvider = delayProvider ?? new DelayProvider();
} }
public static async Task<DownloadStationDownloader> Init(String? gid, String uri, String filePath, String downloadPath, String? category) public event EventHandler<DownloadCompleteEventArgs>? DownloadComplete;
{ public event EventHandler<DownloadProgressEventArgs>? DownloadProgress;
if (Settings.Get.DownloadClient.DownloadStationUrl == null)
{
throw new("No URL specified for Synology download station");
}
if (Settings.Get.DownloadClient.DownloadStationUsername == null || Settings.Get.DownloadClient.DownloadStationPassword == null)
{
throw new("No username/password specified for Synology download station");
}
var synologyClient = new SynologyClient(Settings.Get.DownloadClient.DownloadStationUrl);
await synologyClient.LoginAsync(Settings.Get.DownloadClient.DownloadStationUsername, Settings.Get.DownloadClient.DownloadStationPassword);
String? remotePath = null;
String? rootPath;
if (!String.IsNullOrWhiteSpace(Settings.Get.DownloadClient.DownloadStationDownloadPath))
{
rootPath = Settings.Get.DownloadClient.DownloadStationDownloadPath;
}
else
{
var config = await synologyClient.DownloadStationApi().InfoEndpoint().GetConfigAsync();
rootPath = config.DefaultDestination;
}
if (rootPath != null)
{
if (String.IsNullOrWhiteSpace(category))
{
remotePath = Path.Combine(rootPath, downloadPath).Replace('\\', '/');
}
else
{
remotePath = Path.Combine(rootPath, category, downloadPath).Replace('\\', '/');
}
}
return new(gid, uri, remotePath, filePath, downloadPath, synologyClient);
}
public async Task Cancel() public async Task Cancel()
{ {
@ -173,13 +136,6 @@ public class DownloadStationDownloader : IDownloader
throw new($"Unable to download file"); throw new($"Unable to download file");
} }
private async Task<String?> GetGidFromUri()
{
var tasks = await _synologyClient.DownloadStationApi().TaskEndpoint().ListAsync();
return tasks.Task?.FirstOrDefault(t => t.Additional?.Detail?.Uri == _uri)?.Id;
}
public async Task Pause() public async Task Pause()
{ {
_logger.Debug($"Pausing download {_uri} {_gid}"); _logger.Debug($"Pausing download {_uri} {_gid}");
@ -200,6 +156,56 @@ public class DownloadStationDownloader : IDownloader
} }
} }
public static async Task<DownloadStationDownloader> Init(String? gid, String uri, String filePath, String downloadPath, String? category)
{
if (Settings.Get.DownloadClient.DownloadStationUrl == null)
{
throw new("No URL specified for Synology download station");
}
if (Settings.Get.DownloadClient.DownloadStationUsername == null || Settings.Get.DownloadClient.DownloadStationPassword == null)
{
throw new("No username/password specified for Synology download station");
}
var synologyClient = new SynologyClient(Settings.Get.DownloadClient.DownloadStationUrl);
await synologyClient.LoginAsync(Settings.Get.DownloadClient.DownloadStationUsername, Settings.Get.DownloadClient.DownloadStationPassword);
String? remotePath = null;
String? rootPath;
if (!String.IsNullOrWhiteSpace(Settings.Get.DownloadClient.DownloadStationDownloadPath))
{
rootPath = Settings.Get.DownloadClient.DownloadStationDownloadPath;
}
else
{
var config = await synologyClient.DownloadStationApi().InfoEndpoint().GetConfigAsync();
rootPath = config.DefaultDestination;
}
if (rootPath != null)
{
if (String.IsNullOrWhiteSpace(category))
{
remotePath = Path.Combine(rootPath, downloadPath).Replace('\\', '/');
}
else
{
remotePath = Path.Combine(rootPath, category, downloadPath).Replace('\\', '/');
}
}
return new(gid, uri, remotePath, filePath, downloadPath, synologyClient);
}
private async Task<String?> GetGidFromUri()
{
var tasks = await _synologyClient.DownloadStationApi().TaskEndpoint().ListAsync();
return tasks.Task?.FirstOrDefault(t => t.Additional?.Detail?.Uri == _uri)?.Id;
}
public async Task Update() public async Task Update()
{ {
if (_gid == null) if (_gid == null)

View file

@ -6,14 +6,13 @@ namespace RdtClient.Service.Services.Downloaders;
public class SymlinkDownloader(String uri, String destinationPath, String path, Provider? clientKind) : IDownloader public class SymlinkDownloader(String uri, String destinationPath, String path, Provider? clientKind) : IDownloader
{ {
public event EventHandler<DownloadCompleteEventArgs>? DownloadComplete; private const Int32 MaxRetries = 10;
public event EventHandler<DownloadProgressEventArgs>? DownloadProgress;
private readonly CancellationTokenSource _cancellationToken = new(); private readonly CancellationTokenSource _cancellationToken = new();
private readonly ILogger _logger = Log.ForContext<SymlinkDownloader>(); private readonly ILogger _logger = Log.ForContext<SymlinkDownloader>();
public event EventHandler<DownloadCompleteEventArgs>? DownloadComplete;
private const Int32 MaxRetries = 10; public event EventHandler<DownloadProgressEventArgs>? DownloadProgress;
public async Task<String> Download() public async Task<String> Download()
{ {
@ -23,9 +22,9 @@ public class SymlinkDownloader(String uri, String destinationPath, String path,
{ {
var filePath = new FileInfo(path); var filePath = new FileInfo(path);
var rcloneMountPath = Settings.Get.DownloadClient.RcloneMountPath.TrimEnd(['\\', '/']); var rcloneMountPath = Settings.Get.DownloadClient.RcloneMountPath.TrimEnd('\\', '/');
var searchSubDirectories = rcloneMountPath.EndsWith('*'); var searchSubDirectories = rcloneMountPath.EndsWith('*');
rcloneMountPath = rcloneMountPath.TrimEnd('*').TrimEnd(['\\', '/']); rcloneMountPath = rcloneMountPath.TrimEnd('*').TrimEnd('\\', '/');
if (!Directory.Exists(rcloneMountPath)) if (!Directory.Exists(rcloneMountPath))
{ {
@ -35,7 +34,7 @@ public class SymlinkDownloader(String uri, String destinationPath, String path,
var fileName = filePath.Name; var fileName = filePath.Name;
var fileExtension = filePath.Extension; var fileExtension = filePath.Extension;
var fileNameWithoutExtension = fileName.Replace(fileExtension, ""); var fileNameWithoutExtension = fileName.Replace(fileExtension, "");
var pathWithoutFileName = path.Replace(fileName, "").TrimEnd(['\\', '/']); var pathWithoutFileName = path.Replace(fileName, "").TrimEnd('\\', '/');
var searchPath = Path.Combine(rcloneMountPath, pathWithoutFileName); var searchPath = Path.Combine(rcloneMountPath, pathWithoutFileName);
List<String> unWantedExtensions = List<String> unWantedExtensions =
@ -95,7 +94,7 @@ public class SymlinkDownloader(String uri, String destinationPath, String path,
potentialFilePaths.Add(directoryInfo.Name); potentialFilePaths.Add(directoryInfo.Name);
directoryInfo = directoryInfo.Parent; directoryInfo = directoryInfo.Parent;
if (directoryInfo.FullName.TrimEnd(['\\', '/']) == rcloneMountPath) if (directoryInfo.FullName.TrimEnd('\\', '/') == rcloneMountPath)
{ {
break; break;
} }
@ -182,10 +181,11 @@ public class SymlinkDownloader(String uri, String destinationPath, String path,
} }
catch (Exception ex) catch (Exception ex)
{ {
DownloadComplete?.Invoke(this, new() DownloadComplete?.Invoke(this,
{ new()
Error = ex.Message {
}); Error = ex.Message
});
throw; throw;
} }

View file

@ -12,12 +12,12 @@ public interface IEnricher
} }
/// <summary> /// <summary>
/// Enriches magnet links and torrents by adding trackers from the tracker list grabber. /// Enriches magnet links and torrents by adding trackers from the tracker list grabber.
/// </summary> /// </summary>
public sealed class Enricher(ILogger<Enricher> logger, ITrackerListGrabber trackerListGrabber) : IEnricher public sealed class Enricher(ILogger<Enricher> logger, ITrackerListGrabber trackerListGrabber) : IEnricher
{ {
/// <summary> /// <summary>
/// Add trackers from the tracker list grabber to the magnet link. /// Add trackers from the tracker list grabber to the magnet link.
/// </summary> /// </summary>
/// <param name="magnetLink">Magnet link to add trackers to. Is not modified</param> /// <param name="magnetLink">Magnet link to add trackers to. Is not modified</param>
/// <returns>Magnet link with additional trackers</returns> /// <returns>Magnet link with additional trackers</returns>
@ -127,7 +127,7 @@ public sealed class Enricher(ILogger<Enricher> logger, ITrackerListGrabber track
} }
/// <summary> /// <summary>
/// Add trackers from the tracker list grabber to the .torrent file bytes. /// Add trackers from the tracker list grabber to the .torrent file bytes.
/// </summary> /// </summary>
/// <param name="torrentBytes">Torrent file bytes to add trackers to. Is not modified</param> /// <param name="torrentBytes">Torrent file bytes to add trackers to. Is not modified</param>
/// <returns>Torrent file bytes with additional trackers</returns> /// <returns>Torrent file bytes with additional trackers</returns>

View file

@ -206,13 +206,15 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
} }
var torrentPath = downloadPath; var torrentPath = downloadPath;
if (!String.IsNullOrWhiteSpace(torrent.RdName)) if (!String.IsNullOrWhiteSpace(torrent.RdName))
{ {
// Alldebrid stores single file torrents at the root folder. // Alldebrid stores single file torrents at the root folder.
if (torrent.ClientKind == Provider.AllDebrid && torrent.Files.Count == 1) if (torrent.ClientKind == Provider.AllDebrid && torrent.Files.Count == 1)
{ {
torrentPath = Path.Combine(downloadPath, torrent.Files[0].Path); torrentPath = Path.Combine(downloadPath, torrent.Files[0].Path);
} else }
else
{ {
torrentPath = Path.Combine(downloadPath, torrent.RdName) + Path.DirectorySeparatorChar; torrentPath = Path.Combine(downloadPath, torrent.RdName) + Path.DirectorySeparatorChar;
} }
@ -222,29 +224,31 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
Int64 bytesTotal = 0; Int64 bytesTotal = 0;
Int64 speed = 0; Int64 speed = 0;
Double rdProgress = 0; Double rdProgress = 0;
try try
{ {
rdProgress = (torrent.RdProgress ?? 0.0) / 100.0; rdProgress = (torrent.RdProgress ?? 0.0) / 100.0;
bytesTotal = torrent.RdSize ?? 1; bytesTotal = torrent.RdSize ?? 1;
bytesDone = (Int64) (bytesTotal * rdProgress); bytesDone = (Int64)(bytesTotal * rdProgress);
speed = torrent.RdSpeed ?? 0; speed = torrent.RdSpeed ?? 0;
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, $""" logger.LogError(ex,
Error calculating progress: $"""
RdProgress: {torrent.RdProgress} Error calculating progress:
RdSize: {torrent.RdSize} RdProgress: {torrent.RdProgress}
RdSpeed: {torrent.RdSpeed} RdSize: {torrent.RdSize}
"""); RdSpeed: {torrent.RdSpeed}
""");
} }
if (torrent.Downloads.Count > 0) if (torrent.Downloads.Count > 0)
{ {
bytesDone = torrent.Downloads.Sum(m => m.BytesDone); bytesDone = torrent.Downloads.Sum(m => m.BytesDone);
bytesTotal = torrent.Downloads.Sum(m => m.BytesTotal); bytesTotal = torrent.Downloads.Sum(m => m.BytesTotal);
speed = (Int32) torrent.Downloads.Average(m => m.Speed); speed = (Int32)torrent.Downloads.Average(m => m.Speed);
downloadProgress = bytesTotal > 0 ? (Double) bytesDone / bytesTotal : 0; downloadProgress = bytesTotal > 0 ? (Double)bytesDone / bytesTotal : 0;
} }
var progress = (rdProgress + downloadProgress) / 2.0; var progress = (rdProgress + downloadProgress) / 2.0;
@ -288,7 +292,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
State = "downloading", State = "downloading",
SuperSeeding = false, SuperSeeding = false,
Tags = "", Tags = "",
TimeActive = (Int64) (DateTimeOffset.UtcNow - torrent.Added).TotalSeconds, TimeActive = (Int64)(DateTimeOffset.UtcNow - torrent.Added).TotalSeconds,
TotalSize = bytesTotal, TotalSize = bytesTotal,
Tracker = "udp://tracker.opentrackr.org:1337", Tracker = "udp://tracker.opentrackr.org:1337",
UpLimit = -1, UpLimit = -1,
@ -364,7 +368,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
{ {
bytesDone = torrent.Downloads.Sum(m => m.BytesDone); bytesDone = torrent.Downloads.Sum(m => m.BytesDone);
bytesTotal = torrent.Downloads.Sum(m => m.BytesTotal); bytesTotal = torrent.Downloads.Sum(m => m.BytesTotal);
speed = (Int32) torrent.Downloads.Average(m => m.Speed); speed = (Int32)torrent.Downloads.Average(m => m.Speed);
} }
var result = new TorrentProperties var result = new TorrentProperties
@ -392,7 +396,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
Seeds = 100, Seeds = 100,
SeedsTotal = 100, SeedsTotal = 100,
ShareRatio = 9999, ShareRatio = 9999,
TimeElapsed = (Int64) (DateTimeOffset.UtcNow - torrent.Added).TotalSeconds, TimeElapsed = (Int64)(DateTimeOffset.UtcNow - torrent.Added).TotalSeconds,
TotalDownloaded = bytesDone, TotalDownloaded = bytesDone,
TotalDownloadedSession = bytesDone, TotalDownloadedSession = bytesDone,
TotalSize = bytesTotal, TotalSize = bytesTotal,
@ -513,8 +517,8 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
var allTorrents = await torrents.Get(); var allTorrents = await torrents.Get();
var torrentsToGroup = allTorrents.Where(m => !String.IsNullOrWhiteSpace(m.Category)) var torrentsToGroup = allTorrents.Where(m => !String.IsNullOrWhiteSpace(m.Category))
.Select(m => m.Category!.ToLower()) .Select(m => m.Category!.ToLower())
.ToList(); .ToList();
var categoryList = (Settings.Get.General.Categories ?? "") var categoryList = (Settings.Get.General.Categories ?? "")
.Split(",", StringSplitOptions.RemoveEmptyEntries) .Split(",", StringSplitOptions.RemoveEmptyEntries)

View file

@ -46,7 +46,7 @@ public class Settings(SettingData settingData)
{ {
await settingData.ResetCache(); await settingData.ResetCache();
LoggingLevelSwitch.MinimumLevel = Settings.Get.General.LogLevel switch LoggingLevelSwitch.MinimumLevel = Get.General.LogLevel switch
{ {
LogLevel.Verbose => LogEventLevel.Verbose, LogLevel.Verbose => LogEventLevel.Verbose,
LogLevel.Debug => LogEventLevel.Debug, LogLevel.Debug => LogEventLevel.Debug,

View file

@ -3,9 +3,9 @@ using AllDebridNET;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.TorrentClient; using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using RdtClient.Data.Models.Data;
using File = AllDebridNET.File; using File = AllDebridNET.File;
using Torrent = RdtClient.Data.Models.Data.Torrent; using Torrent = RdtClient.Data.Models.Data.Torrent;
@ -51,36 +51,12 @@ public class AllDebridNetClientFactory(ILogger<AllDebridNetClientFactory> logger
} }
} }
public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAllDebridNetClientFactory allDebridNetClientFactory, IDownloadableFileFilter fileFilter) : ITorrentClient public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAllDebridNetClientFactory allDebridNetClientFactory, IDownloadableFileFilter fileFilter)
: ITorrentClient
{ {
private static readonly Int64 SessionId = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); private static readonly Int64 SessionId = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
private static List<TorrentClientTorrent> _cache = []; private static List<TorrentClientTorrent> _cache = [];
private static Int64 _sessionCounter = 0; private static Int64 _sessionCounter;
private static TorrentClientTorrent Map(Magnet torrent)
{
var files = GetFiles(torrent.Files);
return new()
{
Id = torrent.Id.ToString(),
Filename = torrent.Filename ?? "",
OriginalFilename = torrent.Filename,
Hash = torrent.Hash ?? "",
Bytes = torrent.Size ?? 0,
OriginalBytes = torrent.Size ?? 0,
Host = null,
Split = 0,
Progress = (Int64)Math.Round((torrent.Downloaded ?? 0) * 100.0 / (torrent.Size ?? 1)),
Status = torrent.Status,
StatusCode = torrent.StatusCode ?? 0,
Added = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.UploadDate ?? 0),
Files = files,
Links = [],
Ended = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.CompletionDate ?? 0),
Speed = torrent.DownloadSpeed,
Seeders = torrent.Seeders
};
}
public async Task<IList<TorrentClientTorrent>> GetTorrents() public async Task<IList<TorrentClientTorrent>> GetTorrents()
{ {
@ -98,10 +74,12 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
foreach (var result in results.Magnets ?? []) foreach (var result in results.Magnets ?? [])
{ {
var existing = _cache.FirstOrDefault(m => m.Id == result.Id.ToString()); var existing = _cache.FirstOrDefault(m => m.Id == result.Id.ToString());
if (existing != null) if (existing != null)
{ {
_cache.Remove(existing); _cache.Remove(existing);
} }
_cache.Add(Map(result)); _cache.Add(Map(result));
} }
} }
@ -259,11 +237,13 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
var files = GetFiles(allFiles); var files = GetFiles(allFiles);
return files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes) && f.DownloadLink != null).Select(f => new DownloadInfo 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! FileName = Path.GetFileName(f.Path),
}).ToList(); RestrictedLink = f.DownloadLink!
})
.ToList();
} }
/// <inheritdoc /> /// <inheritdoc />
@ -275,6 +255,32 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
return Task.FromResult(download.FileName); return Task.FromResult(download.FileName);
} }
private static TorrentClientTorrent Map(Magnet torrent)
{
var files = GetFiles(torrent.Files);
return new()
{
Id = torrent.Id.ToString(),
Filename = torrent.Filename ?? "",
OriginalFilename = torrent.Filename,
Hash = torrent.Hash ?? "",
Bytes = torrent.Size ?? 0,
OriginalBytes = torrent.Size ?? 0,
Host = null,
Split = 0,
Progress = (Int64)Math.Round(((torrent.Downloaded ?? 0) * 100.0) / (torrent.Size ?? 1)),
Status = torrent.Status,
StatusCode = torrent.StatusCode ?? 0,
Added = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.UploadDate ?? 0),
Files = files,
Links = [],
Ended = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.CompletionDate ?? 0),
Speed = torrent.DownloadSpeed,
Seeders = torrent.Seeders
};
}
private async Task<TorrentClientTorrent> GetInfo(String torrentId) private async Task<TorrentClientTorrent> GetInfo(String torrentId)
{ {
var result = await allDebridNetClientFactory.GetClient().Magnet.StatusAsync(torrentId) ?? throw new($"Unable to find magnet with ID {torrentId}"); var result = await allDebridNetClientFactory.GetClient().Magnet.StatusAsync(torrentId) ?? throw new($"Unable to find magnet with ID {torrentId}");
@ -290,32 +296,33 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAll
} }
return files.SelectMany(file => return files.SelectMany(file =>
{ {
var currentPath = String.IsNullOrEmpty(parentPath) var currentPath = String.IsNullOrEmpty(parentPath)
? file.FolderOrFileName ? file.FolderOrFileName
: Path.Combine(parentPath, file.FolderOrFileName); : Path.Combine(parentPath, file.FolderOrFileName);
var result = new List<TorrentClientFile>(); var result = new List<TorrentClientFile>();
// If it's a file (has size) // If it's a file (has size)
if (file.Size.HasValue) if (file.Size.HasValue)
{ {
result.Add(new() result.Add(new()
{ {
Path = currentPath, Path = currentPath,
Bytes = file.Size.Value, Bytes = file.Size.Value,
DownloadLink = file.DownloadLink DownloadLink = file.DownloadLink
}); });
} }
// Process sub-nodes if they exist // Process sub-nodes if they exist
if (file.SubNodes != null) if (file.SubNodes != null)
{ {
result.AddRange(GetFiles(file.SubNodes, currentPath)); result.AddRange(GetFiles(file.SubNodes, currentPath));
} }
return result; return result;
}).ToList(); })
.ToList();
} }
private void Log(String message, Torrent? torrent = null) private void Log(String message, Torrent? torrent = null)

View file

@ -1,11 +1,11 @@
using System.Diagnostics; using System.Diagnostics;
using DebridLinkFrNET;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using DebridLinkFrNET;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.TorrentClient; using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using RdtClient.Data.Models.Data;
using Download = RdtClient.Data.Models.Data.Download; using Download = RdtClient.Data.Models.Data.Download;
using Torrent = DebridLinkFrNET.Models.Torrent; using Torrent = DebridLinkFrNET.Models.Torrent;
@ -13,78 +13,6 @@ namespace RdtClient.Service.Services.TorrentClients;
public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : ITorrentClient public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : ITorrentClient
{ {
private DebridLinkFrNETClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("DebridLink API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
var debridLinkClient = new DebridLinkFrNETClient(apiKey, httpClient);
return debridLinkClient;
}
catch (AggregateException ae)
{
foreach (var inner in ae.InnerExceptions)
{
logger.LogError(inner, $"The connection to DebridLink has failed: {inner.Message}");
}
throw;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to DebridLink has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to DebridLink has timed out: {ex.Message}");
throw;
}
}
private TorrentClientTorrent Map(Torrent torrent)
{
return new()
{
Id = torrent.Id ?? "",
Filename = torrent.Name ?? "",
OriginalFilename = torrent.Name ?? "",
Hash = torrent.HashString ?? "",
Bytes = torrent.TotalSize,
OriginalBytes = 0,
Host = torrent.ServerId ?? "",
Split = 0,
Progress = torrent.DownloadPercent,
Status = torrent.Status.ToString(),
Added = DateTimeOffset.FromUnixTimeSeconds(torrent.Created),
Files = (torrent.Files ?? []).Select((m, i) => new TorrentClientFile
{
Path = m.Name ?? "",
Bytes = m.Size,
Id = i,
Selected = true,
DownloadLink = m.DownloadUrl
})
.ToList(),
Links = torrent.Files?.Select(m => m.DownloadUrl.ToString()).ToList(),
Ended = null,
Speed = torrent.UploadSpeed,
Seeders = torrent.PeersConnected,
};
}
public async Task<IList<TorrentClientTorrent>> GetTorrents() public async Task<IList<TorrentClientTorrent>> GetTorrents()
{ {
var page = 0; var page = 0;
@ -92,7 +20,7 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
while (true) while (true)
{ {
var pagedResults = await GetClient().Seedbox.ListAsync(null,page, 50); var pagedResults = await GetClient().Seedbox.ListAsync(null, page, 50);
results.AddRange(pagedResults); results.AddRange(pagedResults);
@ -198,16 +126,16 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
torrent.RdSeeders = rdTorrent.Seeders; torrent.RdSeeders = rdTorrent.Seeders;
torrent.RdStatusRaw = rdTorrent.Status; torrent.RdStatusRaw = rdTorrent.Status;
/* /*
0 Torrent is stopped 0 Torrent is stopped
1 Torrent is queued to verify local data 1 Torrent is queued to verify local data
2 Torrent is verifying local data 2 Torrent is verifying local data
3 Torrent is queued to download 3 Torrent is queued to download
4 Torrent is downloading 4 Torrent is downloading
5 Torrent is queued to seed 5 Torrent is queued to seed
6 Torrent is seeding 6 Torrent is seeding
100 Torrent is stored 100 Torrent is stored
*/ */
torrent.RdStatus = rdTorrent.Status switch torrent.RdStatus = rdTorrent.Status switch
{ {
@ -260,6 +188,87 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
.ToList(); .ToList();
} }
/// <inheritdoc />
public Task<String> GetFileName(Download download)
{
// FileName is set in GetDownlaadInfos
Debug.Assert(download.FileName != null);
return Task.FromResult(download.FileName);
}
private DebridLinkFrNETClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("DebridLink API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
var debridLinkClient = new DebridLinkFrNETClient(apiKey, httpClient);
return debridLinkClient;
}
catch (AggregateException ae)
{
foreach (var inner in ae.InnerExceptions)
{
logger.LogError(inner, $"The connection to DebridLink has failed: {inner.Message}");
}
throw;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to DebridLink has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to DebridLink has timed out: {ex.Message}");
throw;
}
}
private TorrentClientTorrent Map(Torrent torrent)
{
return new()
{
Id = torrent.Id ?? "",
Filename = torrent.Name ?? "",
OriginalFilename = torrent.Name ?? "",
Hash = torrent.HashString ?? "",
Bytes = torrent.TotalSize,
OriginalBytes = 0,
Host = torrent.ServerId ?? "",
Split = 0,
Progress = torrent.DownloadPercent,
Status = torrent.Status.ToString(),
Added = DateTimeOffset.FromUnixTimeSeconds(torrent.Created),
Files = (torrent.Files ?? []).Select((m, i) => new TorrentClientFile
{
Path = m.Name ?? "",
Bytes = m.Size,
Id = i,
Selected = true,
DownloadLink = m.DownloadUrl
})
.ToList(),
Links = torrent.Files?.Select(m => m.DownloadUrl.ToString()).ToList(),
Ended = null,
Speed = torrent.UploadSpeed,
Seeders = torrent.PeersConnected
};
}
private async Task<TorrentClientTorrent> GetInfo(String torrentId) private async Task<TorrentClientTorrent> GetInfo(String torrentId)
{ {
var result = await GetClient().Seedbox.ListAsync(torrentId); var result = await GetClient().Seedbox.ListAsync(torrentId);
@ -277,15 +286,6 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
logger.LogDebug(message); logger.LogDebug(message);
} }
/// <inheritdoc />
public Task<String> GetFileName(Download download)
{
// 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) public static String? GetSymlinkPath(Data.Models.Data.Torrent torrent, Download download)
{ {
if (torrent.RdName == null || download.FileName == null) if (torrent.RdName == null || download.FileName == null)

View file

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

View file

@ -3,78 +3,19 @@ using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using PremiumizeNET; using PremiumizeNET;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.TorrentClient; using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using RdtClient.Data.Models.Data;
using Torrent = RdtClient.Data.Models.Data.Torrent; using Torrent = RdtClient.Data.Models.Data.Torrent;
namespace RdtClient.Service.Services.TorrentClients; namespace RdtClient.Service.Services.TorrentClients;
public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : ITorrentClient public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : ITorrentClient
{ {
private PremiumizeNETClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("Premiumize API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(10);
var premiumizeNetClient = new PremiumizeNETClient(apiKey, httpClient);
return premiumizeNetClient;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to Premiumize has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to Premiumize has timed out: {ex.Message}");
throw;
}
}
private static TorrentClientTorrent Map(Transfer transfer)
{
return new()
{
Id = transfer.Id,
Filename = transfer.Name,
OriginalFilename = transfer.Name,
Hash = transfer.Src,
Bytes = 0,
OriginalBytes = 0,
Host = null,
Split = 0,
Progress = (Int64) ((transfer.Progress ?? 1.0) * 100.0),
Status = transfer.Status,
Message = transfer.Message,
StatusCode = 0,
Added = null,
Files = [],
Links =
[
transfer.FolderId
],
Ended = null,
Speed = 0,
Seeders = 0
};
}
public async Task<IList<TorrentClientTorrent>> GetTorrents() public async Task<IList<TorrentClientTorrent>> GetTorrents()
{ {
var results = await GetClient().Transfers.ListAsync(); var results = await GetClient().Transfers.ListAsync();
return results.Select(Map).ToList(); return results.Select(Map).ToList();
} }
@ -229,7 +170,11 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
Log($"File {file.Name} ({file.Id}) does not contain a link", torrent); Log($"File {file.Name} ({file.Id}) does not contain a link", torrent);
} }
downloadInfos.Add(new () {RestrictedLink = file.Link, FileName = file.Name }); downloadInfos.Add(new()
{
RestrictedLink = file.Link,
FileName = file.Name
});
} }
foreach (var info in downloadInfos) foreach (var info in downloadInfos)
@ -249,6 +194,66 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
return Task.FromResult(download.FileName); return Task.FromResult(download.FileName);
} }
private PremiumizeNETClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("Premiumize API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(10);
var premiumizeNetClient = new PremiumizeNETClient(apiKey, httpClient);
return premiumizeNetClient;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to Premiumize has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to Premiumize has timed out: {ex.Message}");
throw;
}
}
private static TorrentClientTorrent Map(Transfer transfer)
{
return new()
{
Id = transfer.Id,
Filename = transfer.Name,
OriginalFilename = transfer.Name,
Hash = transfer.Src,
Bytes = 0,
OriginalBytes = 0,
Host = null,
Split = 0,
Progress = (Int64)((transfer.Progress ?? 1.0) * 100.0),
Status = transfer.Status,
Message = transfer.Message,
StatusCode = 0,
Added = null,
Files = [],
Links =
[
transfer.FolderId
],
Ended = null,
Speed = 0,
Seeders = 0
};
}
private async Task<TorrentClientTorrent> GetInfo(String id) private async Task<TorrentClientTorrent> GetInfo(String id)
{ {
var results = await GetClient().Transfers.ListAsync(); var results = await GetClient().Transfers.ListAsync();
@ -259,7 +264,6 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
private async Task<List<DownloadInfo>> GetAllDownloadInfos(Torrent torrent, String folderId) private async Task<List<DownloadInfo>> GetAllDownloadInfos(Torrent torrent, String folderId)
{ {
if (String.IsNullOrWhiteSpace(folderId)) if (String.IsNullOrWhiteSpace(folderId))
{ {
return []; return [];
@ -270,6 +274,7 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
if (folder.Content == null) if (folder.Content == null)
{ {
Log($"Found no items in folder {folder.Name} ({folderId})", torrent); Log($"Found no items in folder {folder.Name} ({folderId})", torrent);
return []; return [];
} }
@ -295,7 +300,11 @@ public class PremiumizeTorrentClient(ILogger<PremiumizeTorrentClient> logger, IH
Log($"Found item {item.Name} in folder {folder.Name} ({folderId})", torrent); Log($"Found item {item.Name} in folder {folder.Name} ({folderId})", torrent);
downloadInfos.Add(new () { RestrictedLink = item.Link, FileName = item.Name}); downloadInfos.Add(new()
{
RestrictedLink = item.Link,
FileName = item.Name
});
} }
else if (item.Type == "folder") else if (item.Type == "folder")
{ {

View file

@ -15,84 +15,6 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
{ {
private TimeSpan? _offset; private TimeSpan? _offset;
private RdNetClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("Real-Debrid API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient(DiConfig.RD_CLIENT);
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
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
if (_offset == null)
{
var serverTime = rdtNetClient.Api.GetIsoTimeAsync().Result;
_offset = serverTime.Offset;
}
return rdtNetClient;
}
catch (AggregateException ae)
{
foreach (var inner in ae.InnerExceptions)
{
logger.LogError(inner, $"The connection to RealDebrid has failed: {inner.Message}");
}
throw;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to RealDebrid has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to RealDebrid has timed out: {ex.Message}");
throw;
}
}
private TorrentClientTorrent Map(Torrent torrent)
{
return new()
{
Id = torrent.Id,
Filename = torrent.Filename,
OriginalFilename = torrent.OriginalFilename,
Hash = torrent.Hash,
Bytes = torrent.Bytes,
OriginalBytes = torrent.OriginalBytes,
Host = torrent.Host,
Split = torrent.Split,
Progress = torrent.Progress,
Status = torrent.Status,
Added = ChangeTimeZone(torrent.Added)!.Value,
Files = (torrent.Files ?? []).Select(m => new TorrentClientFile
{
Path = m.Path,
Bytes = m.Bytes,
Id = m.Id,
Selected = m.Selected
}).ToList(),
Links = torrent.Links,
Ended = ChangeTimeZone(torrent.Ended),
Speed = torrent.Speed,
Seeders = torrent.Seeders,
};
}
public async Task<IList<TorrentClientTorrent>> GetTorrents() public async Task<IList<TorrentClientTorrent>> GetTorrents()
{ {
var offset = 0; var offset = 0;
@ -167,7 +89,6 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
files = [.. torrent.Files]; files = [.. torrent.Files];
} }
files = files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes)).ToList(); files = files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes)).ToList();
Log($"Selecting {files.Count}/{torrent.Files.Count} files", torrent); Log($"Selecting {files.Count}/{torrent.Files.Count} files", torrent);
@ -310,7 +231,8 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
Log($"{link}", torrent); Log($"{link}", torrent);
} }
Log($"Torrent has {torrent.Files.Count(m => m.Selected)} selected files out of {torrent.Files.Count} files, found {downloadLinks.Count} links, torrent ended: {torrent.RdEnded}", torrent); Log($"Torrent has {torrent.Files.Count(m => m.Selected)} selected files out of {torrent.Files.Count} files, found {downloadLinks.Count} links, torrent ended: {torrent.RdEnded}",
torrent);
// Check if all the links are set that have been selected // Check if all the links are set that have been selected
if (torrent.Files.Count(m => m.Selected) == downloadLinks.Count) if (torrent.Files.Count(m => m.Selected) == downloadLinks.Count)
@ -361,6 +283,85 @@ public class RealDebridTorrentClient(ILogger<RealDebridTorrentClient> logger, IH
return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last())); return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last()));
} }
private RdNetClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("Real-Debrid API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient(DiConfig.RD_CLIENT);
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
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
if (_offset == null)
{
var serverTime = rdtNetClient.Api.GetIsoTimeAsync().Result;
_offset = serverTime.Offset;
}
return rdtNetClient;
}
catch (AggregateException ae)
{
foreach (var inner in ae.InnerExceptions)
{
logger.LogError(inner, $"The connection to RealDebrid has failed: {inner.Message}");
}
throw;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to RealDebrid has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to RealDebrid has timed out: {ex.Message}");
throw;
}
}
private TorrentClientTorrent Map(Torrent torrent)
{
return new()
{
Id = torrent.Id,
Filename = torrent.Filename,
OriginalFilename = torrent.OriginalFilename,
Hash = torrent.Hash,
Bytes = torrent.Bytes,
OriginalBytes = torrent.OriginalBytes,
Host = torrent.Host,
Split = torrent.Split,
Progress = torrent.Progress,
Status = torrent.Status,
Added = ChangeTimeZone(torrent.Added)!.Value,
Files = (torrent.Files ?? []).Select(m => new TorrentClientFile
{
Path = m.Path,
Bytes = m.Bytes,
Id = m.Id,
Selected = m.Selected
})
.ToList(),
Links = torrent.Links,
Ended = ChangeTimeZone(torrent.Ended),
Speed = torrent.Speed,
Seeders = torrent.Seeders
};
}
private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset) private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset)
{ {
if (_offset == null) if (_offset == null)

View file

@ -1,106 +1,33 @@
using System.Diagnostics; using System.Diagnostics;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MonoTorrent;
using Newtonsoft.Json; using Newtonsoft.Json;
using TorBoxNET;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.TorrentClient;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using TorBoxNET;
using Torrent = RdtClient.Data.Models.Data.Torrent;
namespace RdtClient.Service.Services.TorrentClients; namespace RdtClient.Service.Services.TorrentClients;
public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : ITorrentClient public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : ITorrentClient
{ {
private TimeSpan? _offset; private TimeSpan? _offset;
private TorBoxNetClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("TorBox API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
var torBoxNetClient = new TorBoxNetClient(null, httpClient, 5);
torBoxNetClient.UseApiAuthentication(apiKey);
// Get the server time to fix up the timezones on results
if (_offset == null)
{
var serverTime = DateTimeOffset.UtcNow;
_offset = serverTime.Offset;
}
return torBoxNetClient;
}
catch (AggregateException ae)
{
foreach (var inner in ae.InnerExceptions)
{
logger.LogError(inner, $"The connection to TorBox has failed: {inner.Message}");
}
throw;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to TorBox has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to TorBox has timed out: {ex.Message}");
throw;
}
}
private TorrentClientTorrent Map(TorrentInfoResult torrent)
{
return new()
{
Id = torrent.Hash,
Filename = torrent.Name,
OriginalFilename = torrent.Name,
Hash = torrent.Hash,
Bytes = torrent.Size,
OriginalBytes = torrent.Size,
Host = torrent.DownloadPresent.ToString(),
Split = 0,
Progress = (Int64)(torrent.Progress * 100.0),
Status = torrent.DownloadState,
Added = ChangeTimeZone(torrent.CreatedAt)!.Value,
Files = (torrent.Files ?? []).Select(m => new TorrentClientFile
{
Path = String.Join("/", m.Name.Split('/').Skip(1)),
Bytes = m.Size,
Id = m.Id,
Selected = true
}).ToList(),
Links = [],
Ended = ChangeTimeZone(torrent.UpdatedAt),
Speed = torrent.DownloadSpeed,
Seeders = torrent.Seeds,
};
}
public async Task<IList<TorrentClientTorrent>> GetTorrents() public async Task<IList<TorrentClientTorrent>> GetTorrents()
{ {
var torrents = new List<TorrentInfoResult>(); var torrents = new List<TorrentInfoResult>();
var currentTorrents = await GetClient().Torrents.GetCurrentAsync(true); var currentTorrents = await GetClient().Torrents.GetCurrentAsync(true);
if (currentTorrents != null) if (currentTorrents != null)
{ {
torrents.AddRange(currentTorrents); torrents.AddRange(currentTorrents);
} }
var queuedTorrents = await GetClient().Torrents.GetQueuedAsync(true); var queuedTorrents = await GetClient().Torrents.GetQueuedAsync(true);
if (queuedTorrents != null) if (queuedTorrents != null)
{ {
torrents.AddRange(queuedTorrents); torrents.AddRange(queuedTorrents);
@ -124,11 +51,12 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
{ {
var user = await GetClient().User.GetAsync(true); var user = await GetClient().User.GetAsync(true);
var result = await GetClient().Torrents.AddMagnetAsync(magnetLink, user.Data?.Settings?.SeedTorrents ?? 3, false); var result = await GetClient().Torrents.AddMagnetAsync(magnetLink, user.Data?.Settings?.SeedTorrents ?? 3);
if (result.Error == "ACTIVE_LIMIT") if (result.Error == "ACTIVE_LIMIT")
{ {
var magnetLinkInfo = MonoTorrent.MagnetLink.Parse(magnetLink); var magnetLinkInfo = MagnetLink.Parse(magnetLink);
return magnetLinkInfo.InfoHashes.V1!.ToHex().ToLowerInvariant(); return magnetLinkInfo.InfoHashes.V1!.ToHex().ToLowerInvariant();
} }
@ -140,11 +68,13 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
var user = await GetClient().User.GetAsync(true); var user = await GetClient().User.GetAsync(true);
var result = await GetClient().Torrents.AddFileAsync(bytes, user.Data?.Settings?.SeedTorrents ?? 3); var result = await GetClient().Torrents.AddFileAsync(bytes, user.Data?.Settings?.SeedTorrents ?? 3);
if (result.Error == "ACTIVE_LIMIT") if (result.Error == "ACTIVE_LIMIT")
{ {
using var stream = new MemoryStream(bytes); using var stream = new MemoryStream(bytes);
var torrent = await MonoTorrent.Torrent.LoadAsync(stream); var torrent = await MonoTorrent.Torrent.LoadAsync(stream);
return torrent.InfoHashes.V1!.ToHex().ToLowerInvariant(); return torrent.InfoHashes.V1!.ToHex().ToLowerInvariant();
} }
@ -153,15 +83,16 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
public async Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash) public async Task<IList<TorrentClientAvailableFile>> GetAvailableFiles(String hash)
{ {
var availability = await GetClient().Torrents.GetAvailabilityAsync(hash, listFiles: true); var availability = await GetClient().Torrents.GetAvailabilityAsync(hash, true);
if (availability.Data != null && availability.Data.Count > 0) if (availability.Data != null && availability.Data.Count > 0)
{ {
return (availability.Data[0]?.Files ?? []).Select(file => new TorrentClientAvailableFile return (availability.Data[0]?.Files ?? []).Select(file => new TorrentClientAvailableFile
{ {
Filename = file.Name, Filename = file.Name,
Filesize = file.Size Filesize = file.Size
}).ToList(); })
.ToList();
} }
return []; return [];
@ -265,7 +196,6 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
_ => TorrentStatus.Error _ => TorrentStatus.Error
}; };
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -284,7 +214,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
public async Task<IList<DownloadInfo>?> GetDownloadInfos(Torrent torrent) public async Task<IList<DownloadInfo>?> GetDownloadInfos(Torrent torrent)
{ {
var torrentId = await GetClient().Torrents.GetHashInfoAsync(torrent.Hash, skipCache: true); var torrentId = await GetClient().Torrents.GetHashInfoAsync(torrent.Hash, true);
var downloadableFiles = torrent.Files.Where(file => fileFilter.IsDownloadable(torrent, file.Path, file.Bytes)).ToList(); var downloadableFiles = torrent.Files.Where(file => fileFilter.IsDownloadable(torrent, file.Path, file.Bytes)).ToList();
if (downloadableFiles.Count == torrent.Files.Count && torrent.DownloadClient != Data.Enums.DownloadClient.Symlink && Settings.Get.Provider.PreferZippedDownloads) if (downloadableFiles.Count == torrent.Files.Count && torrent.DownloadClient != Data.Enums.DownloadClient.Symlink && Settings.Get.Provider.PreferZippedDownloads)
@ -320,6 +250,85 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
return Task.FromResult(download.FileName); return Task.FromResult(download.FileName);
} }
private TorBoxNetClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("TorBox API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
var torBoxNetClient = new TorBoxNetClient(null, httpClient, 5);
torBoxNetClient.UseApiAuthentication(apiKey);
// Get the server time to fix up the timezones on results
if (_offset == null)
{
var serverTime = DateTimeOffset.UtcNow;
_offset = serverTime.Offset;
}
return torBoxNetClient;
}
catch (AggregateException ae)
{
foreach (var inner in ae.InnerExceptions)
{
logger.LogError(inner, $"The connection to TorBox has failed: {inner.Message}");
}
throw;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to TorBox has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to TorBox has timed out: {ex.Message}");
throw;
}
}
private TorrentClientTorrent Map(TorrentInfoResult torrent)
{
return new()
{
Id = torrent.Hash,
Filename = torrent.Name,
OriginalFilename = torrent.Name,
Hash = torrent.Hash,
Bytes = torrent.Size,
OriginalBytes = torrent.Size,
Host = torrent.DownloadPresent.ToString(),
Split = 0,
Progress = (Int64)(torrent.Progress * 100.0),
Status = torrent.DownloadState,
Added = ChangeTimeZone(torrent.CreatedAt)!.Value,
Files = (torrent.Files ?? []).Select(m => new TorrentClientFile
{
Path = String.Join("/", m.Name.Split('/').Skip(1)),
Bytes = m.Size,
Id = m.Id,
Selected = true
})
.ToList(),
Links = [],
Ended = ChangeTimeZone(torrent.UpdatedAt),
Speed = torrent.DownloadSpeed,
Seeders = torrent.Seeds
};
}
private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset) private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset)
{ {
if (_offset == null) if (_offset == null)
@ -332,7 +341,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
private async Task<TorrentClientTorrent> GetInfo(String torrentHash) private async Task<TorrentClientTorrent> GetInfo(String torrentHash)
{ {
var result = await GetClient().Torrents.GetHashInfoAsync(torrentHash, skipCache: true); var result = await GetClient().Torrents.GetHashInfoAsync(torrentHash, true);
return Map(result!); return Map(result!);
} }
@ -346,6 +355,7 @@ public class TorBoxTorrentClient(ILogger<TorBoxTorrentClient> logger, IHttpClien
var innerFolder = Directory.GetDirectories(hashDir)[0]; var innerFolder = Directory.GetDirectories(hashDir)[0];
var moveDir = extractPath; var moveDir = extractPath;
if (!extractPath.EndsWith(_torrent.RdName!)) if (!extractPath.EndsWith(_torrent.RdName!))
{ {
moveDir = hashDir; moveDir = hashDir;

View file

@ -1,13 +1,13 @@
using Aria2NET; using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text.Json;
using Aria2NET;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.Internal; using RdtClient.Data.Models.Internal;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using RdtClient.Service.Services.Downloaders; using RdtClient.Service.Services.Downloaders;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text.Json;
namespace RdtClient.Service.Services; namespace RdtClient.Service.Services;
@ -16,13 +16,13 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
public static readonly ConcurrentDictionary<Guid, DownloadClient> ActiveDownloadClients = new(); public static readonly ConcurrentDictionary<Guid, DownloadClient> ActiveDownloadClients = new();
public static readonly ConcurrentDictionary<Guid, UnpackClient> ActiveUnpackClients = new(); public static readonly ConcurrentDictionary<Guid, UnpackClient> ActiveUnpackClients = new();
public static Boolean IsPausedForLowDiskSpace { get; set; }
private readonly HttpClient _httpClient = new() private readonly HttpClient _httpClient = new()
{ {
Timeout = TimeSpan.FromSeconds(10) Timeout = TimeSpan.FromSeconds(10)
}; };
public static Boolean IsPausedForLowDiskSpace { get; set; }
public async Task Initialize() public async Task Initialize()
{ {
Log("Initializing TorrentRunner"); Log("Initializing TorrentRunner");
@ -73,25 +73,30 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
if (String.IsNullOrWhiteSpace(Settings.Get.Provider.ApiKey)) if (String.IsNullOrWhiteSpace(Settings.Get.Provider.ApiKey))
{ {
Log($"No RealDebridApiKey set in settings"); Log($"No RealDebridApiKey set in settings");
return; return;
} }
var settingDownloadLimit = Settings.Get.General.DownloadLimit; var settingDownloadLimit = Settings.Get.General.DownloadLimit;
if (settingDownloadLimit < 1) if (settingDownloadLimit < 1)
{ {
settingDownloadLimit = 1; settingDownloadLimit = 1;
} }
var settingUnpackLimit = Settings.Get.General.UnpackLimit; var settingUnpackLimit = Settings.Get.General.UnpackLimit;
if (settingUnpackLimit < 0) if (settingUnpackLimit < 0)
{ {
settingUnpackLimit = 0; settingUnpackLimit = 0;
} }
var settingDownloadPath = Settings.Get.DownloadClient.DownloadPath; var settingDownloadPath = Settings.Get.DownloadClient.DownloadPath;
if (String.IsNullOrWhiteSpace(settingDownloadPath)) if (String.IsNullOrWhiteSpace(settingDownloadPath))
{ {
logger.LogError("No DownloadPath set in settings"); logger.LogError("No DownloadPath set in settings");
return; return;
} }
@ -163,7 +168,10 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
{ {
// Retry the download if an error is encountered. // Retry the download if an error is encountered.
LogError($"Download reported an error: {downloadClient.Error}", download, download.Torrent); LogError($"Download reported an error: {downloadClient.Error}", download, download.Torrent);
Log($"Download retry count {download.RetryCount}/{download.Torrent!.DownloadRetryAttempts}, torrent retry count {download.Torrent.RetryCount}/{download.Torrent.TorrentRetryAttempts}", download, download.Torrent);
Log($"Download retry count {download.RetryCount}/{download.Torrent!.DownloadRetryAttempts}, torrent retry count {download.Torrent.RetryCount}/{download.Torrent.TorrentRetryAttempts}",
download,
download.Torrent);
if (download.RetryCount < download.Torrent.DownloadRetryAttempts) if (download.RetryCount < download.Torrent.DownloadRetryAttempts)
{ {
@ -246,6 +254,7 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
{ {
await activeDownload.Value.Cancel(); await activeDownload.Value.Cancel();
ActiveDownloadClients.TryRemove(activeDownload.Key, out _); ActiveDownloadClients.TryRemove(activeDownload.Key, out _);
break; break;
} }
} }
@ -258,6 +267,7 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
{ {
activeUnpacks.Value.Cancel(); activeUnpacks.Value.Cancel();
ActiveUnpackClients.TryRemove(activeUnpacks.Key, out _); ActiveUnpackClients.TryRemove(activeUnpacks.Key, out _);
break; break;
} }
} }
@ -273,6 +283,7 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
{ {
await torrents.UpdateRetry(torrent.TorrentId, null, torrent.RetryCount); await torrents.UpdateRetry(torrent.TorrentId, null, torrent.RetryCount);
Log($"Torrent reach max retry count"); Log($"Torrent reach max retry count");
continue; continue;
} }
@ -567,14 +578,14 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
} }
// Check if we have reached the download limit, if so queue the download, but don't start it. // Check if we have reached the download limit, if so queue the download, but don't start it.
if (TorrentRunner.ActiveUnpackClients.Count >= settingUnpackLimit) if (ActiveUnpackClients.Count >= settingUnpackLimit)
{ {
Log($"Not starting unpack because there are already the max number of unpacks active", download, torrent); Log($"Not starting unpack because there are already the max number of unpacks active", download, torrent);
continue; continue;
} }
if (TorrentRunner.ActiveUnpackClients.ContainsKey(download.DownloadId)) if (ActiveUnpackClients.ContainsKey(download.DownloadId))
{ {
Log($"Not starting unpack because this download is already active", download, torrent); Log($"Not starting unpack because this download is already active", download, torrent);
@ -596,7 +607,7 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
// Start the unpacking process // Start the unpacking process
var unpackClient = new UnpackClient(download, downloadPath); var unpackClient = new UnpackClient(download, downloadPath);
if (TorrentRunner.ActiveUnpackClients.TryAdd(download.DownloadId, unpackClient)) if (ActiveUnpackClients.TryAdd(download.DownloadId, unpackClient))
{ {
Log($"Starting unpack", download, torrent); Log($"Starting unpack", download, torrent);
@ -647,8 +658,8 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
} }
// Check if torrent is complete, or if we don't want to download any files to the host. // Check if torrent is complete, or if we don't want to download any files to the host.
if ((torrent.Downloads.Count > 0) || if (torrent.Downloads.Count > 0 ||
torrent.RdStatus == TorrentStatus.Finished && torrent.HostDownloadAction == TorrentHostDownloadAction.DownloadNone) (torrent.RdStatus == TorrentStatus.Finished && torrent.HostDownloadAction == TorrentHostDownloadAction.DownloadNone))
{ {
var completeCount = torrent.Downloads.Count(m => m.Completed != null); var completeCount = torrent.Downloads.Count(m => m.Completed != null);
@ -659,7 +670,7 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
if (totalDownloadBytes > 0) if (totalDownloadBytes > 0)
{ {
completePerc = (Int32)((Double)totalDoneBytes / totalDownloadBytes * 100); completePerc = (Int32)(((Double)totalDoneBytes / totalDownloadBytes) * 100);
} }
if (completeCount == torrent.Downloads.Count) if (completeCount == torrent.Downloads.Count)

View file

@ -38,6 +38,8 @@ public class Torrents(
ReferenceHandler = ReferenceHandler.IgnoreCycles ReferenceHandler = ReferenceHandler.IgnoreCycles
}; };
private static readonly SemaphoreSlim TorrentResetLock = new(1, 1);
private ITorrentClient TorrentClient private ITorrentClient TorrentClient
{ {
get get
@ -54,8 +56,6 @@ public class Torrents(
} }
} }
private static readonly SemaphoreSlim TorrentResetLock = new(1, 1);
public async Task<IList<Torrent>> Get() public async Task<IList<Torrent>> Get()
{ {
var torrents = await torrentData.Get(); var torrents = await torrentData.Get();
@ -112,6 +112,7 @@ public class Torrents(
{ {
var enriched = await enricher.EnrichMagnetLink(magnetLink); var enriched = await enricher.EnrichMagnetLink(magnetLink);
MagnetLink magnet; MagnetLink magnet;
try try
{ {
magnet = MagnetLink.Parse(magnetLink); magnet = MagnetLink.Parse(magnetLink);
@ -119,6 +120,7 @@ public class Torrents(
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "{ex.Message}, trying to parse {magnetLink}", ex.Message, magnetLink); logger.LogError(ex, "{ex.Message}, trying to parse {magnetLink}", ex.Message, magnetLink);
throw new($"{ex.Message}, trying to parse {magnetLink}"); throw new($"{ex.Message}, trying to parse {magnetLink}");
} }
@ -142,6 +144,7 @@ public class Torrents(
if (bannedUrls.Count > 0) if (bannedUrls.Count > 0)
{ {
var bannedUrlsString = String.Join(", ", bannedUrls); var bannedUrlsString = String.Join(", ", bannedUrls);
throw new($"Cannot add torrent, the torrent contains banned trackers: {bannedUrlsString}."); throw new($"Cannot add torrent, the torrent contains banned trackers: {bannedUrlsString}.");
} }
} }
@ -213,6 +216,7 @@ public class Torrents(
if (bannedUrls.Count > 0) if (bannedUrls.Count > 0)
{ {
var bannedUrlsString = String.Join(", ", bannedUrls); var bannedUrlsString = String.Join(", ", bannedUrls);
throw new($"Cannot add torrent, the torrent contains banned trackers: {bannedUrlsString}."); throw new($"Cannot add torrent, the torrent contains banned trackers: {bannedUrlsString}.");
} }
} }
@ -262,9 +266,11 @@ public class Torrents(
{ {
case String magnetLink: case String magnetLink:
await File.WriteAllTextAsync(copyFileName, magnetLink); await File.WriteAllTextAsync(copyFileName, magnetLink);
break; break;
case Byte[] torrentFile: case Byte[] torrentFile:
await File.WriteAllBytesAsync(copyFileName, torrentFile); await File.WriteAllBytesAsync(copyFileName, torrentFile);
break; break;
} }
} }
@ -276,7 +282,7 @@ public class Torrents(
} }
/// <summary> /// <summary>
/// Adds torrent in database to debrid provider and updates database accordingly. /// Adds torrent in database to debrid provider and updates database accordingly.
/// </summary> /// </summary>
/// <param name="torrent">The torrent from the database to upload to the debrid provider</param> /// <param name="torrent">The torrent from the database to upload to the debrid provider</param>
/// <returns>Updated torrent</returns> /// <returns>Updated torrent</returns>
@ -373,7 +379,7 @@ public class Torrents(
} }
/// <summary> /// <summary>
/// Logs a message to the console, sets the error on the torrent and ensures it is not retried. /// Logs a message to the console, sets the error on the torrent and ensures it is not retried.
/// </summary> /// </summary>
/// <param name="torrent">The torrent to mark as "All files excluded"</param> /// <param name="torrent">The torrent to mark as "All files excluded"</param>
private async Task MarkAllFilesExcluded(Torrent torrent) private async Task MarkAllFilesExcluded(Torrent torrent)
@ -511,8 +517,9 @@ public class Torrents(
} }
/// <summary> /// <summary>
/// To be called only when <see cref="Data.Models.Data.Download" />.<see cref="Data.Models.Data.Download.FileName" /> is not set by /// To be called only when <see cref="Data.Models.Data.Download" />.<see cref="Data.Models.Data.Download.FileName" />
/// <see cref="ITorrentClient.GetDownloadInfos" /> /// is not set by
/// <see cref="ITorrentClient.GetDownloadInfos" />
/// </summary> /// </summary>
public async Task<String> RetrieveFileName(Guid downloadId) public async Task<String> RetrieveFileName(Guid downloadId)
{ {
@ -566,7 +573,8 @@ public class Torrents(
{ {
Category = Settings.Get.Provider.Default.Category, Category = Settings.Get.Provider.Default.Category,
DownloadClient = Settings.Get.DownloadClient.Client, DownloadClient = Settings.Get.DownloadClient.Client,
DownloadAction = Settings.Get.Provider.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll, DownloadAction =
Settings.Get.Provider.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
HostDownloadAction = Settings.Get.Provider.Default.HostDownloadAction, HostDownloadAction = Settings.Get.Provider.Default.HostDownloadAction,
FinishedActionDelay = Settings.Get.Provider.Default.FinishedActionDelay, FinishedActionDelay = Settings.Get.Provider.Default.FinishedActionDelay,
FinishedAction = Settings.Get.Provider.Default.FinishedAction, FinishedAction = Settings.Get.Provider.Default.FinishedAction,
@ -887,6 +895,7 @@ public class Torrents(
outputSb.AppendLine(data.Trim()); outputSb.AppendLine(data.Trim());
}; };
process.ErrorDataReceived += (_, data) => process.ErrorDataReceived += (_, data) =>
{ {
if (data == null) if (data == null)

View file

@ -1,6 +1,6 @@
using System.Reflection;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Reflection;
namespace RdtClient.Service.Services; namespace RdtClient.Service.Services;
@ -8,10 +8,10 @@ public class TrackerListGrabber(IHttpClientFactory httpClientFactory, IMemoryCac
{ {
private const String CacheKey = "TrackerList"; private const String CacheKey = "TrackerList";
private Int32? _lastExpirationMinutes;
private static readonly SemaphoreSlim Semaphore = new(1, 1); private static readonly SemaphoreSlim Semaphore = new(1, 1);
private Int32? _lastExpirationMinutes;
public async Task<String[]> GetTrackers() public async Task<String[]> GetTrackers()
{ {
var trackerUrlList = Settings.Get.General.TrackerEnrichmentList; var trackerUrlList = Settings.Get.General.TrackerEnrichmentList;
@ -148,6 +148,7 @@ public class TrackerListGrabber(IHttpClientFactory httpClientFactory, IMemoryCac
{ {
logger.LogDebug("Rejected tracker: {TrackerUrl} - Reason: Invalid format or unsupported scheme.", t); logger.LogDebug("Rejected tracker: {TrackerUrl} - Reason: Invalid format or unsupported scheme.", t);
trackerRejectionCount++; trackerRejectionCount++;
return false; return false;
} }

View file

@ -1,4 +1,5 @@
using RdtClient.Data.Models.Data; using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using RdtClient.Service.Services.TorrentClients; using RdtClient.Service.Services.TorrentClients;
using SharpCompress.Archives; using SharpCompress.Archives;
@ -10,16 +11,15 @@ namespace RdtClient.Service.Services;
public class UnpackClient(Download download, String destinationPath) public class UnpackClient(Download download, String destinationPath)
{ {
private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly Torrent _torrent = download.Torrent ?? throw new($"Torrent is null");
public Boolean Finished { get; private set; } public Boolean Finished { get; private set; }
public String? Error { get; private set; } public String? Error { get; private set; }
public Int32 Progess { get; private set; } public Int32 Progess { get; private set; }
private readonly Torrent _torrent = download.Torrent ?? throw new($"Torrent is null");
private readonly CancellationTokenSource _cancellationTokenSource = new();
public void Start() public void Start()
{ {
Progess = 0; Progess = 0;
@ -104,7 +104,7 @@ public class UnpackClient(Download download, String destinationPath)
await FileHelper.Delete(filePath); await FileHelper.Delete(filePath);
} }
if (_torrent.ClientKind == Data.Enums.Provider.TorBox) if (_torrent.ClientKind == Provider.TorBox)
{ {
TorBoxTorrentClient.MoveHashDirContents(extractPath, _torrent); TorBoxTorrentClient.MoveHashDirContents(extractPath, _torrent);
} }
@ -119,7 +119,6 @@ public class UnpackClient(Download download, String destinationPath)
} }
} }
private static async Task<IList<String>> GetArchiveFiles(String filePath) private static async Task<IList<String>> GetArchiveFiles(String filePath)
{ {
await using Stream stream = File.OpenRead(filePath); await using Stream stream = File.OpenRead(filePath);
@ -127,6 +126,7 @@ public class UnpackClient(Download download, String destinationPath)
var extension = Path.GetExtension(filePath); var extension = Path.GetExtension(filePath);
IArchive archive; IArchive archive;
if (extension == ".zip") if (extension == ".zip")
{ {
archive = ZipArchive.OpenArchive(stream); archive = ZipArchive.OpenArchive(stream);
@ -155,6 +155,7 @@ public class UnpackClient(Download download, String destinationPath)
var extension = Path.GetExtension(filePath); var extension = Path.GetExtension(filePath);
IArchive archive; IArchive archive;
if (extension == ".zip") if (extension == ".zip")
{ {
archive = ZipArchive.OpenArchive(fi); archive = ZipArchive.OpenArchive(fi);

View file

@ -4,11 +4,10 @@ namespace RdtClient.Service.Wrappers;
public interface IProcess : IDisposable public interface IProcess : IDisposable
{ {
public ProcessStartInfo StartInfo { get; set; }
event EventHandler<String?>? OutputDataReceived; event EventHandler<String?>? OutputDataReceived;
event EventHandler<String?>? ErrorDataReceived; event EventHandler<String?>? ErrorDataReceived;
public ProcessStartInfo StartInfo { get; set; }
void BeginOutputReadLine(); void BeginOutputReadLine();
void BeginErrorReadLine(); void BeginErrorReadLine();
Boolean WaitForExit(Int32 milliseconds); Boolean WaitForExit(Int32 milliseconds);

View file

@ -1,6 +1,6 @@
namespace RdtClient.Service.Wrappers; namespace RdtClient.Service.Wrappers;
public class ProcessFactory: IProcessFactory public class ProcessFactory : IProcessFactory
{ {
public IProcess NewProcess() public IProcess NewProcess()
{ {

View file

@ -125,6 +125,7 @@ public class AuthController(Authentication authentication, Settings settings) :
public async Task<ActionResult> Logout() public async Task<ActionResult> Logout()
{ {
await authentication.Logout(); await authentication.Logout();
return Ok(); return Ok();
} }

View file

@ -3,13 +3,13 @@ using Microsoft.AspNetCore.Mvc;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.QBittorrent; using RdtClient.Data.Models.QBittorrent;
using RdtClient.Service.Services; using RdtClient.Service.Services;
using RealDebridException = RDNET.RealDebridException;
namespace RdtClient.Web.Controllers; namespace RdtClient.Web.Controllers;
/// <summary> /// <summary>
/// This API behaves as a regular QBittorrent 4+ API /// This API behaves as a regular QBittorrent 4+ API
/// Documentation is found here: https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1) /// Documentation is found here: https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)
/// </summary> /// </summary>
[ApiController] [ApiController]
[Route("api/v2")] [Route("api/v2")]
@ -59,6 +59,7 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
logger.LogDebug($"Auth logout"); logger.LogDebug($"Auth logout");
await qBittorrent.AuthLogout(); await qBittorrent.AuthLogout();
return Ok(); return Ok();
} }
@ -92,6 +93,7 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
Qt = "5.15.2", Qt = "5.15.2",
Zlib = "1.2.11" Zlib = "1.2.11"
}; };
return Ok(result); return Ok(result);
} }
@ -111,6 +113,7 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
public async Task<ActionResult<AppPreferences>> AppPreferences() public async Task<ActionResult<AppPreferences>> AppPreferences()
{ {
var result = await qBittorrent.AppPreferences(); var result = await qBittorrent.AppPreferences();
return Ok(result); return Ok(result);
} }
@ -130,6 +133,7 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
public ActionResult<AppPreferences> AppDefaultSavePath() public ActionResult<AppPreferences> AppDefaultSavePath()
{ {
var result = Settings.AppDefaultSavePath; var result = Settings.AppDefaultSavePath;
return Ok(result); return Ok(result);
} }
@ -339,7 +343,7 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
return BadRequest($"Invalid torrent link format {url}"); return BadRequest($"Invalid torrent link format {url}");
} }
} }
catch (RDNET.RealDebridException ex) catch (RealDebridException ex)
{ {
// Infringing file. // Infringing file.
if (ex.ErrorCode == 35) if (ex.ErrorCode == 35)

View file

@ -9,6 +9,7 @@ using RdtClient.Data.Models.Internal;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using RdtClient.Service.Services; using RdtClient.Service.Services;
using RdtClient.Service.Services.Downloaders; using RdtClient.Service.Services.Downloaders;
using DownloadClient = RdtClient.Data.Enums.DownloadClient;
namespace RdtClient.Web.Controllers; namespace RdtClient.Web.Controllers;
@ -21,6 +22,7 @@ public class SettingsController(Settings settings, Torrents torrents) : Controll
public ActionResult Get() public ActionResult Get()
{ {
var result = SettingData.GetAll(); var result = SettingData.GetAll();
return Ok(result); return Ok(result);
} }
@ -43,6 +45,7 @@ public class SettingsController(Settings settings, Torrents torrents) : Controll
public async Task<ActionResult<Profile>> Profile() public async Task<ActionResult<Profile>> Profile()
{ {
var profile = await torrents.GetProfile(); var profile = await torrents.GetProfile();
return Ok(profile); return Ok(profile);
} }
@ -103,12 +106,12 @@ public class SettingsController(Settings settings, Torrents torrents) : Controll
Link = "https://34.download.real-debrid.com/speedtest/testDefault.rar", Link = "https://34.download.real-debrid.com/speedtest/testDefault.rar",
Torrent = new() Torrent = new()
{ {
DownloadClient = Settings.Get.DownloadClient.Client == Data.Enums.DownloadClient.Symlink ? Data.Enums.DownloadClient.Bezzad : Settings.Get.DownloadClient.Client, DownloadClient = Settings.Get.DownloadClient.Client == DownloadClient.Symlink ? DownloadClient.Bezzad : Settings.Get.DownloadClient.Client,
RdName = "testDefault.rar" RdName = "testDefault.rar"
} }
}; };
var downloadClient = new DownloadClient(download, download.Torrent, downloadPath, null); var downloadClient = new Service.Services.DownloadClient(download, download.Torrent, downloadPath, null);
await downloadClient.Start(); await downloadClient.Start();

View file

@ -52,6 +52,7 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
public ActionResult<DiskSpaceStatus?> GetDiskSpaceStatus() public ActionResult<DiskSpaceStatus?> GetDiskSpaceStatus()
{ {
var status = DiskSpaceMonitor.GetCurrentStatus(); var status = DiskSpaceMonitor.GetCurrentStatus();
return Ok(status); return Ok(status);
} }
@ -339,5 +340,5 @@ public class TorrentControllerVerifyRegexRequest
{ {
public String? IncludeRegex { get; set; } public String? IncludeRegex { get; set; }
public String? ExcludeRegex { get; set; } public String? ExcludeRegex { get; set; }
public String? MagnetLink { get; set;} public String? MagnetLink { get; set; }
} }

View file

@ -8,7 +8,9 @@ using RdtClient.Service;
using RdtClient.Service.Middleware; using RdtClient.Service.Middleware;
using RdtClient.Service.Services; using RdtClient.Service.Services;
using Serilog; using Serilog;
using Serilog.Debugging;
using Serilog.Events; using Serilog.Events;
using DiConfig = RdtClient.Data.DiConfig;
var builder = WebApplication.CreateBuilder(new WebApplicationOptions var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{ {
@ -51,7 +53,7 @@ if (appSettings.Logging?.File?.Path != null)
.MinimumLevel.Override("System.Net.Http", LogEventLevel.Warning)); .MinimumLevel.Override("System.Net.Http", LogEventLevel.Warning));
} }
Serilog.Debugging.SelfLog.Enable(msg => SelfLog.Enable(msg =>
{ {
Debug.Print(msg); Debug.Print(msg);
Debugger.Break(); Debugger.Break();
@ -69,12 +71,12 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc
options.SlidingExpiration = true; options.SlidingExpiration = true;
}); });
builder.Services.AddAuthorizationBuilder()
builder.Services.AddAuthorizationBuilder().AddPolicy("AuthSetting", policyCorrectUser => .AddPolicy("AuthSetting",
{ policyCorrectUser =>
policyCorrectUser.Requirements.Add(new AuthSettingRequirement()); {
}); policyCorrectUser.Requirements.Add(new AuthSettingRequirement());
});
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options => builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
{ {
@ -93,8 +95,10 @@ builder.Services.ConfigureApplicationCookie(options =>
options.Events.OnRedirectToLogin = context => options.Events.OnRedirectToLogin = context =>
{ {
context.Response.StatusCode = StatusCodes.Status401Unauthorized; context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask; return Task.CompletedTask;
}; };
options.Cookie.Name = "SID"; options.Cookie.Name = "SID";
}); });
@ -132,7 +136,7 @@ builder.Services.AddSignalR(hubOptions =>
builder.Host.UseWindowsService(); builder.Host.UseWindowsService();
RdtClient.Data.DiConfig.Config(builder.Services, appSettings); DiConfig.Config(builder.Services, appSettings);
builder.Services.RegisterRdtServices(); builder.Services.RegisterRdtServices();
try try
@ -160,7 +164,8 @@ try
} }
}); });
var basePath = !String.IsNullOrWhiteSpace(appSettings.BasePath) ? appSettings.BasePath : !String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("BASE_PATH")) ? Environment.GetEnvironmentVariable("BASE_PATH") : null; var basePath = !String.IsNullOrWhiteSpace(appSettings.BasePath) ? appSettings.BasePath :
!String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("BASE_PATH")) ? Environment.GetEnvironmentVariable("BASE_PATH") : null;
if (basePath != null) if (basePath != null)
{ {
@ -180,15 +185,17 @@ try
app.MapControllers(); app.MapControllers();
app.UseWhen(x => !x.Request.Path.StartsWithSegments("/api"), routeBuilder => app.UseWhen(x => !x.Request.Path.StartsWithSegments("/api"),
{ routeBuilder =>
routeBuilder.UseSpaStaticFiles(); {
routeBuilder.UseSpa(spa => routeBuilder.UseSpaStaticFiles();
{
spa.Options.SourcePath = "wwwroot"; routeBuilder.UseSpa(spa =>
spa.Options.DefaultPage = "/index.html"; {
}); spa.Options.SourcePath = "wwwroot";
}); spa.Options.DefaultPage = "/index.html";
});
});
// Run the app // Run the app
app.Run(); app.Run();

View file

@ -5,7 +5,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<UserSecretsId>94c24cba-f03f-4453-a671-3640b517c573</UserSecretsId> <UserSecretsId>94c24cba-f03f-4453-a671-3640b517c573</UserSecretsId>
<Version>1.0.0</Version> <Version>1.0.0</Version>
<AssemblyVersion>1.0.0</AssemblyVersion> <AssemblyVersion>1.0.0</AssemblyVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
@ -38,7 +38,6 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.3" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="10.0.3" />
<PackageReference Include="Serilog" Version="4.3.1" /> <PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />