This commit is contained in:
Roger Far 2026-05-27 22:04:30 -06:00
parent a82e895a03
commit 767038528a
16 changed files with 157 additions and 111 deletions

View file

@ -74,6 +74,7 @@ public class DownloadData(DataContext dataContext, ILogger<DownloadData>? logger
return DownloadAddResult.Added; return DownloadAddResult.Added;
} }
// These shouldn't be possible any longer, but added for safety and until confirmed. // These shouldn't be possible any longer, but added for safety and until confirmed.
catch (DbUpdateException ex) catch (DbUpdateException ex)
{ {
@ -88,7 +89,9 @@ public class DownloadData(DataContext dataContext, ILogger<DownloadData>? logger
if (IsForeignKeyViolation(ex) && !await dataContext.Torrents.AsNoTracking().AnyAsync(m => m.TorrentId == torrentId)) if (IsForeignKeyViolation(ex) && !await dataContext.Torrents.AsNoTracking().AnyAsync(m => m.TorrentId == torrentId))
{ {
logger?.LogDebug("Skipped download creation after the torrent was deleted concurrently. TorrentId: {torrentId}, Path: {path}", torrentId, downloadInfo.RestrictedLink); logger?.LogDebug("Skipped download creation after the torrent was deleted concurrently. TorrentId: {torrentId}, Path: {path}",
torrentId,
downloadInfo.RestrictedLink);
return DownloadAddResult.TorrentMissing; return DownloadAddResult.TorrentMissing;
} }

View file

@ -10,11 +10,11 @@ public class TorrentData(DataContext dataContext, ILogger<TorrentData>? logger =
public async Task<IList<Torrent>> Get() public async Task<IList<Torrent>> Get()
{ {
var torrents = await dataContext.Torrents var torrents = await dataContext.Torrents
.AsNoTracking() .AsNoTracking()
.AsSplitQuery() .AsSplitQuery()
.Include(m => m.Downloads) .Include(m => m.Downloads)
.OrderBy(m => m.Priority ?? 9999) .OrderBy(m => m.Priority ?? 9999)
.ToListAsync(); .ToListAsync();
return torrents.OrderBy(m => m.Priority ?? 9999) return torrents.OrderBy(m => m.Priority ?? 9999)
.ThenBy(m => m.Added) .ThenBy(m => m.Added)

View file

@ -63,6 +63,7 @@ public class Torrent
public DateTimeOffset? RdEnded { get; set; } public DateTimeOffset? RdEnded { get; set; }
public Int64? RdSpeed { get; set; } public Int64? RdSpeed { get; set; }
public Int64? RdSeeders { get; set; } public Int64? RdSeeders { get; set; }
public String? RdFiles public String? RdFiles
{ {
get => _rdFiles; get => _rdFiles;

View file

@ -24,6 +24,7 @@ public class WatchFolderCheckerTests : IDisposable
_serviceProviderMock = new(); _serviceProviderMock = new();
_serviceScopeMock = new(); _serviceScopeMock = new();
_scopeServiceProviderMock = new(); _scopeServiceProviderMock = new();
_settings = new(new() _settings = new(new()
{ {
Watch = new() Watch = new()
@ -33,6 +34,7 @@ public class WatchFolderCheckerTests : IDisposable
}, },
DownloadClient = new() DownloadClient = new()
}); });
_torrentsServiceMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, _settings, new TorrentRunnerState()); _torrentsServiceMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, _settings, new TorrentRunnerState());
_serviceProviderMock _serviceProviderMock

View file

@ -162,6 +162,7 @@ public class TorrentDownloadRaceTests : IAsyncLifetime
var torrentId = Guid.NewGuid(); var torrentId = Guid.NewGuid();
await using var context = CreateContext(); await using var context = CreateContext();
context.Torrents.Add(new() context.Torrents.Add(new()
{ {
TorrentId = torrentId, TorrentId = torrentId,

View file

@ -177,19 +177,20 @@ public class QBittorrentTest
// Assert // Assert
Assert.NotNull(result); Assert.NotNull(result);
Assert.Collection(result!, Assert.Collection(result!,
first => first =>
{ {
Assert.Equal(0, first.Index); Assert.Equal(0, first.Index);
Assert.Equal("good.mkv", first.Name); Assert.Equal("good.mkv", first.Name);
Assert.Equal(1, first.Priority); Assert.Equal(1, first.Priority);
}, },
second => second =>
{ {
Assert.Equal(1, second.Index); Assert.Equal(1, second.Index);
Assert.Equal("dangerous.exe", second.Name); Assert.Equal("dangerous.exe", second.Name);
Assert.Equal(0, second.Priority); Assert.Equal(0, second.Priority);
}); });
} }
[Fact] [Fact]
@ -251,7 +252,11 @@ public class QBittorrentTest
Type = DownloadType.Torrent Type = DownloadType.Torrent
}; };
_torrentsMock.Setup(m => m.Get()).ReturnsAsync(new List<Torrent> { torrent }); _torrentsMock.Setup(m => m.Get())
.ReturnsAsync(new List<Torrent>
{
torrent
});
// Act // Act
var result = await _qBittorrent.TorrentInfo(); var result = await _qBittorrent.TorrentInfo();
@ -311,7 +316,11 @@ public class QBittorrentTest
Type = DownloadType.Torrent Type = DownloadType.Torrent
}; };
_torrentsMock.Setup(m => m.Get()).ReturnsAsync(new List<Torrent> { torrent }); _torrentsMock.Setup(m => m.Get())
.ReturnsAsync(new List<Torrent>
{
torrent
});
var result = await _qBittorrent.TorrentInfo(); var result = await _qBittorrent.TorrentInfo();
@ -376,7 +385,11 @@ public class QBittorrentTest
Type = DownloadType.Torrent Type = DownloadType.Torrent
}; };
_torrentsMock.Setup(m => m.Get()).ReturnsAsync(new List<Torrent> { torrent }); _torrentsMock.Setup(m => m.Get())
.ReturnsAsync(new List<Torrent>
{
torrent
});
var result = await _qBittorrent.TorrentInfo(); var result = await _qBittorrent.TorrentInfo();

