diff --git a/server/RdtClient.Data/Models/QBittorrent/TorrentFileItem.cs b/server/RdtClient.Data/Models/QBittorrent/TorrentFileItem.cs index 530196d..1447d80 100644 --- a/server/RdtClient.Data/Models/QBittorrent/TorrentFileItem.cs +++ b/server/RdtClient.Data/Models/QBittorrent/TorrentFileItem.cs @@ -4,6 +4,21 @@ namespace RdtClient.Data.Models.QBittorrent; public class TorrentFileItem { + [JsonPropertyName("index")] + public Int32 Index { get; set; } + [JsonPropertyName("name")] public String? Name { get; set; } + + [JsonPropertyName("size")] + public Int64 Size { get; set; } + + [JsonPropertyName("progress")] + public Single Progress { get; set; } + + [JsonPropertyName("priority")] + public Int32 Priority { get; set; } + + [JsonPropertyName("is_seed")] + public Boolean IsSeed { get; set; } } diff --git a/server/RdtClient.Data/Models/QBittorrent/TorrentProperties.cs b/server/RdtClient.Data/Models/QBittorrent/TorrentProperties.cs index ea59bac..9fdb939 100644 --- a/server/RdtClient.Data/Models/QBittorrent/TorrentProperties.cs +++ b/server/RdtClient.Data/Models/QBittorrent/TorrentProperties.cs @@ -13,6 +13,9 @@ public class TorrentProperties [JsonPropertyName("completion_date")] public Int64? CompletionDate { get; set; } + [JsonPropertyName("is_private")] + public Boolean IsPrivate { get; set; } + [JsonPropertyName("created_by")] public String? CreatedBy { get; set; } diff --git a/server/RdtClient.Service.Test/Services/QBittorrentTest.cs b/server/RdtClient.Service.Test/Services/QBittorrentTest.cs index 3163962..6e74451 100644 --- a/server/RdtClient.Service.Test/Services/QBittorrentTest.cs +++ b/server/RdtClient.Service.Test/Services/QBittorrentTest.cs @@ -2,7 +2,9 @@ using Microsoft.Extensions.Logging; using Moq; using RdtClient.Data.Enums; using RdtClient.Data.Models.Data; +using RdtClient.Data.Models.DebridClient; using RdtClient.Service.Services; +using System.Text.Json; namespace RdtClient.Service.Test.Services; @@ -136,4 +138,74 @@ public class QBittorrentTest // Current behavior is (1.0 + 0.8) / 2 = 0.9 Assert.Equal(0.9f, result[0].Progress); } + + [Fact] + public async Task TorrentFileContents_ShouldReturnAllFiles_WithIndexesAndPriority() + { + // Arrange + var torrent = new Torrent + { + Hash = "hash1", + Type = DownloadType.Torrent, + RdFiles = JsonSerializer.Serialize(new List + { + new() + { + Id = 1, + Path = "good.mkv", + Bytes = 1000, + Selected = true + }, + new() + { + Id = 2, + Path = "dangerous.exe", + Bytes = 10, + Selected = false + } + }) + }; + + _torrentsMock.Setup(m => m.GetByHash("hash1")).ReturnsAsync(torrent); + + // Act + var result = await _qBittorrent.TorrentFileContents("hash1"); + + // Assert + Assert.NotNull(result); + Assert.Collection(result!, + first => + { + Assert.Equal(0, first.Index); + Assert.Equal("good.mkv", first.Name); + Assert.Equal(1, first.Priority); + }, + second => + { + Assert.Equal(1, second.Index); + Assert.Equal("dangerous.exe", second.Name); + Assert.Equal(0, second.Priority); + }); + } + + [Fact] + public async Task TorrentProperties_ShouldExposePublicTorrentFlag() + { + // Arrange + var torrent = new Torrent + { + Hash = "hash1", + Type = DownloadType.Torrent, + Added = DateTimeOffset.UtcNow + }; + + _torrentsMock.Setup(m => m.GetByHash("hash1")).ReturnsAsync(torrent); + + // Act + var result = await _qBittorrent.TorrentProperties("hash1"); + + // Assert + Assert.NotNull(result); + Assert.False(result!.IsPrivate); + } } diff --git a/server/RdtClient.Service/Services/QBittorrent.cs b/server/RdtClient.Service/Services/QBittorrent.cs index 3a646bb..e2a6cb8 100644 --- a/server/RdtClient.Service/Services/QBittorrent.cs +++ b/server/RdtClient.Service/Services/QBittorrent.cs @@ -330,8 +330,6 @@ public class QBittorrent(ILogger logger, Settings settings, Authent public async Task?> TorrentFileContents(String hash) { - var results = new List(); - var torrent = await torrents.GetByHash(hash); if (torrent == null || torrent.Type != DownloadType.Torrent) @@ -339,17 +337,19 @@ public class QBittorrent(ILogger logger, Settings settings, Authent return null; } - foreach (var file in torrent.Files.Where(m => m.Selected)) - { - var result = new TorrentFileItem + var progress = torrent.Completed.HasValue || torrent.RdStatus == TorrentStatus.Finished ? 1f : 0f; + + return torrent.Files + .Select((file, index) => new TorrentFileItem { - Name = file.Path - }; - - results.Add(result); - } - - return results; + Index = index, + Name = file.Path, + Size = file.Bytes, + Progress = file.Selected ? progress : 0f, + Priority = file.Selected ? 1 : 0, + IsSeed = false + }) + .ToList(); } public async Task TorrentProperties(String hash) @@ -385,6 +385,7 @@ public class QBittorrent(ILogger logger, Settings settings, Authent AdditionDate = torrent.Added.ToUnixTimeSeconds(), Comment = "RealDebridClient ", CompletionDate = torrent.Completed?.ToUnixTimeSeconds() ?? -1, + IsPrivate = false, CreatedBy = "RealDebridClient ", CreationDate = torrent.Added.ToUnixTimeSeconds(), DlLimit = -1, @@ -420,6 +421,11 @@ public class QBittorrent(ILogger logger, Settings settings, Authent return result; } + public async Task TorrentsFilePrio(String hash, IReadOnlyCollection fileIds, Int32 priority) + { + await torrents.UpdateFileSelection(hash, fileIds, priority > 0); + } + public async Task TorrentsDelete(String hash, Boolean deleteFiles) { if (deleteFiles) diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs index e997f1b..d2ed88e 100644 --- a/server/RdtClient.Service/Services/Torrents.cs +++ b/server/RdtClient.Service/Services/Torrents.cs @@ -843,6 +843,43 @@ public class Torrents( await torrentData.UpdateFilesSelected(torrentId, datetime); } + public async Task UpdateFileSelection(String hash, IReadOnlyCollection fileIds, Boolean selected) + { + if (fileIds.Count == 0) + { + return false; + } + + var torrent = await torrentData.GetByHash(hash); + + if (torrent == null) + { + return false; + } + + var files = torrent.Files.ToList(); + + if (files.Count == 0) + { + return false; + } + + foreach (var fileId in fileIds) + { + if (fileId < 0 || fileId >= files.Count) + { + continue; + } + + files[fileId].Selected = selected; + } + + torrent.RdFiles = JsonSerializer.Serialize(files, JsonSerializerOptions); + await torrentData.UpdateRdData(torrent); + + return true; + } + public async Task UpdatePriority(String hash, Int32 priority) { var torrent = await torrentData.GetByHash(hash); diff --git a/server/RdtClient.Web.Test/Controllers/QBittorrentControllerTest.cs b/server/RdtClient.Web.Test/Controllers/QBittorrentControllerTest.cs new file mode 100644 index 0000000..2c961d6 --- /dev/null +++ b/server/RdtClient.Web.Test/Controllers/QBittorrentControllerTest.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using RdtClient.Data.Models.QBittorrent; +using RdtClient.Service.Services; +using RdtClient.Web.Controllers; + +namespace RdtClient.Web.Test.Controllers; + +public class QBittorrentControllerTest +{ + private readonly QBittorrentController _controller; + private readonly Mock _qBittorrentMock; + + public QBittorrentControllerTest() + { + _qBittorrentMock = new(new Mock>().Object, null!, null!, null!, null!); + + _controller = new( + new Mock>().Object, + _qBittorrentMock.Object, + new Mock().Object); + + _controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + }; + } + + [Fact] + public async Task TorrentsInfo_FilterAll_DoesNotFilterOutResults() + { + // Arrange + _qBittorrentMock.Setup(q => q.TorrentInfo()).ReturnsAsync(new List + { + new() + { + Hash = "hash1", + State = "pausedUP", + Progress = 1f + } + }); + + // Act + var result = await _controller.TorrentsInfo(new QBTorrentsInfoRequest + { + Filter = "all", + Hashes = "hash1" + }); + + // Assert + var okResult = Assert.IsType(result.Result); + var payload = Assert.IsAssignableFrom>(okResult.Value); + Assert.Single(payload); + Assert.Equal("hash1", payload[0].Hash); + } + + [Fact] + public async Task TorrentsInfo_FilterCompleted_MatchesPausedUploadTorrents() + { + // Arrange + _qBittorrentMock.Setup(q => q.TorrentInfo()).ReturnsAsync(new List + { + new() + { + Hash = "hash1", + State = "pausedUP", + Progress = 1f + }, + new() + { + Hash = "hash2", + State = "downloading", + Progress = 0.4f + } + }); + + // Act + var result = await _controller.TorrentsInfo(new QBTorrentsInfoRequest + { + Filter = "completed" + }); + + // Assert + var okResult = Assert.IsType(result.Result); + var payload = Assert.IsAssignableFrom>(okResult.Value); + Assert.Single(payload); + Assert.Equal("hash1", payload[0].Hash); + } +} \ No newline at end of file diff --git a/server/RdtClient.Web/Controllers/QBittorrentController.cs b/server/RdtClient.Web/Controllers/QBittorrentController.cs index 6b1bfd4..110e775 100644 --- a/server/RdtClient.Web/Controllers/QBittorrentController.cs +++ b/server/RdtClient.Web/Controllers/QBittorrentController.cs @@ -158,10 +158,7 @@ public class QBittorrentController(ILogger logger, QBitto { var results = await qBittorrent.TorrentInfo(); - if(!String.IsNullOrWhiteSpace(request.Filter)) - { - results = results.Where(m => m.State != null && m.State.Equals(request.Filter, StringComparison.OrdinalIgnoreCase)).ToList(); - } + results = results.Where(m => MatchesFilter(m, request.Filter)).ToList(); if (!String.IsNullOrWhiteSpace(request.Category)) { @@ -193,11 +190,8 @@ public class QBittorrentController(ILogger logger, QBitto public async Task> TorrentsCount([FromQuery] QBTorrentsCountRequest request) { var results = await qBittorrent.TorrentInfo(); + results = results.Where(m => MatchesFilter(m, request.Filter)).ToList(); - if (!String.IsNullOrWhiteSpace(request.Filter)) - { - results = results.Where(m => m.State != null && m.State.Equals(request.Filter, StringComparison.OrdinalIgnoreCase)).ToList(); - } return results.Count; } @@ -433,12 +427,38 @@ public class QBittorrentController(ILogger logger, QBitto [Authorize(Policy = "AuthSetting")] [Route("torrents/filePrio")] [HttpGet] - [HttpPost] - public ActionResult TorrentsFilePrio() + public async Task TorrentsFilePrio([FromQuery] QBTorrentsFilePrioRequest request) { + if (String.IsNullOrWhiteSpace(request.Hash) || String.IsNullOrWhiteSpace(request.Id) || request.Priority == null) + { + return BadRequest(); + } + + var fileIds = request.Id + .Split('|', StringSplitOptions.RemoveEmptyEntries) + .Select(value => Int32.TryParse(value, out var parsedValue) ? parsedValue : (Int32?)null) + .Where(value => value.HasValue) + .Select(value => value!.Value) + .ToList(); + + if (fileIds.Count == 0) + { + return BadRequest(); + } + + await qBittorrent.TorrentsFilePrio(request.Hash, fileIds, request.Priority.Value); + return Ok(); } + [Authorize(Policy = "AuthSetting")] + [Route("torrents/filePrio")] + [HttpPost] + public async Task TorrentsFilePrioPost([FromForm] QBTorrentsFilePrioRequest request) + { + return await TorrentsFilePrio(request); + } + [Authorize(Policy = "AuthSetting")] [Route("torrents/setCategory")] [HttpGet] @@ -626,6 +646,31 @@ public class QBittorrentController(ILogger logger, QBitto { return await TorrentsTrackers(request); } + + private static Boolean MatchesFilter(TorrentInfo torrent, String? filter) + { + if (String.IsNullOrWhiteSpace(filter) || filter.Equals("all", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return filter.ToLowerInvariant() switch + { + "downloading" => torrent.Progress < 1f && !String.Equals(torrent.State, "error", StringComparison.OrdinalIgnoreCase), + "completed" => torrent.Progress >= 1f || torrent.CompletionOn.HasValue, + "paused" => torrent.State?.StartsWith("paused", StringComparison.OrdinalIgnoreCase) == true, + "active" => (torrent.Dlspeed ?? 0) > 0 || (torrent.Upspeed ?? 0) > 0, + "inactive" => (torrent.Dlspeed ?? 0) <= 0 && (torrent.Upspeed ?? 0) <= 0, + "resumed" => torrent.State?.StartsWith("paused", StringComparison.OrdinalIgnoreCase) != true && + !String.Equals(torrent.State, "error", StringComparison.OrdinalIgnoreCase), + "stalled" => String.Equals(torrent.State, "stalledDL", StringComparison.OrdinalIgnoreCase) || + String.Equals(torrent.State, "stalledUP", StringComparison.OrdinalIgnoreCase), + "stalled_uploading" => String.Equals(torrent.State, "stalledUP", StringComparison.OrdinalIgnoreCase), + "stalled_downloading" => String.Equals(torrent.State, "stalledDL", StringComparison.OrdinalIgnoreCase), + "errored" => String.Equals(torrent.State, "error", StringComparison.OrdinalIgnoreCase), + _ => String.Equals(torrent.State, filter, StringComparison.OrdinalIgnoreCase) + }; + } } public class QBAuthLoginRequest @@ -652,6 +697,13 @@ public class QBTorrentsHashRequest public String? Hash { get; set; } } +public class QBTorrentsFilePrioRequest +{ + public String? Hash { get; set; } + public String? Id { get; set; } + public Int32? Priority { get; set; } +} + public class QBTorrentsDeleteRequest { public String? Hashes { get; set; }