Fix qBittorrent compatibility for CleanupArr workflows

This commit is contained in:
jwmann 2026-04-23 22:13:38 -04:00
parent 145ea9a8ff
commit 797c58e654
No known key found for this signature in database
GPG key ID: FEAF88A73A68BD1B
7 changed files with 298 additions and 22 deletions

View file

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

View file

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

View file

@ -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<DebridClientFile>
{
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);
}
}

View file

@ -330,8 +330,6 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
public async Task<IList<TorrentFileItem>?> TorrentFileContents(String hash)
{
var results = new List<TorrentFileItem>();
var torrent = await torrents.GetByHash(hash);
if (torrent == null || torrent.Type != DownloadType.Torrent)
@ -339,17 +337,19 @@ public class QBittorrent(ILogger<QBittorrent> 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?> TorrentProperties(String hash)
@ -385,6 +385,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
AdditionDate = torrent.Added.ToUnixTimeSeconds(),
Comment = "RealDebridClient <https://github.com/rogerfar/rdt-client>",
CompletionDate = torrent.Completed?.ToUnixTimeSeconds() ?? -1,
IsPrivate = false,
CreatedBy = "RealDebridClient <https://github.com/rogerfar/rdt-client>",
CreationDate = torrent.Added.ToUnixTimeSeconds(),
DlLimit = -1,
@ -420,6 +421,11 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
return result;
}
public async Task TorrentsFilePrio(String hash, IReadOnlyCollection<Int32> fileIds, Int32 priority)
{
await torrents.UpdateFileSelection(hash, fileIds, priority > 0);
}
public async Task TorrentsDelete(String hash, Boolean deleteFiles)
{
if (deleteFiles)

View file

@ -843,6 +843,43 @@ public class Torrents(
await torrentData.UpdateFilesSelected(torrentId, datetime);
}
public async Task<Boolean> UpdateFileSelection(String hash, IReadOnlyCollection<Int32> 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);

View file

@ -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<QBittorrent> _qBittorrentMock;
public QBittorrentControllerTest()
{
_qBittorrentMock = new(new Mock<ILogger<QBittorrent>>().Object, null!, null!, null!, null!);
_controller = new(
new Mock<ILogger<QBittorrentController>>().Object,
_qBittorrentMock.Object,
new Mock<IHttpClientFactory>().Object);
_controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext()
};
}
[Fact]
public async Task TorrentsInfo_FilterAll_DoesNotFilterOutResults()
{
// Arrange
_qBittorrentMock.Setup(q => q.TorrentInfo()).ReturnsAsync(new List<TorrentInfo>
{
new()
{
Hash = "hash1",
State = "pausedUP",
Progress = 1f
}
});
// Act
var result = await _controller.TorrentsInfo(new QBTorrentsInfoRequest
{
Filter = "all",
Hashes = "hash1"
});
// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var payload = Assert.IsAssignableFrom<IList<TorrentInfo>>(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<TorrentInfo>
{
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<OkObjectResult>(result.Result);
var payload = Assert.IsAssignableFrom<IList<TorrentInfo>>(okResult.Value);
Assert.Single(payload);
Assert.Equal("hash1", payload[0].Hash);
}
}

View file

@ -158,10 +158,7 @@ public class QBittorrentController(ILogger<QBittorrentController> 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<QBittorrentController> logger, QBitto
public async Task<ActionResult<Int32>> 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<QBittorrentController> logger, QBitto
[Authorize(Policy = "AuthSetting")]
[Route("torrents/filePrio")]
[HttpGet]
[HttpPost]
public ActionResult TorrentsFilePrio()
public async Task<ActionResult> 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<ActionResult> TorrentsFilePrioPost([FromForm] QBTorrentsFilePrioRequest request)
{
return await TorrentsFilePrio(request);
}
[Authorize(Policy = "AuthSetting")]
[Route("torrents/setCategory")]
[HttpGet]
@ -626,6 +646,31 @@ public class QBittorrentController(ILogger<QBittorrentController> 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; }