View file

@ -36,10 +36,12 @@ public class TorrentRunnerTest
}; };
var torrentDataMock = new Mock<ITorrentData>(MockBehavior.Strict); var torrentDataMock = new Mock<ITorrentData>(MockBehavior.Strict);
torrentDataMock.Setup(m => m.Get()).ReturnsAsync(new List<Torrent>
{ torrentDataMock.Setup(m => m.Get())
erroredTorrent .ReturnsAsync(new List<Torrent>
}); {
erroredTorrent
});
var torrents = new Torrents(Mock.Of<ILogger<Torrents>>(), var torrents = new Torrents(Mock.Of<ILogger<Torrents>>(),
torrentDataMock.Object, torrentDataMock.Object,

View file

@ -12,7 +12,8 @@ using Torrent = RdtClient.Data.Models.Data.Torrent;
namespace RdtClient.Service.Services.DebridClients; namespace RdtClient.Service.Services.DebridClients;
public class PremiumizeDebridClient(ILogger<PremiumizeDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter, ISettings settings) : IDebridClient public class PremiumizeDebridClient(ILogger<PremiumizeDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter, ISettings settings)
: IDebridClient
{ {
private const String TransferCreateUrl = "https://www.premiumize.me/api/transfer/create"; private const String TransferCreateUrl = "https://www.premiumize.me/api/transfer/create";
@ -331,11 +332,11 @@ public class PremiumizeDebridClient(ILogger<PremiumizeDebridClient> logger, IHtt
private static String FormatPremiumizeError(RawTransferCreateResponse result) private static String FormatPremiumizeError(RawTransferCreateResponse result)
{ {
return String.Join(": ", new[] return String.Join(": ",
{ new[]
result.Code, {
result.Message result.Code, result.Message
}.Where(m => !String.IsNullOrWhiteSpace(m))); }.Where(m => !String.IsNullOrWhiteSpace(m)));
} }
private static Boolean IsRateLimitMessage(String message) private static Boolean IsRateLimitMessage(String message)

View file

@ -12,7 +12,8 @@ using Torrent = RDNET.Torrent;
namespace RdtClient.Service.Services.DebridClients; namespace RdtClient.Service.Services.DebridClients;
public class RealDebridDebridClient(ILogger<RealDebridDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter, ISettings settings) : IDebridClient public class RealDebridDebridClient(ILogger<RealDebridDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter, ISettings settings)
: IDebridClient
{ {
private TimeSpan? _offset; private TimeSpan? _offset;

View file

@ -11,7 +11,12 @@ using Torrent = RdtClient.Data.Models.Data.Torrent;
namespace RdtClient.Service.Services.DebridClients; namespace RdtClient.Service.Services.DebridClients;
public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter, IRateLimitCoordinator coordinator, ISettings settings) public class TorBoxDebridClient(
ILogger<TorBoxDebridClient> logger,
IHttpClientFactory httpClientFactory,
IDownloadableFileFilter fileFilter,
IRateLimitCoordinator coordinator,
ISettings settings)
: IDebridClient : IDebridClient
{ {
private const String TorBoxApiHost = "api.torbox.app"; private const String TorBoxApiHost = "api.torbox.app";
@ -69,10 +74,12 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
return await HandleAddTorrentErrors(async asQueued => return await HandleAddTorrentErrors(async asQueued =>
{ {
var user = await GetClient().User.GetAsync(true); var user = await GetClient().User.GetAsync(true);
var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW).Torrents.AddMagnetAsync(magnetLink,
user.Data?.Settings?.SeedTorrents ?? 3, var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW)
allowZip: Settings.Get.Provider.PreferZippedDownloads, .Torrents.AddMagnetAsync(magnetLink,
as_queued: asQueued); user.Data?.Settings?.SeedTorrents ?? 3,
allowZip: Settings.Get.Provider.PreferZippedDownloads,
as_queued: asQueued);
return result.Data!.Hash!; return result.Data!.Hash!;
}); });
@ -83,10 +90,12 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
return await HandleAddTorrentErrors(async asQueued => return await HandleAddTorrentErrors(async asQueued =>
{ {
var user = await GetClient().User.GetAsync(true); var user = await GetClient().User.GetAsync(true);
var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW).Torrents.AddFileAsync(bytes,
user.Data?.Settings?.SeedTorrents ?? 3, var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW)
allowZip: Settings.Get.Provider.PreferZippedDownloads, .Torrents.AddFileAsync(bytes,
as_queued: asQueued); user.Data?.Settings?.SeedTorrents ?? 3,
allowZip: Settings.Get.Provider.PreferZippedDownloads,
as_queued: asQueued);
return result.Data!.Hash!; return result.Data!.Hash!;
}); });

View file

@ -346,13 +346,13 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
} }
var topLevelSelectedFiles = torrent.Files var topLevelSelectedFiles = torrent.Files
.Where(m => m.Selected && !String.IsNullOrWhiteSpace(m.Path)) .Where(m => m.Selected && !String.IsNullOrWhiteSpace(m.Path))
.Select(m => m.Path.Trim('/').Trim('\\')) .Select(m => m.Path.Trim('/').Trim('\\'))
.Where(m => m.IndexOfAny(['/', '\\']) < 0) .Where(m => m.IndexOfAny(['/', '\\']) < 0)
.Select(Path.GetFileName) .Select(Path.GetFileName)
.Where(m => !String.IsNullOrWhiteSpace(m)) .Where(m => !String.IsNullOrWhiteSpace(m))
.Distinct(StringComparer.OrdinalIgnoreCase) .Distinct(StringComparer.OrdinalIgnoreCase)
.ToList(); .ToList();
if (topLevelSelectedFiles.Count == 1) if (topLevelSelectedFiles.Count == 1)
{ {
@ -501,16 +501,16 @@ public class QBittorrent(ILogger<QBittorrent> logger, ISettings settings, Authen
var progress = torrent.Completed.HasValue || torrent.RdStatus == TorrentStatus.Finished ? 1f : 0f; var progress = torrent.Completed.HasValue || torrent.RdStatus == TorrentStatus.Finished ? 1f : 0f;
return torrent.Files return torrent.Files
.Select((file, index) => new TorrentFileItem .Select((file, index) => new TorrentFileItem
{ {
Index = index, Index = index,
Name = file.Path, Name = file.Path,
Size = file.Bytes, Size = file.Bytes,
Progress = file.Selected ? progress : 0f, Progress = file.Selected ? progress : 0f,
Priority = file.Selected ? 1 : 0, Priority = file.Selected ? 1 : 0,
IsSeed = false IsSeed = false
}) })
.ToList(); .ToList();
} }
public async Task<TorrentProperties?> TorrentProperties(String hash) public async Task<TorrentProperties?> TorrentProperties(String hash)

View file

@ -367,8 +367,10 @@ public class TorrentRunner(
} }
// Process torrents in DebridQueue // Process torrents in DebridQueue
var torrentsToAddToProvider = allTorrents.Where(m => m.Completed == null && m.Error == null && m.RdId == null && m.RdAdded == null && m.FileOrMagnet != null && m.RdStatus == TorrentStatus.Queued) var torrentsToAddToProvider = allTorrents
.ToList(); .Where(m => m.Completed == null && m.Error == null && m.RdId == null && m.RdAdded == null && m.FileOrMagnet != null &&
m.RdStatus == TorrentStatus.Queued)
.ToList();
if (torrentsToAddToProvider.Count != 0) if (torrentsToAddToProvider.Count != 0)
{ {

View file

@ -1137,18 +1137,19 @@ public class Torrents(
torrent.RdFiles); torrent.RdFiles);
} }
private readonly record struct TorrentRdState(String? RdName, private readonly record struct TorrentRdState(
Int64? RdSize, String? RdName,
String? RdHost, Int64? RdSize,
Int64? RdSplit, String? RdHost,
Int64? RdProgress, Int64? RdSplit,
TorrentStatus? RdStatus, Int64? RdProgress,
String? RdStatusRaw, TorrentStatus? RdStatus,
DateTimeOffset? RdAdded, String? RdStatusRaw,
DateTimeOffset? RdEnded, DateTimeOffset? RdAdded,
Int64? RdSpeed, DateTimeOffset? RdEnded,
Int64? RdSeeders, Int64? RdSpeed,
String? RdFiles); Int64? RdSeeders,
String? RdFiles);
private void Log(String message, Download? download, Torrent? torrent) private void Log(String message, Download? download, Torrent? torrent)
{ {

View file

@ -21,12 +21,11 @@ public class QBittorrentControllerTest
_qBittorrentMock = new(new Mock<ILogger<QBittorrent>>().Object, _settings, null!, null!, null!, new TorrentRunnerState()); _qBittorrentMock = new(new Mock<ILogger<QBittorrent>>().Object, _settings, null!, null!, null!, new TorrentRunnerState());
_torrentsMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, _settings, new TorrentRunnerState()); _torrentsMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, _settings, new TorrentRunnerState());
_controller = new( _controller = new(new Mock<ILogger<QBittorrentController>>().Object,
new Mock<ILogger<QBittorrentController>>().Object, _qBittorrentMock.Object,
_qBittorrentMock.Object, new Mock<IHttpClientFactory>().Object,
new Mock<IHttpClientFactory>().Object, _settings,
_settings, _torrentsMock.Object);
_torrentsMock.Object);
_controller.ControllerContext = new() _controller.ControllerContext = new()
{ {
@ -38,15 +37,16 @@ public class QBittorrentControllerTest
public async Task TorrentsInfo_FilterAll_DoesNotFilterOutResults() public async Task TorrentsInfo_FilterAll_DoesNotFilterOutResults()
{ {
// Arrange // Arrange
_qBittorrentMock.Setup(q => q.TorrentInfo()).ReturnsAsync(new List<TorrentInfo> _qBittorrentMock.Setup(q => q.TorrentInfo())
{ .ReturnsAsync(new List<TorrentInfo>
new() {
{ new()
Hash = "hash1", {
State = "pausedUP", Hash = "hash1",
Progress = 1f State = "pausedUP",
} Progress = 1f
}); }
});
// Act // Act
var result = await _controller.TorrentsInfo(new() var result = await _controller.TorrentsInfo(new()
@ -66,21 +66,22 @@ public class QBittorrentControllerTest
public async Task TorrentsInfo_FilterCompleted_MatchesPausedUploadTorrents() public async Task TorrentsInfo_FilterCompleted_MatchesPausedUploadTorrents()
{ {
// Arrange // Arrange
_qBittorrentMock.Setup(q => q.TorrentInfo()).ReturnsAsync(new List<TorrentInfo> _qBittorrentMock.Setup(q => q.TorrentInfo())
{ .ReturnsAsync(new List<TorrentInfo>
new() {
{ new()
Hash = "hash1", {
State = "pausedUP", Hash = "hash1",
Progress = 1f State = "pausedUP",
}, Progress = 1f
new() },
{ new()
Hash = "hash2", {
State = "downloading", Hash = "hash2",
Progress = 0.4f State = "downloading",
} Progress = 0.4f
}); }
});
// Act // Act
var result = await _controller.TorrentsInfo(new() var result = await _controller.TorrentsInfo(new()

View file

@ -44,6 +44,7 @@ public class SabnzbdHandlerTest
{ {
// Arrange // Arrange
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
var httpContext = new DefaultHttpContext var httpContext = new DefaultHttpContext
{ {
Request = Request =
@ -69,6 +70,7 @@ public class SabnzbdHandlerTest
{ {
// Arrange // Arrange
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
var httpContext = new DefaultHttpContext var httpContext = new DefaultHttpContext
{ {
Request = Request =
@ -96,6 +98,7 @@ public class SabnzbdHandlerTest
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
var httpContext = new DefaultHttpContext(); var httpContext = new DefaultHttpContext();
httpContext.Request.ContentType = "application/x-www-form-urlencoded"; httpContext.Request.ContentType = "application/x-www-form-urlencoded";
httpContext.Request.Form = new FormCollection(new() httpContext.Request.Form = new FormCollection(new()
{ {
{ {
@ -120,6 +123,7 @@ public class SabnzbdHandlerTest
{ {
// Arrange // Arrange
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
var httpContext = new DefaultHttpContext var httpContext = new DefaultHttpContext
{ {
Request = Request =
@ -145,6 +149,7 @@ public class SabnzbdHandlerTest
{ {
// Arrange // Arrange
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
var httpContext = new DefaultHttpContext var httpContext = new DefaultHttpContext
{ {
Request = Request =
@ -192,6 +197,7 @@ public class SabnzbdHandlerTest
{ {
// Arrange // Arrange
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
var httpContext = new DefaultHttpContext var httpContext = new DefaultHttpContext
{ {
Request = Request =
@ -217,6 +223,7 @@ public class SabnzbdHandlerTest
{ {
// Arrange // Arrange
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
var httpContext = new DefaultHttpContext var httpContext = new DefaultHttpContext
{ {
Request = Request =
@ -245,6 +252,7 @@ public class SabnzbdHandlerTest
{ {
// Arrange // Arrange
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
var httpContext = new DefaultHttpContext var httpContext = new DefaultHttpContext
{ {
Request = Request =

View file

@ -14,7 +14,8 @@ namespace RdtClient.Web.Controllers;
[ApiController] [ApiController]
[Route("api/v2")] [Route("api/v2")]
[Route("qbittorrent/api/v2")] [Route("qbittorrent/api/v2")]
public class QBittorrentController(ILogger<QBittorrentController> logger, QBittorrent qBittorrent, IHttpClientFactory httpClientFactory, ISettings settings, Torrents torrents) : Controller public class QBittorrentController(ILogger<QBittorrentController> logger, QBittorrent qBittorrent, IHttpClientFactory httpClientFactory, ISettings settings, Torrents torrents)
: Controller
{ {
[AllowAnonymous] [AllowAnonymous]
[Route("/version/api")] [Route("/version/api")]
@ -369,6 +370,7 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
foreach (var url in urls) foreach (var url in urls)
{ {
Torrent? torrent; Torrent? torrent;
if (url.StartsWith("magnet")) if (url.StartsWith("magnet"))
{ {
torrent = await qBittorrent.TorrentsAddMagnet(url.Trim(), request.Category, null); torrent = await qBittorrent.TorrentsAddMagnet(url.Trim(), request.Category, null);
@ -394,7 +396,7 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
return Ok(); return Ok();
} }
[Authorize(Policy = "AuthSetting")] [Authorize(Policy = "AuthSetting")]
[Route("torrents/add")] [Route("torrents/add")]
[HttpPost] [HttpPost]
@ -439,11 +441,11 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
} }
var fileIds = request.Id var fileIds = request.Id
.Split('|', StringSplitOptions.RemoveEmptyEntries) .Split('|', StringSplitOptions.RemoveEmptyEntries)
.Select(value => Int32.TryParse(value, out var parsedValue) ? parsedValue : (Int32?)null) .Select(value => Int32.TryParse(value, out var parsedValue) ? parsedValue : (Int32?)null)
.Where(value => value.HasValue) .Where(value => value.HasValue)
.Select(value => value!.Value) .Select(value => value!.Value)
.ToList(); .ToList();
if (fileIds.Count == 0) if (fileIds.Count == 0)
{ {
@ -675,7 +677,7 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
_ => String.Equals(torrent.State, filter, StringComparison.OrdinalIgnoreCase) _ => String.Equals(torrent.State, filter, StringComparison.OrdinalIgnoreCase)
}; };
} }
private async Task<Boolean> WaitForTorrent(Guid torrentId) private async Task<Boolean> WaitForTorrent(Guid torrentId)
{ {
while (true) while (true)
@ -715,7 +717,6 @@ public class QBTorrentsInfoRequest
public String? Hashes { get; set; } public String? Hashes { get; set; }
} }
public class QBTorrentsCountRequest public class QBTorrentsCountRequest
{ {
public String? Filter { get; set; } public String? Filter { get; set; }