Fix qBittorrent compatibility for CleanupArr workflows
This commit is contained in:
parent
145ea9a8ff
commit
797c58e654
7 changed files with 298 additions and 22 deletions
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue