mirror of
https://github.com/lantean-code/qbtmud.git
synced 2025-10-25 00:53:42 +00:00
Compare commits
15 Commits
1.2.0
...
feature/v5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
075ea9f855 | ||
|
|
d01204a703 | ||
|
|
ab1c594b07 | ||
|
|
6a5d8b2610 | ||
|
|
b8412bb232 | ||
|
|
e64a13c7c9 | ||
|
|
e4ea79a8ed | ||
|
|
0976b72411 | ||
|
|
965fbcd010 | ||
|
|
3d0dbde9f4 | ||
|
|
5b4fbde7b2 | ||
|
|
0db0ad4374 | ||
|
|
c390d83e4d | ||
|
|
8dd29c238d | ||
|
|
fca17edfd1 |
506
Lantean.QBTMud.Test/Services/PeerDataManagerTests.cs
Normal file
506
Lantean.QBTMud.Test/Services/PeerDataManagerTests.cs
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
using AwesomeAssertions;
|
||||||
|
using Lantean.QBTMud.Models;
|
||||||
|
using Lantean.QBTMud.Services;
|
||||||
|
using QbtPeer = Lantean.QBitTorrentClient.Models.Peer;
|
||||||
|
using QbtTorrentPeers = Lantean.QBitTorrentClient.Models.TorrentPeers;
|
||||||
|
|
||||||
|
namespace Lantean.QBTMud.Test.Services
|
||||||
|
{
|
||||||
|
public class PeerDataManagerTests
|
||||||
|
{
|
||||||
|
private readonly PeerDataManager _target = new PeerDataManager();
|
||||||
|
|
||||||
|
// ---------- CreatePeerList ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_NullPeers_WHEN_CreatePeerList_THEN_EmptyPeerList()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var input = new QbtTorrentPeers(
|
||||||
|
fullUpdate: false,
|
||||||
|
peers: null,
|
||||||
|
peersRemoved: null,
|
||||||
|
requestId: 1,
|
||||||
|
showFlags: null);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = _target.CreatePeerList(input);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Peers.Should().NotBeNull();
|
||||||
|
result.Peers.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_MultiplePeers_WHEN_CreatePeerList_THEN_MapsAllFieldsAndKeys()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var p1 = new QbtPeer(
|
||||||
|
client: "qBittorrent/4.6.0",
|
||||||
|
connection: "TCP",
|
||||||
|
country: "UK",
|
||||||
|
countryCode: "GB",
|
||||||
|
downloadSpeed: 1200,
|
||||||
|
downloaded: 11,
|
||||||
|
files: "file1.mkv",
|
||||||
|
flags: "D U H",
|
||||||
|
flagsDescription: "downloading, uploading, high priority",
|
||||||
|
iPAddress: "1.1.1.1",
|
||||||
|
i2pDestination: null,
|
||||||
|
clientId: "ClientA",
|
||||||
|
port: 6881,
|
||||||
|
progress: 0.5f,
|
||||||
|
relevance: 0.7f,
|
||||||
|
uploadSpeed: 3400,
|
||||||
|
uploaded: 22);
|
||||||
|
|
||||||
|
var p2 = new QbtPeer(
|
||||||
|
client: "Transmission/4.0",
|
||||||
|
connection: "uTP",
|
||||||
|
country: "Canada",
|
||||||
|
countryCode: "CA",
|
||||||
|
downloadSpeed: 2200,
|
||||||
|
downloaded: 33,
|
||||||
|
files: "file2.mp4",
|
||||||
|
flags: "Q",
|
||||||
|
flagsDescription: "queued",
|
||||||
|
iPAddress: "2.2.2.2",
|
||||||
|
i2pDestination: null,
|
||||||
|
clientId: "ClientB",
|
||||||
|
port: 51413,
|
||||||
|
progress: 0.9f,
|
||||||
|
relevance: 0.1f,
|
||||||
|
uploadSpeed: 100,
|
||||||
|
uploaded: 44);
|
||||||
|
|
||||||
|
var dict = new Dictionary<string, QbtPeer>
|
||||||
|
{
|
||||||
|
["1.1.1.1:6881"] = p1,
|
||||||
|
["2.2.2.2:51413"] = p2
|
||||||
|
};
|
||||||
|
|
||||||
|
var input = new QbtTorrentPeers(
|
||||||
|
fullUpdate: true,
|
||||||
|
peers: dict,
|
||||||
|
peersRemoved: null,
|
||||||
|
requestId: 2,
|
||||||
|
showFlags: true);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = _target.CreatePeerList(input);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Peers.Count.Should().Be(2);
|
||||||
|
|
||||||
|
result.Peers.Should().ContainKey("1.1.1.1:6881");
|
||||||
|
var a = result.Peers["1.1.1.1:6881"];
|
||||||
|
a.Key.Should().Be("1.1.1.1:6881");
|
||||||
|
a.Client.Should().Be("qBittorrent/4.6.0");
|
||||||
|
a.ClientId.Should().Be("ClientA");
|
||||||
|
a.Connection.Should().Be("TCP");
|
||||||
|
a.Country.Should().Be("UK");
|
||||||
|
a.CountryCode.Should().Be("GB");
|
||||||
|
a.Downloaded.Should().Be(11);
|
||||||
|
a.DownloadSpeed.Should().Be(1200);
|
||||||
|
a.Files.Should().Be("file1.mkv");
|
||||||
|
a.Flags.Should().Be("D U H");
|
||||||
|
a.FlagsDescription.Should().Be("downloading, uploading, high priority");
|
||||||
|
a.IPAddress.Should().Be("1.1.1.1");
|
||||||
|
a.Port.Should().Be(6881);
|
||||||
|
a.Progress.Should().Be(0.5f);
|
||||||
|
a.Relevance.Should().Be(0.7f);
|
||||||
|
a.Uploaded.Should().Be(22);
|
||||||
|
a.UploadSpeed.Should().Be(3400);
|
||||||
|
|
||||||
|
result.Peers.Should().ContainKey("2.2.2.2:51413");
|
||||||
|
var b = result.Peers["2.2.2.2:51413"];
|
||||||
|
b.Key.Should().Be("2.2.2.2:51413");
|
||||||
|
b.Client.Should().Be("Transmission/4.0");
|
||||||
|
b.ClientId.Should().Be("ClientB");
|
||||||
|
b.Connection.Should().Be("uTP");
|
||||||
|
b.Country.Should().Be("Canada");
|
||||||
|
b.CountryCode.Should().Be("CA");
|
||||||
|
b.Downloaded.Should().Be(33);
|
||||||
|
b.DownloadSpeed.Should().Be(2200);
|
||||||
|
b.Files.Should().Be("file2.mp4");
|
||||||
|
b.Flags.Should().Be("Q");
|
||||||
|
b.FlagsDescription.Should().Be("queued");
|
||||||
|
b.IPAddress.Should().Be("2.2.2.2");
|
||||||
|
b.Port.Should().Be(51413);
|
||||||
|
b.Progress.Should().Be(0.9f);
|
||||||
|
b.Relevance.Should().Be(0.1f);
|
||||||
|
b.Uploaded.Should().Be(44);
|
||||||
|
b.UploadSpeed.Should().Be(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_PeerWithNullNumerics_WHEN_CreatePeerList_THEN_DefaultsToZeros()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var nullish = new QbtPeer(
|
||||||
|
client: "ClientX",
|
||||||
|
connection: "TCP",
|
||||||
|
country: null,
|
||||||
|
countryCode: null,
|
||||||
|
downloadSpeed: null, // -> 0
|
||||||
|
downloaded: null, // -> 0
|
||||||
|
files: "file.dat",
|
||||||
|
flags: "",
|
||||||
|
flagsDescription: "",
|
||||||
|
iPAddress: "9.9.9.9",
|
||||||
|
i2pDestination: null,
|
||||||
|
clientId: "CID",
|
||||||
|
port: null, // -> 0
|
||||||
|
progress: null, // -> 0
|
||||||
|
relevance: null, // -> 0
|
||||||
|
uploadSpeed: null, // -> 0
|
||||||
|
uploaded: null); // -> 0
|
||||||
|
|
||||||
|
var input = new QbtTorrentPeers(
|
||||||
|
fullUpdate: false,
|
||||||
|
peers: new Dictionary<string, QbtPeer> { ["9.9.9.1:0"] = nullish },
|
||||||
|
peersRemoved: null,
|
||||||
|
requestId: 3,
|
||||||
|
showFlags: null);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = _target.CreatePeerList(input);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
result.Peers.Should().ContainKey("9.9.9.1:0");
|
||||||
|
var p = result.Peers["9.9.9.1:0"];
|
||||||
|
p.Client.Should().Be("ClientX");
|
||||||
|
p.Connection.Should().Be("TCP");
|
||||||
|
p.DownloadSpeed.Should().Be(0);
|
||||||
|
p.Downloaded.Should().Be(0);
|
||||||
|
p.Port.Should().Be(0);
|
||||||
|
p.Progress.Should().Be(0f);
|
||||||
|
p.Relevance.Should().Be(0f);
|
||||||
|
p.UploadSpeed.Should().Be(0);
|
||||||
|
p.Uploaded.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- MergeTorrentPeers ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_NoChanges_WHEN_MergeTorrentPeers_THEN_DoesNothing()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var peerList = new PeerList(new Dictionary<string, Peer>
|
||||||
|
{
|
||||||
|
["1.1.1.1:6881"] = new Peer(
|
||||||
|
key: "1.1.1.1:6881",
|
||||||
|
client: "qB",
|
||||||
|
clientId: "A",
|
||||||
|
connection: "TCP",
|
||||||
|
country: "UK",
|
||||||
|
countryCode: "GB",
|
||||||
|
downloaded: 1,
|
||||||
|
downloadSpeed: 2,
|
||||||
|
files: "f",
|
||||||
|
flags: "D",
|
||||||
|
flagsDescription: "down",
|
||||||
|
iPAddress: "1.1.1.1",
|
||||||
|
port: 6881,
|
||||||
|
progress: 0.1f,
|
||||||
|
relevance: 0.2f,
|
||||||
|
uploaded: 3,
|
||||||
|
uploadSpeed: 4)
|
||||||
|
});
|
||||||
|
|
||||||
|
var input = new QbtTorrentPeers(
|
||||||
|
fullUpdate: false,
|
||||||
|
peers: null,
|
||||||
|
peersRemoved: null,
|
||||||
|
requestId: 10,
|
||||||
|
showFlags: null);
|
||||||
|
|
||||||
|
// act
|
||||||
|
_target.MergeTorrentPeers(input, peerList);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
peerList.Peers.Count.Should().Be(1);
|
||||||
|
peerList.Peers.Should().ContainKey("1.1.1.1:6881");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_PeersRemovedWithExistingAndMissing_WHEN_MergeTorrentPeers_THEN_RemovesExistingOnly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var peerList = new PeerList(new Dictionary<string, Peer>
|
||||||
|
{
|
||||||
|
["a"] = new Peer("a", "c", "id", "TCP", null, null, 0, 0, "f", "F", "FD", "10.0.0.1", 1111, 0, 0, 0, 0),
|
||||||
|
["b"] = new Peer("b", "c2", "id2", "uTP", null, null, 0, 0, "f2", "F2", "FD2", "10.0.0.2", 2222, 0, 0, 0, 0),
|
||||||
|
});
|
||||||
|
|
||||||
|
var input = new QbtTorrentPeers(
|
||||||
|
fullUpdate: false,
|
||||||
|
peers: null,
|
||||||
|
peersRemoved: new List<string> { "a", "missing" },
|
||||||
|
requestId: 11,
|
||||||
|
showFlags: null);
|
||||||
|
|
||||||
|
// act
|
||||||
|
_target.MergeTorrentPeers(input, peerList);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
peerList.Peers.Count.Should().Be(1);
|
||||||
|
peerList.Peers.Should().ContainKey("b");
|
||||||
|
peerList.Peers.Should().NotContainKey("a");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_NewPeers_WHEN_MergeTorrentPeers_THEN_AddsAllWithProperMapping()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var peerList = new PeerList(new Dictionary<string, Peer>());
|
||||||
|
|
||||||
|
var q1 = new QbtPeer(
|
||||||
|
client: "Client1",
|
||||||
|
connection: "TCP",
|
||||||
|
country: "US",
|
||||||
|
countryCode: "US",
|
||||||
|
downloadSpeed: 1000,
|
||||||
|
downloaded: 10,
|
||||||
|
files: "a.mkv",
|
||||||
|
flags: "D",
|
||||||
|
flagsDescription: "down",
|
||||||
|
iPAddress: "3.3.3.3",
|
||||||
|
i2pDestination: null,
|
||||||
|
clientId: "ID1",
|
||||||
|
port: 6000,
|
||||||
|
progress: 0.4f,
|
||||||
|
relevance: 0.9f,
|
||||||
|
uploadSpeed: 50,
|
||||||
|
uploaded: 5);
|
||||||
|
|
||||||
|
var q2 = new QbtPeer(
|
||||||
|
client: "Client2",
|
||||||
|
connection: "uTP",
|
||||||
|
country: "DE",
|
||||||
|
countryCode: "DE",
|
||||||
|
downloadSpeed: 2000,
|
||||||
|
downloaded: 20,
|
||||||
|
files: "b.mp4",
|
||||||
|
flags: "",
|
||||||
|
flagsDescription: "",
|
||||||
|
iPAddress: "4.4.4.4",
|
||||||
|
i2pDestination: null,
|
||||||
|
clientId: "ID2",
|
||||||
|
port: 7000,
|
||||||
|
progress: 0.8f,
|
||||||
|
relevance: 0.1f,
|
||||||
|
uploadSpeed: 150,
|
||||||
|
uploaded: 15);
|
||||||
|
|
||||||
|
var input = new QbtTorrentPeers(
|
||||||
|
fullUpdate: true,
|
||||||
|
peers: new Dictionary<string, QbtPeer>
|
||||||
|
{
|
||||||
|
["3.3.3.3:6000"] = q1,
|
||||||
|
["4.4.4.4:7000"] = q2
|
||||||
|
},
|
||||||
|
peersRemoved: null,
|
||||||
|
requestId: 12,
|
||||||
|
showFlags: true);
|
||||||
|
|
||||||
|
// act
|
||||||
|
_target.MergeTorrentPeers(input, peerList);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
peerList.Peers.Count.Should().Be(2);
|
||||||
|
|
||||||
|
var p1 = peerList.Peers["3.3.3.3:6000"];
|
||||||
|
p1.Client.Should().Be("Client1");
|
||||||
|
p1.ClientId.Should().Be("ID1");
|
||||||
|
p1.Connection.Should().Be("TCP");
|
||||||
|
p1.Country.Should().Be("US");
|
||||||
|
p1.CountryCode.Should().Be("US");
|
||||||
|
p1.Downloaded.Should().Be(10);
|
||||||
|
p1.DownloadSpeed.Should().Be(1000);
|
||||||
|
p1.Files.Should().Be("a.mkv");
|
||||||
|
p1.Flags.Should().Be("D");
|
||||||
|
p1.FlagsDescription.Should().Be("down");
|
||||||
|
p1.IPAddress.Should().Be("3.3.3.3");
|
||||||
|
p1.Port.Should().Be(6000);
|
||||||
|
p1.Progress.Should().Be(0.4f);
|
||||||
|
p1.Relevance.Should().Be(0.9f);
|
||||||
|
p1.Uploaded.Should().Be(5);
|
||||||
|
p1.UploadSpeed.Should().Be(50);
|
||||||
|
|
||||||
|
var p2 = peerList.Peers["4.4.4.4:7000"];
|
||||||
|
p2.Client.Should().Be("Client2");
|
||||||
|
p2.ClientId.Should().Be("ID2");
|
||||||
|
p2.Connection.Should().Be("uTP");
|
||||||
|
p2.Country.Should().Be("DE");
|
||||||
|
p2.CountryCode.Should().Be("DE");
|
||||||
|
p2.Downloaded.Should().Be(20);
|
||||||
|
p2.DownloadSpeed.Should().Be(2000);
|
||||||
|
p2.Files.Should().Be("b.mp4");
|
||||||
|
p2.Flags.Should().Be("");
|
||||||
|
p2.FlagsDescription.Should().Be("");
|
||||||
|
p2.IPAddress.Should().Be("4.4.4.4");
|
||||||
|
p2.Port.Should().Be(7000);
|
||||||
|
p2.Progress.Should().Be(0.8f);
|
||||||
|
p2.Relevance.Should().Be(0.1f);
|
||||||
|
p2.Uploaded.Should().Be(15);
|
||||||
|
p2.UploadSpeed.Should().Be(150);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_ExistingPeer_AND_UpdateWithPartialNulls_WHEN_MergeTorrentPeers_THEN_OnlyNonNullFieldsChange()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var existing = new Peer(
|
||||||
|
key: "5.5.5.5:6881",
|
||||||
|
client: "OldClient",
|
||||||
|
clientId: "OldID",
|
||||||
|
connection: "TCP",
|
||||||
|
country: "ES",
|
||||||
|
countryCode: "ES",
|
||||||
|
downloaded: 111,
|
||||||
|
downloadSpeed: 222,
|
||||||
|
files: "old.dat",
|
||||||
|
flags: "X",
|
||||||
|
flagsDescription: "old",
|
||||||
|
iPAddress: "5.5.5.5",
|
||||||
|
port: 6881,
|
||||||
|
progress: 0.11f,
|
||||||
|
relevance: 0.22f,
|
||||||
|
uploaded: 333,
|
||||||
|
uploadSpeed: 444);
|
||||||
|
|
||||||
|
var peerList = new PeerList(new Dictionary<string, Peer>
|
||||||
|
{
|
||||||
|
[existing.Key] = existing
|
||||||
|
});
|
||||||
|
|
||||||
|
var update = new QbtPeer(
|
||||||
|
client: null, // keep OldClient
|
||||||
|
connection: "uTP", // overwrite
|
||||||
|
country: null, // keep ES
|
||||||
|
countryCode: "FR", // overwrite
|
||||||
|
downloadSpeed: null, // keep 222
|
||||||
|
downloaded: 999, // overwrite
|
||||||
|
files: null, // keep old.dat
|
||||||
|
flags: "N", // overwrite
|
||||||
|
flagsDescription: null, // keep old
|
||||||
|
iPAddress: null, // keep 5.5.5.5
|
||||||
|
i2pDestination: null,
|
||||||
|
clientId: "NewID", // overwrite
|
||||||
|
port: null, // keep 6881
|
||||||
|
progress: 0.77f, // overwrite
|
||||||
|
relevance: null, // keep 0.22
|
||||||
|
uploadSpeed: 888, // overwrite
|
||||||
|
uploaded: null); // keep 333
|
||||||
|
|
||||||
|
var input = new QbtTorrentPeers(
|
||||||
|
fullUpdate: false,
|
||||||
|
peers: new Dictionary<string, QbtPeer> { [existing.Key] = update },
|
||||||
|
peersRemoved: null,
|
||||||
|
requestId: 13,
|
||||||
|
showFlags: null);
|
||||||
|
|
||||||
|
// act
|
||||||
|
_target.MergeTorrentPeers(input, peerList);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
var p = peerList.Peers[existing.Key];
|
||||||
|
p.Client.Should().Be("OldClient");
|
||||||
|
p.ClientId.Should().Be("NewID");
|
||||||
|
p.Connection.Should().Be("uTP");
|
||||||
|
p.Country.Should().Be("ES");
|
||||||
|
p.CountryCode.Should().Be("FR");
|
||||||
|
p.DownloadSpeed.Should().Be(222);
|
||||||
|
p.Downloaded.Should().Be(999);
|
||||||
|
p.Files.Should().Be("old.dat");
|
||||||
|
p.Flags.Should().Be("N");
|
||||||
|
p.FlagsDescription.Should().Be("old");
|
||||||
|
p.IPAddress.Should().Be("5.5.5.5");
|
||||||
|
p.Port.Should().Be(6881);
|
||||||
|
p.Progress.Should().Be(0.77f);
|
||||||
|
p.Relevance.Should().Be(0.22f);
|
||||||
|
p.UploadSpeed.Should().Be(888);
|
||||||
|
p.Uploaded.Should().Be(333);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_KeyRemovedThenReaddedInSameMerge_WHEN_MergeTorrentPeers_THEN_PresentWithNewValues()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var key = "6.6.6.6:6001";
|
||||||
|
|
||||||
|
var oldPeer = new Peer(
|
||||||
|
key: key,
|
||||||
|
client: "Old",
|
||||||
|
clientId: "OID",
|
||||||
|
connection: "TCP",
|
||||||
|
country: null,
|
||||||
|
countryCode: null,
|
||||||
|
downloaded: 1,
|
||||||
|
downloadSpeed: 2,
|
||||||
|
files: "old",
|
||||||
|
flags: "O",
|
||||||
|
flagsDescription: "old",
|
||||||
|
iPAddress: "6.6.6.6",
|
||||||
|
port: 6001,
|
||||||
|
progress: 0.1f,
|
||||||
|
relevance: 0.2f,
|
||||||
|
uploaded: 3,
|
||||||
|
uploadSpeed: 4);
|
||||||
|
|
||||||
|
var peerList = new PeerList(new Dictionary<string, Peer> { [key] = oldPeer });
|
||||||
|
|
||||||
|
var newPeer = new QbtPeer(
|
||||||
|
client: "New",
|
||||||
|
connection: "uTP",
|
||||||
|
country: "NL",
|
||||||
|
countryCode: "NL",
|
||||||
|
downloadSpeed: 999,
|
||||||
|
downloaded: 111,
|
||||||
|
files: "new",
|
||||||
|
flags: "N",
|
||||||
|
flagsDescription: "new",
|
||||||
|
iPAddress: "6.6.6.6",
|
||||||
|
i2pDestination: null,
|
||||||
|
clientId: "NID",
|
||||||
|
port: 6001,
|
||||||
|
progress: 0.9f,
|
||||||
|
relevance: 0.8f,
|
||||||
|
uploadSpeed: 777,
|
||||||
|
uploaded: 333);
|
||||||
|
|
||||||
|
var input = new QbtTorrentPeers(
|
||||||
|
fullUpdate: false,
|
||||||
|
peers: new Dictionary<string, QbtPeer> { [key] = newPeer },
|
||||||
|
peersRemoved: new List<string> { key },
|
||||||
|
requestId: 14,
|
||||||
|
showFlags: null);
|
||||||
|
|
||||||
|
// act
|
||||||
|
_target.MergeTorrentPeers(input, peerList);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
peerList.Peers.Should().ContainKey(key);
|
||||||
|
var p = peerList.Peers[key];
|
||||||
|
p.Client.Should().Be("New");
|
||||||
|
p.ClientId.Should().Be("NID");
|
||||||
|
p.Connection.Should().Be("uTP");
|
||||||
|
p.Country.Should().Be("NL");
|
||||||
|
p.CountryCode.Should().Be("NL");
|
||||||
|
p.Downloaded.Should().Be(111);
|
||||||
|
p.DownloadSpeed.Should().Be(999);
|
||||||
|
p.Files.Should().Be("new");
|
||||||
|
p.Flags.Should().Be("N");
|
||||||
|
p.FlagsDescription.Should().Be("new");
|
||||||
|
p.IPAddress.Should().Be("6.6.6.6");
|
||||||
|
p.Port.Should().Be(6001);
|
||||||
|
p.Progress.Should().Be(0.9f);
|
||||||
|
p.Relevance.Should().Be(0.8f);
|
||||||
|
p.Uploaded.Should().Be(333);
|
||||||
|
p.UploadSpeed.Should().Be(777);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
832
Lantean.QBTMud.Test/Services/PreferencesDataManagerTests.cs
Normal file
832
Lantean.QBTMud.Test/Services/PreferencesDataManagerTests.cs
Normal file
@@ -0,0 +1,832 @@
|
|||||||
|
using AwesomeAssertions;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
using Lantean.QBTMud.Services;
|
||||||
|
|
||||||
|
namespace Lantean.QBTMud.Test.Services
|
||||||
|
{
|
||||||
|
public class PreferencesDataManagerTests
|
||||||
|
{
|
||||||
|
private readonly PreferencesDataManager _target = new PreferencesDataManager();
|
||||||
|
|
||||||
|
// ---------- Builders ----------
|
||||||
|
|
||||||
|
private static UpdatePreferences BuildAllSetA()
|
||||||
|
{
|
||||||
|
return new UpdatePreferences
|
||||||
|
{
|
||||||
|
AddToTopOfQueue = true,
|
||||||
|
AddStoppedEnabled = true,
|
||||||
|
AddTrackers = "a, b, c",
|
||||||
|
AddTrackersEnabled = true,
|
||||||
|
AltDlLimit = 1,
|
||||||
|
AltUpLimit = 2,
|
||||||
|
AlternativeWebuiEnabled = true,
|
||||||
|
AlternativeWebuiPath = "/alt-a",
|
||||||
|
AnnounceIp = "1.2.3.4",
|
||||||
|
AnnouncePort = 6881,
|
||||||
|
AnnounceToAllTiers = true,
|
||||||
|
AnnounceToAllTrackers = true,
|
||||||
|
AnonymousMode = true,
|
||||||
|
AppInstanceName = "app-a",
|
||||||
|
AsyncIoThreads = 3,
|
||||||
|
AutoDeleteMode = 4,
|
||||||
|
AutoTmmEnabled = true,
|
||||||
|
AutorunEnabled = true,
|
||||||
|
AutorunOnTorrentAddedEnabled = true,
|
||||||
|
AutorunOnTorrentAddedProgram = "prog-added-a",
|
||||||
|
AutorunProgram = "prog-a",
|
||||||
|
BannedIPs = "10.0.0.1;10.0.0.2",
|
||||||
|
BdecodeDepthLimit = 5,
|
||||||
|
BdecodeTokenLimit = 6,
|
||||||
|
BittorrentProtocol = 7,
|
||||||
|
BlockPeersOnPrivilegedPorts = true,
|
||||||
|
BypassAuthSubnetWhitelist = "192.168.0.0/24",
|
||||||
|
BypassAuthSubnetWhitelistEnabled = true,
|
||||||
|
BypassLocalAuth = true,
|
||||||
|
CategoryChangedTmmEnabled = true,
|
||||||
|
CheckingMemoryUse = 8,
|
||||||
|
ConnectionSpeed = 9,
|
||||||
|
CurrentInterfaceAddress = "eth0-addr-a",
|
||||||
|
CurrentInterfaceName = "eth0-name-a",
|
||||||
|
CurrentNetworkInterface = "eth0-a",
|
||||||
|
Dht = true,
|
||||||
|
DhtBootstrapNodes = "node-a;node-b",
|
||||||
|
DiskCache = 10,
|
||||||
|
DiskCacheTtl = 11,
|
||||||
|
DiskIoReadMode = 12,
|
||||||
|
DiskIoType = 13,
|
||||||
|
DiskIoWriteMode = 14,
|
||||||
|
DiskQueueSize = 15,
|
||||||
|
DlLimit = 16,
|
||||||
|
DontCountSlowTorrents = true,
|
||||||
|
DyndnsDomain = "dyndns-a",
|
||||||
|
DyndnsEnabled = true,
|
||||||
|
DyndnsPassword = "dyndns-pass-a",
|
||||||
|
DyndnsService = 17,
|
||||||
|
DyndnsUsername = "dyndns-user-a",
|
||||||
|
EmbeddedTrackerPort = 18,
|
||||||
|
EmbeddedTrackerPortForwarding = true,
|
||||||
|
EnableCoalesceReadWrite = true,
|
||||||
|
EnableEmbeddedTracker = true,
|
||||||
|
EnableMultiConnectionsFromSameIp = true,
|
||||||
|
EnablePieceExtentAffinity = true,
|
||||||
|
EnableUploadSuggestions = true,
|
||||||
|
Encryption = 19,
|
||||||
|
ExcludedFileNames = "*.tmp;*.bak",
|
||||||
|
ExcludedFileNamesEnabled = true,
|
||||||
|
ExportDir = "/export-a",
|
||||||
|
ExportDirFin = "/export-fin-a",
|
||||||
|
FileLogAge = 20,
|
||||||
|
FileLogAgeType = 21,
|
||||||
|
FileLogBackupEnabled = true,
|
||||||
|
FileLogDeleteOld = true,
|
||||||
|
FileLogEnabled = true,
|
||||||
|
FileLogMaxSize = 22,
|
||||||
|
FileLogPath = "/log-a",
|
||||||
|
FilePoolSize = 23,
|
||||||
|
HashingThreads = 24,
|
||||||
|
I2pAddress = "i2p-addr-a",
|
||||||
|
I2pEnabled = true,
|
||||||
|
I2pInboundLength = 25,
|
||||||
|
I2pInboundQuantity = 26,
|
||||||
|
I2pMixedMode = true,
|
||||||
|
I2pOutboundLength = 27,
|
||||||
|
I2pOutboundQuantity = 28,
|
||||||
|
I2pPort = 29,
|
||||||
|
IdnSupportEnabled = true,
|
||||||
|
IncompleteFilesExt = true,
|
||||||
|
UseUnwantedFolder = true,
|
||||||
|
IpFilterEnabled = true,
|
||||||
|
IpFilterPath = "/ipfilter-a",
|
||||||
|
IpFilterTrackers = true,
|
||||||
|
LimitLanPeers = true,
|
||||||
|
LimitTcpOverhead = true,
|
||||||
|
LimitUtpRate = true,
|
||||||
|
ListenPort = 30,
|
||||||
|
SslEnabled = true,
|
||||||
|
SslListenPort = 4430,
|
||||||
|
Locale = "en-A",
|
||||||
|
Lsd = true,
|
||||||
|
MailNotificationAuthEnabled = true,
|
||||||
|
MailNotificationEmail = "mail@a",
|
||||||
|
MailNotificationEnabled = true,
|
||||||
|
MailNotificationPassword = "mail-pass-a",
|
||||||
|
MailNotificationSender = "sender-a",
|
||||||
|
MailNotificationSmtp = "smtp-a",
|
||||||
|
MailNotificationSslEnabled = true,
|
||||||
|
MailNotificationUsername = "mail-user-a",
|
||||||
|
MarkOfTheWeb = true,
|
||||||
|
MaxActiveCheckingTorrents = 31,
|
||||||
|
MaxActiveDownloads = 32,
|
||||||
|
MaxActiveTorrents = 33,
|
||||||
|
MaxActiveUploads = 34,
|
||||||
|
MaxConcurrentHttpAnnounces = 35,
|
||||||
|
MaxConnec = 36,
|
||||||
|
MaxConnecPerTorrent = 37,
|
||||||
|
MaxInactiveSeedingTime = 38,
|
||||||
|
MaxInactiveSeedingTimeEnabled = true,
|
||||||
|
MaxRatio = 1.1f,
|
||||||
|
MaxRatioAct = 39,
|
||||||
|
MaxRatioEnabled = null, // kept null to avoid Validate() conflict in tests that don't call Validate
|
||||||
|
MaxSeedingTime = 40,
|
||||||
|
MaxSeedingTimeEnabled = null, // same as above
|
||||||
|
MaxUploads = 41,
|
||||||
|
MaxUploadsPerTorrent = 42,
|
||||||
|
MemoryWorkingSetLimit = 43,
|
||||||
|
MergeTrackers = true,
|
||||||
|
OutgoingPortsMax = 44,
|
||||||
|
OutgoingPortsMin = 45,
|
||||||
|
PeerTos = 46,
|
||||||
|
PeerTurnover = 47,
|
||||||
|
PeerTurnoverCutoff = 48,
|
||||||
|
PeerTurnoverInterval = 49,
|
||||||
|
PerformanceWarning = true,
|
||||||
|
Pex = true,
|
||||||
|
PreallocateAll = true,
|
||||||
|
ProxyAuthEnabled = true,
|
||||||
|
ProxyBittorrent = true,
|
||||||
|
ProxyHostnameLookup = true,
|
||||||
|
ProxyIp = "proxy-ip-a",
|
||||||
|
ProxyMisc = true,
|
||||||
|
ProxyPassword = "proxy-pass-a",
|
||||||
|
ProxyPeerConnections = true,
|
||||||
|
ProxyPort = 8080,
|
||||||
|
ProxyRss = true,
|
||||||
|
ProxyType = "http",
|
||||||
|
ProxyUsername = "proxy-user-a",
|
||||||
|
PythonExecutablePath = "/python-a",
|
||||||
|
QueueingEnabled = true,
|
||||||
|
RandomPort = true,
|
||||||
|
ReannounceWhenAddressChanged = true,
|
||||||
|
RecheckCompletedTorrents = true,
|
||||||
|
RefreshInterval = 50,
|
||||||
|
RequestQueueSize = 51,
|
||||||
|
ResolvePeerCountries = true,
|
||||||
|
ResumeDataStorageType = "fastresume",
|
||||||
|
RssAutoDownloadingEnabled = true,
|
||||||
|
RssDownloadRepackProperEpisodes = true,
|
||||||
|
RssFetchDelay = 5200L,
|
||||||
|
RssMaxArticlesPerFeed = 52,
|
||||||
|
RssProcessingEnabled = true,
|
||||||
|
RssRefreshInterval = 53,
|
||||||
|
RssSmartEpisodeFilters = "s01e01",
|
||||||
|
SavePath = "/save-a",
|
||||||
|
SavePathChangedTmmEnabled = true,
|
||||||
|
SaveResumeDataInterval = 54,
|
||||||
|
SaveStatisticsInterval = 540,
|
||||||
|
ScanDirs = new Dictionary<string, SaveLocation>
|
||||||
|
{
|
||||||
|
["watch"] = SaveLocation.Create(0),
|
||||||
|
["default"] = SaveLocation.Create(1),
|
||||||
|
["custom"] = SaveLocation.Create("/dls/a")
|
||||||
|
},
|
||||||
|
ScheduleFromHour = 1,
|
||||||
|
ScheduleFromMin = 2,
|
||||||
|
ScheduleToHour = 3,
|
||||||
|
ScheduleToMin = 4,
|
||||||
|
SchedulerDays = 5,
|
||||||
|
SchedulerEnabled = true,
|
||||||
|
SendBufferLowWatermark = 55,
|
||||||
|
SendBufferWatermark = 56,
|
||||||
|
SendBufferWatermarkFactor = 57,
|
||||||
|
SlowTorrentDlRateThreshold = 58,
|
||||||
|
SlowTorrentInactiveTimer = 59,
|
||||||
|
SlowTorrentUlRateThreshold = 60,
|
||||||
|
SocketBacklogSize = 61,
|
||||||
|
SocketReceiveBufferSize = 62,
|
||||||
|
SocketSendBufferSize = 63,
|
||||||
|
SsrfMitigation = true,
|
||||||
|
StopTrackerTimeout = 64,
|
||||||
|
TempPath = "/tmp-a",
|
||||||
|
TempPathEnabled = true,
|
||||||
|
TorrentChangedTmmEnabled = true,
|
||||||
|
TorrentContentLayout = "original",
|
||||||
|
TorrentContentRemoveOption = "to_trash",
|
||||||
|
TorrentFileSizeLimit = 65,
|
||||||
|
TorrentStopCondition = "metadata_received",
|
||||||
|
UpLimit = 66,
|
||||||
|
UploadChokingAlgorithm = 67,
|
||||||
|
UploadSlotsBehavior = 68,
|
||||||
|
Upnp = true,
|
||||||
|
UpnpLeaseDuration = 69,
|
||||||
|
UseCategoryPathsInManualMode = true,
|
||||||
|
UseHttps = true,
|
||||||
|
IgnoreSslErrors = true,
|
||||||
|
UseSubcategories = true,
|
||||||
|
UtpTcpMixedMode = 70,
|
||||||
|
ValidateHttpsTrackerCertificate = true,
|
||||||
|
WebUiAddress = "0.0.0.0",
|
||||||
|
WebUiApiKey = "api-key-a",
|
||||||
|
WebUiBanDuration = 71,
|
||||||
|
WebUiClickjackingProtectionEnabled = true,
|
||||||
|
WebUiCsrfProtectionEnabled = true,
|
||||||
|
WebUiCustomHttpHeaders = "X-Header: A",
|
||||||
|
WebUiDomainList = "a.example.com",
|
||||||
|
WebUiHostHeaderValidationEnabled = true,
|
||||||
|
WebUiHttpsCertPath = "/cert-a",
|
||||||
|
WebUiHttpsKeyPath = "/key-a",
|
||||||
|
WebUiMaxAuthFailCount = 72,
|
||||||
|
WebUiPort = 8081,
|
||||||
|
WebUiReverseProxiesList = "10.0.0.0/8",
|
||||||
|
WebUiReverseProxyEnabled = true,
|
||||||
|
WebUiSecureCookieEnabled = true,
|
||||||
|
WebUiSessionTimeout = 73,
|
||||||
|
WebUiUpnp = true,
|
||||||
|
WebUiUseCustomHttpHeadersEnabled = true,
|
||||||
|
WebUiUsername = "admin-a",
|
||||||
|
WebUiPassword = "pass-a",
|
||||||
|
ConfirmTorrentDeletion = true,
|
||||||
|
ConfirmTorrentRecheck = true,
|
||||||
|
StatusBarExternalIp = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UpdatePreferences BuildAllSetB_AllNonNull()
|
||||||
|
{
|
||||||
|
return new UpdatePreferences
|
||||||
|
{
|
||||||
|
AddToTopOfQueue = false,
|
||||||
|
AddStoppedEnabled = false,
|
||||||
|
AddTrackers = "x, y",
|
||||||
|
AddTrackersEnabled = false,
|
||||||
|
AltDlLimit = 101,
|
||||||
|
AltUpLimit = 102,
|
||||||
|
AlternativeWebuiEnabled = false,
|
||||||
|
AlternativeWebuiPath = "/alt-b",
|
||||||
|
AnnounceIp = "5.6.7.8",
|
||||||
|
AnnouncePort = 6882,
|
||||||
|
AnnounceToAllTiers = false,
|
||||||
|
AnnounceToAllTrackers = false,
|
||||||
|
AnonymousMode = false,
|
||||||
|
AppInstanceName = "app-b",
|
||||||
|
AsyncIoThreads = 103,
|
||||||
|
AutoDeleteMode = 104,
|
||||||
|
AutoTmmEnabled = false,
|
||||||
|
AutorunEnabled = false,
|
||||||
|
AutorunOnTorrentAddedEnabled = false,
|
||||||
|
AutorunOnTorrentAddedProgram = "prog-added-b",
|
||||||
|
AutorunProgram = "prog-b",
|
||||||
|
BannedIPs = "10.1.1.1",
|
||||||
|
BdecodeDepthLimit = 105,
|
||||||
|
BdecodeTokenLimit = 106,
|
||||||
|
BittorrentProtocol = 107,
|
||||||
|
BlockPeersOnPrivilegedPorts = false,
|
||||||
|
BypassAuthSubnetWhitelist = "10.10.0.0/16",
|
||||||
|
BypassAuthSubnetWhitelistEnabled = false,
|
||||||
|
BypassLocalAuth = false,
|
||||||
|
CategoryChangedTmmEnabled = false,
|
||||||
|
CheckingMemoryUse = 108,
|
||||||
|
ConnectionSpeed = 109,
|
||||||
|
CurrentInterfaceAddress = "eth1-addr-b",
|
||||||
|
CurrentInterfaceName = "eth1-name-b",
|
||||||
|
CurrentNetworkInterface = "eth1-b",
|
||||||
|
Dht = false,
|
||||||
|
DhtBootstrapNodes = "node-c",
|
||||||
|
DiskCache = 110,
|
||||||
|
DiskCacheTtl = 111,
|
||||||
|
DiskIoReadMode = 112,
|
||||||
|
DiskIoType = 113,
|
||||||
|
DiskIoWriteMode = 114,
|
||||||
|
DiskQueueSize = 115,
|
||||||
|
DlLimit = 116,
|
||||||
|
DontCountSlowTorrents = false,
|
||||||
|
DyndnsDomain = "dyndns-b",
|
||||||
|
DyndnsEnabled = false,
|
||||||
|
DyndnsPassword = "dyndns-pass-b",
|
||||||
|
DyndnsService = 117,
|
||||||
|
DyndnsUsername = "dyndns-user-b",
|
||||||
|
EmbeddedTrackerPort = 118,
|
||||||
|
EmbeddedTrackerPortForwarding = false,
|
||||||
|
EnableCoalesceReadWrite = false,
|
||||||
|
EnableEmbeddedTracker = false,
|
||||||
|
EnableMultiConnectionsFromSameIp = false,
|
||||||
|
EnablePieceExtentAffinity = false,
|
||||||
|
EnableUploadSuggestions = false,
|
||||||
|
Encryption = 119,
|
||||||
|
ExcludedFileNames = "*.cache",
|
||||||
|
ExcludedFileNamesEnabled = false,
|
||||||
|
ExportDir = "/export-b",
|
||||||
|
ExportDirFin = "/export-fin-b",
|
||||||
|
FileLogAge = 120,
|
||||||
|
FileLogAgeType = 121,
|
||||||
|
FileLogBackupEnabled = false,
|
||||||
|
FileLogDeleteOld = false,
|
||||||
|
FileLogEnabled = false,
|
||||||
|
FileLogMaxSize = 122,
|
||||||
|
FileLogPath = "/log-b",
|
||||||
|
FilePoolSize = 123,
|
||||||
|
HashingThreads = 124,
|
||||||
|
I2pAddress = "i2p-addr-b",
|
||||||
|
I2pEnabled = false,
|
||||||
|
I2pInboundLength = 125,
|
||||||
|
I2pInboundQuantity = 126,
|
||||||
|
I2pMixedMode = false,
|
||||||
|
I2pOutboundLength = 127,
|
||||||
|
I2pOutboundQuantity = 128,
|
||||||
|
I2pPort = 129,
|
||||||
|
IdnSupportEnabled = false,
|
||||||
|
IncompleteFilesExt = false,
|
||||||
|
UseUnwantedFolder = false,
|
||||||
|
IpFilterEnabled = false,
|
||||||
|
IpFilterPath = "/ipfilter-b",
|
||||||
|
IpFilterTrackers = false,
|
||||||
|
LimitLanPeers = false,
|
||||||
|
LimitTcpOverhead = false,
|
||||||
|
LimitUtpRate = false,
|
||||||
|
ListenPort = 130,
|
||||||
|
SslEnabled = false,
|
||||||
|
SslListenPort = 4431,
|
||||||
|
Locale = "en-B",
|
||||||
|
Lsd = false,
|
||||||
|
MailNotificationAuthEnabled = false,
|
||||||
|
MailNotificationEmail = "mail@b",
|
||||||
|
MailNotificationEnabled = false,
|
||||||
|
MailNotificationPassword = "mail-pass-b",
|
||||||
|
MailNotificationSender = "sender-b",
|
||||||
|
MailNotificationSmtp = "smtp-b",
|
||||||
|
MailNotificationSslEnabled = false,
|
||||||
|
MailNotificationUsername = "mail-user-b",
|
||||||
|
MarkOfTheWeb = false,
|
||||||
|
MaxActiveCheckingTorrents = 131,
|
||||||
|
MaxActiveDownloads = 132,
|
||||||
|
MaxActiveTorrents = 133,
|
||||||
|
MaxActiveUploads = 134,
|
||||||
|
MaxConcurrentHttpAnnounces = 135,
|
||||||
|
MaxConnec = 136,
|
||||||
|
MaxConnecPerTorrent = 137,
|
||||||
|
MaxInactiveSeedingTime = 238, // non-null and different from A
|
||||||
|
MaxInactiveSeedingTimeEnabled = false, // non-null here
|
||||||
|
MaxRatio = 2.2f, // non-null here
|
||||||
|
MaxRatioAct = 139,
|
||||||
|
MaxRatioEnabled = true, // non-null here
|
||||||
|
MaxSeedingTime = 240, // non-null here
|
||||||
|
MaxSeedingTimeEnabled = true, // non-null here
|
||||||
|
MaxUploads = 141,
|
||||||
|
MaxUploadsPerTorrent = 142,
|
||||||
|
MemoryWorkingSetLimit = 143,
|
||||||
|
MergeTrackers = false,
|
||||||
|
OutgoingPortsMax = 144,
|
||||||
|
OutgoingPortsMin = 145,
|
||||||
|
PeerTos = 146,
|
||||||
|
PeerTurnover = 147,
|
||||||
|
PeerTurnoverCutoff = 148,
|
||||||
|
PeerTurnoverInterval = 149,
|
||||||
|
PerformanceWarning = false,
|
||||||
|
Pex = false,
|
||||||
|
PreallocateAll = false,
|
||||||
|
ProxyAuthEnabled = false,
|
||||||
|
ProxyBittorrent = false,
|
||||||
|
ProxyHostnameLookup = false,
|
||||||
|
ProxyIp = "proxy-ip-b",
|
||||||
|
ProxyMisc = false,
|
||||||
|
ProxyPassword = "proxy-pass-b",
|
||||||
|
ProxyPeerConnections = false,
|
||||||
|
ProxyPort = 8888,
|
||||||
|
ProxyRss = false,
|
||||||
|
ProxyType = "socks5",
|
||||||
|
ProxyUsername = "proxy-user-b",
|
||||||
|
PythonExecutablePath = "/python-b",
|
||||||
|
QueueingEnabled = false,
|
||||||
|
RandomPort = false,
|
||||||
|
ReannounceWhenAddressChanged = false,
|
||||||
|
RecheckCompletedTorrents = false,
|
||||||
|
RefreshInterval = 150,
|
||||||
|
RequestQueueSize = 151,
|
||||||
|
ResolvePeerCountries = false,
|
||||||
|
ResumeDataStorageType = "sqlite",
|
||||||
|
RssAutoDownloadingEnabled = false,
|
||||||
|
RssDownloadRepackProperEpisodes = false,
|
||||||
|
RssFetchDelay = 5300L,
|
||||||
|
RssMaxArticlesPerFeed = 152,
|
||||||
|
RssProcessingEnabled = false,
|
||||||
|
RssRefreshInterval = 153,
|
||||||
|
RssSmartEpisodeFilters = "s02e02",
|
||||||
|
SavePath = "/save-b",
|
||||||
|
SavePathChangedTmmEnabled = false,
|
||||||
|
SaveResumeDataInterval = 154,
|
||||||
|
SaveStatisticsInterval = 1540,
|
||||||
|
ScanDirs = new Dictionary<string, SaveLocation>
|
||||||
|
{
|
||||||
|
["watch"] = SaveLocation.Create(0),
|
||||||
|
["default"] = SaveLocation.Create(1),
|
||||||
|
["custom"] = SaveLocation.Create("/dls/b")
|
||||||
|
},
|
||||||
|
ScheduleFromHour = 11,
|
||||||
|
ScheduleFromMin = 12,
|
||||||
|
ScheduleToHour = 13,
|
||||||
|
ScheduleToMin = 14,
|
||||||
|
SchedulerDays = 15,
|
||||||
|
SchedulerEnabled = false,
|
||||||
|
SendBufferLowWatermark = 155,
|
||||||
|
SendBufferWatermark = 156,
|
||||||
|
SendBufferWatermarkFactor = 157,
|
||||||
|
SlowTorrentDlRateThreshold = 158,
|
||||||
|
SlowTorrentInactiveTimer = 159,
|
||||||
|
SlowTorrentUlRateThreshold = 160,
|
||||||
|
SocketBacklogSize = 161,
|
||||||
|
SocketReceiveBufferSize = 162,
|
||||||
|
SocketSendBufferSize = 163,
|
||||||
|
SsrfMitigation = false,
|
||||||
|
StopTrackerTimeout = 164,
|
||||||
|
TempPath = "/tmp-b",
|
||||||
|
TempPathEnabled = false,
|
||||||
|
TorrentChangedTmmEnabled = false,
|
||||||
|
TorrentContentLayout = "subfolder",
|
||||||
|
TorrentContentRemoveOption = "delete",
|
||||||
|
TorrentFileSizeLimit = 165,
|
||||||
|
TorrentStopCondition = "files_checked",
|
||||||
|
UpLimit = 166,
|
||||||
|
UploadChokingAlgorithm = 167,
|
||||||
|
UploadSlotsBehavior = 168,
|
||||||
|
Upnp = false,
|
||||||
|
UpnpLeaseDuration = 169,
|
||||||
|
UseCategoryPathsInManualMode = false,
|
||||||
|
UseHttps = false,
|
||||||
|
IgnoreSslErrors = false,
|
||||||
|
UseSubcategories = false,
|
||||||
|
UtpTcpMixedMode = 170,
|
||||||
|
ValidateHttpsTrackerCertificate = false,
|
||||||
|
WebUiAddress = "127.0.0.1",
|
||||||
|
WebUiApiKey = "api-key-b",
|
||||||
|
WebUiBanDuration = 171,
|
||||||
|
WebUiClickjackingProtectionEnabled = false,
|
||||||
|
WebUiCsrfProtectionEnabled = false,
|
||||||
|
WebUiCustomHttpHeaders = "X-Header: B",
|
||||||
|
WebUiDomainList = "b.example.com",
|
||||||
|
WebUiHostHeaderValidationEnabled = false,
|
||||||
|
WebUiHttpsCertPath = "/cert-b",
|
||||||
|
WebUiHttpsKeyPath = "/key-b",
|
||||||
|
WebUiMaxAuthFailCount = 172,
|
||||||
|
WebUiPort = 8181,
|
||||||
|
WebUiReverseProxiesList = "192.168.0.0/16",
|
||||||
|
WebUiReverseProxyEnabled = false,
|
||||||
|
WebUiSecureCookieEnabled = false,
|
||||||
|
WebUiSessionTimeout = 173,
|
||||||
|
WebUiUpnp = false,
|
||||||
|
WebUiUseCustomHttpHeadersEnabled = false,
|
||||||
|
WebUiUsername = "admin-b",
|
||||||
|
WebUiPassword = "pass-b",
|
||||||
|
ConfirmTorrentDeletion = false,
|
||||||
|
ConfirmTorrentRecheck = false,
|
||||||
|
StatusBarExternalIp = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UpdatePreferences BuildPartialChange()
|
||||||
|
{
|
||||||
|
// Only a handful of fields are non-null; the rest null (=> retain originals).
|
||||||
|
return new UpdatePreferences
|
||||||
|
{
|
||||||
|
AddToTopOfQueue = false, // bool
|
||||||
|
AltDlLimit = 222, // int
|
||||||
|
SavePath = "/save-partial", // string
|
||||||
|
MaxRatio = 3.3f, // float (leave MaxRatioEnabled null)
|
||||||
|
ProxyIp = "proxy-new", // string
|
||||||
|
TempPathEnabled = false, // bool
|
||||||
|
WebUiPort = 9090, // int
|
||||||
|
RssFetchDelay = 7777L, // long
|
||||||
|
ScanDirs = new Dictionary<string, SaveLocation>
|
||||||
|
{
|
||||||
|
["watch"] = SaveLocation.Create(0),
|
||||||
|
["custom"] = SaveLocation.Create("/new/custom")
|
||||||
|
},
|
||||||
|
// everything else null => “do not change”
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertAllEqual(UpdatePreferences actual, UpdatePreferences expected)
|
||||||
|
{
|
||||||
|
// value-by-value assertions (no reflection)
|
||||||
|
actual.AddToTopOfQueue.Should().Be(expected.AddToTopOfQueue);
|
||||||
|
actual.AddStoppedEnabled.Should().Be(expected.AddStoppedEnabled);
|
||||||
|
actual.AddTrackers.Should().Be(expected.AddTrackers);
|
||||||
|
actual.AddTrackersEnabled.Should().Be(expected.AddTrackersEnabled);
|
||||||
|
actual.AltDlLimit.Should().Be(expected.AltDlLimit);
|
||||||
|
actual.AltUpLimit.Should().Be(expected.AltUpLimit);
|
||||||
|
actual.AlternativeWebuiEnabled.Should().Be(expected.AlternativeWebuiEnabled);
|
||||||
|
actual.AlternativeWebuiPath.Should().Be(expected.AlternativeWebuiPath);
|
||||||
|
actual.AnnounceIp.Should().Be(expected.AnnounceIp);
|
||||||
|
actual.AnnouncePort.Should().Be(expected.AnnouncePort);
|
||||||
|
actual.AnnounceToAllTiers.Should().Be(expected.AnnounceToAllTiers);
|
||||||
|
actual.AnnounceToAllTrackers.Should().Be(expected.AnnounceToAllTrackers);
|
||||||
|
actual.AnonymousMode.Should().Be(expected.AnonymousMode);
|
||||||
|
actual.AppInstanceName.Should().Be(expected.AppInstanceName);
|
||||||
|
actual.AsyncIoThreads.Should().Be(expected.AsyncIoThreads);
|
||||||
|
actual.AutoDeleteMode.Should().Be(expected.AutoDeleteMode);
|
||||||
|
actual.AutoTmmEnabled.Should().Be(expected.AutoTmmEnabled);
|
||||||
|
actual.AutorunEnabled.Should().Be(expected.AutorunEnabled);
|
||||||
|
actual.AutorunOnTorrentAddedEnabled.Should().Be(expected.AutorunOnTorrentAddedEnabled);
|
||||||
|
actual.AutorunOnTorrentAddedProgram.Should().Be(expected.AutorunOnTorrentAddedProgram);
|
||||||
|
actual.AutorunProgram.Should().Be(expected.AutorunProgram);
|
||||||
|
actual.BannedIPs.Should().Be(expected.BannedIPs);
|
||||||
|
actual.BdecodeDepthLimit.Should().Be(expected.BdecodeDepthLimit);
|
||||||
|
actual.BdecodeTokenLimit.Should().Be(expected.BdecodeTokenLimit);
|
||||||
|
actual.BittorrentProtocol.Should().Be(expected.BittorrentProtocol);
|
||||||
|
actual.BlockPeersOnPrivilegedPorts.Should().Be(expected.BlockPeersOnPrivilegedPorts);
|
||||||
|
actual.BypassAuthSubnetWhitelist.Should().Be(expected.BypassAuthSubnetWhitelist);
|
||||||
|
actual.BypassAuthSubnetWhitelistEnabled.Should().Be(expected.BypassAuthSubnetWhitelistEnabled);
|
||||||
|
actual.BypassLocalAuth.Should().Be(expected.BypassLocalAuth);
|
||||||
|
actual.CategoryChangedTmmEnabled.Should().Be(expected.CategoryChangedTmmEnabled);
|
||||||
|
actual.CheckingMemoryUse.Should().Be(expected.CheckingMemoryUse);
|
||||||
|
actual.ConnectionSpeed.Should().Be(expected.ConnectionSpeed);
|
||||||
|
actual.CurrentInterfaceAddress.Should().Be(expected.CurrentInterfaceAddress);
|
||||||
|
actual.CurrentInterfaceName.Should().Be(expected.CurrentInterfaceName);
|
||||||
|
actual.CurrentNetworkInterface.Should().Be(expected.CurrentNetworkInterface);
|
||||||
|
actual.Dht.Should().Be(expected.Dht);
|
||||||
|
actual.DhtBootstrapNodes.Should().Be(expected.DhtBootstrapNodes);
|
||||||
|
actual.DiskCache.Should().Be(expected.DiskCache);
|
||||||
|
actual.DiskCacheTtl.Should().Be(expected.DiskCacheTtl);
|
||||||
|
actual.DiskIoReadMode.Should().Be(expected.DiskIoReadMode);
|
||||||
|
actual.DiskIoType.Should().Be(expected.DiskIoType);
|
||||||
|
actual.DiskIoWriteMode.Should().Be(expected.DiskIoWriteMode);
|
||||||
|
actual.DiskQueueSize.Should().Be(expected.DiskQueueSize);
|
||||||
|
actual.DlLimit.Should().Be(expected.DlLimit);
|
||||||
|
actual.DontCountSlowTorrents.Should().Be(expected.DontCountSlowTorrents);
|
||||||
|
actual.DyndnsDomain.Should().Be(expected.DyndnsDomain);
|
||||||
|
actual.DyndnsEnabled.Should().Be(expected.DyndnsEnabled);
|
||||||
|
actual.DyndnsPassword.Should().Be(expected.DyndnsPassword);
|
||||||
|
actual.DyndnsService.Should().Be(expected.DyndnsService);
|
||||||
|
actual.DyndnsUsername.Should().Be(expected.DyndnsUsername);
|
||||||
|
actual.EmbeddedTrackerPort.Should().Be(expected.EmbeddedTrackerPort);
|
||||||
|
actual.EmbeddedTrackerPortForwarding.Should().Be(expected.EmbeddedTrackerPortForwarding);
|
||||||
|
actual.EnableCoalesceReadWrite.Should().Be(expected.EnableCoalesceReadWrite);
|
||||||
|
actual.EnableEmbeddedTracker.Should().Be(expected.EnableEmbeddedTracker);
|
||||||
|
actual.EnableMultiConnectionsFromSameIp.Should().Be(expected.EnableMultiConnectionsFromSameIp);
|
||||||
|
actual.EnablePieceExtentAffinity.Should().Be(expected.EnablePieceExtentAffinity);
|
||||||
|
actual.EnableUploadSuggestions.Should().Be(expected.EnableUploadSuggestions);
|
||||||
|
actual.Encryption.Should().Be(expected.Encryption);
|
||||||
|
actual.ExcludedFileNames.Should().Be(expected.ExcludedFileNames);
|
||||||
|
actual.ExcludedFileNamesEnabled.Should().Be(expected.ExcludedFileNamesEnabled);
|
||||||
|
actual.ExportDir.Should().Be(expected.ExportDir);
|
||||||
|
actual.ExportDirFin.Should().Be(expected.ExportDirFin);
|
||||||
|
actual.FileLogAge.Should().Be(expected.FileLogAge);
|
||||||
|
actual.FileLogAgeType.Should().Be(expected.FileLogAgeType);
|
||||||
|
actual.FileLogBackupEnabled.Should().Be(expected.FileLogBackupEnabled);
|
||||||
|
actual.FileLogDeleteOld.Should().Be(expected.FileLogDeleteOld);
|
||||||
|
actual.FileLogEnabled.Should().Be(expected.FileLogEnabled);
|
||||||
|
actual.FileLogMaxSize.Should().Be(expected.FileLogMaxSize);
|
||||||
|
actual.FileLogPath.Should().Be(expected.FileLogPath);
|
||||||
|
actual.FilePoolSize.Should().Be(expected.FilePoolSize);
|
||||||
|
actual.HashingThreads.Should().Be(expected.HashingThreads);
|
||||||
|
actual.I2pAddress.Should().Be(expected.I2pAddress);
|
||||||
|
actual.I2pEnabled.Should().Be(expected.I2pEnabled);
|
||||||
|
actual.I2pInboundLength.Should().Be(expected.I2pInboundLength);
|
||||||
|
actual.I2pInboundQuantity.Should().Be(expected.I2pInboundQuantity);
|
||||||
|
actual.I2pMixedMode.Should().Be(expected.I2pMixedMode);
|
||||||
|
actual.I2pOutboundLength.Should().Be(expected.I2pOutboundLength);
|
||||||
|
actual.I2pOutboundQuantity.Should().Be(expected.I2pOutboundQuantity);
|
||||||
|
actual.I2pPort.Should().Be(expected.I2pPort);
|
||||||
|
actual.IdnSupportEnabled.Should().Be(expected.IdnSupportEnabled);
|
||||||
|
actual.IncompleteFilesExt.Should().Be(expected.IncompleteFilesExt);
|
||||||
|
actual.UseUnwantedFolder.Should().Be(expected.UseUnwantedFolder);
|
||||||
|
actual.IpFilterEnabled.Should().Be(expected.IpFilterEnabled);
|
||||||
|
actual.IpFilterPath.Should().Be(expected.IpFilterPath);
|
||||||
|
actual.IpFilterTrackers.Should().Be(expected.IpFilterTrackers);
|
||||||
|
actual.LimitLanPeers.Should().Be(expected.LimitLanPeers);
|
||||||
|
actual.LimitTcpOverhead.Should().Be(expected.LimitTcpOverhead);
|
||||||
|
actual.LimitUtpRate.Should().Be(expected.LimitUtpRate);
|
||||||
|
actual.ListenPort.Should().Be(expected.ListenPort);
|
||||||
|
actual.SslEnabled.Should().Be(expected.SslEnabled);
|
||||||
|
actual.SslListenPort.Should().Be(expected.SslListenPort);
|
||||||
|
actual.Locale.Should().Be(expected.Locale);
|
||||||
|
actual.Lsd.Should().Be(expected.Lsd);
|
||||||
|
actual.MailNotificationAuthEnabled.Should().Be(expected.MailNotificationAuthEnabled);
|
||||||
|
actual.MailNotificationEmail.Should().Be(expected.MailNotificationEmail);
|
||||||
|
actual.MailNotificationEnabled.Should().Be(expected.MailNotificationEnabled);
|
||||||
|
actual.MailNotificationPassword.Should().Be(expected.MailNotificationPassword);
|
||||||
|
actual.MailNotificationSender.Should().Be(expected.MailNotificationSender);
|
||||||
|
actual.MailNotificationSmtp.Should().Be(expected.MailNotificationSmtp);
|
||||||
|
actual.MailNotificationSslEnabled.Should().Be(expected.MailNotificationSslEnabled);
|
||||||
|
actual.MailNotificationUsername.Should().Be(expected.MailNotificationUsername);
|
||||||
|
actual.MarkOfTheWeb.Should().Be(expected.MarkOfTheWeb);
|
||||||
|
actual.MaxActiveCheckingTorrents.Should().Be(expected.MaxActiveCheckingTorrents);
|
||||||
|
actual.MaxActiveDownloads.Should().Be(expected.MaxActiveDownloads);
|
||||||
|
actual.MaxActiveTorrents.Should().Be(expected.MaxActiveTorrents);
|
||||||
|
actual.MaxActiveUploads.Should().Be(expected.MaxActiveUploads);
|
||||||
|
actual.MaxConcurrentHttpAnnounces.Should().Be(expected.MaxConcurrentHttpAnnounces);
|
||||||
|
actual.MaxConnec.Should().Be(expected.MaxConnec);
|
||||||
|
actual.MaxConnecPerTorrent.Should().Be(expected.MaxConnecPerTorrent);
|
||||||
|
actual.MaxInactiveSeedingTime.Should().Be(expected.MaxInactiveSeedingTime);
|
||||||
|
actual.MaxInactiveSeedingTimeEnabled.Should().Be(expected.MaxInactiveSeedingTimeEnabled);
|
||||||
|
actual.MaxRatio.Should().Be(expected.MaxRatio);
|
||||||
|
actual.MaxRatioAct.Should().Be(expected.MaxRatioAct);
|
||||||
|
actual.MaxRatioEnabled.Should().Be(expected.MaxRatioEnabled);
|
||||||
|
actual.MaxSeedingTime.Should().Be(expected.MaxSeedingTime);
|
||||||
|
actual.MaxSeedingTimeEnabled.Should().Be(expected.MaxSeedingTimeEnabled);
|
||||||
|
actual.MaxUploads.Should().Be(expected.MaxUploads);
|
||||||
|
actual.MaxUploadsPerTorrent.Should().Be(expected.MaxUploadsPerTorrent);
|
||||||
|
actual.MemoryWorkingSetLimit.Should().Be(expected.MemoryWorkingSetLimit);
|
||||||
|
actual.MergeTrackers.Should().Be(expected.MergeTrackers);
|
||||||
|
actual.OutgoingPortsMax.Should().Be(expected.OutgoingPortsMax);
|
||||||
|
actual.OutgoingPortsMin.Should().Be(expected.OutgoingPortsMin);
|
||||||
|
actual.PeerTos.Should().Be(expected.PeerTos);
|
||||||
|
actual.PeerTurnover.Should().Be(expected.PeerTurnover);
|
||||||
|
actual.PeerTurnoverCutoff.Should().Be(expected.PeerTurnoverCutoff);
|
||||||
|
actual.PeerTurnoverInterval.Should().Be(expected.PeerTurnoverInterval);
|
||||||
|
actual.PerformanceWarning.Should().Be(expected.PerformanceWarning);
|
||||||
|
actual.Pex.Should().Be(expected.Pex);
|
||||||
|
actual.PreallocateAll.Should().Be(expected.PreallocateAll);
|
||||||
|
actual.ProxyAuthEnabled.Should().Be(expected.ProxyAuthEnabled);
|
||||||
|
actual.ProxyBittorrent.Should().Be(expected.ProxyBittorrent);
|
||||||
|
actual.ProxyHostnameLookup.Should().Be(expected.ProxyHostnameLookup);
|
||||||
|
actual.ProxyIp.Should().Be(expected.ProxyIp);
|
||||||
|
actual.ProxyMisc.Should().Be(expected.ProxyMisc);
|
||||||
|
actual.ProxyPassword.Should().Be(expected.ProxyPassword);
|
||||||
|
actual.ProxyPeerConnections.Should().Be(expected.ProxyPeerConnections);
|
||||||
|
actual.ProxyPort.Should().Be(expected.ProxyPort);
|
||||||
|
actual.ProxyRss.Should().Be(expected.ProxyRss);
|
||||||
|
actual.ProxyType.Should().Be(expected.ProxyType);
|
||||||
|
actual.ProxyUsername.Should().Be(expected.ProxyUsername);
|
||||||
|
actual.PythonExecutablePath.Should().Be(expected.PythonExecutablePath);
|
||||||
|
actual.QueueingEnabled.Should().Be(expected.QueueingEnabled);
|
||||||
|
actual.RandomPort.Should().Be(expected.RandomPort);
|
||||||
|
actual.ReannounceWhenAddressChanged.Should().Be(expected.ReannounceWhenAddressChanged);
|
||||||
|
actual.RecheckCompletedTorrents.Should().Be(expected.RecheckCompletedTorrents);
|
||||||
|
actual.RefreshInterval.Should().Be(expected.RefreshInterval);
|
||||||
|
actual.RequestQueueSize.Should().Be(expected.RequestQueueSize);
|
||||||
|
actual.ResolvePeerCountries.Should().Be(expected.ResolvePeerCountries);
|
||||||
|
actual.ResumeDataStorageType.Should().Be(expected.ResumeDataStorageType);
|
||||||
|
actual.RssAutoDownloadingEnabled.Should().Be(expected.RssAutoDownloadingEnabled);
|
||||||
|
actual.RssDownloadRepackProperEpisodes.Should().Be(expected.RssDownloadRepackProperEpisodes);
|
||||||
|
actual.RssFetchDelay.Should().Be(expected.RssFetchDelay);
|
||||||
|
actual.RssMaxArticlesPerFeed.Should().Be(expected.RssMaxArticlesPerFeed);
|
||||||
|
actual.RssProcessingEnabled.Should().Be(expected.RssProcessingEnabled);
|
||||||
|
actual.RssRefreshInterval.Should().Be(expected.RssRefreshInterval);
|
||||||
|
actual.RssSmartEpisodeFilters.Should().Be(expected.RssSmartEpisodeFilters);
|
||||||
|
actual.SavePath.Should().Be(expected.SavePath);
|
||||||
|
actual.SavePathChangedTmmEnabled.Should().Be(expected.SavePathChangedTmmEnabled);
|
||||||
|
actual.SaveResumeDataInterval.Should().Be(expected.SaveResumeDataInterval);
|
||||||
|
actual.SaveStatisticsInterval.Should().Be(expected.SaveStatisticsInterval);
|
||||||
|
|
||||||
|
if (expected.ScanDirs is null)
|
||||||
|
{
|
||||||
|
actual.ScanDirs.Should().BeNull();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
actual.ScanDirs.Should().NotBeNull();
|
||||||
|
actual.ScanDirs!.Count.Should().Be(expected.ScanDirs.Count);
|
||||||
|
foreach (var kv in expected.ScanDirs)
|
||||||
|
{
|
||||||
|
actual.ScanDirs.Should().ContainKey(kv.Key);
|
||||||
|
var act = actual.ScanDirs[kv.Key];
|
||||||
|
var exp = kv.Value;
|
||||||
|
act.IsWatchedFolder.Should().Be(exp.IsWatchedFolder);
|
||||||
|
act.IsDefaultFolder.Should().Be(exp.IsDefaultFolder);
|
||||||
|
act.SavePath.Should().Be(exp.SavePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual.ScheduleFromHour.Should().Be(expected.ScheduleFromHour);
|
||||||
|
actual.ScheduleFromMin.Should().Be(expected.ScheduleFromMin);
|
||||||
|
actual.ScheduleToHour.Should().Be(expected.ScheduleToHour);
|
||||||
|
actual.ScheduleToMin.Should().Be(expected.ScheduleToMin);
|
||||||
|
actual.SchedulerDays.Should().Be(expected.SchedulerDays);
|
||||||
|
actual.SchedulerEnabled.Should().Be(expected.SchedulerEnabled);
|
||||||
|
actual.SendBufferLowWatermark.Should().Be(expected.SendBufferLowWatermark);
|
||||||
|
actual.SendBufferWatermark.Should().Be(expected.SendBufferWatermark);
|
||||||
|
actual.SendBufferWatermarkFactor.Should().Be(expected.SendBufferWatermarkFactor);
|
||||||
|
actual.SlowTorrentDlRateThreshold.Should().Be(expected.SlowTorrentDlRateThreshold);
|
||||||
|
actual.SlowTorrentInactiveTimer.Should().Be(expected.SlowTorrentInactiveTimer);
|
||||||
|
actual.SlowTorrentUlRateThreshold.Should().Be(expected.SlowTorrentUlRateThreshold);
|
||||||
|
actual.SocketBacklogSize.Should().Be(expected.SocketBacklogSize);
|
||||||
|
actual.SocketReceiveBufferSize.Should().Be(expected.SocketReceiveBufferSize);
|
||||||
|
actual.SocketSendBufferSize.Should().Be(expected.SocketSendBufferSize);
|
||||||
|
actual.SsrfMitigation.Should().Be(expected.SsrfMitigation);
|
||||||
|
actual.StopTrackerTimeout.Should().Be(expected.StopTrackerTimeout);
|
||||||
|
actual.TempPath.Should().Be(expected.TempPath);
|
||||||
|
actual.TempPathEnabled.Should().Be(expected.TempPathEnabled);
|
||||||
|
actual.TorrentChangedTmmEnabled.Should().Be(expected.TorrentChangedTmmEnabled);
|
||||||
|
actual.TorrentContentLayout.Should().Be(expected.TorrentContentLayout);
|
||||||
|
actual.TorrentContentRemoveOption.Should().Be(expected.TorrentContentRemoveOption);
|
||||||
|
actual.TorrentFileSizeLimit.Should().Be(expected.TorrentFileSizeLimit);
|
||||||
|
actual.TorrentStopCondition.Should().Be(expected.TorrentStopCondition);
|
||||||
|
actual.UpLimit.Should().Be(expected.UpLimit);
|
||||||
|
actual.UploadChokingAlgorithm.Should().Be(expected.UploadChokingAlgorithm);
|
||||||
|
actual.UploadSlotsBehavior.Should().Be(expected.UploadSlotsBehavior);
|
||||||
|
actual.Upnp.Should().Be(expected.Upnp);
|
||||||
|
actual.UpnpLeaseDuration.Should().Be(expected.UpnpLeaseDuration);
|
||||||
|
actual.UseCategoryPathsInManualMode.Should().Be(expected.UseCategoryPathsInManualMode);
|
||||||
|
actual.UseHttps.Should().Be(expected.UseHttps);
|
||||||
|
actual.IgnoreSslErrors.Should().Be(expected.IgnoreSslErrors);
|
||||||
|
actual.UseSubcategories.Should().Be(expected.UseSubcategories);
|
||||||
|
actual.UtpTcpMixedMode.Should().Be(expected.UtpTcpMixedMode);
|
||||||
|
actual.ValidateHttpsTrackerCertificate.Should().Be(expected.ValidateHttpsTrackerCertificate);
|
||||||
|
actual.WebUiAddress.Should().Be(expected.WebUiAddress);
|
||||||
|
actual.WebUiApiKey.Should().Be(expected.WebUiApiKey);
|
||||||
|
actual.WebUiBanDuration.Should().Be(expected.WebUiBanDuration);
|
||||||
|
actual.WebUiClickjackingProtectionEnabled.Should().Be(expected.WebUiClickjackingProtectionEnabled);
|
||||||
|
actual.WebUiCsrfProtectionEnabled.Should().Be(expected.WebUiCsrfProtectionEnabled);
|
||||||
|
actual.WebUiCustomHttpHeaders.Should().Be(expected.WebUiCustomHttpHeaders);
|
||||||
|
actual.WebUiDomainList.Should().Be(expected.WebUiDomainList);
|
||||||
|
actual.WebUiHostHeaderValidationEnabled.Should().Be(expected.WebUiHostHeaderValidationEnabled);
|
||||||
|
actual.WebUiHttpsCertPath.Should().Be(expected.WebUiHttpsCertPath);
|
||||||
|
actual.WebUiHttpsKeyPath.Should().Be(expected.WebUiHttpsKeyPath);
|
||||||
|
actual.WebUiMaxAuthFailCount.Should().Be(expected.WebUiMaxAuthFailCount);
|
||||||
|
actual.WebUiPort.Should().Be(expected.WebUiPort);
|
||||||
|
actual.WebUiReverseProxiesList.Should().Be(expected.WebUiReverseProxiesList);
|
||||||
|
actual.WebUiReverseProxyEnabled.Should().Be(expected.WebUiReverseProxyEnabled);
|
||||||
|
actual.WebUiSecureCookieEnabled.Should().Be(expected.WebUiSecureCookieEnabled);
|
||||||
|
actual.WebUiSessionTimeout.Should().Be(expected.WebUiSessionTimeout);
|
||||||
|
actual.WebUiUpnp.Should().Be(expected.WebUiUpnp);
|
||||||
|
actual.WebUiUseCustomHttpHeadersEnabled.Should().Be(expected.WebUiUseCustomHttpHeadersEnabled);
|
||||||
|
actual.WebUiUsername.Should().Be(expected.WebUiUsername);
|
||||||
|
actual.WebUiPassword.Should().Be(expected.WebUiPassword);
|
||||||
|
actual.ConfirmTorrentDeletion.Should().Be(expected.ConfirmTorrentDeletion);
|
||||||
|
actual.ConfirmTorrentRecheck.Should().Be(expected.ConfirmTorrentRecheck);
|
||||||
|
actual.StatusBarExternalIp.Should().Be(expected.StatusBarExternalIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Tests ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_NullOriginal_AND_ChangedHasAllNonNullValues_WHEN_MergePreferences_THEN_AllFieldsCopied()
|
||||||
|
{
|
||||||
|
var changed = BuildAllSetB_AllNonNull();
|
||||||
|
|
||||||
|
var result = _target.MergePreferences(null, changed);
|
||||||
|
|
||||||
|
AssertAllEqual(result, changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_OriginalExists_AND_ChangedHasAllNonNullValues_WHEN_MergePreferences_THEN_AllFieldsOverwritten()
|
||||||
|
{
|
||||||
|
var original = BuildAllSetA();
|
||||||
|
var changed = BuildAllSetB_AllNonNull();
|
||||||
|
|
||||||
|
var result = _target.MergePreferences(original, changed);
|
||||||
|
|
||||||
|
AssertAllEqual(result, changed);
|
||||||
|
result.ScanDirs.Should().BeSameAs(changed.ScanDirs);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_OriginalExists_AND_ChangedHasAllNullValues_WHEN_MergePreferences_THEN_AllOriginalRetained()
|
||||||
|
{
|
||||||
|
var original = BuildAllSetA();
|
||||||
|
var changed = new UpdatePreferences(); // everything null
|
||||||
|
|
||||||
|
var result = _target.MergePreferences(original, changed);
|
||||||
|
|
||||||
|
AssertAllEqual(result, original);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_OriginalExists_AND_ChangedHasMixOfNullAndNonNull_WHEN_MergePreferences_THEN_OnlyNonNullOverwrite()
|
||||||
|
{
|
||||||
|
var original = BuildAllSetA();
|
||||||
|
var changed = BuildPartialChange();
|
||||||
|
|
||||||
|
var result = _target.MergePreferences(original, changed);
|
||||||
|
|
||||||
|
// Build expected by starting from A, then applying only the non-null fields we set in BuildPartialChange()
|
||||||
|
var expected = BuildAllSetA();
|
||||||
|
expected.AddToTopOfQueue = false;
|
||||||
|
expected.AltDlLimit = 222;
|
||||||
|
expected.SavePath = "/save-partial";
|
||||||
|
expected.MaxRatio = 3.3f;
|
||||||
|
// NOTE: MaxRatioEnabled remains as in A (null) because changed.MaxRatioEnabled is null.
|
||||||
|
expected.ProxyIp = "proxy-new";
|
||||||
|
expected.TempPathEnabled = false;
|
||||||
|
expected.WebUiPort = 9090;
|
||||||
|
expected.RssFetchDelay = 7777L;
|
||||||
|
expected.ScanDirs = new Dictionary<string, SaveLocation>
|
||||||
|
{
|
||||||
|
["watch"] = SaveLocation.Create(0),
|
||||||
|
["custom"] = SaveLocation.Create("/new/custom")
|
||||||
|
};
|
||||||
|
|
||||||
|
AssertAllEqual(result, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Validate() rule tests ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_MaxRatioAndMaxRatioEnabledBothSet_WHEN_Validate_THEN_Throws()
|
||||||
|
{
|
||||||
|
var p = new UpdatePreferences { MaxRatio = 1.0f, MaxRatioEnabled = true };
|
||||||
|
Action act = () => p.Validate();
|
||||||
|
act.Should().Throw<InvalidOperationException>()
|
||||||
|
.WithMessage("*max_ratio or max_ratio_enabled*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_MaxSeedingTimeAndMaxSeedingTimeEnabledBothSet_WHEN_Validate_THEN_Throws()
|
||||||
|
{
|
||||||
|
var p = new UpdatePreferences { MaxSeedingTime = 10, MaxSeedingTimeEnabled = true };
|
||||||
|
Action act = () => p.Validate();
|
||||||
|
act.Should().Throw<InvalidOperationException>()
|
||||||
|
.WithMessage("*max_seeding_time or max_seeding_time_enabled*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_MaxInactiveSeedingTimeAndEnabledBothSet_WHEN_Validate_THEN_Throws()
|
||||||
|
{
|
||||||
|
var p = new UpdatePreferences { MaxInactiveSeedingTime = 10, MaxInactiveSeedingTimeEnabled = true };
|
||||||
|
Action act = () => p.Validate();
|
||||||
|
act.Should().Throw<InvalidOperationException>()
|
||||||
|
.WithMessage("*max_inactive_seeding_time or max_inactive_seeding_time_enabled*");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
301
Lantean.QBTMud.Test/Services/RssDataManagerTests.cs
Normal file
301
Lantean.QBTMud.Test/Services/RssDataManagerTests.cs
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
using AwesomeAssertions;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
using Lantean.QBTMud.Services;
|
||||||
|
|
||||||
|
namespace Lantean.QBTMud.Test.Services
|
||||||
|
{
|
||||||
|
public class RssDataManagerTests
|
||||||
|
{
|
||||||
|
private readonly RssDataManager _target = new RssDataManager();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_MultipleFeedsWithAndWithoutArticles_WHEN_CreateRssList_THEN_MetaCountsAndFlatteningCorrect()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var items = new Dictionary<string, RssItem>
|
||||||
|
{
|
||||||
|
// key != uid on purpose in another test; here keep them equal
|
||||||
|
["feed-a"] = new RssItem(
|
||||||
|
articles: new[]
|
||||||
|
{
|
||||||
|
new Lantean.QBitTorrentClient.Models.RssArticle(
|
||||||
|
category: "cat-1", comments: "c1", date: "2025-01-01",
|
||||||
|
description: "d1", id: "a1", link: "http://a/1",
|
||||||
|
thumbnail: "http://a/t1", title: "t1",
|
||||||
|
torrentURL: "http://a.torrent/1", isRead: false),
|
||||||
|
new Lantean.QBitTorrentClient.Models.RssArticle(
|
||||||
|
category: "cat-2", comments: "c2", date: "2025-01-02",
|
||||||
|
description: "d2", id: "a2", link: "http://a/2",
|
||||||
|
thumbnail: "http://a/t2", title: "t2",
|
||||||
|
torrentURL: "http://a.torrent/2", isRead: true),
|
||||||
|
new Lantean.QBitTorrentClient.Models.RssArticle(
|
||||||
|
category: "cat-3", comments: "c3", date: "2025-01-03",
|
||||||
|
description: "d3", id: "a3", link: "http://a/3",
|
||||||
|
thumbnail: "http://a/t3", title: "t3",
|
||||||
|
torrentURL: "http://a.torrent/3", isRead: false),
|
||||||
|
},
|
||||||
|
hasError: false,
|
||||||
|
isLoading: false,
|
||||||
|
lastBuildDate: "2025-01-05",
|
||||||
|
title: "Feed A",
|
||||||
|
uid: "feed-a",
|
||||||
|
url: "http://feed/a"),
|
||||||
|
|
||||||
|
// feed with one read article
|
||||||
|
["feed-b"] = new RssItem(
|
||||||
|
articles: new[]
|
||||||
|
{
|
||||||
|
new Lantean.QBitTorrentClient.Models.RssArticle(
|
||||||
|
category: "movies", comments: null, date: "2025-02-01",
|
||||||
|
description: null, id: "b1", link: "http://b/1",
|
||||||
|
thumbnail: null, title: "m1",
|
||||||
|
torrentURL: "http://b.torrent/1", isRead: true),
|
||||||
|
},
|
||||||
|
hasError: true,
|
||||||
|
isLoading: true,
|
||||||
|
lastBuildDate: "2025-02-02",
|
||||||
|
title: "Feed B",
|
||||||
|
uid: "feed-b",
|
||||||
|
url: "http://feed/b"),
|
||||||
|
|
||||||
|
// feed with null article list -> should create feed only
|
||||||
|
["feed-c"] = new RssItem(
|
||||||
|
articles: null,
|
||||||
|
hasError: false,
|
||||||
|
isLoading: false,
|
||||||
|
lastBuildDate: "2025-03-03",
|
||||||
|
title: "Feed C",
|
||||||
|
uid: "feed-c",
|
||||||
|
url: "http://feed/c"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = _target.CreateRssList(items);
|
||||||
|
|
||||||
|
// assert: feeds exist
|
||||||
|
result.Feeds.Keys.Should().BeEquivalentTo(new[] { "feed-a", "feed-b", "feed-c" });
|
||||||
|
|
||||||
|
// feed-a meta and counts
|
||||||
|
var fa = result.Feeds["feed-a"];
|
||||||
|
fa.Uid.Should().Be("feed-a");
|
||||||
|
fa.Url.Should().Be("http://feed/a");
|
||||||
|
fa.Title.Should().Be("Feed A");
|
||||||
|
fa.LastBuildDate.Should().Be("2025-01-05");
|
||||||
|
fa.HasError.Should().BeFalse();
|
||||||
|
fa.IsLoading.Should().BeFalse();
|
||||||
|
fa.ArticleCount.Should().Be(3);
|
||||||
|
fa.UnreadCount.Should().Be(2); // two IsRead=false in feed-a
|
||||||
|
|
||||||
|
// feed-b meta and counts
|
||||||
|
var fb = result.Feeds["feed-b"];
|
||||||
|
fb.Uid.Should().Be("feed-b");
|
||||||
|
fb.Url.Should().Be("http://feed/b");
|
||||||
|
fb.Title.Should().Be("Feed B");
|
||||||
|
fb.LastBuildDate.Should().Be("2025-02-02");
|
||||||
|
fb.HasError.Should().BeTrue();
|
||||||
|
fb.IsLoading.Should().BeTrue();
|
||||||
|
fb.ArticleCount.Should().Be(1);
|
||||||
|
fb.UnreadCount.Should().Be(0);
|
||||||
|
|
||||||
|
// feed-c meta and counts
|
||||||
|
var fc = result.Feeds["feed-c"];
|
||||||
|
fc.Uid.Should().Be("feed-c");
|
||||||
|
fc.Url.Should().Be("http://feed/c");
|
||||||
|
fc.Title.Should().Be("Feed C");
|
||||||
|
fc.LastBuildDate.Should().Be("2025-03-03");
|
||||||
|
fc.HasError.Should().BeFalse();
|
||||||
|
fc.IsLoading.Should().BeFalse();
|
||||||
|
fc.ArticleCount.Should().Be(0);
|
||||||
|
fc.UnreadCount.Should().Be(0);
|
||||||
|
|
||||||
|
// articles flattened correctly (3 + 1)
|
||||||
|
result.Articles.Count.Should().Be(4);
|
||||||
|
result.Articles.Any(a => a.Feed == "feed-c").Should().BeFalse(); // none from 'c'
|
||||||
|
|
||||||
|
// total unread
|
||||||
|
result.UnreadCount.Should().Be(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_ClientArticleFieldsNulls_WHEN_CreateRssList_THEN_NullsPropagateWithoutNormalization()
|
||||||
|
{
|
||||||
|
// arrange: provide nulls in fields (the implementation uses '!' but does not normalize)
|
||||||
|
var items = new Dictionary<string, RssItem>
|
||||||
|
{
|
||||||
|
["feed-null"] = new RssItem(
|
||||||
|
articles: new[]
|
||||||
|
{
|
||||||
|
new Lantean.QBitTorrentClient.Models.RssArticle(
|
||||||
|
category: null, comments: null, date: null,
|
||||||
|
description: null, id: null, link: null,
|
||||||
|
thumbnail: null, title: null,
|
||||||
|
torrentURL: null, isRead: false),
|
||||||
|
},
|
||||||
|
hasError: false,
|
||||||
|
isLoading: false,
|
||||||
|
lastBuildDate: null,
|
||||||
|
title: null,
|
||||||
|
uid: "uid-null",
|
||||||
|
url: "http://feed/null"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = _target.CreateRssList(items);
|
||||||
|
|
||||||
|
// assert feed meta
|
||||||
|
result.Feeds.Count.Should().Be(1);
|
||||||
|
var f = result.Feeds["feed-null"];
|
||||||
|
f.Uid.Should().Be("uid-null");
|
||||||
|
f.Url.Should().Be("http://feed/null");
|
||||||
|
f.Title.Should().BeNull();
|
||||||
|
f.LastBuildDate.Should().BeNull();
|
||||||
|
f.ArticleCount.Should().Be(1);
|
||||||
|
f.UnreadCount.Should().Be(1);
|
||||||
|
|
||||||
|
// assert article null propagation (no normalization to empty string)
|
||||||
|
var art = result.Articles.Single();
|
||||||
|
art.Feed.Should().Be("feed-null"); // dictionary key is used as article.Feed
|
||||||
|
art.Category.Should().BeNull();
|
||||||
|
art.Comments.Should().BeNull();
|
||||||
|
art.Date.Should().BeNull();
|
||||||
|
art.Description.Should().BeNull();
|
||||||
|
art.Id.Should().BeNull();
|
||||||
|
art.Link.Should().BeNull();
|
||||||
|
art.Thumbnail.Should().BeNull();
|
||||||
|
art.Title.Should().BeNull();
|
||||||
|
art.TorrentURL.Should().BeNull();
|
||||||
|
art.IsRead.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_UidDiffersFromDictionaryKey_WHEN_CreateRssList_THEN_ArticleFeedUsesKey_And_FeedUidUsesItemUid()
|
||||||
|
{
|
||||||
|
// arrange: key != uid
|
||||||
|
var items = new Dictionary<string, RssItem>
|
||||||
|
{
|
||||||
|
["dict-key"] = new RssItem(
|
||||||
|
articles: new[]
|
||||||
|
{
|
||||||
|
new Lantean.QBitTorrentClient.Models.RssArticle(
|
||||||
|
category: "x", comments: "c", date: "d",
|
||||||
|
description: "desc", id: "id1", link: "l",
|
||||||
|
thumbnail: "t", title: "title",
|
||||||
|
torrentURL: "u", isRead: true),
|
||||||
|
},
|
||||||
|
hasError: false,
|
||||||
|
isLoading: false,
|
||||||
|
lastBuildDate: "lb",
|
||||||
|
title: "T",
|
||||||
|
uid: "uid-different",
|
||||||
|
url: "http://u"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = _target.CreateRssList(items);
|
||||||
|
|
||||||
|
// assert: feed uid == item.Uid; article.Feed == dictionary key
|
||||||
|
var feed = result.Feeds["dict-key"];
|
||||||
|
feed.Uid.Should().Be("uid-different");
|
||||||
|
var art = result.Articles.Single();
|
||||||
|
art.Feed.Should().Be("dict-key");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_RssListWithUnread_WHEN_MarkAllUnreadAsRead_THEN_AllFeedUnreadZero_And_TotalZero()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var items = new Dictionary<string, RssItem>
|
||||||
|
{
|
||||||
|
["fa"] = new RssItem(
|
||||||
|
articles: new[]
|
||||||
|
{
|
||||||
|
new Lantean.QBitTorrentClient.Models.RssArticle(
|
||||||
|
category: "c", comments: "c", date: "d1",
|
||||||
|
description: "d", id: "a1", link: "l",
|
||||||
|
thumbnail: "t", title: "t",
|
||||||
|
torrentURL: "u", isRead: false),
|
||||||
|
new Lantean.QBitTorrentClient.Models.RssArticle(
|
||||||
|
category: "c", comments: "c", date: "d2",
|
||||||
|
description: "d", id: "a2", link: "l",
|
||||||
|
thumbnail: "t", title: "t",
|
||||||
|
torrentURL: "u", isRead: false),
|
||||||
|
},
|
||||||
|
hasError: false, isLoading: false,
|
||||||
|
lastBuildDate: "lb", title: "TA", uid: "fa", url: "http://fa"),
|
||||||
|
|
||||||
|
["fb"] = new RssItem(
|
||||||
|
articles: new[]
|
||||||
|
{
|
||||||
|
new Lantean.QBitTorrentClient.Models.RssArticle(
|
||||||
|
category: "c", comments: "c", date: "d3",
|
||||||
|
description: "d", id: "b1", link: "l",
|
||||||
|
thumbnail: "t", title: "t",
|
||||||
|
torrentURL: "u", isRead: true),
|
||||||
|
},
|
||||||
|
hasError: false, isLoading: false,
|
||||||
|
lastBuildDate: "lb", title: "TB", uid: "fb", url: "http://fb"),
|
||||||
|
};
|
||||||
|
var list = _target.CreateRssList(items);
|
||||||
|
list.UnreadCount.Should().Be(2);
|
||||||
|
|
||||||
|
// act
|
||||||
|
list.MarkAllUnreadAsRead();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
list.UnreadCount.Should().Be(0);
|
||||||
|
foreach (var f in list.Feeds.Values)
|
||||||
|
{
|
||||||
|
f.UnreadCount.Should().Be(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_RssListWithMultipleFeeds_WHEN_MarkAsUnread_ForSpecificFeed_THEN_OnlyThatFeedZeroed()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var items = new Dictionary<string, RssItem>
|
||||||
|
{
|
||||||
|
["fa"] = new RssItem(
|
||||||
|
articles: new[]
|
||||||
|
{
|
||||||
|
new Lantean.QBitTorrentClient.Models.RssArticle(
|
||||||
|
category: "c", comments: "c", date: "d1",
|
||||||
|
description: "d", id: "a1", link: "l",
|
||||||
|
thumbnail: "t", title: "t",
|
||||||
|
torrentURL: "u", isRead: false),
|
||||||
|
new Lantean.QBitTorrentClient.Models.RssArticle(
|
||||||
|
category: "c", comments: "c", date: "d2",
|
||||||
|
description: "d", id: "a2", link: "l",
|
||||||
|
thumbnail: "t", title: "t",
|
||||||
|
torrentURL: "u", isRead: false),
|
||||||
|
},
|
||||||
|
hasError: false, isLoading: false,
|
||||||
|
lastBuildDate: "lb", title: "TA", uid: "fa", url: "http://fa"),
|
||||||
|
|
||||||
|
["fb"] = new RssItem(
|
||||||
|
articles: new[]
|
||||||
|
{
|
||||||
|
new Lantean.QBitTorrentClient.Models.RssArticle(
|
||||||
|
category: "c", comments: "c", date: "d3",
|
||||||
|
description: "d", id: "b1", link: "l",
|
||||||
|
thumbnail: "t", title: "t",
|
||||||
|
torrentURL: "u", isRead: false),
|
||||||
|
},
|
||||||
|
hasError: false, isLoading: false,
|
||||||
|
lastBuildDate: "lb", title: "TB", uid: "fb", url: "http://fb"),
|
||||||
|
};
|
||||||
|
var list = _target.CreateRssList(items);
|
||||||
|
list.UnreadCount.Should().Be(3);
|
||||||
|
list.Feeds["fa"].UnreadCount.Should().Be(2);
|
||||||
|
list.Feeds["fb"].UnreadCount.Should().Be(1);
|
||||||
|
|
||||||
|
// act
|
||||||
|
list.MarkAsUnread("fa"); // per implementation: zeroes the feed's unread count
|
||||||
|
|
||||||
|
// assert
|
||||||
|
list.Feeds["fa"].UnreadCount.Should().Be(0);
|
||||||
|
list.Feeds["fb"].UnreadCount.Should().Be(1);
|
||||||
|
list.UnreadCount.Should().Be(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
using AwesomeAssertions;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
using Lantean.QBTMud.Models;
|
||||||
|
using Lantean.QBTMud.Services;
|
||||||
|
|
||||||
|
using MudPriority = Lantean.QBTMud.Models.Priority;
|
||||||
|
using QbtPriority = Lantean.QBitTorrentClient.Models.Priority;
|
||||||
|
|
||||||
|
namespace Lantean.QBTMud.Test.Services
|
||||||
|
{
|
||||||
|
public class TorrentDataManagerContentsListTests
|
||||||
|
{
|
||||||
|
private readonly TorrentDataManager _target = new TorrentDataManager();
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// CreateContentsList tests
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_NoFiles_WHEN_CreateContentsList_THEN_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var files = Array.Empty<FileData>();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = _target.CreateContentsList(files);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_SingleRootFile_WHEN_CreateContentsList_THEN_CreatesSingleFileNode()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var files = new[]
|
||||||
|
{
|
||||||
|
new FileData(
|
||||||
|
index: 5,
|
||||||
|
name: "file1.mkv",
|
||||||
|
size: 100,
|
||||||
|
progress: 0.5f,
|
||||||
|
priority: QbtPriority.Normal,
|
||||||
|
isSeed: false,
|
||||||
|
pieceRange: Array.Empty<int>(),
|
||||||
|
availability: 3.2f)
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = _target.CreateContentsList(files);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
result.Count.Should().Be(1);
|
||||||
|
result.ContainsKey("file1.mkv").Should().BeTrue();
|
||||||
|
|
||||||
|
var file = result["file1.mkv"];
|
||||||
|
file.IsFolder.Should().BeFalse();
|
||||||
|
file.Name.Should().Be("file1.mkv");
|
||||||
|
file.DisplayName.Should().Be("file1.mkv");
|
||||||
|
file.Index.Should().Be(5);
|
||||||
|
file.Path.Should().Be(""); // root
|
||||||
|
file.Level.Should().Be(0);
|
||||||
|
file.Size.Should().Be(100);
|
||||||
|
file.Progress.Should().Be(0.5f);
|
||||||
|
file.Availability.Should().Be(3.2f);
|
||||||
|
file.Priority.Should().Be(MudPriority.Normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_NestedFiles_AND_UnwantedFolder_WHEN_CreateContentsList_THEN_Skips_Unwanted_And_Aggregates()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var files = new[]
|
||||||
|
{
|
||||||
|
// ".unwanted" folder is skipped as a directory, but the file remains under "a"
|
||||||
|
new FileData(10, "a/.unwanted/skip.bin", 10, 0.8f, QbtPriority.Normal, false, Array.Empty<int>(), 2.0f),
|
||||||
|
new FileData(11, "a/b/c1.txt", 30, 0.4f, QbtPriority.High, false, Array.Empty<int>(), 1.0f),
|
||||||
|
new FileData(12, "a/b/c2.txt", 70, 0.9f, QbtPriority.DoNotDownload, false, Array.Empty<int>(), 1.5f),
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = _target.CreateContentsList(files);
|
||||||
|
|
||||||
|
// assert: keys present
|
||||||
|
result.ContainsKey("a").Should().BeTrue();
|
||||||
|
result.ContainsKey("a/b").Should().BeTrue();
|
||||||
|
result.ContainsKey("a/.unwanted/skip.bin").Should().BeTrue();
|
||||||
|
result.ContainsKey("a/b/c1.txt").Should().BeTrue();
|
||||||
|
result.ContainsKey("a/b/c2.txt").Should().BeTrue();
|
||||||
|
|
||||||
|
// NOTE: CreateContentsList aggregates using TOTAL size as denominator (not "active" size).
|
||||||
|
// For "a/b":
|
||||||
|
// size = 30 + 70 = 100
|
||||||
|
// progressSum = 0.4*30 (DND child excluded from sum) = 12
|
||||||
|
// availabilitySum = 1.0*30 = 30
|
||||||
|
// => progress = 12 / 100 = 0.12
|
||||||
|
// => availability = 30 / 100 = 0.3
|
||||||
|
// priority = Mixed (High vs DoNotDownload)
|
||||||
|
var ab = result["a/b"];
|
||||||
|
ab.IsFolder.Should().BeTrue();
|
||||||
|
ab.Level.Should().Be(1);
|
||||||
|
ab.Size.Should().Be(100);
|
||||||
|
ab.Progress.Should().BeApproximately(0.12f, 1e-6f);
|
||||||
|
ab.Availability.Should().BeApproximately(0.3f, 1e-6f);
|
||||||
|
ab.Priority.Should().Be(MudPriority.Mixed);
|
||||||
|
|
||||||
|
// For "a":
|
||||||
|
// children: "a/.unwanted/skip.bin" (size=10, p=0.8, avail=2.0, Normal)
|
||||||
|
// "a/b" (size=100, p=0.12, avail=0.3, Mixed)
|
||||||
|
// size = 110
|
||||||
|
// progressSum = 0.8*10 + 0.12*100 = 8 + 12 = 20 => progress = 20/110 ≈ 0.181818...
|
||||||
|
// availabilitySum = 2.0*10 + 0.3*100 = 20 + 30 = 50 => availability = 50/110 ≈ 0.454545...
|
||||||
|
// priority = Mixed (Normal vs Mixed)
|
||||||
|
var a = result["a"];
|
||||||
|
a.IsFolder.Should().BeTrue();
|
||||||
|
a.Level.Should().Be(0);
|
||||||
|
a.Size.Should().Be(110);
|
||||||
|
a.Progress.Should().BeApproximately(20f / 110f, 1e-6f);
|
||||||
|
a.Availability.Should().BeApproximately(50f / 110f, 1e-6f);
|
||||||
|
a.Priority.Should().Be(MudPriority.Mixed);
|
||||||
|
|
||||||
|
// folder indices are less than min file index (10); deeper folder created later => smaller index
|
||||||
|
a.Index.Should().BeLessThan(10);
|
||||||
|
ab.Index.Should().BeLessThan(10);
|
||||||
|
ab.Index.Should().BeLessThan(a.Index);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_AllChildrenDoNotDownload_WHEN_CreateContentsList_THEN_FolderProgressAndAvailabilityAreZero_AndPriorityDND()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var files = new[]
|
||||||
|
{
|
||||||
|
new FileData(4, "d/x.bin", 50, 0.5f, QbtPriority.DoNotDownload, false, Array.Empty<int>(), 0.9f),
|
||||||
|
new FileData(5, "d/y.bin", 50, 1.2f, QbtPriority.DoNotDownload, false, Array.Empty<int>(), 1.1f)
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = _target.CreateContentsList(files);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
result.ContainsKey("d").Should().BeTrue();
|
||||||
|
var d = result["d"];
|
||||||
|
d.IsFolder.Should().BeTrue();
|
||||||
|
d.Size.Should().Be(100);
|
||||||
|
d.Progress.Should().Be(0f); // activeSize == 0
|
||||||
|
d.Availability.Should().Be(0f); // activeSize == 0
|
||||||
|
d.Priority.Should().Be(MudPriority.DoNotDownload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// MergeContentsList tests
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_NoFiles_AND_EmptyContents_WHEN_MergeContentsList_THEN_ReturnsFalseAndNoChange()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var files = Array.Empty<FileData>();
|
||||||
|
var contents = new Dictionary<string, ContentItem>();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var changed = _target.MergeContentsList(files, contents);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
changed.Should().BeFalse();
|
||||||
|
contents.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_NoFiles_AND_ExistingContents_WHEN_MergeContentsList_THEN_ClearsAndReturnsTrue()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var contents = new Dictionary<string, ContentItem>
|
||||||
|
{
|
||||||
|
["folder"] = new ContentItem("folder", "folder", -1, MudPriority.Normal, 0, 0, 0, true, 0),
|
||||||
|
["folder/file.txt"] = new ContentItem("folder/file.txt", "file.txt", 10, MudPriority.Normal, 0.5f, 100, 1.0f, false, 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
var changed = _target.MergeContentsList(Array.Empty<FileData>(), contents);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
changed.Should().BeTrue();
|
||||||
|
contents.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_NewFilesOnly_WHEN_MergeContentsList_THEN_AddsFilesAndDirectoriesAndAggregates()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var files = new[]
|
||||||
|
{
|
||||||
|
new FileData(7, "root1.txt", 10, 0.6f, QbtPriority.Normal, false, Array.Empty<int>(), 2.0f),
|
||||||
|
new FileData(8, "folder1/file1.bin", 30, 0.4f, QbtPriority.High, false, Array.Empty<int>(), 1.0f),
|
||||||
|
new FileData(9, "folder1/file2.bin", 70, 0.9f, QbtPriority.DoNotDownload, false, Array.Empty<int>(), 1.5f)
|
||||||
|
};
|
||||||
|
var contents = new Dictionary<string, ContentItem>();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var changed = _target.MergeContentsList(files, contents);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
changed.Should().BeTrue();
|
||||||
|
|
||||||
|
// keys present
|
||||||
|
contents.ContainsKey("root1.txt").Should().BeTrue();
|
||||||
|
contents.ContainsKey("folder1").Should().BeTrue();
|
||||||
|
contents.ContainsKey("folder1/file1.bin").Should().BeTrue();
|
||||||
|
contents.ContainsKey("folder1/file2.bin").Should().BeTrue();
|
||||||
|
|
||||||
|
// directory indexes should be < min file index (7)
|
||||||
|
contents["folder1"].Index.Should().BeLessThan(7);
|
||||||
|
|
||||||
|
// aggregation on folder1 (same math as earlier)
|
||||||
|
var folder = contents["folder1"];
|
||||||
|
folder.IsFolder.Should().BeTrue();
|
||||||
|
folder.Size.Should().Be(100);
|
||||||
|
folder.Progress.Should().BeApproximately(0.4f, 1e-6f);
|
||||||
|
folder.Availability.Should().BeApproximately(1.0f, 1e-6f);
|
||||||
|
folder.Priority.Should().Be(MudPriority.Mixed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_ExistingFile_WHEN_MergeContentsList_WithChanges_THEN_UpdatesInPlaceAndReturnsTrue()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var contents = new Dictionary<string, ContentItem>
|
||||||
|
{
|
||||||
|
// pre-existing directory with expected rollup for the prior file state
|
||||||
|
["folder"] = new ContentItem("folder", "folder", -100, MudPriority.Normal, 0.50f, 100, 1.20f, true, 0),
|
||||||
|
// existing file which will be updated (progress delta > tolerance, size changed, availability delta > tolerance)
|
||||||
|
["folder/file.txt"] = new ContentItem("folder/file.txt", "file.txt", 10, MudPriority.Normal, 0.50f, 100, 1.20f, false, 1),
|
||||||
|
// an extra entry that should be removed (not seen)
|
||||||
|
["old.bin"] = new ContentItem("old.bin", "old.bin", 5, MudPriority.Normal, 0.1f, 10, 1.0f, false, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
var files = new[]
|
||||||
|
{
|
||||||
|
// same path but changed values
|
||||||
|
new FileData(10, "folder/file.txt", 120, 0.5003f /* > tol from 0.50 by 0.0003 */, QbtPriority.Normal, false, Array.Empty<int>(), 1.2003f)
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
var changed = _target.MergeContentsList(files, contents);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
changed.Should().BeTrue();
|
||||||
|
|
||||||
|
contents.ContainsKey("folder/file.txt").Should().BeTrue();
|
||||||
|
contents.ContainsKey("folder").Should().BeTrue();
|
||||||
|
contents.ContainsKey("old.bin").Should().BeFalse(); // removed
|
||||||
|
|
||||||
|
var file = contents["folder/file.txt"];
|
||||||
|
file.Size.Should().Be(120);
|
||||||
|
file.Progress.Should().Be(0.5003f);
|
||||||
|
file.Availability.Should().Be(1.2003f);
|
||||||
|
|
||||||
|
// folder roll-up should update too:
|
||||||
|
var folder = contents["folder"];
|
||||||
|
folder.Size.Should().Be(120);
|
||||||
|
folder.Progress.Should().BeApproximately(0.5003f, 1e-6f);
|
||||||
|
folder.Availability.Should().BeApproximately(1.2003f, 1e-6f);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_ExistingItemsWithNoMaterialChangeWithinTolerance_WHEN_MergeContentsList_THEN_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
// file defines a folder rollup: size 100, progress 0.5000, availability 1.2000, Normal
|
||||||
|
var contents = new Dictionary<string, ContentItem>
|
||||||
|
{
|
||||||
|
["folder"] = new ContentItem("folder", "folder", -1, MudPriority.Normal, 0.5000f, 100, 1.2000f, true, 0),
|
||||||
|
["folder/file.txt"] = new ContentItem("folder/file.txt", "file.txt", 10, MudPriority.Normal, 0.5000f, 100, 1.2000f, false, 1)
|
||||||
|
};
|
||||||
|
var files = new[]
|
||||||
|
{
|
||||||
|
// diffs are within tolerance (0.0001f) -> should NOT update
|
||||||
|
new FileData(10, "folder/file.txt", 100, 0.50005f, QbtPriority.Normal, false, Array.Empty<int>(), 1.20005f)
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
var changed = _target.MergeContentsList(files, contents);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
changed.Should().BeFalse();
|
||||||
|
|
||||||
|
// nothing should have moved
|
||||||
|
contents["folder/file.txt"].Progress.Should().Be(0.5000f);
|
||||||
|
contents["folder/file.txt"].Availability.Should().Be(1.2000f);
|
||||||
|
contents["folder/file.txt"].Size.Should().Be(100);
|
||||||
|
|
||||||
|
contents["folder"].Progress.Should().Be(0.5000f);
|
||||||
|
contents["folder"].Availability.Should().Be(1.2000f);
|
||||||
|
contents["folder"].Size.Should().Be(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_FileUnderUnwantedDirectory_WHEN_MergeContentsList_THEN_CreatesParentButSkipsUnwantedFolder()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var contents = new Dictionary<string, ContentItem>();
|
||||||
|
var files = new[]
|
||||||
|
{
|
||||||
|
new FileData(20, "x/.unwanted/y.bin", 10, 0.8f, QbtPriority.Normal, false, Array.Empty<int>(), 2.0f)
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
var changed = _target.MergeContentsList(files, contents);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
changed.Should().BeTrue();
|
||||||
|
contents.ContainsKey("x").Should().BeTrue();
|
||||||
|
contents.ContainsKey("x/.unwanted/y.bin").Should().BeTrue();
|
||||||
|
contents.Keys.Any(k => k.Contains(".unwanted") && k.EndsWith('/')).Should().BeFalse(); // no explicit ".unwanted" directory key
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_ProgressAboveOne_WHEN_MergeContentsList_THEN_DirectoryProgressIsClampedToOne()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var contents = new Dictionary<string, ContentItem>();
|
||||||
|
var files = new[]
|
||||||
|
{
|
||||||
|
new FileData(2, "c/fileA", 10, 2.5f /* > 1 */, QbtPriority.Normal, false, Array.Empty<int>(), 3.0f)
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
var changed = _target.MergeContentsList(files, contents);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
changed.Should().BeTrue();
|
||||||
|
var dir = contents["c"];
|
||||||
|
dir.Progress.Should().Be(1.0f); // clamped
|
||||||
|
dir.Size.Should().Be(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_ProgressBelowZero_WHEN_MergeContentsList_THEN_DirectoryProgressIsClampedToZero()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var contents = new Dictionary<string, ContentItem>();
|
||||||
|
var files = new[]
|
||||||
|
{
|
||||||
|
new FileData(3, "d/fileB", 10, -0.5f /* < 0 */, QbtPriority.Normal, false, Array.Empty<int>(), 3.0f)
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
var changed = _target.MergeContentsList(files, contents);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
changed.Should().BeTrue();
|
||||||
|
var dir = contents["d"];
|
||||||
|
dir.Progress.Should().Be(0.0f); // clamped
|
||||||
|
dir.Size.Should().Be(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_ExistingDirectories_THEN_NewFolderIndicesAreLessThanMinOfExistingAndIncoming()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
// existing directory with index -2 and an existing independent file with index 50
|
||||||
|
var contents = new Dictionary<string, ContentItem>
|
||||||
|
{
|
||||||
|
["exist"] = new ContentItem("exist", "exist", -2, MudPriority.Normal, 0f, 0L, 0f, true, 0),
|
||||||
|
["exist/keep.txt"] = new ContentItem("exist/keep.txt", "keep.txt", 50, MudPriority.Normal, 0.1f, 1L, 0.2f, false, 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
// incoming files have min index 5
|
||||||
|
var files = new[]
|
||||||
|
{
|
||||||
|
new FileData(5, "new/f1", 10, 0.3f, QbtPriority.Normal, false, Array.Empty<int>(), 1.0f),
|
||||||
|
new FileData(9, "new/f2", 15, 0.6f, QbtPriority.Normal, false, Array.Empty<int>(), 1.0f),
|
||||||
|
new FileData(11, "exist/keep.txt", 1, 0.1f, QbtPriority.Normal, false, Array.Empty<int>(), 0.2f) // matches existing
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
var changed = _target.MergeContentsList(files, contents);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
changed.Should().BeTrue();
|
||||||
|
contents.ContainsKey("new").Should().BeTrue();
|
||||||
|
|
||||||
|
var minExistingIndex = contents.Values.Where(c => !c.IsFolder).Select(c => c.Index).DefaultIfEmpty(int.MaxValue).Min();
|
||||||
|
var newFolder = contents["new"];
|
||||||
|
newFolder.IsFolder.Should().BeTrue();
|
||||||
|
// index is computed from nextFolderIndex = Min(minExistingIndex, minFileIndex) - 1
|
||||||
|
// minExistingIndex (files) = min(50, 11) = 11 → min with minFileIndex(=5) => 5, so folder index < 5
|
||||||
|
newFolder.Index.Should().BeLessThan(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
552
Lantean.QBTMud.Test/Services/TorrentDataManagerMainDataTests.cs
Normal file
552
Lantean.QBTMud.Test/Services/TorrentDataManagerMainDataTests.cs
Normal file
@@ -0,0 +1,552 @@
|
|||||||
|
using AwesomeAssertions;
|
||||||
|
using Lantean.QBTMud.Helpers;
|
||||||
|
using Lantean.QBTMud.Models;
|
||||||
|
using Lantean.QBTMud.Services;
|
||||||
|
using Client = Lantean.QBitTorrentClient.Models;
|
||||||
|
|
||||||
|
namespace Lantean.QBTMud.Test.Services
|
||||||
|
{
|
||||||
|
public class TorrentDataManagerMainDataTests
|
||||||
|
{
|
||||||
|
private readonly TorrentDataManager _target = new TorrentDataManager();
|
||||||
|
|
||||||
|
// -------------------- CreateMainData --------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_EmptyInput_WHEN_CreateMainData_THEN_EmptyCollections_And_DefaultStates()
|
||||||
|
{
|
||||||
|
var client = new Client.MainData(
|
||||||
|
responseId: 1,
|
||||||
|
fullUpdate: true,
|
||||||
|
torrents: null,
|
||||||
|
torrentsRemoved: null,
|
||||||
|
categories: null,
|
||||||
|
categoriesRemoved: null,
|
||||||
|
tags: null,
|
||||||
|
tagsRemoved: null,
|
||||||
|
trackers: new Dictionary<string, IReadOnlyList<string>>(),
|
||||||
|
trackersRemoved: null,
|
||||||
|
serverState: null);
|
||||||
|
|
||||||
|
var result = _target.CreateMainData(client);
|
||||||
|
|
||||||
|
result.Torrents.Should().BeEmpty();
|
||||||
|
result.Tags.Should().BeEmpty();
|
||||||
|
result.Categories.Should().BeEmpty();
|
||||||
|
result.Trackers.Should().BeEmpty();
|
||||||
|
|
||||||
|
// Default from app GlobalTransferInfo() is "Unknown"
|
||||||
|
result.ServerState.ConnectionStatus.Should().Be("Unknown");
|
||||||
|
result.ServerState.UseSubcategories.Should().BeFalse();
|
||||||
|
|
||||||
|
result.TagState.Should().ContainKeys(FilterHelper.TAG_ALL, FilterHelper.TAG_UNTAGGED);
|
||||||
|
result.TagState[FilterHelper.TAG_ALL].Should().BeEmpty();
|
||||||
|
result.TagState[FilterHelper.TAG_UNTAGGED].Should().BeEmpty();
|
||||||
|
|
||||||
|
result.CategoriesState.Should().ContainKeys(FilterHelper.CATEGORY_ALL, FilterHelper.CATEGORY_UNCATEGORIZED);
|
||||||
|
result.CategoriesState[FilterHelper.CATEGORY_ALL].Should().BeEmpty();
|
||||||
|
result.CategoriesState[FilterHelper.CATEGORY_UNCATEGORIZED].Should().BeEmpty();
|
||||||
|
|
||||||
|
foreach (var s in Enum.GetValues<Status>())
|
||||||
|
{
|
||||||
|
result.StatusState.Should().ContainKey(s.ToString());
|
||||||
|
result.StatusState[s.ToString()].Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
result.TrackersState.Should().ContainKeys(FilterHelper.TRACKER_ALL, FilterHelper.TRACKER_TRACKERLESS);
|
||||||
|
result.TrackersState[FilterHelper.TRACKER_ALL].Should().BeEmpty();
|
||||||
|
result.TrackersState[FilterHelper.TRACKER_TRACKERLESS].Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_PopulatedInput_WHEN_CreateMainData_THEN_Maps_All_And_Builds_FilterStates()
|
||||||
|
{
|
||||||
|
var hash = "abc123";
|
||||||
|
var clientTorrent = new Client.Torrent
|
||||||
|
{
|
||||||
|
Name = "Movie A",
|
||||||
|
State = "downloading",
|
||||||
|
UploadSpeed = 0,
|
||||||
|
Category = "Movies/HD",
|
||||||
|
Tags = new[] { " tagA\tignored", "", "tagB" },
|
||||||
|
Tracker = "udp://tracker1",
|
||||||
|
AddedOn = 111,
|
||||||
|
AmountLeft = 1,
|
||||||
|
AutomaticTorrentManagement = true,
|
||||||
|
Availability = 1.0f,
|
||||||
|
Completed = 0,
|
||||||
|
CompletionOn = 0,
|
||||||
|
ContentPath = "/content",
|
||||||
|
DownloadLimit = 0,
|
||||||
|
DownloadSpeed = 1000,
|
||||||
|
Downloaded = 200,
|
||||||
|
DownloadedSession = 50,
|
||||||
|
EstimatedTimeOfArrival = 100,
|
||||||
|
FirstLastPiecePriority = false,
|
||||||
|
ForceStart = false,
|
||||||
|
InfoHashV1 = "v1",
|
||||||
|
InfoHashV2 = "v2",
|
||||||
|
LastActivity = 1,
|
||||||
|
MagnetUri = "magnet:?xt",
|
||||||
|
MaxRatio = 9.9f,
|
||||||
|
MaxSeedingTime = 0,
|
||||||
|
NumberComplete = 1,
|
||||||
|
NumberIncomplete = 1,
|
||||||
|
NumberLeeches = 0,
|
||||||
|
NumberSeeds = 5,
|
||||||
|
Priority = 1,
|
||||||
|
Progress = 0.2f,
|
||||||
|
Ratio = 0.1f,
|
||||||
|
RatioLimit = 0,
|
||||||
|
SavePath = "/save",
|
||||||
|
SeedingTime = 0,
|
||||||
|
SeedingTimeLimit = 0,
|
||||||
|
SeenComplete = 0,
|
||||||
|
SequentialDownload = false,
|
||||||
|
Size = 1000,
|
||||||
|
SuperSeeding = false,
|
||||||
|
TimeActive = 10,
|
||||||
|
TotalSize = 1000,
|
||||||
|
UploadLimit = 0,
|
||||||
|
Uploaded = 0,
|
||||||
|
UploadedSession = 0,
|
||||||
|
Reannounce = 0,
|
||||||
|
InactiveSeedingTimeLimit = 0,
|
||||||
|
MaxInactiveSeedingTime = 0,
|
||||||
|
Popularity = 0,
|
||||||
|
DownloadPath = "/dl",
|
||||||
|
RootPath = "/root",
|
||||||
|
IsPrivate = false,
|
||||||
|
ShareLimitAction = Client.ShareLimitAction.Default,
|
||||||
|
Comment = "c"
|
||||||
|
};
|
||||||
|
|
||||||
|
var torrents = new Dictionary<string, Client.Torrent> { [hash] = clientTorrent };
|
||||||
|
|
||||||
|
var categories = new Dictionary<string, Client.Category>
|
||||||
|
{
|
||||||
|
["Movies"] = new Client.Category("Movies", "/movies", downloadPath: null),
|
||||||
|
["Movies/HD"] = new Client.Category("Movies/HD", "/movies/hd", downloadPath: null)
|
||||||
|
};
|
||||||
|
|
||||||
|
var trackers = new Dictionary<string, IReadOnlyList<string>>
|
||||||
|
{
|
||||||
|
["udp://tracker1"] = new List<string> { hash }
|
||||||
|
};
|
||||||
|
|
||||||
|
var clientServer = new Client.ServerState(
|
||||||
|
allTimeDownloaded: 10,
|
||||||
|
allTimeUploaded: 20,
|
||||||
|
averageTimeQueue: 30,
|
||||||
|
connectionStatus: "connected",
|
||||||
|
dHTNodes: 2,
|
||||||
|
downloadInfoData: 3,
|
||||||
|
downloadInfoSpeed: 4,
|
||||||
|
downloadRateLimit: 5,
|
||||||
|
freeSpaceOnDisk: 6,
|
||||||
|
globalRatio: 7.5f,
|
||||||
|
queuedIOJobs: 8,
|
||||||
|
queuing: true,
|
||||||
|
readCacheHits: 9.1f,
|
||||||
|
readCacheOverload: 10.2f,
|
||||||
|
refreshInterval: 11,
|
||||||
|
totalBuffersSize: 12,
|
||||||
|
totalPeerConnections: 13,
|
||||||
|
totalQueuedSize: 14,
|
||||||
|
totalWastedSession: 15,
|
||||||
|
uploadInfoData: 16,
|
||||||
|
uploadInfoSpeed: 17,
|
||||||
|
uploadRateLimit: 18,
|
||||||
|
useAltSpeedLimits: false,
|
||||||
|
useSubcategories: true,
|
||||||
|
writeCacheOverload: 19.3f,
|
||||||
|
lastExternalAddressV4: "1.2.3.4",
|
||||||
|
lastExternalAddressV6: "2001::1");
|
||||||
|
|
||||||
|
var client = new Client.MainData(
|
||||||
|
responseId: 2,
|
||||||
|
fullUpdate: true,
|
||||||
|
torrents: torrents,
|
||||||
|
torrentsRemoved: null,
|
||||||
|
categories: categories,
|
||||||
|
categoriesRemoved: null,
|
||||||
|
tags: new[] { " tagA", "tagA", "tagB", "" },
|
||||||
|
tagsRemoved: null,
|
||||||
|
trackers: trackers,
|
||||||
|
trackersRemoved: null,
|
||||||
|
serverState: clientServer);
|
||||||
|
|
||||||
|
var result = _target.CreateMainData(client);
|
||||||
|
|
||||||
|
result.Torrents.Should().ContainKey(hash);
|
||||||
|
var mapped = result.Torrents[hash];
|
||||||
|
mapped.Name.Should().Be("Movie A");
|
||||||
|
mapped.Category.Should().Be("Movies/HD");
|
||||||
|
mapped.Tags.Should().BeEquivalentTo(new[] { "tagA", "tagB" }, o => o.WithoutStrictOrdering());
|
||||||
|
mapped.Tracker.Should().Be("udp://tracker1");
|
||||||
|
|
||||||
|
result.TagState[FilterHelper.TAG_ALL].Should().Contain(hash);
|
||||||
|
result.TagState[FilterHelper.TAG_UNTAGGED].Should().NotContain(hash);
|
||||||
|
result.TagState["tagA"].Should().Contain(hash);
|
||||||
|
result.TagState["tagB"].Should().Contain(hash);
|
||||||
|
|
||||||
|
result.CategoriesState[FilterHelper.CATEGORY_ALL].Should().Contain(hash);
|
||||||
|
result.CategoriesState[FilterHelper.CATEGORY_UNCATEGORIZED].Should().NotContain(hash);
|
||||||
|
result.CategoriesState["Movies/HD"].Should().Contain(hash);
|
||||||
|
result.CategoriesState["Movies"].Should().Contain(hash);
|
||||||
|
|
||||||
|
result.TrackersState[FilterHelper.TRACKER_ALL].Should().Contain(hash);
|
||||||
|
result.TrackersState[FilterHelper.TRACKER_TRACKERLESS].Should().NotContain(hash);
|
||||||
|
result.TrackersState["udp://tracker1"].Should().Contain(hash);
|
||||||
|
|
||||||
|
result.StatusState[nameof(Status.Downloading)].Should().Contain(hash);
|
||||||
|
result.StatusState[nameof(Status.Active)].Should().Contain(hash);
|
||||||
|
result.StatusState[nameof(Status.Inactive)].Should().NotContain(hash);
|
||||||
|
result.StatusState[nameof(Status.Stalled)].Should().NotContain(hash);
|
||||||
|
|
||||||
|
result.ServerState.ConnectionStatus.Should().Be("connected");
|
||||||
|
result.ServerState.UseSubcategories.Should().BeTrue();
|
||||||
|
result.ServerState.LastExternalAddressV4.Should().Be("1.2.3.4");
|
||||||
|
result.ServerState.LastExternalAddressV6.Should().Be("2001::1");
|
||||||
|
result.ServerState.GlobalRatio.Should().Be(7.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- MergeMainData: removals --------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_ExistingData_WHEN_Merge_Removals_THEN_ItemsAndStateAreRemoved_And_Flagged()
|
||||||
|
{
|
||||||
|
var hash = "h1";
|
||||||
|
var clientTorrent = new Client.Torrent
|
||||||
|
{
|
||||||
|
Name = "T1",
|
||||||
|
State = "downloading",
|
||||||
|
UploadSpeed = 0,
|
||||||
|
Category = "Cat/Sub",
|
||||||
|
Tags = new[] { "x", "y" },
|
||||||
|
Tracker = "udp://t1"
|
||||||
|
};
|
||||||
|
var client = new Client.MainData(
|
||||||
|
responseId: 1,
|
||||||
|
fullUpdate: true,
|
||||||
|
torrents: new Dictionary<string, Client.Torrent> { [hash] = clientTorrent },
|
||||||
|
torrentsRemoved: null,
|
||||||
|
categories: new Dictionary<string, Client.Category>
|
||||||
|
{
|
||||||
|
["Cat/Sub"] = new Client.Category("Cat/Sub", "/cat/sub", null)
|
||||||
|
},
|
||||||
|
categoriesRemoved: null,
|
||||||
|
tags: new[] { "x", "y" },
|
||||||
|
tagsRemoved: null,
|
||||||
|
trackers: new Dictionary<string, IReadOnlyList<string>> { ["udp://t1"] = new List<string> { hash } },
|
||||||
|
trackersRemoved: null,
|
||||||
|
serverState: new Client.ServerState(
|
||||||
|
allTimeDownloaded: 0,
|
||||||
|
allTimeUploaded: 0,
|
||||||
|
averageTimeQueue: 0,
|
||||||
|
connectionStatus: "connected",
|
||||||
|
dHTNodes: 0,
|
||||||
|
downloadInfoData: 0,
|
||||||
|
downloadInfoSpeed: 0,
|
||||||
|
downloadRateLimit: 0,
|
||||||
|
freeSpaceOnDisk: 0,
|
||||||
|
globalRatio: 0f,
|
||||||
|
queuedIOJobs: 0,
|
||||||
|
queuing: false,
|
||||||
|
readCacheHits: 0f,
|
||||||
|
readCacheOverload: 0f,
|
||||||
|
refreshInterval: 0,
|
||||||
|
totalBuffersSize: 0,
|
||||||
|
totalPeerConnections: 0,
|
||||||
|
totalQueuedSize: 0,
|
||||||
|
totalWastedSession: 0,
|
||||||
|
uploadInfoData: 0,
|
||||||
|
uploadInfoSpeed: 0,
|
||||||
|
uploadRateLimit: 0,
|
||||||
|
useAltSpeedLimits: false,
|
||||||
|
useSubcategories: true,
|
||||||
|
writeCacheOverload: 0f,
|
||||||
|
lastExternalAddressV4: "4",
|
||||||
|
lastExternalAddressV6: "6"));
|
||||||
|
var existing = _target.CreateMainData(client);
|
||||||
|
|
||||||
|
var delta = new Client.MainData(
|
||||||
|
responseId: 2,
|
||||||
|
fullUpdate: false,
|
||||||
|
torrents: null,
|
||||||
|
torrentsRemoved: new[] { hash },
|
||||||
|
categories: null,
|
||||||
|
categoriesRemoved: new[] { "Cat/Sub" },
|
||||||
|
tags: null,
|
||||||
|
tagsRemoved: new[] { "x" },
|
||||||
|
trackers: new Dictionary<string, IReadOnlyList<string>>(),
|
||||||
|
trackersRemoved: new[] { "udp://t1" },
|
||||||
|
serverState: null);
|
||||||
|
|
||||||
|
var changed = _target.MergeMainData(delta, existing, out var filterChanged);
|
||||||
|
|
||||||
|
changed.Should().BeTrue();
|
||||||
|
filterChanged.Should().BeTrue();
|
||||||
|
|
||||||
|
existing.Torrents.Should().NotContainKey(hash);
|
||||||
|
existing.Categories.Should().NotContainKey("Cat/Sub");
|
||||||
|
existing.Trackers.Should().NotContainKey("udp://t1");
|
||||||
|
existing.TagState.Should().NotContainKey("x");
|
||||||
|
|
||||||
|
existing.TagState[FilterHelper.TAG_ALL].Should().NotContain(hash);
|
||||||
|
existing.CategoriesState[FilterHelper.CATEGORY_ALL].Should().NotContain(hash);
|
||||||
|
foreach (var kv in existing.StatusState)
|
||||||
|
{
|
||||||
|
kv.Value.Should().NotContain(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.TrackersState[FilterHelper.TRACKER_ALL].Should().NotContain(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- MergeMainData: additions (from empty) --------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_EmptyExisting_WHEN_Merge_Additions_THEN_TorrentAndStatesAdded_And_Flagged()
|
||||||
|
{
|
||||||
|
var existing = _target.CreateMainData(
|
||||||
|
new Client.MainData(0, true, null, null, null, null, null, null,
|
||||||
|
new Dictionary<string, IReadOnlyList<string>>(), null, null));
|
||||||
|
|
||||||
|
var hash = "z1";
|
||||||
|
var addTorrent = new Client.Torrent
|
||||||
|
{
|
||||||
|
Name = "Zed",
|
||||||
|
State = "downloading",
|
||||||
|
UploadSpeed = 0,
|
||||||
|
Category = "",
|
||||||
|
Tags = Array.Empty<string>(),
|
||||||
|
Tracker = ""
|
||||||
|
};
|
||||||
|
|
||||||
|
var delta = new Client.MainData(
|
||||||
|
responseId: 1,
|
||||||
|
fullUpdate: false,
|
||||||
|
torrents: new Dictionary<string, Client.Torrent> { [hash] = addTorrent },
|
||||||
|
torrentsRemoved: null,
|
||||||
|
categories: new Dictionary<string, Client.Category>(),
|
||||||
|
categoriesRemoved: null,
|
||||||
|
tags: Array.Empty<string>(),
|
||||||
|
tagsRemoved: null,
|
||||||
|
trackers: new Dictionary<string, IReadOnlyList<string>>(),
|
||||||
|
trackersRemoved: null,
|
||||||
|
serverState: null);
|
||||||
|
|
||||||
|
var changed = _target.MergeMainData(delta, existing, out var filterChanged);
|
||||||
|
|
||||||
|
changed.Should().BeTrue();
|
||||||
|
filterChanged.Should().BeTrue();
|
||||||
|
|
||||||
|
existing.Torrents.Should().ContainKey(hash);
|
||||||
|
|
||||||
|
existing.TagState[FilterHelper.TAG_ALL].Should().Contain(hash);
|
||||||
|
existing.TagState[FilterHelper.TAG_UNTAGGED].Should().Contain(hash);
|
||||||
|
|
||||||
|
existing.CategoriesState[FilterHelper.CATEGORY_ALL].Should().Contain(hash);
|
||||||
|
existing.CategoriesState[FilterHelper.CATEGORY_UNCATEGORIZED].Should().Contain(hash);
|
||||||
|
|
||||||
|
existing.TrackersState[FilterHelper.TRACKER_ALL].Should().Contain(hash);
|
||||||
|
existing.TrackersState[FilterHelper.TRACKER_TRACKERLESS].Should().Contain(hash);
|
||||||
|
|
||||||
|
existing.StatusState[nameof(Status.Downloading)].Should().Contain(hash);
|
||||||
|
existing.StatusState[nameof(Status.Active)].Should().Contain(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- MergeMainData: updating an existing torrent (filter-affecting changes) --------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_ExistingTorrent_WHEN_UpdateCategoryTagsStateTrackerAndSpeed_THEN_FilterSetsAdjusted()
|
||||||
|
{
|
||||||
|
var hash = "h2";
|
||||||
|
var start = new Client.MainData(
|
||||||
|
responseId: 1,
|
||||||
|
fullUpdate: true,
|
||||||
|
torrents: new Dictionary<string, Client.Torrent>
|
||||||
|
{
|
||||||
|
[hash] = new Client.Torrent
|
||||||
|
{
|
||||||
|
Name = "A",
|
||||||
|
State = "stalledDL",
|
||||||
|
UploadSpeed = 0,
|
||||||
|
Category = "",
|
||||||
|
Tags = Array.Empty<string>(),
|
||||||
|
Tracker = ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
torrentsRemoved: null,
|
||||||
|
categories: new Dictionary<string, Client.Category>(),
|
||||||
|
categoriesRemoved: null,
|
||||||
|
tags: Array.Empty<string>(),
|
||||||
|
tagsRemoved: null,
|
||||||
|
trackers: new Dictionary<string, IReadOnlyList<string>>(),
|
||||||
|
trackersRemoved: null,
|
||||||
|
serverState: new Client.ServerState(
|
||||||
|
allTimeDownloaded: 0,
|
||||||
|
allTimeUploaded: 0,
|
||||||
|
averageTimeQueue: 0,
|
||||||
|
connectionStatus: "connected",
|
||||||
|
dHTNodes: 0,
|
||||||
|
downloadInfoData: 0,
|
||||||
|
downloadInfoSpeed: 0,
|
||||||
|
downloadRateLimit: 0,
|
||||||
|
freeSpaceOnDisk: 0,
|
||||||
|
globalRatio: 0f,
|
||||||
|
queuedIOJobs: 0,
|
||||||
|
queuing: false,
|
||||||
|
readCacheHits: 0f,
|
||||||
|
readCacheOverload: 0f,
|
||||||
|
refreshInterval: 0,
|
||||||
|
totalBuffersSize: 0,
|
||||||
|
totalPeerConnections: 0,
|
||||||
|
totalQueuedSize: 0,
|
||||||
|
totalWastedSession: 0,
|
||||||
|
uploadInfoData: 0,
|
||||||
|
uploadInfoSpeed: 0,
|
||||||
|
uploadRateLimit: 0,
|
||||||
|
useAltSpeedLimits: false,
|
||||||
|
useSubcategories: true,
|
||||||
|
writeCacheOverload: 0f));
|
||||||
|
var list = _target.CreateMainData(start);
|
||||||
|
|
||||||
|
list.ServerState.UseSubcategories = true;
|
||||||
|
|
||||||
|
var update = new Client.MainData(
|
||||||
|
responseId: 2,
|
||||||
|
fullUpdate: false,
|
||||||
|
torrents: new Dictionary<string, Client.Torrent>
|
||||||
|
{
|
||||||
|
[hash] = new Client.Torrent
|
||||||
|
{
|
||||||
|
Name = "A",
|
||||||
|
State = "stalledDL",
|
||||||
|
UploadSpeed = 10,
|
||||||
|
Category = "Cat/Sub",
|
||||||
|
Tags = new[] { " x\tid " },
|
||||||
|
Tracker = "udp://zzz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
torrentsRemoved: null,
|
||||||
|
categories: new Dictionary<string, Client.Category>(),
|
||||||
|
categoriesRemoved: null,
|
||||||
|
tags: new[] { " x\tgarbage " },
|
||||||
|
tagsRemoved: null,
|
||||||
|
trackers: new Dictionary<string, IReadOnlyList<string>> { ["udp://zzz"] = new List<string> { hash } },
|
||||||
|
trackersRemoved: null,
|
||||||
|
serverState: null);
|
||||||
|
|
||||||
|
var changed = _target.MergeMainData(update, list, out var filterChanged);
|
||||||
|
|
||||||
|
changed.Should().BeTrue();
|
||||||
|
filterChanged.Should().BeTrue();
|
||||||
|
|
||||||
|
list.TagState[FilterHelper.TAG_UNTAGGED].Should().NotContain(hash);
|
||||||
|
list.TagState["x"].Should().Contain(hash);
|
||||||
|
|
||||||
|
list.CategoriesState[FilterHelper.CATEGORY_UNCATEGORIZED].Should().NotContain(hash);
|
||||||
|
list.CategoriesState["Cat/Sub"].Should().Contain(hash);
|
||||||
|
list.CategoriesState["Cat"].Should().Contain(hash);
|
||||||
|
|
||||||
|
list.TrackersState[FilterHelper.TRACKER_TRACKERLESS].Should().NotContain(hash);
|
||||||
|
list.TrackersState["udp://zzz"].Should().Contain(hash);
|
||||||
|
|
||||||
|
list.StatusState[nameof(Status.Inactive)].Should().NotContain(hash);
|
||||||
|
list.StatusState[nameof(Status.Active)].Should().Contain(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- MergeMainData: trackers & categories dictionary update paths --------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_ExistingTrackersAndCategories_WHEN_SequenceChangesOrSavePathChanges_THEN_DataChangedTrue()
|
||||||
|
{
|
||||||
|
var h = "a";
|
||||||
|
var start = new Client.MainData(
|
||||||
|
responseId: 1,
|
||||||
|
fullUpdate: true,
|
||||||
|
torrents: new Dictionary<string, Client.Torrent> { [h] = new Client.Torrent { Name = "N", State = "downloading", UploadSpeed = 0, Category = "C", Tags = Array.Empty<string>(), Tracker = "t1" } },
|
||||||
|
torrentsRemoved: null,
|
||||||
|
categories: new Dictionary<string, Client.Category> { ["C"] = new Client.Category("C", "/a", null) },
|
||||||
|
categoriesRemoved: null,
|
||||||
|
tags: Array.Empty<string>(),
|
||||||
|
tagsRemoved: null,
|
||||||
|
trackers: new Dictionary<string, IReadOnlyList<string>> { ["t1"] = new List<string> { h } },
|
||||||
|
trackersRemoved: null,
|
||||||
|
serverState: null);
|
||||||
|
var list = _target.CreateMainData(start);
|
||||||
|
|
||||||
|
var delta = new Client.MainData(
|
||||||
|
responseId: 2,
|
||||||
|
fullUpdate: false,
|
||||||
|
torrents: null,
|
||||||
|
torrentsRemoved: null,
|
||||||
|
categories: new Dictionary<string, Client.Category> { ["C"] = new Client.Category("C", "/b", null) },
|
||||||
|
categoriesRemoved: null,
|
||||||
|
tags: null,
|
||||||
|
tagsRemoved: null,
|
||||||
|
trackers: new Dictionary<string, IReadOnlyList<string>> { ["t1"] = new List<string> { h, "other" } },
|
||||||
|
trackersRemoved: null,
|
||||||
|
serverState: null);
|
||||||
|
|
||||||
|
var changed = _target.MergeMainData(delta, list, out var filterChanged);
|
||||||
|
|
||||||
|
changed.Should().BeTrue();
|
||||||
|
filterChanged.Should().BeFalse();
|
||||||
|
|
||||||
|
list.Trackers["t1"].Should().Equal(new[] { h, "other" });
|
||||||
|
list.Categories["C"].SavePath.Should().Be("/b");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- MergeMainData: ServerState update --------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_Existing_WHEN_ServerStateFieldsChange_THEN_DataChangedTrue_And_ValuesUpdated()
|
||||||
|
{
|
||||||
|
var existing = _target.CreateMainData(
|
||||||
|
new Client.MainData(0, true, null, null, null, null, null, null,
|
||||||
|
new Dictionary<string, IReadOnlyList<string>>(), null,
|
||||||
|
new Client.ServerState(
|
||||||
|
allTimeDownloaded: 1, allTimeUploaded: 2, averageTimeQueue: 3,
|
||||||
|
connectionStatus: "connected", dHTNodes: 4, downloadInfoData: 5,
|
||||||
|
downloadInfoSpeed: 6, downloadRateLimit: 7, freeSpaceOnDisk: 8, globalRatio: 9.0f,
|
||||||
|
queuedIOJobs: 10, queuing: true, readCacheHits: 11.0f, readCacheOverload: 12.0f,
|
||||||
|
refreshInterval: 13, totalBuffersSize: 14, totalPeerConnections: 15, totalQueuedSize: 16,
|
||||||
|
totalWastedSession: 17, uploadInfoData: 18, uploadInfoSpeed: 19, uploadRateLimit: 20,
|
||||||
|
useAltSpeedLimits: false, useSubcategories: false, writeCacheOverload: 21.0f,
|
||||||
|
lastExternalAddressV4: "4", lastExternalAddressV6: "6")));
|
||||||
|
|
||||||
|
var delta = new Client.MainData(
|
||||||
|
responseId: 1,
|
||||||
|
fullUpdate: false,
|
||||||
|
torrents: null, torrentsRemoved: null,
|
||||||
|
categories: null, categoriesRemoved: null,
|
||||||
|
tags: null, tagsRemoved: null,
|
||||||
|
trackers: new Dictionary<string, IReadOnlyList<string>>(), trackersRemoved: null,
|
||||||
|
serverState: new Client.ServerState(
|
||||||
|
allTimeDownloaded: 100, allTimeUploaded: 200, averageTimeQueue: 300,
|
||||||
|
connectionStatus: "stopped", dHTNodes: 40, downloadInfoData: 50,
|
||||||
|
downloadInfoSpeed: 60, downloadRateLimit: 70, freeSpaceOnDisk: 80, globalRatio: 1.5f,
|
||||||
|
queuedIOJobs: 1000, queuing: false, readCacheHits: 0.5f, readCacheOverload: 0.2f,
|
||||||
|
refreshInterval: 99, totalBuffersSize: 77, totalPeerConnections: 88, totalQueuedSize: 66,
|
||||||
|
totalWastedSession: 55, uploadInfoData: 44, uploadInfoSpeed: 33, uploadRateLimit: 22,
|
||||||
|
useAltSpeedLimits: true, useSubcategories: true, writeCacheOverload: 0.1f,
|
||||||
|
lastExternalAddressV4: "8.8.8.8", lastExternalAddressV6: "fe80::1"));
|
||||||
|
|
||||||
|
var changed = _target.MergeMainData(delta, existing, out var filterChanged);
|
||||||
|
|
||||||
|
changed.Should().BeTrue();
|
||||||
|
filterChanged.Should().BeFalse();
|
||||||
|
|
||||||
|
var s = existing.ServerState;
|
||||||
|
s.ConnectionStatus.Should().Be("stopped");
|
||||||
|
s.DHTNodes.Should().Be(40);
|
||||||
|
s.DownloadInfoSpeed.Should().Be(60);
|
||||||
|
s.UploadRateLimit.Should().Be(22);
|
||||||
|
s.UseSubcategories.Should().BeTrue();
|
||||||
|
s.LastExternalAddressV4.Should().Be("8.8.8.8");
|
||||||
|
s.LastExternalAddressV6.Should().Be("fe80::1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||||||
readme.md = readme.md
|
readme.md = readme.md
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lantean.QBitTorrentClient.Test", "Lantean.QBitTorrentClient.Test\Lantean.QBitTorrentClient.Test.csproj", "{796E865C-7AA6-4BD9-B12F-394801199A75}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -33,6 +35,10 @@ Global
|
|||||||
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Release|Any CPU.Build.0 = Release|Any CPU
|
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{796E865C-7AA6-4BD9-B12F-394801199A75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{796E865C-7AA6-4BD9-B12F-394801199A75}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{796E865C-7AA6-4BD9-B12F-394801199A75}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{796E865C-7AA6-4BD9-B12F-394801199A75}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -1,33 +1,50 @@
|
|||||||
<MudGrid>
|
@using Lantean.QBitTorrentClient.Models
|
||||||
|
|
||||||
|
<MudGrid>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<MudSwitch Label="Additional Options" @bind-Value="Expanded" LabelPlacement="Placement.End" />
|
<MudSwitch Label="Additional Options" @bind-Value="Expanded" LabelPlacement="Placement.End" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
<MudCollapse Expanded="Expanded">
|
<MudCollapse Expanded="Expanded">
|
||||||
<MudGrid>
|
<MudGrid Class="mt-2">
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<MudSelect Label="Torrent Management Mode" @bind-Value="TorrentManagementMode" Variant="Variant.Outlined">
|
<MudSelect T="bool" Label="Torrent management mode" Value="@TorrentManagementMode" ValueChanged="@SetTorrentManagementMode" Variant="Variant.Outlined">
|
||||||
<MudSelectItem Value="false">Manual</MudSelectItem>
|
<MudSelectItem Value="@false">Manual</MudSelectItem>
|
||||||
<MudSelectItem Value="true">Automatic</MudSelectItem>
|
<MudSelectItem Value="@true">Automatic</MudSelectItem>
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6">
|
||||||
|
<MudTextField T="string" Label="Save files to location" Value="@SavePath" ValueChanged="@SavePathChanged" Variant="Variant.Outlined" Disabled="@TorrentManagementMode" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6">
|
||||||
|
<FieldSwitch Label="Use incomplete save path" Value="@UseDownloadPath" ValueChanged="@SetUseDownloadPath" Disabled="@TorrentManagementMode" />
|
||||||
|
</MudItem>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<MudTextField Label="Save files to location" @bind-Value="SavePath" Variant="Variant.Outlined"></MudTextField>
|
<MudTextField T="string" Label="Incomplete save path" Value="@DownloadPath" ValueChanged="@DownloadPathChanged" Variant="Variant.Outlined" Disabled="@DownloadPathDisabled" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
@if (ShowCookieOption)
|
@if (ShowCookieOption)
|
||||||
{
|
{
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<MudTextField Label="Cookie" @bind-Value="Cookie" Variant="Variant.Outlined"></MudTextField>
|
<MudTextField Label="Cookie" @bind-Value="Cookie" Variant="Variant.Outlined" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
}
|
}
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<MudTextField Label="Rename" @bind-Value="RenameTorrent" Variant="Variant.Outlined"></MudTextField>
|
<MudTextField Label="Rename" @bind-Value="RenameTorrent" Variant="Variant.Outlined" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<MudSelect Label="Category" @bind-Value="Category" Variant="Variant.Outlined">
|
<MudSelect T="string" Label="Category" Value="@Category" ValueChanged="@CategoryChanged" Variant="Variant.Outlined" Clearable="true">
|
||||||
@foreach (var category in Categories)
|
<MudSelectItem Value="@string.Empty">None</MudSelectItem>
|
||||||
|
@foreach (var category in CategoryOptions)
|
||||||
{
|
{
|
||||||
<MudSelectItem Value="category">@category</MudSelectItem>
|
<MudSelectItem Value="@category.Name">@category.Name</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudSelect T="string" Label="Tags" Variant="Variant.Outlined" MultiSelection="true" SelectedValues="@SelectedTags" SelectedValuesChanged="@SelectedTagsChanged" Disabled="@(AvailableTags.Count == 0)">
|
||||||
|
@foreach (var tag in AvailableTags)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@tag">@tag</MudSelectItem>
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
@@ -38,7 +55,7 @@
|
|||||||
<FieldSwitch Label="Add to top of queue" @bind-Value="AddToTopOfQueue" />
|
<FieldSwitch Label="Add to top of queue" @bind-Value="AddToTopOfQueue" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<MudSelect Label="Stop condition" @bind-Value="StopCondition" Variant="Variant.Outlined">
|
<MudSelect T="string" Label="Stop condition" Value="@StopCondition" ValueChanged="@StopConditionChanged" Variant="Variant.Outlined">
|
||||||
<MudSelectItem Value="@("None")">None</MudSelectItem>
|
<MudSelectItem Value="@("None")">None</MudSelectItem>
|
||||||
<MudSelectItem Value="@("MetadataReceived")">Metadata received</MudSelectItem>
|
<MudSelectItem Value="@("MetadataReceived")">Metadata received</MudSelectItem>
|
||||||
<MudSelectItem Value="@("FilesChecked")">Files checked</MudSelectItem>
|
<MudSelectItem Value="@("FilesChecked")">Files checked</MudSelectItem>
|
||||||
@@ -47,22 +64,58 @@
|
|||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<FieldSwitch Label="Skip hash check" @bind-Value="SkipHashCheck" />
|
<FieldSwitch Label="Skip hash check" @bind-Value="SkipHashCheck" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudSelect Label="Content layout" @bind-Value="ContentLayout" Variant="Variant.Outlined">
|
<MudItem xs="12">
|
||||||
|
<MudSelect T="string" Label="Content layout" Value="@ContentLayout" ValueChanged="@ContentLayoutChanged" Variant="Variant.Outlined">
|
||||||
<MudSelectItem Value="@("Original")">Original</MudSelectItem>
|
<MudSelectItem Value="@("Original")">Original</MudSelectItem>
|
||||||
<MudSelectItem Value="@("Subfolder")">Create subfolder</MudSelectItem>
|
<MudSelectItem Value="@("Subfolder")">Create subfolder</MudSelectItem>
|
||||||
<MudSelectItem Value="@("NoSubfolder")">Don't create subfolder'</MudSelectItem>
|
<MudSelectItem Value="@("NoSubfolder")">Don't create subfolder</MudSelectItem>
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<FieldSwitch Label="Download in sequentual order" @bind-Value="DownloadInSequentialOrder" />
|
<FieldSwitch Label="Download in sequential order" @bind-Value="DownloadInSequentialOrder" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<FieldSwitch Label="Download first and last pieces first" @bind-Value="DownloadFirstAndLastPiecesFirst" />
|
<FieldSwitch Label="Download first and last pieces first" @bind-Value="DownloadFirstAndLastPiecesFirst" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12" sm="6">
|
||||||
<MudNumericField Label="Limit download rate" @bind-Value="DownloadLimit" Variant="Variant.Outlined" Min="0" />
|
<MudNumericField Label="Limit download rate" @bind-Value="DownloadLimit" Variant="Variant.Outlined" Min="0" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12" sm="6">
|
||||||
<MudNumericField Label="Limit upload rate" @bind-Value="UploadLimit" Variant="Variant.Outlined" Min="0" />
|
<MudNumericField Label="Limit upload rate" @bind-Value="UploadLimit" Variant="Variant.Outlined" Min="0" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudSelect T="ShareLimitMode" Label="Share limit preset" Value="@SelectedShareLimitMode" ValueChanged="@ShareLimitModeChanged" Variant="Variant.Outlined">
|
||||||
|
<MudSelectItem Value="@ShareLimitMode.Global">Use global share limit</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@ShareLimitMode.NoLimit">Set no share limit</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@ShareLimitMode.Custom">Set custom share limit</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="4">
|
||||||
|
<FieldSwitch Label="Ratio" Value="@RatioLimitEnabled" ValueChanged="@RatioLimitEnabledChanged" Disabled="@(!IsCustomShareLimit)" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="8">
|
||||||
|
<MudNumericField T="float" Label="Ratio limit" Value="@RatioLimit" ValueChanged="@RatioLimitChanged" Disabled="@(!RatioLimitEnabled || !IsCustomShareLimit)" Min="0" Step="0.1f" Format="F2" Variant="Variant.Outlined" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="4">
|
||||||
|
<FieldSwitch Label="Total minutes" Value="@SeedingTimeLimitEnabled" ValueChanged="@SeedingTimeLimitEnabledChanged" Disabled="@(!IsCustomShareLimit)" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="8">
|
||||||
|
<MudNumericField T="int" Label="Total minutes" Value="@SeedingTimeLimit" ValueChanged="@SeedingTimeLimitChanged" Disabled="@(!SeedingTimeLimitEnabled || !IsCustomShareLimit)" Min="1" Variant="Variant.Outlined" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="4">
|
||||||
|
<FieldSwitch Label="Inactive minutes" Value="@InactiveSeedingTimeLimitEnabled" ValueChanged="@InactiveSeedingTimeLimitEnabledChanged" Disabled="@(!IsCustomShareLimit)" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="8">
|
||||||
|
<MudNumericField T="int" Label="Inactive minutes" Value="@InactiveSeedingTimeLimit" ValueChanged="@InactiveSeedingTimeLimitChanged" Disabled="@(!InactiveSeedingTimeLimitEnabled || !IsCustomShareLimit)" Min="1" Variant="Variant.Outlined" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudSelect T="ShareLimitAction" Label="Action when limit is reached" Value="@SelectedShareLimitAction" ValueChanged="@ShareLimitActionChanged" Disabled="@(!IsCustomShareLimit)" Variant="Variant.Outlined">
|
||||||
|
<MudSelectItem Value="@ShareLimitAction.Default">Default</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@ShareLimitAction.Stop">Stop torrent</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@ShareLimitAction.Remove">Remove torrent</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@ShareLimitAction.RemoveWithContent">Remove torrent and data</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@ShareLimitAction.EnableSuperSeeding">Enable super seeding</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
</MudCollapse>
|
</MudCollapse>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Lantean.QBitTorrentClient;
|
using Lantean.QBitTorrentClient;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
@@ -6,6 +7,15 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
{
|
{
|
||||||
public partial class AddTorrentOptions
|
public partial class AddTorrentOptions
|
||||||
{
|
{
|
||||||
|
private readonly List<CategoryOption> _categoryOptions = new();
|
||||||
|
private readonly Dictionary<string, CategoryOption> _categoryLookup = new(StringComparer.Ordinal);
|
||||||
|
private string _manualSavePath = string.Empty;
|
||||||
|
private bool _manualUseDownloadPath;
|
||||||
|
private string _manualDownloadPath = string.Empty;
|
||||||
|
private string _defaultSavePath = string.Empty;
|
||||||
|
private string _defaultDownloadPath = string.Empty;
|
||||||
|
private bool _defaultDownloadPathEnabled;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IApiClient ApiClient { get; set; } = default!;
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|
||||||
@@ -16,15 +26,25 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
|
|
||||||
protected bool TorrentManagementMode { get; set; }
|
protected bool TorrentManagementMode { get; set; }
|
||||||
|
|
||||||
protected string SavePath { get; set; } = default!;
|
protected string SavePath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
protected string DownloadPath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
protected bool UseDownloadPath { get; set; }
|
||||||
|
|
||||||
|
protected bool DownloadPathDisabled => TorrentManagementMode || !UseDownloadPath;
|
||||||
|
|
||||||
protected string? Cookie { get; set; }
|
protected string? Cookie { get; set; }
|
||||||
|
|
||||||
protected string? RenameTorrent { get; set; }
|
protected string? RenameTorrent { get; set; }
|
||||||
|
|
||||||
protected IEnumerable<string> Categories { get; set; } = [];
|
protected IReadOnlyList<CategoryOption> CategoryOptions => _categoryOptions;
|
||||||
|
|
||||||
protected string? Category { get; set; }
|
protected string? Category { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
protected List<string> AvailableTags { get; private set; } = [];
|
||||||
|
|
||||||
|
protected HashSet<string> SelectedTags { get; private set; } = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
protected bool StartTorrent { get; set; } = true;
|
protected bool StartTorrent { get; set; } = true;
|
||||||
|
|
||||||
@@ -32,41 +52,232 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
|
|
||||||
protected string StopCondition { get; set; } = "None";
|
protected string StopCondition { get; set; } = "None";
|
||||||
|
|
||||||
protected bool SkipHashCheck { get; set; } = false;
|
protected bool SkipHashCheck { get; set; }
|
||||||
|
|
||||||
protected string ContentLayout { get; set; } = "Original";
|
protected string ContentLayout { get; set; } = "Original";
|
||||||
|
|
||||||
protected bool DownloadInSequentialOrder { get; set; } = false;
|
protected bool DownloadInSequentialOrder { get; set; }
|
||||||
|
|
||||||
protected bool DownloadFirstAndLastPiecesFirst { get; set; } = false;
|
protected bool DownloadFirstAndLastPiecesFirst { get; set; }
|
||||||
|
|
||||||
protected long DownloadLimit { get; set; }
|
protected long DownloadLimit { get; set; }
|
||||||
|
|
||||||
protected long UploadLimit { get; set; }
|
protected long UploadLimit { get; set; }
|
||||||
|
|
||||||
|
protected ShareLimitMode SelectedShareLimitMode { get; set; } = ShareLimitMode.Global;
|
||||||
|
|
||||||
|
protected bool RatioLimitEnabled { get; set; }
|
||||||
|
|
||||||
|
protected float RatioLimit { get; set; } = 1.0f;
|
||||||
|
|
||||||
|
protected bool SeedingTimeLimitEnabled { get; set; }
|
||||||
|
|
||||||
|
protected int SeedingTimeLimit { get; set; } = 1440;
|
||||||
|
|
||||||
|
protected bool InactiveSeedingTimeLimitEnabled { get; set; }
|
||||||
|
|
||||||
|
protected int InactiveSeedingTimeLimit { get; set; } = 1440;
|
||||||
|
|
||||||
|
protected ShareLimitAction SelectedShareLimitAction { get; set; } = ShareLimitAction.Default;
|
||||||
|
|
||||||
|
protected bool IsCustomShareLimit => SelectedShareLimitMode == ShareLimitMode.Custom;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
var categories = await ApiClient.GetAllCategories();
|
var categories = await ApiClient.GetAllCategories();
|
||||||
Categories = categories.Select(c => c.Key).ToList();
|
foreach (var (name, value) in categories.OrderBy(kvp => kvp.Key, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var option = new CategoryOption(name, value.SavePath, value.DownloadPath);
|
||||||
|
_categoryOptions.Add(option);
|
||||||
|
_categoryLookup[name] = option;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tags = await ApiClient.GetAllTags();
|
||||||
|
AvailableTags = tags.OrderBy(t => t, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
|
||||||
var preferences = await ApiClient.GetApplicationPreferences();
|
var preferences = await ApiClient.GetApplicationPreferences();
|
||||||
|
|
||||||
TorrentManagementMode = preferences.AutoTmmEnabled;
|
TorrentManagementMode = preferences.AutoTmmEnabled;
|
||||||
SavePath = preferences.SavePath;
|
|
||||||
StartTorrent = !preferences.StartPausedEnabled;
|
_defaultSavePath = preferences.SavePath ?? string.Empty;
|
||||||
|
_manualSavePath = _defaultSavePath;
|
||||||
|
SavePath = _defaultSavePath;
|
||||||
|
|
||||||
|
_defaultDownloadPath = preferences.TempPath ?? string.Empty;
|
||||||
|
_defaultDownloadPathEnabled = preferences.TempPathEnabled;
|
||||||
|
_manualDownloadPath = _defaultDownloadPath;
|
||||||
|
_manualUseDownloadPath = preferences.TempPathEnabled;
|
||||||
|
UseDownloadPath = _manualUseDownloadPath;
|
||||||
|
DownloadPath = UseDownloadPath ? _manualDownloadPath : string.Empty;
|
||||||
|
|
||||||
|
StartTorrent = !preferences.AddStoppedEnabled;
|
||||||
AddToTopOfQueue = preferences.AddToTopOfQueue;
|
AddToTopOfQueue = preferences.AddToTopOfQueue;
|
||||||
StopCondition = preferences.TorrentStopCondition;
|
StopCondition = preferences.TorrentStopCondition;
|
||||||
ContentLayout = preferences.TorrentContentLayout;
|
ContentLayout = preferences.TorrentContentLayout;
|
||||||
|
|
||||||
|
RatioLimitEnabled = preferences.MaxRatioEnabled;
|
||||||
|
RatioLimit = preferences.MaxRatio;
|
||||||
|
SeedingTimeLimitEnabled = preferences.MaxSeedingTimeEnabled;
|
||||||
|
if (preferences.MaxSeedingTimeEnabled)
|
||||||
|
{
|
||||||
|
SeedingTimeLimit = preferences.MaxSeedingTime;
|
||||||
|
}
|
||||||
|
InactiveSeedingTimeLimitEnabled = preferences.MaxInactiveSeedingTimeEnabled;
|
||||||
|
if (preferences.MaxInactiveSeedingTimeEnabled)
|
||||||
|
{
|
||||||
|
InactiveSeedingTimeLimit = preferences.MaxInactiveSeedingTime;
|
||||||
|
}
|
||||||
|
SelectedShareLimitAction = MapShareLimitAction(preferences.MaxRatioAct);
|
||||||
|
|
||||||
|
if (TorrentManagementMode)
|
||||||
|
{
|
||||||
|
ApplyAutomaticPaths();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SetTorrentManagementMode(bool value)
|
||||||
|
{
|
||||||
|
if (TorrentManagementMode == value)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TorrentManagementMode = value;
|
||||||
|
if (TorrentManagementMode)
|
||||||
|
{
|
||||||
|
ApplyAutomaticPaths();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RestoreManualPaths();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SavePathChanged(string value)
|
||||||
|
{
|
||||||
|
SavePath = value;
|
||||||
|
if (!TorrentManagementMode)
|
||||||
|
{
|
||||||
|
_manualSavePath = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SetUseDownloadPath(bool value)
|
||||||
|
{
|
||||||
|
if (TorrentManagementMode)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_manualUseDownloadPath = value;
|
||||||
|
UseDownloadPath = value;
|
||||||
|
|
||||||
|
if (value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_manualDownloadPath))
|
||||||
|
{
|
||||||
|
_manualDownloadPath = string.IsNullOrWhiteSpace(_defaultDownloadPath) ? string.Empty : _defaultDownloadPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadPath = _manualDownloadPath;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_manualDownloadPath = DownloadPath;
|
||||||
|
DownloadPath = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void DownloadPathChanged(string value)
|
||||||
|
{
|
||||||
|
DownloadPath = value;
|
||||||
|
if (!TorrentManagementMode && UseDownloadPath)
|
||||||
|
{
|
||||||
|
_manualDownloadPath = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void CategoryChanged(string? value)
|
||||||
|
{
|
||||||
|
Category = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||||
|
if (TorrentManagementMode)
|
||||||
|
{
|
||||||
|
ApplyAutomaticPaths();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SelectedTagsChanged(IEnumerable<string> tags)
|
||||||
|
{
|
||||||
|
SelectedTags = tags is null
|
||||||
|
? new HashSet<string>(StringComparer.Ordinal)
|
||||||
|
: new HashSet<string>(tags, StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void StopConditionChanged(string value)
|
||||||
|
{
|
||||||
|
StopCondition = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ContentLayoutChanged(string value)
|
||||||
|
{
|
||||||
|
ContentLayout = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ShareLimitModeChanged(ShareLimitMode mode)
|
||||||
|
{
|
||||||
|
SelectedShareLimitMode = mode;
|
||||||
|
if (mode != ShareLimitMode.Custom)
|
||||||
|
{
|
||||||
|
RatioLimitEnabled = false;
|
||||||
|
SeedingTimeLimitEnabled = false;
|
||||||
|
InactiveSeedingTimeLimitEnabled = false;
|
||||||
|
SelectedShareLimitAction = ShareLimitAction.Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void RatioLimitEnabledChanged(bool value)
|
||||||
|
{
|
||||||
|
RatioLimitEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void RatioLimitChanged(float value)
|
||||||
|
{
|
||||||
|
RatioLimit = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SeedingTimeLimitEnabledChanged(bool value)
|
||||||
|
{
|
||||||
|
SeedingTimeLimitEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SeedingTimeLimitChanged(int value)
|
||||||
|
{
|
||||||
|
SeedingTimeLimit = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void InactiveSeedingTimeLimitEnabledChanged(bool value)
|
||||||
|
{
|
||||||
|
InactiveSeedingTimeLimitEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void InactiveSeedingTimeLimitChanged(int value)
|
||||||
|
{
|
||||||
|
InactiveSeedingTimeLimit = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ShareLimitActionChanged(ShareLimitAction value)
|
||||||
|
{
|
||||||
|
SelectedShareLimitAction = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TorrentOptions GetTorrentOptions()
|
public TorrentOptions GetTorrentOptions()
|
||||||
{
|
{
|
||||||
return new TorrentOptions(
|
var options = new TorrentOptions(
|
||||||
TorrentManagementMode,
|
TorrentManagementMode,
|
||||||
SavePath,
|
_manualSavePath,
|
||||||
Cookie,
|
Cookie,
|
||||||
RenameTorrent,
|
RenameTorrent,
|
||||||
Category,
|
string.IsNullOrWhiteSpace(Category) ? null : Category,
|
||||||
StartTorrent,
|
StartTorrent,
|
||||||
AddToTopOfQueue,
|
AddToTopOfQueue,
|
||||||
StopCondition,
|
StopCondition,
|
||||||
@@ -76,6 +287,154 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
DownloadFirstAndLastPiecesFirst,
|
DownloadFirstAndLastPiecesFirst,
|
||||||
DownloadLimit,
|
DownloadLimit,
|
||||||
UploadLimit);
|
UploadLimit);
|
||||||
|
|
||||||
|
options.UseDownloadPath = TorrentManagementMode ? null : UseDownloadPath;
|
||||||
|
options.DownloadPath = (!TorrentManagementMode && UseDownloadPath) ? DownloadPath : null;
|
||||||
|
options.Tags = SelectedTags.Count > 0 ? SelectedTags.ToArray() : null;
|
||||||
|
|
||||||
|
switch (SelectedShareLimitMode)
|
||||||
|
{
|
||||||
|
case ShareLimitMode.Global:
|
||||||
|
options.RatioLimit = Limits.GlobalLimit;
|
||||||
|
options.SeedingTimeLimit = Limits.GlobalLimit;
|
||||||
|
options.InactiveSeedingTimeLimit = Limits.GlobalLimit;
|
||||||
|
options.ShareLimitAction = ShareLimitAction.Default.ToString();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ShareLimitMode.NoLimit:
|
||||||
|
options.RatioLimit = Limits.NoLimit;
|
||||||
|
options.SeedingTimeLimit = Limits.NoLimit;
|
||||||
|
options.InactiveSeedingTimeLimit = Limits.NoLimit;
|
||||||
|
options.ShareLimitAction = ShareLimitAction.Default.ToString();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ShareLimitMode.Custom:
|
||||||
|
options.RatioLimit = RatioLimitEnabled ? RatioLimit : Limits.NoLimit;
|
||||||
|
options.SeedingTimeLimit = SeedingTimeLimitEnabled ? SeedingTimeLimit : Limits.NoLimit;
|
||||||
|
options.InactiveSeedingTimeLimit = InactiveSeedingTimeLimitEnabled ? InactiveSeedingTimeLimit : Limits.NoLimit;
|
||||||
|
options.ShareLimitAction = SelectedShareLimitAction.ToString();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyAutomaticPaths()
|
||||||
|
{
|
||||||
|
SavePath = ResolveAutomaticSavePath();
|
||||||
|
var (enabled, path) = ResolveAutomaticDownloadPath();
|
||||||
|
UseDownloadPath = enabled;
|
||||||
|
DownloadPath = enabled ? path ?? string.Empty : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RestoreManualPaths()
|
||||||
|
{
|
||||||
|
SavePath = _manualSavePath;
|
||||||
|
UseDownloadPath = _manualUseDownloadPath;
|
||||||
|
DownloadPath = _manualUseDownloadPath ? _manualDownloadPath : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveAutomaticSavePath()
|
||||||
|
{
|
||||||
|
var category = GetSelectedCategory();
|
||||||
|
if (category is null)
|
||||||
|
{
|
||||||
|
return _defaultSavePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(category.SavePath))
|
||||||
|
{
|
||||||
|
return category.SavePath!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_defaultSavePath) && !string.IsNullOrWhiteSpace(category.Name))
|
||||||
|
{
|
||||||
|
return Path.Combine(_defaultSavePath, category.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _defaultSavePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (bool Enabled, string? Path) ResolveAutomaticDownloadPath()
|
||||||
|
{
|
||||||
|
var category = GetSelectedCategory();
|
||||||
|
if (category is null)
|
||||||
|
{
|
||||||
|
if (!_defaultDownloadPathEnabled)
|
||||||
|
{
|
||||||
|
return (false, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true, _defaultDownloadPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category.DownloadPath is null)
|
||||||
|
{
|
||||||
|
if (!_defaultDownloadPathEnabled)
|
||||||
|
{
|
||||||
|
return (false, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true, ComposeDefaultDownloadPath(category.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!category.DownloadPath.Enabled)
|
||||||
|
{
|
||||||
|
return (false, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(category.DownloadPath.Path))
|
||||||
|
{
|
||||||
|
return (true, category.DownloadPath.Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true, ComposeDefaultDownloadPath(category.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ComposeDefaultDownloadPath(string categoryName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_defaultDownloadPath))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(categoryName))
|
||||||
|
{
|
||||||
|
return _defaultDownloadPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.Combine(_defaultDownloadPath, categoryName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CategoryOption? GetSelectedCategory()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(Category))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _categoryLookup.TryGetValue(Category, out var option) ? option : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ShareLimitAction MapShareLimitAction(int preferenceValue)
|
||||||
|
{
|
||||||
|
return preferenceValue switch
|
||||||
|
{
|
||||||
|
0 => ShareLimitAction.Stop,
|
||||||
|
1 => ShareLimitAction.Remove,
|
||||||
|
2 => ShareLimitAction.RemoveWithContent,
|
||||||
|
3 => ShareLimitAction.EnableSuperSeeding,
|
||||||
|
_ => ShareLimitAction.Default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected enum ShareLimitMode
|
||||||
|
{
|
||||||
|
Global,
|
||||||
|
NoLimit,
|
||||||
|
Custom
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sealed record CategoryOption(string Name, string? SavePath, DownloadPathOption? DownloadPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
protected IApiClient ApiClient { get; set; } = default!;
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDataManager DataManager { get; set; } = default!;
|
protected ITorrentDataManager DataManager { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected ILocalStorageService LocalStorage { get; set; } = default!;
|
protected ILocalStorageService LocalStorage { get; set; } = default!;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
<MudNumericField T="int" Label="Ignore Subsequent Matches for (0 to Disable)" Value="IgnoreDays" ValueChanged="IgnoreDaysChanged" Disabled="@(SelectedRuleName is null)" Variant="Variant.Outlined" />
|
<MudNumericField T="int" Label="Ignore Subsequent Matches for (0 to Disable)" Value="IgnoreDays" ValueChanged="IgnoreDaysChanged" Disabled="@(SelectedRuleName is null)" Variant="Variant.Outlined" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<MudSelect T="string" Label="Add paused" Value="AddPaused" ValueChanged="AddPausedChanged" Disabled="@(SelectedRuleName is null)" Variant="Variant.Outlined">
|
<MudSelect T="string" Label="Add stopped" Value="AddStopped" ValueChanged="AddStoppedChanged" Disabled="@(SelectedRuleName is null)" Variant="Variant.Outlined">
|
||||||
<MudSelectItem Value="@("default")">Use global settings</MudSelectItem>
|
<MudSelectItem Value="@("default")">Use global settings</MudSelectItem>
|
||||||
<MudSelectItem Value="@("always")">Always</MudSelectItem>
|
<MudSelectItem Value="@("always")">Always</MudSelectItem>
|
||||||
<MudSelectItem Value="@("never")">Never</MudSelectItem>
|
<MudSelectItem Value="@("never")">Never</MudSelectItem>
|
||||||
|
|||||||
@@ -114,11 +114,11 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
SelectedRule.IgnoreDays = value;
|
SelectedRule.IgnoreDays = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected string? AddPaused { get; set; }
|
protected string? AddStopped { get; set; }
|
||||||
|
|
||||||
protected void AddPausedChanged(string value)
|
protected void AddStoppedChanged(string value)
|
||||||
{
|
{
|
||||||
AddPaused = value;
|
AddStopped = value;
|
||||||
switch (value)
|
switch (value)
|
||||||
{
|
{
|
||||||
case "default":
|
case "default":
|
||||||
@@ -273,15 +273,15 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
switch (SelectedRule.TorrentParams.Stopped)
|
switch (SelectedRule.TorrentParams.Stopped)
|
||||||
{
|
{
|
||||||
case null:
|
case null:
|
||||||
AddPaused = "default";
|
AddStopped = "default";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case true:
|
case true:
|
||||||
AddPaused = "always";
|
AddStopped = "always";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case false:
|
case false:
|
||||||
AddPaused = "never";
|
AddStopped = "never";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@inherits SubmittableDialog
|
@inherits SubmittableDialog
|
||||||
|
@using Lantean.QBitTorrentClient.Models
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -34,6 +35,15 @@
|
|||||||
<MudItem xs="9">
|
<MudItem xs="9">
|
||||||
<MudNumericField T="int" Value="InactiveMinutes" ValueChanged="InactiveMinutesChanged" Disabled="@(!(CustomEnabled && InactiveMinutesEnabled))" Min="1" Max="1024000" Variant="Variant.Outlined" Adornment="Adornment.End" AdornmentText="minutes" />
|
<MudNumericField T="int" Value="InactiveMinutes" ValueChanged="InactiveMinutesChanged" Disabled="@(!(CustomEnabled && InactiveMinutesEnabled))" Min="1" Max="1024000" Variant="Variant.Outlined" Adornment="Adornment.End" AdornmentText="minutes" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudSelect T="ShareLimitAction" Label="Action when limit is reached" Value="SelectedShareLimitAction" ValueChanged="ShareLimitActionChanged" Disabled="@(!CustomEnabled)" Variant="Variant.Outlined">
|
||||||
|
<MudSelectItem Value="ShareLimitAction.Default">Default</MudSelectItem>
|
||||||
|
<MudSelectItem Value="ShareLimitAction.Stop">Stop torrent</MudSelectItem>
|
||||||
|
<MudSelectItem Value="ShareLimitAction.Remove">Remove torrent</MudSelectItem>
|
||||||
|
<MudSelectItem Value="ShareLimitAction.RemoveWithContent">Remove torrent and data</MudSelectItem>
|
||||||
|
<MudSelectItem Value="ShareLimitAction.EnableSuperSeeding">Enable super seeding</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Lantean.QBitTorrentClient;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Lantean.QBitTorrentClient;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
@@ -16,6 +19,9 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public ShareRatioMax? Value { get; set; }
|
public ShareRatioMax? Value { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public ShareRatioMax? CurrentValue { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool Disabled { get; set; }
|
public bool Disabled { get; set; }
|
||||||
|
|
||||||
@@ -33,6 +39,8 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
|
|
||||||
protected int InactiveMinutes { get; set; }
|
protected int InactiveMinutes { get; set; }
|
||||||
|
|
||||||
|
protected ShareLimitAction SelectedShareLimitAction { get; set; } = ShareLimitAction.Default;
|
||||||
|
|
||||||
protected bool CustomEnabled => ShareRatioType == 0;
|
protected bool CustomEnabled => ShareRatioType == 0;
|
||||||
|
|
||||||
protected void RatioEnabledChanged(bool value)
|
protected void RatioEnabledChanged(bool value)
|
||||||
@@ -65,40 +73,75 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
InactiveMinutes = value;
|
InactiveMinutes = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void ShareLimitActionChanged(ShareLimitAction value)
|
||||||
|
{
|
||||||
|
SelectedShareLimitAction = value;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
if (Value is null || Value.RatioLimit == Limits.GlobalLimit && Value.SeedingTimeLimit == Limits.GlobalLimit && Value.InactiveSeedingTimeLimit == Limits.GlobalLimit)
|
RatioEnabled = false;
|
||||||
|
TotalMinutesEnabled = false;
|
||||||
|
InactiveMinutesEnabled = false;
|
||||||
|
|
||||||
|
var baseline = Value ?? CurrentValue;
|
||||||
|
SelectedShareLimitAction = baseline?.ShareLimitAction ?? ShareLimitAction.Default;
|
||||||
|
|
||||||
|
if (baseline is null || baseline.RatioLimit == Limits.GlobalLimit && baseline.SeedingTimeLimit == Limits.GlobalLimit && baseline.InactiveSeedingTimeLimit == Limits.GlobalLimit)
|
||||||
{
|
{
|
||||||
ShareRatioType = Limits.GlobalLimit;
|
ShareRatioType = Limits.GlobalLimit;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else if (Value.MaxRatio == Limits.NoLimit && Value.MaxSeedingTime == Limits.NoLimit && Value.MaxInactiveSeedingTime == Limits.NoLimit)
|
|
||||||
|
if (baseline.MaxRatio == Limits.NoLimit && baseline.MaxSeedingTime == Limits.NoLimit && baseline.MaxInactiveSeedingTime == Limits.NoLimit)
|
||||||
{
|
{
|
||||||
ShareRatioType = Limits.NoLimit;
|
ShareRatioType = Limits.NoLimit;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShareRatioType = 0;
|
||||||
|
|
||||||
|
if (baseline.RatioLimit >= 0)
|
||||||
|
{
|
||||||
|
RatioEnabled = true;
|
||||||
|
Ratio = baseline.RatioLimit;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ShareRatioType = 0;
|
Ratio = 0;
|
||||||
if (Value.RatioLimit >= 0)
|
|
||||||
{
|
|
||||||
RatioEnabled = true;
|
|
||||||
Ratio = Value.RatioLimit;
|
|
||||||
}
|
}
|
||||||
if (Value.SeedingTimeLimit >= 0)
|
|
||||||
|
if (baseline.SeedingTimeLimit >= 0)
|
||||||
{
|
{
|
||||||
TotalMinutesEnabled = true;
|
TotalMinutesEnabled = true;
|
||||||
TotalMinutes = (int)Value.SeedingTimeLimit;
|
TotalMinutes = (int)baseline.SeedingTimeLimit;
|
||||||
}
|
}
|
||||||
if (Value.InactiveSeedingTimeLimit >= 0)
|
else
|
||||||
|
{
|
||||||
|
TotalMinutes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseline.InactiveSeedingTimeLimit >= 0)
|
||||||
{
|
{
|
||||||
InactiveMinutesEnabled = true;
|
InactiveMinutesEnabled = true;
|
||||||
InactiveMinutes = (int)Value.InactiveSeedingTimeLimit;
|
InactiveMinutes = (int)baseline.InactiveSeedingTimeLimit;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
InactiveMinutes = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void ShareRatioTypeChanged(int value)
|
protected void ShareRatioTypeChanged(int value)
|
||||||
{
|
{
|
||||||
ShareRatioType = value;
|
ShareRatioType = value;
|
||||||
|
if (!CustomEnabled)
|
||||||
|
{
|
||||||
|
RatioEnabled = false;
|
||||||
|
TotalMinutesEnabled = false;
|
||||||
|
InactiveMinutesEnabled = false;
|
||||||
|
SelectedShareLimitAction = ShareLimitAction.Default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void Cancel()
|
protected void Cancel()
|
||||||
@@ -112,16 +155,19 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
if (ShareRatioType == Limits.GlobalLimit)
|
if (ShareRatioType == Limits.GlobalLimit)
|
||||||
{
|
{
|
||||||
result.RatioLimit = result.SeedingTimeLimit = result.InactiveSeedingTimeLimit = Limits.GlobalLimit;
|
result.RatioLimit = result.SeedingTimeLimit = result.InactiveSeedingTimeLimit = Limits.GlobalLimit;
|
||||||
|
result.ShareLimitAction = ShareLimitAction.Default;
|
||||||
}
|
}
|
||||||
else if (ShareRatioType == Limits.NoLimit)
|
else if (ShareRatioType == Limits.NoLimit)
|
||||||
{
|
{
|
||||||
result.RatioLimit = result.SeedingTimeLimit = result.InactiveSeedingTimeLimit = Limits.NoLimit;
|
result.RatioLimit = result.SeedingTimeLimit = result.InactiveSeedingTimeLimit = Limits.NoLimit;
|
||||||
|
result.ShareLimitAction = ShareLimitAction.Default;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
result.RatioLimit = RatioEnabled ? Ratio : Limits.NoLimit;
|
result.RatioLimit = RatioEnabled ? Ratio : Limits.NoLimit;
|
||||||
result.SeedingTimeLimit = TotalMinutesEnabled ? TotalMinutes : Limits.NoLimit;
|
result.SeedingTimeLimit = TotalMinutesEnabled ? TotalMinutes : Limits.NoLimit;
|
||||||
result.InactiveSeedingTimeLimit = InactiveMinutesEnabled ? InactiveMinutes : Limits.NoLimit;
|
result.InactiveSeedingTimeLimit = InactiveMinutesEnabled ? InactiveMinutes : Limits.NoLimit;
|
||||||
|
result.ShareLimitAction = SelectedShareLimitAction;
|
||||||
}
|
}
|
||||||
MudDialog.Close(DialogResult.Ok(result));
|
MudDialog.Close(DialogResult.Ok(result));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
protected ILocalStorageService LocalStorage { get; set; } = default!;
|
protected ILocalStorageService LocalStorage { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDataManager DataManager { get; set; } = default!;
|
protected ITorrentDataManager DataManager { get; set; } = default!;
|
||||||
|
|
||||||
protected HashSet<string> ExpandedNodes { get; set; } = [];
|
protected HashSet<string> ExpandedNodes { get; set; } = [];
|
||||||
|
|
||||||
|
|||||||
@@ -65,8 +65,8 @@
|
|||||||
{
|
{
|
||||||
return __builder =>
|
return __builder =>
|
||||||
{
|
{
|
||||||
<MudMenuItem Icon="@Icons.Material.Filled.PlayArrow" IconColor="Color.Success" OnClick="@(e => ResumeTorrents(type))">Resume torrents</MudMenuItem>
|
<MudMenuItem Icon="@Icons.Material.Filled.PlayArrow" IconColor="Color.Success" OnClick="@(e => StartTorrents(type))">Start torrents</MudMenuItem>
|
||||||
<MudMenuItem Icon="@Icons.Material.Filled.Pause" IconColor="Color.Warning" OnClick="@(e => PauseTorrents(type))">Pause torrents</MudMenuItem>
|
<MudMenuItem Icon="@Icons.Material.Filled.Stop" IconColor="Color.Warning" OnClick="@(e => StopTorrents(type))">Stop torrents</MudMenuItem>
|
||||||
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="@(e => RemoveTorrents(type))">Remove torrents</MudMenuItem>
|
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="@(e => RemoveTorrents(type))">Remove torrents</MudMenuItem>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Lantean.QBTMud.Models;
|
|||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Lantean.QBTMud.Components
|
namespace Lantean.QBTMud.Components
|
||||||
{
|
{
|
||||||
@@ -352,25 +353,25 @@ namespace Lantean.QBTMud.Components
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task ResumeTorrents(string type)
|
protected async Task StartTorrents(string type)
|
||||||
{
|
{
|
||||||
var torrents = GetAffectedTorrentHashes(type);
|
var torrents = GetAffectedTorrentHashes(type);
|
||||||
|
|
||||||
await ApiClient.ResumeTorrents(torrents);
|
await ApiClient.StartTorrents(hashes: torrents.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task PauseTorrents(string type)
|
protected async Task StopTorrents(string type)
|
||||||
{
|
{
|
||||||
var torrents = GetAffectedTorrentHashes(type);
|
var torrents = GetAffectedTorrentHashes(type);
|
||||||
|
|
||||||
await ApiClient.PauseTorrents(torrents);
|
await ApiClient.StopTorrents(hashes: torrents.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task RemoveTorrents(string type)
|
protected async Task RemoveTorrents(string type)
|
||||||
{
|
{
|
||||||
var torrents = GetAffectedTorrentHashes(type);
|
var torrents = GetAffectedTorrentHashes(type);
|
||||||
|
|
||||||
await DialogService.InvokeDeleteTorrentDialog(ApiClient, [.. torrents]);
|
await DialogService.InvokeDeleteTorrentDialog(ApiClient, Preferences?.ConfirmTorrentDeletion == true, [.. torrents]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Dictionary<string, int> GetTags()
|
private Dictionary<string, int> GetTags()
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
protected IApiClient ApiClient { get; set; } = default!;
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDataManager DataManager { get; set; } = default!;
|
protected ITorrentDataManager DataManager { get; set; } = default!;
|
||||||
|
|
||||||
protected IReadOnlyList<PieceState> Pieces { get; set; } = [];
|
protected IReadOnlyList<PieceState> Pieces { get; set; } = [];
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,21 @@
|
|||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
|
|
||||||
|
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
|
||||||
|
<MudCardHeader>
|
||||||
|
<CardHeaderContent>
|
||||||
|
<MudText Typo="Typo.subtitle2">Confirmation</MudText>
|
||||||
|
</CardHeaderContent>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent Class="pt-0">
|
||||||
|
<MudGrid>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<FieldSwitch Label="Confirm torrent recheck" Value="ConfirmTorrentRecheck" ValueChanged="ConfirmTorrentRecheckChanged" />
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
|
||||||
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4 mt-4">
|
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4 mt-4">
|
||||||
<MudCardHeader>
|
<MudCardHeader>
|
||||||
<CardHeaderContent>
|
<CardHeaderContent>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
protected int SaveResumeDataInterval { get; private set; }
|
protected int SaveResumeDataInterval { get; private set; }
|
||||||
protected int TorrentFileSizeLimit { get; private set; }
|
protected int TorrentFileSizeLimit { get; private set; }
|
||||||
protected bool RecheckCompletedTorrents { get; private set; }
|
protected bool RecheckCompletedTorrents { get; private set; }
|
||||||
|
|
||||||
|
protected bool ConfirmTorrentRecheck { get; private set; }
|
||||||
protected string? AppInstanceName { get; private set; }
|
protected string? AppInstanceName { get; private set; }
|
||||||
protected int RefreshInterval { get; private set; }
|
protected int RefreshInterval { get; private set; }
|
||||||
protected bool ResolvePeerCountries { get; private set; }
|
protected bool ResolvePeerCountries { get; private set; }
|
||||||
@@ -97,6 +99,7 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
SaveResumeDataInterval = Preferences.SaveResumeDataInterval;
|
SaveResumeDataInterval = Preferences.SaveResumeDataInterval;
|
||||||
TorrentFileSizeLimit = Preferences.TorrentFileSizeLimit / 1024 / 1024;
|
TorrentFileSizeLimit = Preferences.TorrentFileSizeLimit / 1024 / 1024;
|
||||||
RecheckCompletedTorrents = Preferences.RecheckCompletedTorrents;
|
RecheckCompletedTorrents = Preferences.RecheckCompletedTorrents;
|
||||||
|
ConfirmTorrentRecheck = Preferences.ConfirmTorrentRecheck;
|
||||||
AppInstanceName = Preferences.AppInstanceName;
|
AppInstanceName = Preferences.AppInstanceName;
|
||||||
RefreshInterval = Preferences.RefreshInterval;
|
RefreshInterval = Preferences.RefreshInterval;
|
||||||
ResolvePeerCountries = Preferences.ResolvePeerCountries;
|
ResolvePeerCountries = Preferences.ResolvePeerCountries;
|
||||||
@@ -209,6 +212,13 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async Task ConfirmTorrentRecheckChanged(bool value)
|
||||||
|
{
|
||||||
|
ConfirmTorrentRecheck = value;
|
||||||
|
UpdatePreferences.ConfirmTorrentRecheck = value;
|
||||||
|
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
||||||
|
}
|
||||||
|
|
||||||
protected async Task AppInstanceNameChanged(string value)
|
protected async Task AppInstanceNameChanged(string value)
|
||||||
{
|
{
|
||||||
AppInstanceName = value;
|
AppInstanceName = value;
|
||||||
|
|||||||
@@ -17,6 +17,24 @@
|
|||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
|
|
||||||
|
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
|
||||||
|
<MudCardHeader>
|
||||||
|
<CardHeaderContent>
|
||||||
|
<MudText Typo="Typo.subtitle2">Transfer List</MudText>
|
||||||
|
</CardHeaderContent>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent Class="pt-0">
|
||||||
|
<MudGrid>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<FieldSwitch Label="Confirm when deleting torrents" Value="ConfirmTorrentDeletion" ValueChanged="ConfirmTorrentDeletionChanged" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<FieldSwitch Label="Show external IP in status bar" Value="StatusBarExternalIp" ValueChanged="StatusBarExternalIpChanged" />
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
|
||||||
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
|
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
|
||||||
<MudCardHeader>
|
<MudCardHeader>
|
||||||
<CardHeaderContent>
|
<CardHeaderContent>
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
{
|
{
|
||||||
public partial class BehaviourOptions : Options
|
public partial class BehaviourOptions : Options
|
||||||
{
|
{
|
||||||
|
protected bool ConfirmTorrentDeletion { get; set; }
|
||||||
|
|
||||||
|
protected bool StatusBarExternalIp { get; set; }
|
||||||
|
|
||||||
protected bool FileLogEnabled { get; set; }
|
protected bool FileLogEnabled { get; set; }
|
||||||
|
|
||||||
protected string? FileLogPath { get; set; }
|
protected string? FileLogPath { get; set; }
|
||||||
@@ -27,6 +31,8 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfirmTorrentDeletion = Preferences.ConfirmTorrentDeletion;
|
||||||
|
StatusBarExternalIp = Preferences.StatusBarExternalIp;
|
||||||
FileLogEnabled = Preferences.FileLogEnabled;
|
FileLogEnabled = Preferences.FileLogEnabled;
|
||||||
FileLogPath = Preferences.FileLogPath;
|
FileLogPath = Preferences.FileLogPath;
|
||||||
FileLogBackupEnabled = Preferences.FileLogBackupEnabled;
|
FileLogBackupEnabled = Preferences.FileLogBackupEnabled;
|
||||||
@@ -39,6 +45,20 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async Task ConfirmTorrentDeletionChanged(bool value)
|
||||||
|
{
|
||||||
|
ConfirmTorrentDeletion = value;
|
||||||
|
UpdatePreferences.ConfirmTorrentDeletion = value;
|
||||||
|
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task StatusBarExternalIpChanged(bool value)
|
||||||
|
{
|
||||||
|
StatusBarExternalIp = value;
|
||||||
|
UpdatePreferences.StatusBarExternalIp = value;
|
||||||
|
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
||||||
|
}
|
||||||
|
|
||||||
protected async Task FileLogEnabledChanged(bool value)
|
protected async Task FileLogEnabledChanged(bool value)
|
||||||
{
|
{
|
||||||
FileLogEnabled = value;
|
FileLogEnabled = value;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<FieldSwitch Label="Add to top of queue" Value="AddToTopOfQueue" ValueChanged="AddToTopOfQueueChanged" />
|
<FieldSwitch Label="Add to top of queue" Value="AddToTopOfQueue" ValueChanged="AddToTopOfQueueChanged" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<FieldSwitch Label="Do not start the download automatically" Value="StartPausedEnabled" ValueChanged="StartPausedEnabledChanged" />
|
<FieldSwitch Label="Do not start the download automatically" Value="AddStoppedEnabled" ValueChanged="AddStoppedEnabledChanged" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<MudSelect T="string" Label="Torrent stop condition" Value="TorrentStopCondition" ValueChanged="TorrentStopConditionChanged" Variant="Variant.Outlined">
|
<MudSelect T="string" Label="Torrent stop condition" Value="TorrentStopCondition" ValueChanged="TorrentStopConditionChanged" Variant="Variant.Outlined">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
{
|
{
|
||||||
protected string? TorrentContentLayout { get; set; }
|
protected string? TorrentContentLayout { get; set; }
|
||||||
protected bool AddToTopOfQueue { get; set; }
|
protected bool AddToTopOfQueue { get; set; }
|
||||||
protected bool StartPausedEnabled { get; set; }
|
protected bool AddStoppedEnabled { get; set; }
|
||||||
protected string? TorrentStopCondition { get; set; }
|
protected string? TorrentStopCondition { get; set; }
|
||||||
protected bool AutoDeleteMode { get; set; }
|
protected bool AutoDeleteMode { get; set; }
|
||||||
protected bool PreallocateAll { get; set; }
|
protected bool PreallocateAll { get; set; }
|
||||||
@@ -51,7 +51,7 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
// when adding a torrent
|
// when adding a torrent
|
||||||
TorrentContentLayout = Preferences.TorrentContentLayout;
|
TorrentContentLayout = Preferences.TorrentContentLayout;
|
||||||
AddToTopOfQueue = Preferences.AddToTopOfQueue;
|
AddToTopOfQueue = Preferences.AddToTopOfQueue;
|
||||||
StartPausedEnabled = Preferences.StartPausedEnabled;
|
AddStoppedEnabled = Preferences.AddStoppedEnabled;
|
||||||
TorrentStopCondition = Preferences.TorrentStopCondition;
|
TorrentStopCondition = Preferences.TorrentStopCondition;
|
||||||
AutoDeleteMode = Preferences.AutoDeleteMode == 1;
|
AutoDeleteMode = Preferences.AutoDeleteMode == 1;
|
||||||
PreallocateAll = Preferences.PreallocateAll;
|
PreallocateAll = Preferences.PreallocateAll;
|
||||||
@@ -116,10 +116,10 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task StartPausedEnabledChanged(bool value)
|
protected async Task AddStoppedEnabledChanged(bool value)
|
||||||
{
|
{
|
||||||
StartPausedEnabled = value;
|
AddStoppedEnabled = value;
|
||||||
UpdatePreferences.StartPausedEnabled = value;
|
UpdatePreferences.AddStoppedEnabled = value;
|
||||||
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
protected IApiClient ApiClient { get; set; } = default!;
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDataManager DataManager { get; set; } = default!;
|
protected IPeerDataManager PeerDataManager { get; set; } = default!;
|
||||||
|
|
||||||
protected PeerList? PeerList { get; set; }
|
protected PeerList? PeerList { get; set; }
|
||||||
|
|
||||||
@@ -78,11 +78,11 @@ namespace Lantean.QBTMud.Components
|
|||||||
var peers = await ApiClient.GetTorrentPeersData(Hash, _requestId);
|
var peers = await ApiClient.GetTorrentPeersData(Hash, _requestId);
|
||||||
if (PeerList is null || peers.FullUpdate)
|
if (PeerList is null || peers.FullUpdate)
|
||||||
{
|
{
|
||||||
PeerList = DataManager.CreatePeerList(peers);
|
PeerList = PeerDataManager.CreatePeerList(peers);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DataManager.MergeTorrentPeers(peers, PeerList);
|
PeerDataManager.MergeTorrentPeers(peers, PeerList);
|
||||||
}
|
}
|
||||||
_requestId = peers.RequestId;
|
_requestId = peers.RequestId;
|
||||||
|
|
||||||
@@ -200,11 +200,11 @@ namespace Lantean.QBTMud.Components
|
|||||||
}
|
}
|
||||||
if (PeerList is null || peers.FullUpdate)
|
if (PeerList is null || peers.FullUpdate)
|
||||||
{
|
{
|
||||||
PeerList = DataManager.CreatePeerList(peers);
|
PeerList = PeerDataManager.CreatePeerList(peers);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DataManager.MergeTorrentPeers(peers, PeerList);
|
PeerDataManager.MergeTorrentPeers(peers, PeerList);
|
||||||
}
|
}
|
||||||
|
|
||||||
_requestId = peers.RequestId;
|
_requestId = peers.RequestId;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
public ISnackbar Snackbar { get; set; } = default!;
|
public ISnackbar Snackbar { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
public IDataManager DataManager { get; set; } = default!;
|
public ITorrentDataManager DataManager { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
public IJSRuntime JSRuntime { get; set; } = default!;
|
public IJSRuntime JSRuntime { get; set; } = default!;
|
||||||
@@ -37,9 +37,6 @@ namespace Lantean.QBTMud.Components
|
|||||||
[Inject]
|
[Inject]
|
||||||
protected IKeyboardService KeyboardService { get; set; } = default!;
|
protected IKeyboardService KeyboardService { get; set; } = default!;
|
||||||
|
|
||||||
[CascadingParameter(Name = "Version")]
|
|
||||||
public string? Version { get; set; }
|
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
[EditorRequired]
|
[EditorRequired]
|
||||||
public IEnumerable<string> Hashes { get; set; } = default!;
|
public IEnumerable<string> Hashes { get; set; } = default!;
|
||||||
@@ -71,14 +68,12 @@ namespace Lantean.QBTMud.Components
|
|||||||
|
|
||||||
protected bool OverlayVisible { get; set; }
|
protected bool OverlayVisible { get; set; }
|
||||||
|
|
||||||
protected int MajorVersion => VersionHelper.GetMajorVersion(Version);
|
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
_actions =
|
_actions =
|
||||||
[
|
[
|
||||||
new("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Resume)),
|
new("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Start)),
|
||||||
new("pause", "Pause", MajorVersion < 5 ? Icons.Material.Filled.Pause : Icons.Material.Filled.Stop, Color.Warning, CreateCallback(Pause)),
|
new("stop", "Stop", Icons.Material.Filled.Stop, Color.Warning, CreateCallback(Stop)),
|
||||||
new("forceStart", "Force start", Icons.Material.Filled.Forward, Color.Warning, CreateCallback(ForceStart)),
|
new("forceStart", "Force start", Icons.Material.Filled.Forward, Color.Warning, CreateCallback(ForceStart)),
|
||||||
new("delete", "Remove", Icons.Material.Filled.Delete, Color.Error, CreateCallback(Remove), separatorBefore: true),
|
new("delete", "Remove", Icons.Material.Filled.Delete, Color.Error, CreateCallback(Remove), separatorBefore: true),
|
||||||
new("setLocation", "Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation), separatorBefore: true),
|
new("setLocation", "Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation), separatorBefore: true),
|
||||||
@@ -109,6 +104,8 @@ namespace Lantean.QBTMud.Components
|
|||||||
new("copyHashv2", "Info hash v2", Icons.Material.Filled.Tag, Color.Info, CreateCallback(() => Copy(t => t.InfoHashV2))),
|
new("copyHashv2", "Info hash v2", Icons.Material.Filled.Tag, Color.Info, CreateCallback(() => Copy(t => t.InfoHashV2))),
|
||||||
new("copyMagnet", "Magnet link", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.MagnetUri))),
|
new("copyMagnet", "Magnet link", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.MagnetUri))),
|
||||||
new("copyId", "Torrent ID", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Hash))),
|
new("copyId", "Torrent ID", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Hash))),
|
||||||
|
new("copyComment", "Comment", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Comment))),
|
||||||
|
new("copyContentPath", "Content path", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.ContentPath))),
|
||||||
]),
|
]),
|
||||||
new("export", "Export", Icons.Material.Filled.SaveAlt, Color.Info, CreateCallback(Export)),
|
new("export", "Export", Icons.Material.Filled.SaveAlt, Color.Info, CreateCallback(Export)),
|
||||||
];
|
];
|
||||||
@@ -146,33 +143,17 @@ namespace Lantean.QBTMud.Components
|
|||||||
OverlayVisible = value;
|
OverlayVisible = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task Pause()
|
protected async Task Stop()
|
||||||
{
|
{
|
||||||
if (MajorVersion < 5)
|
await ApiClient.StopTorrents(hashes: Hashes.ToArray());
|
||||||
{
|
|
||||||
await ApiClient.PauseTorrents(Hashes);
|
|
||||||
Snackbar.Add("Torrent paused.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await ApiClient.StopTorrents(Hashes);
|
|
||||||
Snackbar.Add("Torrent stopped.");
|
Snackbar.Add("Torrent stopped.");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task Resume()
|
protected async Task Start()
|
||||||
{
|
{
|
||||||
if (MajorVersion < 5)
|
await ApiClient.StartTorrents(hashes: Hashes.ToArray());
|
||||||
{
|
|
||||||
await ApiClient.ResumeTorrents(Hashes);
|
|
||||||
Snackbar.Add("Torrent resumed.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await ApiClient.StartTorrents(Hashes);
|
|
||||||
Snackbar.Add("Torrent started.");
|
Snackbar.Add("Torrent started.");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task ForceStart()
|
protected async Task ForceStart()
|
||||||
{
|
{
|
||||||
@@ -182,7 +163,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
|
|
||||||
protected async Task Remove()
|
protected async Task Remove()
|
||||||
{
|
{
|
||||||
var deleted = await DialogService.InvokeDeleteTorrentDialog(ApiClient, Hashes.ToArray());
|
var deleted = await DialogService.InvokeDeleteTorrentDialog(ApiClient, Preferences?.ConfirmTorrentDeletion == true, Hashes.ToArray());
|
||||||
|
|
||||||
if (deleted)
|
if (deleted)
|
||||||
{
|
{
|
||||||
@@ -278,7 +259,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
|
|
||||||
protected async Task ForceRecheck()
|
protected async Task ForceRecheck()
|
||||||
{
|
{
|
||||||
await ApiClient.RecheckTorrents(null, Hashes.ToArray());
|
await DialogService.ForceRecheckAsync(ApiClient, Hashes, Preferences?.ConfirmTorrentRecheck == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task ForceReannounce()
|
protected async Task ForceReannounce()
|
||||||
@@ -385,8 +366,8 @@ namespace Lantean.QBTMud.Components
|
|||||||
var allAreFirstLastPiecePrio = true;
|
var allAreFirstLastPiecePrio = true;
|
||||||
var thereAreFirstLastPiecePrio = false;
|
var thereAreFirstLastPiecePrio = false;
|
||||||
var allAreDownloaded = true;
|
var allAreDownloaded = true;
|
||||||
var allArePaused = true;
|
var allAreStopped = true;
|
||||||
var thereArePaused = false;
|
var thereAreStopped = false;
|
||||||
var allAreForceStart = true;
|
var allAreForceStart = true;
|
||||||
var thereAreForceStart = false;
|
var thereAreForceStart = false;
|
||||||
var allAreSuperSeeding = true;
|
var allAreSuperSeeding = true;
|
||||||
@@ -424,27 +405,13 @@ namespace Lantean.QBTMud.Components
|
|||||||
allAreSuperSeeding = false;
|
allAreSuperSeeding = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MajorVersion < 5)
|
|
||||||
{
|
|
||||||
if (torrent.State != "pausedUP" && torrent.State != "pausedDL")
|
|
||||||
{
|
|
||||||
allArePaused = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
thereArePaused = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (torrent.State != "stoppedUP" && torrent.State != "stoppedDL")
|
if (torrent.State != "stoppedUP" && torrent.State != "stoppedDL")
|
||||||
{
|
{
|
||||||
allArePaused = false;
|
allAreStopped = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
thereArePaused = true;
|
thereAreStopped = true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!torrent.ForceStart)
|
if (!torrent.ForceStart)
|
||||||
@@ -532,7 +499,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
actionStates["superSeeding"] = ActionState.Hidden;
|
actionStates["superSeeding"] = ActionState.Hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allArePaused)
|
if (allAreStopped)
|
||||||
{
|
{
|
||||||
actionStates["pause"] = ActionState.Hidden;
|
actionStates["pause"] = ActionState.Hidden;
|
||||||
}
|
}
|
||||||
@@ -540,13 +507,11 @@ namespace Lantean.QBTMud.Components
|
|||||||
{
|
{
|
||||||
actionStates["forceStart"] = ActionState.Hidden;
|
actionStates["forceStart"] = ActionState.Hidden;
|
||||||
}
|
}
|
||||||
else if (!thereArePaused && !thereAreForceStart)
|
else if (!thereAreStopped && !thereAreForceStart)
|
||||||
{
|
{
|
||||||
actionStates["start"] = ActionState.Hidden;
|
actionStates["start"] = ActionState.Hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MajorVersion >= 5)
|
|
||||||
{
|
|
||||||
if (actionStates.TryGetValue("start", out ActionState? startActionState))
|
if (actionStates.TryGetValue("start", out ActionState? startActionState))
|
||||||
{
|
{
|
||||||
startActionState.TextOverride = "Start";
|
startActionState.TextOverride = "Start";
|
||||||
@@ -564,7 +529,6 @@ namespace Lantean.QBTMud.Components
|
|||||||
{
|
{
|
||||||
actionStates["pause"] = new ActionState { TextOverride = "Stop" };
|
actionStates["pause"] = new ActionState { TextOverride = "Stop" };
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!allAreAutoTmm && thereAreAutoTmm)
|
if (!allAreAutoTmm && thereAreAutoTmm)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
protected IApiClient ApiClient { get; set; } = default!;
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDataManager DataManager { get; set; } = default!;
|
protected ITorrentDataManager DataManager { get; set; } = default!;
|
||||||
|
|
||||||
protected IReadOnlyList<TorrentTracker>? TrackerList { get; set; }
|
protected IReadOnlyList<TorrentTracker>? TrackerList { get; set; }
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ApiClient.AddTrackersToTorrent(Hash, trackers);
|
await ApiClient.AddTrackersToTorrent(trackers, hashes: new[] { Hash });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Task EditTrackerToolbar()
|
protected Task EditTrackerToolbar()
|
||||||
@@ -211,7 +211,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ApiClient.RemoveTrackers(Hash, [tracker.Url]);
|
await ApiClient.RemoveTrackers([tracker.Url], hashes: new[] { Hash });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Task CopyTrackerUrlToolbar()
|
protected Task CopyTrackerUrlToolbar()
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
protected IApiClient ApiClient { get; set; } = default!;
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDataManager DataManager { get; set; } = default!;
|
protected ITorrentDataManager DataManager { get; set; } = default!;
|
||||||
|
|
||||||
protected IReadOnlyList<WebSeed>? WebSeeds { get; set; }
|
protected IReadOnlyList<WebSeed>? WebSeeds { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Lantean.QBitTorrentClient;
|
using Lantean.QBitTorrentClient;
|
||||||
|
using ShareLimitAction = Lantean.QBitTorrentClient.Models.ShareLimitAction;
|
||||||
using Lantean.QBTMud.Components.Dialogs;
|
using Lantean.QBTMud.Components.Dialogs;
|
||||||
using Lantean.QBTMud.Filter;
|
using Lantean.QBTMud.Filter;
|
||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
@@ -56,7 +57,7 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
var addTorrentParams = CreateAddTorrentParams(options);
|
var addTorrentParams = CreateAddTorrentParams(options);
|
||||||
addTorrentParams.Torrents = files;
|
addTorrentParams.Torrents = files;
|
||||||
|
|
||||||
await apiClient.AddTorrent(addTorrentParams);
|
_ = await apiClient.AddTorrent(addTorrentParams);
|
||||||
|
|
||||||
foreach (var stream in streams)
|
foreach (var stream in streams)
|
||||||
{
|
{
|
||||||
@@ -74,18 +75,19 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
{
|
{
|
||||||
addTorrentParams.ContentLayout = Enum.Parse<QBitTorrentClient.Models.TorrentContentLayout>(options.ContentLayout);
|
addTorrentParams.ContentLayout = Enum.Parse<QBitTorrentClient.Models.TorrentContentLayout>(options.ContentLayout);
|
||||||
}
|
}
|
||||||
if (string.IsNullOrEmpty(options.Cookie))
|
|
||||||
{
|
|
||||||
addTorrentParams.Cookie = options.Cookie;
|
|
||||||
}
|
|
||||||
addTorrentParams.DownloadLimit = options.DownloadLimit;
|
addTorrentParams.DownloadLimit = options.DownloadLimit;
|
||||||
|
if (!string.IsNullOrWhiteSpace(options.DownloadPath))
|
||||||
|
{
|
||||||
addTorrentParams.DownloadPath = options.DownloadPath;
|
addTorrentParams.DownloadPath = options.DownloadPath;
|
||||||
|
}
|
||||||
addTorrentParams.FirstLastPiecePriority = options.DownloadFirstAndLastPiecesFirst;
|
addTorrentParams.FirstLastPiecePriority = options.DownloadFirstAndLastPiecesFirst;
|
||||||
addTorrentParams.InactiveSeedingTimeLimit = options.InactiveSeedingTimeLimit;
|
addTorrentParams.InactiveSeedingTimeLimit = options.InactiveSeedingTimeLimit;
|
||||||
addTorrentParams.Paused = !options.StartTorrent;
|
|
||||||
addTorrentParams.RatioLimit = options.RatioLimit;
|
addTorrentParams.RatioLimit = options.RatioLimit;
|
||||||
addTorrentParams.RenameTorrent = options.RenameTorrent;
|
addTorrentParams.RenameTorrent = options.RenameTorrent;
|
||||||
|
if (!options.TorrentManagementMode)
|
||||||
|
{
|
||||||
addTorrentParams.SavePath = options.SavePath;
|
addTorrentParams.SavePath = options.SavePath;
|
||||||
|
}
|
||||||
addTorrentParams.SeedingTimeLimit = options.SeedingTimeLimit;
|
addTorrentParams.SeedingTimeLimit = options.SeedingTimeLimit;
|
||||||
addTorrentParams.SequentialDownload = options.DownloadInSequentialOrder;
|
addTorrentParams.SequentialDownload = options.DownloadInSequentialOrder;
|
||||||
if (!string.IsNullOrEmpty(options.ShareLimitAction))
|
if (!string.IsNullOrEmpty(options.ShareLimitAction))
|
||||||
@@ -100,7 +102,10 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
addTorrentParams.Stopped = !options.StartTorrent;
|
addTorrentParams.Stopped = !options.StartTorrent;
|
||||||
addTorrentParams.Tags = options.Tags;
|
addTorrentParams.Tags = options.Tags;
|
||||||
addTorrentParams.UploadLimit = options.UploadLimit;
|
addTorrentParams.UploadLimit = options.UploadLimit;
|
||||||
|
if (options.UseDownloadPath.HasValue)
|
||||||
|
{
|
||||||
addTorrentParams.UseDownloadPath = options.UseDownloadPath;
|
addTorrentParams.UseDownloadPath = options.UseDownloadPath;
|
||||||
|
}
|
||||||
return addTorrentParams;
|
return addTorrentParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,10 +128,10 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
var addTorrentParams = CreateAddTorrentParams(options);
|
var addTorrentParams = CreateAddTorrentParams(options);
|
||||||
addTorrentParams.Urls = options.Urls;
|
addTorrentParams.Urls = options.Urls;
|
||||||
|
|
||||||
await apiClient.AddTorrent(addTorrentParams);
|
_ = await apiClient.AddTorrent(addTorrentParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<bool> InvokeDeleteTorrentDialog(this IDialogService dialogService, IApiClient apiClient, params string[] hashes)
|
public static async Task<bool> InvokeDeleteTorrentDialog(this IDialogService dialogService, IApiClient apiClient, bool confirmTorrentDeletion, params string[] hashes)
|
||||||
{
|
{
|
||||||
if (hashes.Length == 0)
|
if (hashes.Length == 0)
|
||||||
{
|
{
|
||||||
@@ -138,6 +143,12 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
{ nameof(DeleteDialog.Count), hashes.Length }
|
{ nameof(DeleteDialog.Count), hashes.Length }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!confirmTorrentDeletion)
|
||||||
|
{
|
||||||
|
await apiClient.DeleteTorrents(hashes: hashes, deleteFiles: false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
var reference = await dialogService.ShowAsync<DeleteDialog>($"Remove torrent{(hashes.Length == 1 ? "" : "s")}?", parameters, ConfirmDialogOptions);
|
var reference = await dialogService.ShowAsync<DeleteDialog>($"Remove torrent{(hashes.Length == 1 ? "" : "s")}?", parameters, ConfirmDialogOptions);
|
||||||
var dialogResult = await reference.Result;
|
var dialogResult = await reference.Result;
|
||||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
||||||
@@ -150,6 +161,28 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task ForceRecheckAsync(this IDialogService dialogService, IApiClient apiClient, IEnumerable<string> hashes, bool confirmTorrentRecheck)
|
||||||
|
{
|
||||||
|
var hashArray = hashes?.ToArray() ?? [];
|
||||||
|
if (hashArray.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (confirmTorrentRecheck)
|
||||||
|
{
|
||||||
|
var content = $"Are you sure you want to recheck the selected torrent{(hashArray.Length == 1 ? "" : "s")}?";
|
||||||
|
|
||||||
|
var confirmed = await dialogService.ShowConfirmDialog("Force recheck", content);
|
||||||
|
if (!confirmed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await apiClient.RecheckTorrents(null, hashArray);
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task InvokeDownloadRateDialog(this IDialogService dialogService, IApiClient apiClient, long rate, IEnumerable<string> hashes)
|
public static async Task InvokeDownloadRateDialog(this IDialogService dialogService, IApiClient apiClient, long rate, IEnumerable<string> hashes)
|
||||||
{
|
{
|
||||||
Func<long, string> valueDisplayFunc = v => v == Limits.NoLimit ? "∞" : v.ToString();
|
Func<long, string> valueDisplayFunc = v => v == Limits.NoLimit ? "∞" : v.ToString();
|
||||||
@@ -217,21 +250,30 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
|
|
||||||
public static async Task InvokeShareRatioDialog(this IDialogService dialogService, IApiClient apiClient, IEnumerable<Torrent> torrents)
|
public static async Task InvokeShareRatioDialog(this IDialogService dialogService, IApiClient apiClient, IEnumerable<Torrent> torrents)
|
||||||
{
|
{
|
||||||
var torrentShareRatios = torrents.Select(t => new ShareRatioMax
|
var torrentList = torrents.ToList();
|
||||||
|
if (torrentList.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var shareRatioValues = torrentList.Select(t => new ShareRatioMax
|
||||||
{
|
{
|
||||||
InactiveSeedingTimeLimit = t.InactiveSeedingTimeLimit,
|
InactiveSeedingTimeLimit = t.InactiveSeedingTimeLimit,
|
||||||
MaxInactiveSeedingTime = t.InactiveSeedingTimeLimit,
|
MaxInactiveSeedingTime = t.MaxInactiveSeedingTime,
|
||||||
MaxRatio = t.MaxRatio,
|
MaxRatio = t.MaxRatio,
|
||||||
MaxSeedingTime = t.MaxSeedingTime,
|
MaxSeedingTime = t.MaxSeedingTime,
|
||||||
RatioLimit = t.RatioLimit,
|
RatioLimit = t.RatioLimit,
|
||||||
SeedingTimeLimit = t.SeedingTimeLimit,
|
SeedingTimeLimit = t.SeedingTimeLimit,
|
||||||
});
|
ShareLimitAction = t.ShareLimitAction,
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
var torrentsHaveSameShareRatio = torrentShareRatios.Distinct().Count() == 1;
|
var referenceValue = shareRatioValues[0];
|
||||||
|
var torrentsHaveSameShareRatio = shareRatioValues.Distinct().Count() == 1;
|
||||||
|
|
||||||
var parameters = new DialogParameters
|
var parameters = new DialogParameters
|
||||||
{
|
{
|
||||||
{ nameof(ShareRatioDialog.Value), torrentsHaveSameShareRatio ? torrentShareRatios.FirstOrDefault() : null },
|
{ nameof(ShareRatioDialog.Value), torrentsHaveSameShareRatio ? referenceValue : null },
|
||||||
|
{ nameof(ShareRatioDialog.CurrentValue), referenceValue },
|
||||||
};
|
};
|
||||||
var result = await dialogService.ShowAsync<ShareRatioDialog>("Share ratio", parameters, FormDialogOptions);
|
var result = await dialogService.ShowAsync<ShareRatioDialog>("Share ratio", parameters, FormDialogOptions);
|
||||||
|
|
||||||
@@ -243,7 +285,7 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
|
|
||||||
var shareRatio = (ShareRatio)dialogResult.Data;
|
var shareRatio = (ShareRatio)dialogResult.Data;
|
||||||
|
|
||||||
await apiClient.SetTorrentShareLimit(shareRatio.RatioLimit, shareRatio.SeedingTimeLimit, shareRatio.InactiveSeedingTimeLimit, null, torrents.Select(t => t.Hash).ToArray());
|
await apiClient.SetTorrentShareLimit(shareRatio.RatioLimit, shareRatio.SeedingTimeLimit, shareRatio.InactiveSeedingTimeLimit, shareRatio.ShareLimitAction ?? ShareLimitAction.Default, hashes: torrentList.Select(t => t.Hash).ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task InvokeStringFieldDialog(this IDialogService dialogService, string title, string label, string? value, Func<string, Task> onSuccess)
|
public static async Task InvokeStringFieldDialog(this IDialogService dialogService, string title, string label, string? value, Func<string, Task> onSuccess)
|
||||||
@@ -437,3 +479,5 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using ByteSizeLib;
|
using ByteSizeLib;
|
||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
|
using Lantean.QBitTorrentClient;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -404,8 +405,6 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
Status.Downloading => (Icons.Material.Filled.Downloading, Color.Success),
|
Status.Downloading => (Icons.Material.Filled.Downloading, Color.Success),
|
||||||
Status.Seeding => (Icons.Material.Filled.Upload, Color.Info),
|
Status.Seeding => (Icons.Material.Filled.Upload, Color.Info),
|
||||||
Status.Completed => (Icons.Material.Filled.Check, Color.Default),
|
Status.Completed => (Icons.Material.Filled.Check, Color.Default),
|
||||||
Status.Resumed => (Icons.Material.Filled.PlayArrow, Color.Success),
|
|
||||||
Status.Paused => (Icons.Material.Filled.Pause, Color.Default),
|
|
||||||
Status.Stopped => (Icons.Material.Filled.Stop, Color.Default),
|
Status.Stopped => (Icons.Material.Filled.Stop, Color.Default),
|
||||||
Status.Active => (Icons.Material.Filled.Sort, Color.Success),
|
Status.Active => (Icons.Material.Filled.Sort, Color.Success),
|
||||||
Status.Inactive => (Icons.Material.Filled.Sort, Color.Error),
|
Status.Inactive => (Icons.Material.Filled.Sort, Color.Error),
|
||||||
@@ -417,5 +416,25 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
_ => (Icons.Material.Filled.QuestionMark, Color.Inherit),
|
_ => (Icons.Material.Filled.QuestionMark, Color.Inherit),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string Bool(bool value, string trueText = "Yes", string falseText = "No")
|
||||||
|
{
|
||||||
|
return value ? trueText : falseText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string RatioLimit(float value)
|
||||||
|
{
|
||||||
|
if (value == Limits.GlobalLimit)
|
||||||
|
{
|
||||||
|
return "Global";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value <= Limits.NoLimit)
|
||||||
|
{
|
||||||
|
return "∞";
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.ToString("0.00");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,15 +200,8 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Status.Resumed:
|
case Status.Stopped:
|
||||||
if (!state.Contains("resumed"))
|
if (state != "stoppedDL" && state != "stoppedUP")
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Status.Paused:
|
|
||||||
if (!state.Contains("paused") && !state.Contains("stopped"))
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
namespace Lantean.QBTMud.Helpers
|
|
||||||
{
|
|
||||||
internal static class VersionHelper
|
|
||||||
{
|
|
||||||
private static int? _version;
|
|
||||||
|
|
||||||
private const int _defaultVersion = 5;
|
|
||||||
|
|
||||||
public static int DefaultVersion => _defaultVersion;
|
|
||||||
|
|
||||||
public static int GetMajorVersion(string? version)
|
|
||||||
{
|
|
||||||
if (_version is not null)
|
|
||||||
{
|
|
||||||
return _version.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(version))
|
|
||||||
{
|
|
||||||
return _defaultVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Version.TryParse(version?.Replace("v", ""), out var theVersion))
|
|
||||||
{
|
|
||||||
return _defaultVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
_version = theVersion.Major;
|
|
||||||
|
|
||||||
return _version.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,4 +23,8 @@
|
|||||||
<ProjectReference Include="..\Lantean.QBitTorrentClient\Lantean.QBitTorrentClient.csproj" />
|
<ProjectReference Include="..\Lantean.QBitTorrentClient\Lantean.QBitTorrentClient.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="Lantean.QBTMud.Test" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -33,6 +33,14 @@
|
|||||||
}
|
}
|
||||||
<MudSpacer />
|
<MudSpacer />
|
||||||
<MudText Class="mx-2 mb-1 d-none d-sm-flex">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
|
<MudText Class="mx-2 mb-1 d-none d-sm-flex">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
|
||||||
|
@{
|
||||||
|
var externalIpLabel = Preferences?.StatusBarExternalIp == true ? BuildExternalIpLabel(MainData?.ServerState) : null;
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrEmpty(externalIpLabel))
|
||||||
|
{
|
||||||
|
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
|
||||||
|
<MudText Class="mx-2 mb-1 d-none d-sm-flex">@externalIpLabel</MudText>
|
||||||
|
}
|
||||||
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
|
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
|
||||||
<MudText Class="mx-2 mb-1 d-none d-sm-flex">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
|
<MudText Class="mx-2 mb-1 d-none d-sm-flex">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
|
||||||
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
|
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ namespace Lantean.QBTMud.Layout
|
|||||||
protected IApiClient ApiClient { get; set; } = default!;
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDataManager DataManager { get; set; } = default!;
|
protected ITorrentDataManager DataManager { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected NavigationManager NavigationManager { get; set; } = default!;
|
protected NavigationManager NavigationManager { get; set; } = default!;
|
||||||
@@ -97,7 +97,7 @@ namespace Lantean.QBTMud.Layout
|
|||||||
Preferences = await ApiClient.GetApplicationPreferences();
|
Preferences = await ApiClient.GetApplicationPreferences();
|
||||||
Version = await ApiClient.GetApplicationVersion();
|
Version = await ApiClient.GetApplicationVersion();
|
||||||
var data = await ApiClient.GetMainData(_requestId);
|
var data = await ApiClient.GetMainData(_requestId);
|
||||||
MainData = DataManager.CreateMainData(data, Version);
|
MainData = DataManager.CreateMainData(data);
|
||||||
MarkTorrentsDirty();
|
MarkTorrentsDirty();
|
||||||
|
|
||||||
_requestId = data.ResponseId;
|
_requestId = data.ResponseId;
|
||||||
@@ -145,7 +145,7 @@ namespace Lantean.QBTMud.Layout
|
|||||||
|
|
||||||
if (MainData is null || data.FullUpdate)
|
if (MainData is null || data.FullUpdate)
|
||||||
{
|
{
|
||||||
MainData = DataManager.CreateMainData(data, Version);
|
MainData = DataManager.CreateMainData(data);
|
||||||
MarkTorrentsDirty();
|
MarkTorrentsDirty();
|
||||||
shouldRender = true;
|
shouldRender = true;
|
||||||
}
|
}
|
||||||
@@ -201,6 +201,32 @@ namespace Lantean.QBTMud.Layout
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? BuildExternalIpLabel(ServerState? serverState)
|
||||||
|
{
|
||||||
|
if (serverState is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var v4 = serverState.LastExternalAddressV4;
|
||||||
|
var v6 = serverState.LastExternalAddressV6;
|
||||||
|
var hasV4 = !string.IsNullOrWhiteSpace(v4);
|
||||||
|
var hasV6 = !string.IsNullOrWhiteSpace(v6);
|
||||||
|
|
||||||
|
if (!hasV4 && !hasV6)
|
||||||
|
{
|
||||||
|
return "External IP: N/A";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasV4 && hasV6)
|
||||||
|
{
|
||||||
|
return $"External IPs: {v4}, {v6}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var address = hasV4 ? v4 : v6;
|
||||||
|
return $"External IP: {address}";
|
||||||
|
}
|
||||||
|
|
||||||
private void OnCategoryChanged(string category)
|
private void OnCategoryChanged(string category)
|
||||||
{
|
{
|
||||||
if (Category == category)
|
if (Category == category)
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
Dictionary<string, HashSet<string>> tagState,
|
Dictionary<string, HashSet<string>> tagState,
|
||||||
Dictionary<string, HashSet<string>> categoriesState,
|
Dictionary<string, HashSet<string>> categoriesState,
|
||||||
Dictionary<string, HashSet<string>> statusState,
|
Dictionary<string, HashSet<string>> statusState,
|
||||||
Dictionary<string, HashSet<string>> trackersState,
|
Dictionary<string, HashSet<string>> trackersState)
|
||||||
int majorVersion)
|
|
||||||
{
|
{
|
||||||
Torrents = torrents.ToDictionary();
|
Torrents = torrents.ToDictionary();
|
||||||
Tags = tags.ToHashSet();
|
Tags = tags.ToHashSet();
|
||||||
@@ -23,7 +22,6 @@
|
|||||||
CategoriesState = categoriesState;
|
CategoriesState = categoriesState;
|
||||||
StatusState = statusState;
|
StatusState = statusState;
|
||||||
TrackersState = trackersState;
|
TrackersState = trackersState;
|
||||||
MajorVersion = majorVersion;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, Torrent> Torrents { get; }
|
public Dictionary<string, Torrent> Torrents { get; }
|
||||||
@@ -38,6 +36,5 @@
|
|||||||
public Dictionary<string, HashSet<string>> TrackersState { get; }
|
public Dictionary<string, HashSet<string>> TrackersState { get; }
|
||||||
public string? SelectedTorrentHash { get; set; }
|
public string? SelectedTorrentHash { get; set; }
|
||||||
public bool LostConnection { get; set; }
|
public bool LostConnection { get; set; }
|
||||||
public int MajorVersion { get; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,17 @@
|
|||||||
long uploadRateLimit,
|
long uploadRateLimit,
|
||||||
bool useAltSpeedLimits,
|
bool useAltSpeedLimits,
|
||||||
bool useSubcategories,
|
bool useSubcategories,
|
||||||
float writeCacheOverload) : base(connectionStatus, dHTNodes, downloadInfoData, downloadInfoSpeed, downloadRateLimit, uploadInfoData, uploadInfoSpeed, uploadRateLimit)
|
float writeCacheOverload,
|
||||||
|
string lastExternalAddressV4,
|
||||||
|
string lastExternalAddressV6) : base(
|
||||||
|
connectionStatus,
|
||||||
|
dHTNodes,
|
||||||
|
downloadInfoData,
|
||||||
|
downloadInfoSpeed,
|
||||||
|
downloadRateLimit,
|
||||||
|
uploadInfoData,
|
||||||
|
uploadInfoSpeed,
|
||||||
|
uploadRateLimit)
|
||||||
{
|
{
|
||||||
AllTimeDownloaded = allTimeDownloaded;
|
AllTimeDownloaded = allTimeDownloaded;
|
||||||
AllTimeUploaded = allTimeUploaded;
|
AllTimeUploaded = allTimeUploaded;
|
||||||
@@ -46,6 +56,8 @@
|
|||||||
UseAltSpeedLimits = useAltSpeedLimits;
|
UseAltSpeedLimits = useAltSpeedLimits;
|
||||||
UseSubcategories = useSubcategories;
|
UseSubcategories = useSubcategories;
|
||||||
WriteCacheOverload = writeCacheOverload;
|
WriteCacheOverload = writeCacheOverload;
|
||||||
|
LastExternalAddressV4 = lastExternalAddressV4;
|
||||||
|
LastExternalAddressV6 = lastExternalAddressV6;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerState()
|
public ServerState()
|
||||||
@@ -85,5 +97,9 @@
|
|||||||
public bool UseSubcategories { get; set; }
|
public bool UseSubcategories { get; set; }
|
||||||
|
|
||||||
public float WriteCacheOverload { get; set; }
|
public float WriteCacheOverload { get; set; }
|
||||||
|
|
||||||
|
public string LastExternalAddressV4 { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string LastExternalAddressV6 { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
namespace Lantean.QBTMud.Models
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
|
||||||
|
namespace Lantean.QBTMud.Models
|
||||||
{
|
{
|
||||||
public record ShareRatio
|
public record ShareRatio
|
||||||
{
|
{
|
||||||
public float RatioLimit { get; set; }
|
public float RatioLimit { get; set; }
|
||||||
public float SeedingTimeLimit { get; set; }
|
public float SeedingTimeLimit { get; set; }
|
||||||
public float InactiveSeedingTimeLimit { get; set; }
|
public float InactiveSeedingTimeLimit { get; set; }
|
||||||
|
public ShareLimitAction? ShareLimitAction { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public record ShareRatioMax : ShareRatio
|
public record ShareRatioMax : ShareRatio
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
Downloading,
|
Downloading,
|
||||||
Seeding,
|
Seeding,
|
||||||
Completed,
|
Completed,
|
||||||
Resumed,
|
|
||||||
Paused,
|
|
||||||
Stopped,
|
Stopped,
|
||||||
Active,
|
Active,
|
||||||
Inactive,
|
Inactive,
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
namespace Lantean.QBTMud.Models
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
|
||||||
|
namespace Lantean.QBTMud.Models
|
||||||
{
|
{
|
||||||
public class Torrent
|
public class Torrent
|
||||||
{
|
{
|
||||||
@@ -52,7 +57,13 @@
|
|||||||
long uploadSpeed,
|
long uploadSpeed,
|
||||||
long reannounce,
|
long reannounce,
|
||||||
float inactiveSeedingTimeLimit,
|
float inactiveSeedingTimeLimit,
|
||||||
float maxInactiveSeedingTime)
|
float maxInactiveSeedingTime,
|
||||||
|
float popularity,
|
||||||
|
string downloadPath,
|
||||||
|
string rootPath,
|
||||||
|
bool isPrivate,
|
||||||
|
ShareLimitAction shareLimitAction,
|
||||||
|
string comment)
|
||||||
{
|
{
|
||||||
Hash = hash;
|
Hash = hash;
|
||||||
AddedOn = addedOn;
|
AddedOn = addedOn;
|
||||||
@@ -104,21 +115,31 @@
|
|||||||
Reannounce = reannounce;
|
Reannounce = reannounce;
|
||||||
InactiveSeedingTimeLimit = inactiveSeedingTimeLimit;
|
InactiveSeedingTimeLimit = inactiveSeedingTimeLimit;
|
||||||
MaxInactiveSeedingTime = maxInactiveSeedingTime;
|
MaxInactiveSeedingTime = maxInactiveSeedingTime;
|
||||||
|
Popularity = popularity;
|
||||||
|
DownloadPath = downloadPath;
|
||||||
|
RootPath = rootPath;
|
||||||
|
IsPrivate = isPrivate;
|
||||||
|
ShareLimitAction = shareLimitAction;
|
||||||
|
Comment = comment;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Torrent()
|
protected Torrent()
|
||||||
{
|
{
|
||||||
Hash = "";
|
Hash = string.Empty;
|
||||||
Category = "";
|
Category = string.Empty;
|
||||||
ContentPath = "";
|
ContentPath = string.Empty;
|
||||||
InfoHashV1 = "";
|
InfoHashV1 = string.Empty;
|
||||||
InfoHashV2 = "";
|
InfoHashV2 = string.Empty;
|
||||||
MagnetUri = "";
|
MagnetUri = string.Empty;
|
||||||
Name = "";
|
Name = string.Empty;
|
||||||
SavePath = "";
|
SavePath = string.Empty;
|
||||||
State = "";
|
DownloadPath = string.Empty;
|
||||||
Tags = [];
|
RootPath = string.Empty;
|
||||||
Tracker = "";
|
State = string.Empty;
|
||||||
|
Tags = new List<string>();
|
||||||
|
Tracker = string.Empty;
|
||||||
|
ShareLimitAction = ShareLimitAction.Default;
|
||||||
|
Comment = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Hash { get; }
|
public string Hash { get; }
|
||||||
@@ -183,8 +204,14 @@
|
|||||||
|
|
||||||
public float RatioLimit { get; set; }
|
public float RatioLimit { get; set; }
|
||||||
|
|
||||||
|
public float Popularity { get; set; }
|
||||||
|
|
||||||
public string SavePath { get; set; }
|
public string SavePath { get; set; }
|
||||||
|
|
||||||
|
public string DownloadPath { get; set; }
|
||||||
|
|
||||||
|
public string RootPath { get; set; }
|
||||||
|
|
||||||
public long SeedingTime { get; set; }
|
public long SeedingTime { get; set; }
|
||||||
|
|
||||||
public int SeedingTimeLimit { get; set; }
|
public int SeedingTimeLimit { get; set; }
|
||||||
@@ -221,6 +248,12 @@
|
|||||||
|
|
||||||
public float MaxInactiveSeedingTime { get; set; }
|
public float MaxInactiveSeedingTime { get; set; }
|
||||||
|
|
||||||
|
public bool IsPrivate { get; set; }
|
||||||
|
|
||||||
|
public ShareLimitAction ShareLimitAction { get; set; }
|
||||||
|
|
||||||
|
public string Comment { get; set; }
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
public override bool Equals(object? obj)
|
||||||
{
|
{
|
||||||
if (obj is null)
|
if (obj is null)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace Lantean.QBTMud.Pages
|
|||||||
protected IApiClient ApiClient { get; set; } = default!;
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDataManager DataManager { get; set; } = default!;
|
protected IPreferencesDataManager PreferencesDataManager { get; set; } = default!;
|
||||||
|
|
||||||
[CascadingParameter(Name = "DrawerOpen")]
|
[CascadingParameter(Name = "DrawerOpen")]
|
||||||
public bool DrawerOpen { get; set; }
|
public bool DrawerOpen { get; set; }
|
||||||
@@ -61,7 +61,7 @@ namespace Lantean.QBTMud.Pages
|
|||||||
|
|
||||||
protected void PreferencesChanged(UpdatePreferences preferences)
|
protected void PreferencesChanged(UpdatePreferences preferences)
|
||||||
{
|
{
|
||||||
UpdatePreferences = DataManager.MergePreferences(UpdatePreferences, preferences);
|
UpdatePreferences = PreferencesDataManager.MergePreferences(UpdatePreferences, preferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task ValidateExit(LocationChangingContext context)
|
protected async Task ValidateExit(LocationChangingContext context)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace Lantean.QBTMud.Pages
|
|||||||
protected NavigationManager NavigationManager { get; set; } = default!;
|
protected NavigationManager NavigationManager { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDataManager DataManager { get; set; } = default!;
|
protected IRssDataManager RssDataManager { get; set; } = default!;
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MainData? MainData { get; set; }
|
public MainData? MainData { get; set; }
|
||||||
@@ -115,7 +115,7 @@ namespace Lantean.QBTMud.Pages
|
|||||||
private async Task GetRssList()
|
private async Task GetRssList()
|
||||||
{
|
{
|
||||||
var items = await ApiClient.GetAllRssItems(true);
|
var items = await ApiClient.GetAllRssItems(true);
|
||||||
RssList = DataManager.CreateRssList(items);
|
RssList = RssDataManager.CreateRssList(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task DownloadItem(string? url)
|
protected async Task DownloadItem(string? url)
|
||||||
|
|||||||
@@ -295,6 +295,7 @@ namespace Lantean.QBTMud.Pages
|
|||||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Up Speed", t => t.UploadSpeed, t => DisplayHelpers.Speed(t.UploadSpeed)),
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Up Speed", t => t.UploadSpeed, t => DisplayHelpers.Speed(t.UploadSpeed)),
|
||||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("ETA", t => t.EstimatedTimeOfArrival, t => DisplayHelpers.Duration(t.EstimatedTimeOfArrival)),
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("ETA", t => t.EstimatedTimeOfArrival, t => DisplayHelpers.Duration(t.EstimatedTimeOfArrival)),
|
||||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Ratio", t => t.Ratio, t => t.Ratio.ToString("0.00")),
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Ratio", t => t.Ratio, t => t.Ratio.ToString("0.00")),
|
||||||
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Popularity", t => t.Popularity, t => t.Popularity.ToString("0.00")),
|
||||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Category", t => t.Category),
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Category", t => t.Category),
|
||||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Tags", t => t.Tags, t => string.Join(", ", t.Tags)),
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Tags", t => t.Tags, t => string.Join(", ", t.Tags)),
|
||||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Added On", t => t.AddedOn, t => DisplayHelpers.DateTime(t.AddedOn)),
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Added On", t => t.AddedOn, t => DisplayHelpers.DateTime(t.AddedOn)),
|
||||||
@@ -310,11 +311,15 @@ namespace Lantean.QBTMud.Pages
|
|||||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Time Active", t => t.TimeActive, t => DisplayHelpers.Duration(t.TimeActive), enabled: false),
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Time Active", t => t.TimeActive, t => DisplayHelpers.Duration(t.TimeActive), enabled: false),
|
||||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Save path", t => t.SavePath, enabled: false),
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Save path", t => t.SavePath, enabled: false),
|
||||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Completed", t => t.Completed, t => DisplayHelpers.Size(t.Completed), enabled: false),
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Completed", t => t.Completed, t => DisplayHelpers.Size(t.Completed), enabled: false),
|
||||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Ratio Limit", t => t.RatioLimit, t => t.Ratio.ToString("0.00"), enabled: false),
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Ratio Limit", t => t.RatioLimit, t => DisplayHelpers.RatioLimit(t.RatioLimit), enabled: false),
|
||||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Last Seen Complete", t => t.SeenComplete, t => DisplayHelpers.DateTime(t.SeenComplete), enabled: false),
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Last Seen Complete", t => t.SeenComplete, t => DisplayHelpers.DateTime(t.SeenComplete), enabled: false),
|
||||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Last Activity", t => t.LastActivity, t => DisplayHelpers.DateTime(t.LastActivity), enabled: false),
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Last Activity", t => t.LastActivity, t => DisplayHelpers.DateTime(t.LastActivity), enabled: false),
|
||||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Availability", t => t.Availability, t => t.Availability.ToString("0.##"), enabled: false),
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Availability", t => t.Availability, t => t.Availability.ToString("0.##"), enabled: false),
|
||||||
//ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Reannounce In", t => t.Reannounce, enabled: false),
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Incomplete Save Path", t => t.DownloadPath, t => DisplayHelpers.EmptyIfNull(t.DownloadPath), enabled: false),
|
||||||
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Info Hash v1", t => t.InfoHashV1, t => DisplayHelpers.EmptyIfNull(t.InfoHashV1), enabled: false),
|
||||||
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Info Hash v2", t => t.InfoHashV2, t => DisplayHelpers.EmptyIfNull(t.InfoHashV2), enabled: false),
|
||||||
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Reannounce In", t => t.Reannounce, t => DisplayHelpers.Duration(t.Reannounce), enabled: false),
|
||||||
|
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Private", t => t.IsPrivate, t => DisplayHelpers.Bool(t.IsPrivate), enabled: false),
|
||||||
];
|
];
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
|
|||||||
@@ -41,7 +41,11 @@ namespace Lantean.QBTMud
|
|||||||
builder.Services.AddScoped<ApiClient>();
|
builder.Services.AddScoped<ApiClient>();
|
||||||
builder.Services.AddScoped<IApiClient, ApiClient>();
|
builder.Services.AddScoped<IApiClient, ApiClient>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<IDataManager, DataManager>();
|
builder.Services.AddSingleton<ITorrentDataManager, TorrentDataManager>();
|
||||||
|
builder.Services.AddSingleton<IPeerDataManager, PeerDataManager>();
|
||||||
|
builder.Services.AddSingleton<IPreferencesDataManager, PreferencesDataManager>();
|
||||||
|
builder.Services.AddSingleton<IRssDataManager, RssDataManager>();
|
||||||
|
|
||||||
builder.Services.AddBlazoredLocalStorage();
|
builder.Services.AddBlazoredLocalStorage();
|
||||||
builder.Services.AddSingleton<IClipboardService, ClipboardService>();
|
builder.Services.AddSingleton<IClipboardService, ClipboardService>();
|
||||||
builder.Services.AddTransient<IKeyboardService, KeyboardService>();
|
builder.Services.AddTransient<IKeyboardService, KeyboardService>();
|
||||||
|
|||||||
11
Lantean.QBTMud/Services/IPeerDataManager.cs
Normal file
11
Lantean.QBTMud/Services/IPeerDataManager.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Lantean.QBTMud.Models;
|
||||||
|
|
||||||
|
namespace Lantean.QBTMud.Services
|
||||||
|
{
|
||||||
|
public interface IPeerDataManager
|
||||||
|
{
|
||||||
|
PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers);
|
||||||
|
|
||||||
|
void MergeTorrentPeers(QBitTorrentClient.Models.TorrentPeers torrentPeers, PeerList peerList);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Lantean.QBTMud/Services/IPreferencesDataManager.cs
Normal file
7
Lantean.QBTMud/Services/IPreferencesDataManager.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Lantean.QBTMud.Services
|
||||||
|
{
|
||||||
|
public interface IPreferencesDataManager
|
||||||
|
{
|
||||||
|
QBitTorrentClient.Models.UpdatePreferences MergePreferences(QBitTorrentClient.Models.UpdatePreferences? original, QBitTorrentClient.Models.UpdatePreferences changed);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Lantean.QBTMud/Services/IRssDataManager.cs
Normal file
9
Lantean.QBTMud/Services/IRssDataManager.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Lantean.QBTMud.Models;
|
||||||
|
|
||||||
|
namespace Lantean.QBTMud.Services
|
||||||
|
{
|
||||||
|
public interface IRssDataManager
|
||||||
|
{
|
||||||
|
RssList CreateRssList(IReadOnlyDictionary<string, QBitTorrentClient.Models.RssItem> rssItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,24 +2,16 @@
|
|||||||
|
|
||||||
namespace Lantean.QBTMud.Services
|
namespace Lantean.QBTMud.Services
|
||||||
{
|
{
|
||||||
public interface IDataManager
|
public interface ITorrentDataManager
|
||||||
{
|
{
|
||||||
MainData CreateMainData(QBitTorrentClient.Models.MainData mainData, string version);
|
MainData CreateMainData(QBitTorrentClient.Models.MainData mainData);
|
||||||
|
|
||||||
Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent);
|
Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent);
|
||||||
|
|
||||||
bool MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList, out bool filterChanged);
|
bool MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList, out bool filterChanged);
|
||||||
|
|
||||||
PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers);
|
|
||||||
|
|
||||||
void MergeTorrentPeers(QBitTorrentClient.Models.TorrentPeers torrentPeers, PeerList peerList);
|
|
||||||
|
|
||||||
Dictionary<string, ContentItem> CreateContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files);
|
Dictionary<string, ContentItem> CreateContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files);
|
||||||
|
|
||||||
bool MergeContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files, Dictionary<string, ContentItem> contents);
|
bool MergeContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files, Dictionary<string, ContentItem> contents);
|
||||||
|
|
||||||
QBitTorrentClient.Models.UpdatePreferences MergePreferences(QBitTorrentClient.Models.UpdatePreferences? original, QBitTorrentClient.Models.UpdatePreferences changed);
|
|
||||||
|
|
||||||
RssList CreateRssList(IReadOnlyDictionary<string, QBitTorrentClient.Models.RssItem> rssItems);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
94
Lantean.QBTMud/Services/PeerDataManager.cs
Normal file
94
Lantean.QBTMud/Services/PeerDataManager.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
using Lantean.QBTMud.Models;
|
||||||
|
|
||||||
|
namespace Lantean.QBTMud.Services
|
||||||
|
{
|
||||||
|
public class PeerDataManager : IPeerDataManager
|
||||||
|
{
|
||||||
|
public PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers)
|
||||||
|
{
|
||||||
|
var peers = new Dictionary<string, Peer>();
|
||||||
|
if (torrentPeers.Peers is not null)
|
||||||
|
{
|
||||||
|
foreach (var (key, peer) in torrentPeers.Peers)
|
||||||
|
{
|
||||||
|
var newPeer = CreatePeer(key, peer);
|
||||||
|
|
||||||
|
peers[key] = newPeer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var peerList = new PeerList(peers);
|
||||||
|
|
||||||
|
return peerList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MergeTorrentPeers(QBitTorrentClient.Models.TorrentPeers torrentPeers, PeerList peerList)
|
||||||
|
{
|
||||||
|
if (torrentPeers.PeersRemoved is not null)
|
||||||
|
{
|
||||||
|
foreach (var key in torrentPeers.PeersRemoved)
|
||||||
|
{
|
||||||
|
peerList.Peers.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrentPeers.Peers is not null)
|
||||||
|
{
|
||||||
|
foreach (var (key, peer) in torrentPeers.Peers)
|
||||||
|
{
|
||||||
|
if (!peerList.Peers.TryGetValue(key, out var existingPeer))
|
||||||
|
{
|
||||||
|
var newPeer = CreatePeer(key, peer);
|
||||||
|
peerList.Peers.Add(key, newPeer);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UpdatePeer(existingPeer, peer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Peer CreatePeer(string key, QBitTorrentClient.Models.Peer peer)
|
||||||
|
{
|
||||||
|
return new Peer(
|
||||||
|
key,
|
||||||
|
peer.Client!,
|
||||||
|
peer.ClientId!,
|
||||||
|
peer.Connection!,
|
||||||
|
peer.Country,
|
||||||
|
peer.CountryCode,
|
||||||
|
peer.Downloaded.GetValueOrDefault(),
|
||||||
|
peer.DownloadSpeed.GetValueOrDefault(),
|
||||||
|
peer.Files!,
|
||||||
|
peer.Flags!,
|
||||||
|
peer.FlagsDescription!,
|
||||||
|
peer.IPAddress!,
|
||||||
|
peer.Port.GetValueOrDefault(),
|
||||||
|
peer.Progress.GetValueOrDefault(),
|
||||||
|
peer.Relevance.GetValueOrDefault(),
|
||||||
|
peer.Uploaded.GetValueOrDefault(),
|
||||||
|
peer.UploadSpeed.GetValueOrDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdatePeer(Peer existingPeer, QBitTorrentClient.Models.Peer peer)
|
||||||
|
{
|
||||||
|
existingPeer.Client = peer.Client ?? existingPeer.Client;
|
||||||
|
existingPeer.ClientId = peer.ClientId ?? existingPeer.ClientId;
|
||||||
|
existingPeer.Connection = peer.Connection ?? existingPeer.Connection;
|
||||||
|
existingPeer.Country = peer.Country ?? existingPeer.Country;
|
||||||
|
existingPeer.CountryCode = peer.CountryCode ?? existingPeer.CountryCode;
|
||||||
|
existingPeer.Downloaded = peer.Downloaded ?? existingPeer.Downloaded;
|
||||||
|
existingPeer.DownloadSpeed = peer.DownloadSpeed ?? existingPeer.DownloadSpeed;
|
||||||
|
existingPeer.Files = peer.Files ?? existingPeer.Files;
|
||||||
|
existingPeer.Flags = peer.Flags ?? existingPeer.Flags;
|
||||||
|
existingPeer.FlagsDescription = peer.FlagsDescription ?? existingPeer.FlagsDescription;
|
||||||
|
existingPeer.IPAddress = peer.IPAddress ?? existingPeer.IPAddress;
|
||||||
|
existingPeer.Port = peer.Port ?? existingPeer.Port;
|
||||||
|
existingPeer.Progress = peer.Progress ?? existingPeer.Progress;
|
||||||
|
existingPeer.Relevance = peer.Relevance ?? existingPeer.Relevance;
|
||||||
|
existingPeer.Uploaded = peer.Uploaded ?? existingPeer.Uploaded;
|
||||||
|
existingPeer.UploadSpeed = peer.UploadSpeed ?? existingPeer.UploadSpeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
459
Lantean.QBTMud/Services/PreferencesDataManager.cs
Normal file
459
Lantean.QBTMud/Services/PreferencesDataManager.cs
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
namespace Lantean.QBTMud.Services
|
||||||
|
{
|
||||||
|
public class PreferencesDataManager : IPreferencesDataManager
|
||||||
|
{
|
||||||
|
public QBitTorrentClient.Models.UpdatePreferences MergePreferences(
|
||||||
|
QBitTorrentClient.Models.UpdatePreferences? original,
|
||||||
|
QBitTorrentClient.Models.UpdatePreferences changed)
|
||||||
|
{
|
||||||
|
if (original is null)
|
||||||
|
{
|
||||||
|
original = new QBitTorrentClient.Models.UpdatePreferences
|
||||||
|
{
|
||||||
|
AddToTopOfQueue = changed.AddToTopOfQueue,
|
||||||
|
AddStoppedEnabled = changed.AddStoppedEnabled,
|
||||||
|
AddTrackers = changed.AddTrackers,
|
||||||
|
AddTrackersEnabled = changed.AddTrackersEnabled,
|
||||||
|
AltDlLimit = changed.AltDlLimit,
|
||||||
|
AltUpLimit = changed.AltUpLimit,
|
||||||
|
AlternativeWebuiEnabled = changed.AlternativeWebuiEnabled,
|
||||||
|
AlternativeWebuiPath = changed.AlternativeWebuiPath,
|
||||||
|
AnnounceIp = changed.AnnounceIp,
|
||||||
|
AnnouncePort = changed.AnnouncePort,
|
||||||
|
AnnounceToAllTiers = changed.AnnounceToAllTiers,
|
||||||
|
AnnounceToAllTrackers = changed.AnnounceToAllTrackers,
|
||||||
|
AnonymousMode = changed.AnonymousMode,
|
||||||
|
AppInstanceName = changed.AppInstanceName,
|
||||||
|
AsyncIoThreads = changed.AsyncIoThreads,
|
||||||
|
AutoDeleteMode = changed.AutoDeleteMode,
|
||||||
|
AutoTmmEnabled = changed.AutoTmmEnabled,
|
||||||
|
AutorunEnabled = changed.AutorunEnabled,
|
||||||
|
AutorunOnTorrentAddedEnabled = changed.AutorunOnTorrentAddedEnabled,
|
||||||
|
AutorunOnTorrentAddedProgram = changed.AutorunOnTorrentAddedProgram,
|
||||||
|
AutorunProgram = changed.AutorunProgram,
|
||||||
|
BannedIPs = changed.BannedIPs,
|
||||||
|
BdecodeDepthLimit = changed.BdecodeDepthLimit,
|
||||||
|
BdecodeTokenLimit = changed.BdecodeTokenLimit,
|
||||||
|
BittorrentProtocol = changed.BittorrentProtocol,
|
||||||
|
BlockPeersOnPrivilegedPorts = changed.BlockPeersOnPrivilegedPorts,
|
||||||
|
BypassAuthSubnetWhitelist = changed.BypassAuthSubnetWhitelist,
|
||||||
|
BypassAuthSubnetWhitelistEnabled = changed.BypassAuthSubnetWhitelistEnabled,
|
||||||
|
BypassLocalAuth = changed.BypassLocalAuth,
|
||||||
|
CategoryChangedTmmEnabled = changed.CategoryChangedTmmEnabled,
|
||||||
|
CheckingMemoryUse = changed.CheckingMemoryUse,
|
||||||
|
ConnectionSpeed = changed.ConnectionSpeed,
|
||||||
|
CurrentInterfaceAddress = changed.CurrentInterfaceAddress,
|
||||||
|
CurrentInterfaceName = changed.CurrentInterfaceName,
|
||||||
|
CurrentNetworkInterface = changed.CurrentNetworkInterface,
|
||||||
|
Dht = changed.Dht,
|
||||||
|
DhtBootstrapNodes = changed.DhtBootstrapNodes,
|
||||||
|
DiskCache = changed.DiskCache,
|
||||||
|
DiskCacheTtl = changed.DiskCacheTtl,
|
||||||
|
DiskIoReadMode = changed.DiskIoReadMode,
|
||||||
|
DiskIoType = changed.DiskIoType,
|
||||||
|
DiskIoWriteMode = changed.DiskIoWriteMode,
|
||||||
|
DiskQueueSize = changed.DiskQueueSize,
|
||||||
|
DlLimit = changed.DlLimit,
|
||||||
|
DontCountSlowTorrents = changed.DontCountSlowTorrents,
|
||||||
|
DyndnsDomain = changed.DyndnsDomain,
|
||||||
|
DyndnsEnabled = changed.DyndnsEnabled,
|
||||||
|
DyndnsPassword = changed.DyndnsPassword,
|
||||||
|
DyndnsService = changed.DyndnsService,
|
||||||
|
DyndnsUsername = changed.DyndnsUsername,
|
||||||
|
EmbeddedTrackerPort = changed.EmbeddedTrackerPort,
|
||||||
|
EmbeddedTrackerPortForwarding = changed.EmbeddedTrackerPortForwarding,
|
||||||
|
EnableCoalesceReadWrite = changed.EnableCoalesceReadWrite,
|
||||||
|
EnableEmbeddedTracker = changed.EnableEmbeddedTracker,
|
||||||
|
EnableMultiConnectionsFromSameIp = changed.EnableMultiConnectionsFromSameIp,
|
||||||
|
EnablePieceExtentAffinity = changed.EnablePieceExtentAffinity,
|
||||||
|
EnableUploadSuggestions = changed.EnableUploadSuggestions,
|
||||||
|
Encryption = changed.Encryption,
|
||||||
|
ExcludedFileNames = changed.ExcludedFileNames,
|
||||||
|
ExcludedFileNamesEnabled = changed.ExcludedFileNamesEnabled,
|
||||||
|
ExportDir = changed.ExportDir,
|
||||||
|
ExportDirFin = changed.ExportDirFin,
|
||||||
|
FileLogAge = changed.FileLogAge,
|
||||||
|
FileLogAgeType = changed.FileLogAgeType,
|
||||||
|
FileLogBackupEnabled = changed.FileLogBackupEnabled,
|
||||||
|
FileLogDeleteOld = changed.FileLogDeleteOld,
|
||||||
|
FileLogEnabled = changed.FileLogEnabled,
|
||||||
|
FileLogMaxSize = changed.FileLogMaxSize,
|
||||||
|
FileLogPath = changed.FileLogPath,
|
||||||
|
FilePoolSize = changed.FilePoolSize,
|
||||||
|
HashingThreads = changed.HashingThreads,
|
||||||
|
I2pAddress = changed.I2pAddress,
|
||||||
|
I2pEnabled = changed.I2pEnabled,
|
||||||
|
I2pInboundLength = changed.I2pInboundLength,
|
||||||
|
I2pInboundQuantity = changed.I2pInboundQuantity,
|
||||||
|
I2pMixedMode = changed.I2pMixedMode,
|
||||||
|
I2pOutboundLength = changed.I2pOutboundLength,
|
||||||
|
I2pOutboundQuantity = changed.I2pOutboundQuantity,
|
||||||
|
I2pPort = changed.I2pPort,
|
||||||
|
IdnSupportEnabled = changed.IdnSupportEnabled,
|
||||||
|
IncompleteFilesExt = changed.IncompleteFilesExt,
|
||||||
|
UseUnwantedFolder = changed.UseUnwantedFolder,
|
||||||
|
IpFilterEnabled = changed.IpFilterEnabled,
|
||||||
|
IpFilterPath = changed.IpFilterPath,
|
||||||
|
IpFilterTrackers = changed.IpFilterTrackers,
|
||||||
|
LimitLanPeers = changed.LimitLanPeers,
|
||||||
|
LimitTcpOverhead = changed.LimitTcpOverhead,
|
||||||
|
LimitUtpRate = changed.LimitUtpRate,
|
||||||
|
ListenPort = changed.ListenPort,
|
||||||
|
SslEnabled = changed.SslEnabled,
|
||||||
|
SslListenPort = changed.SslListenPort,
|
||||||
|
Locale = changed.Locale,
|
||||||
|
Lsd = changed.Lsd,
|
||||||
|
MailNotificationAuthEnabled = changed.MailNotificationAuthEnabled,
|
||||||
|
MailNotificationEmail = changed.MailNotificationEmail,
|
||||||
|
MailNotificationEnabled = changed.MailNotificationEnabled,
|
||||||
|
MailNotificationPassword = changed.MailNotificationPassword,
|
||||||
|
MailNotificationSender = changed.MailNotificationSender,
|
||||||
|
MailNotificationSmtp = changed.MailNotificationSmtp,
|
||||||
|
MailNotificationSslEnabled = changed.MailNotificationSslEnabled,
|
||||||
|
MailNotificationUsername = changed.MailNotificationUsername,
|
||||||
|
MarkOfTheWeb = changed.MarkOfTheWeb,
|
||||||
|
MaxActiveCheckingTorrents = changed.MaxActiveCheckingTorrents,
|
||||||
|
MaxActiveDownloads = changed.MaxActiveDownloads,
|
||||||
|
MaxActiveTorrents = changed.MaxActiveTorrents,
|
||||||
|
MaxActiveUploads = changed.MaxActiveUploads,
|
||||||
|
MaxConcurrentHttpAnnounces = changed.MaxConcurrentHttpAnnounces,
|
||||||
|
MaxConnec = changed.MaxConnec,
|
||||||
|
MaxConnecPerTorrent = changed.MaxConnecPerTorrent,
|
||||||
|
MaxInactiveSeedingTime = changed.MaxInactiveSeedingTime,
|
||||||
|
MaxInactiveSeedingTimeEnabled = changed.MaxInactiveSeedingTimeEnabled,
|
||||||
|
MaxRatio = changed.MaxRatio,
|
||||||
|
MaxRatioAct = changed.MaxRatioAct,
|
||||||
|
MaxRatioEnabled = changed.MaxRatioEnabled,
|
||||||
|
MaxSeedingTime = changed.MaxSeedingTime,
|
||||||
|
MaxSeedingTimeEnabled = changed.MaxSeedingTimeEnabled,
|
||||||
|
MaxUploads = changed.MaxUploads,
|
||||||
|
MaxUploadsPerTorrent = changed.MaxUploadsPerTorrent,
|
||||||
|
MemoryWorkingSetLimit = changed.MemoryWorkingSetLimit,
|
||||||
|
MergeTrackers = changed.MergeTrackers,
|
||||||
|
OutgoingPortsMax = changed.OutgoingPortsMax,
|
||||||
|
OutgoingPortsMin = changed.OutgoingPortsMin,
|
||||||
|
PeerTos = changed.PeerTos,
|
||||||
|
PeerTurnover = changed.PeerTurnover,
|
||||||
|
PeerTurnoverCutoff = changed.PeerTurnoverCutoff,
|
||||||
|
PeerTurnoverInterval = changed.PeerTurnoverInterval,
|
||||||
|
PerformanceWarning = changed.PerformanceWarning,
|
||||||
|
Pex = changed.Pex,
|
||||||
|
PreallocateAll = changed.PreallocateAll,
|
||||||
|
ProxyAuthEnabled = changed.ProxyAuthEnabled,
|
||||||
|
ProxyBittorrent = changed.ProxyBittorrent,
|
||||||
|
ProxyHostnameLookup = changed.ProxyHostnameLookup,
|
||||||
|
ProxyIp = changed.ProxyIp,
|
||||||
|
ProxyMisc = changed.ProxyMisc,
|
||||||
|
ProxyPassword = changed.ProxyPassword,
|
||||||
|
ProxyPeerConnections = changed.ProxyPeerConnections,
|
||||||
|
ProxyPort = changed.ProxyPort,
|
||||||
|
ProxyRss = changed.ProxyRss,
|
||||||
|
ProxyType = changed.ProxyType,
|
||||||
|
ProxyUsername = changed.ProxyUsername,
|
||||||
|
PythonExecutablePath = changed.PythonExecutablePath,
|
||||||
|
QueueingEnabled = changed.QueueingEnabled,
|
||||||
|
RandomPort = changed.RandomPort,
|
||||||
|
ReannounceWhenAddressChanged = changed.ReannounceWhenAddressChanged,
|
||||||
|
RecheckCompletedTorrents = changed.RecheckCompletedTorrents,
|
||||||
|
RefreshInterval = changed.RefreshInterval,
|
||||||
|
RequestQueueSize = changed.RequestQueueSize,
|
||||||
|
ResolvePeerCountries = changed.ResolvePeerCountries,
|
||||||
|
ResumeDataStorageType = changed.ResumeDataStorageType,
|
||||||
|
RssAutoDownloadingEnabled = changed.RssAutoDownloadingEnabled,
|
||||||
|
RssDownloadRepackProperEpisodes = changed.RssDownloadRepackProperEpisodes,
|
||||||
|
RssFetchDelay = changed.RssFetchDelay,
|
||||||
|
RssMaxArticlesPerFeed = changed.RssMaxArticlesPerFeed,
|
||||||
|
RssProcessingEnabled = changed.RssProcessingEnabled,
|
||||||
|
RssRefreshInterval = changed.RssRefreshInterval,
|
||||||
|
RssSmartEpisodeFilters = changed.RssSmartEpisodeFilters,
|
||||||
|
SavePath = changed.SavePath,
|
||||||
|
SavePathChangedTmmEnabled = changed.SavePathChangedTmmEnabled,
|
||||||
|
SaveResumeDataInterval = changed.SaveResumeDataInterval,
|
||||||
|
SaveStatisticsInterval = changed.SaveStatisticsInterval,
|
||||||
|
ScanDirs = changed.ScanDirs,
|
||||||
|
ScheduleFromHour = changed.ScheduleFromHour,
|
||||||
|
ScheduleFromMin = changed.ScheduleFromMin,
|
||||||
|
ScheduleToHour = changed.ScheduleToHour,
|
||||||
|
ScheduleToMin = changed.ScheduleToMin,
|
||||||
|
SchedulerDays = changed.SchedulerDays,
|
||||||
|
SchedulerEnabled = changed.SchedulerEnabled,
|
||||||
|
SendBufferLowWatermark = changed.SendBufferLowWatermark,
|
||||||
|
SendBufferWatermark = changed.SendBufferWatermark,
|
||||||
|
SendBufferWatermarkFactor = changed.SendBufferWatermarkFactor,
|
||||||
|
SlowTorrentDlRateThreshold = changed.SlowTorrentDlRateThreshold,
|
||||||
|
SlowTorrentInactiveTimer = changed.SlowTorrentInactiveTimer,
|
||||||
|
SlowTorrentUlRateThreshold = changed.SlowTorrentUlRateThreshold,
|
||||||
|
SocketBacklogSize = changed.SocketBacklogSize,
|
||||||
|
SocketReceiveBufferSize = changed.SocketReceiveBufferSize,
|
||||||
|
SocketSendBufferSize = changed.SocketSendBufferSize,
|
||||||
|
SsrfMitigation = changed.SsrfMitigation,
|
||||||
|
StopTrackerTimeout = changed.StopTrackerTimeout,
|
||||||
|
TempPath = changed.TempPath,
|
||||||
|
TempPathEnabled = changed.TempPathEnabled,
|
||||||
|
TorrentChangedTmmEnabled = changed.TorrentChangedTmmEnabled,
|
||||||
|
TorrentContentLayout = changed.TorrentContentLayout,
|
||||||
|
TorrentContentRemoveOption = changed.TorrentContentRemoveOption,
|
||||||
|
TorrentFileSizeLimit = changed.TorrentFileSizeLimit,
|
||||||
|
TorrentStopCondition = changed.TorrentStopCondition,
|
||||||
|
UpLimit = changed.UpLimit,
|
||||||
|
UploadChokingAlgorithm = changed.UploadChokingAlgorithm,
|
||||||
|
UploadSlotsBehavior = changed.UploadSlotsBehavior,
|
||||||
|
Upnp = changed.Upnp,
|
||||||
|
UpnpLeaseDuration = changed.UpnpLeaseDuration,
|
||||||
|
UseCategoryPathsInManualMode = changed.UseCategoryPathsInManualMode,
|
||||||
|
UseHttps = changed.UseHttps,
|
||||||
|
IgnoreSslErrors = changed.IgnoreSslErrors,
|
||||||
|
UseSubcategories = changed.UseSubcategories,
|
||||||
|
UtpTcpMixedMode = changed.UtpTcpMixedMode,
|
||||||
|
ValidateHttpsTrackerCertificate = changed.ValidateHttpsTrackerCertificate,
|
||||||
|
WebUiAddress = changed.WebUiAddress,
|
||||||
|
WebUiApiKey = changed.WebUiApiKey,
|
||||||
|
WebUiBanDuration = changed.WebUiBanDuration,
|
||||||
|
WebUiClickjackingProtectionEnabled = changed.WebUiClickjackingProtectionEnabled,
|
||||||
|
WebUiCsrfProtectionEnabled = changed.WebUiCsrfProtectionEnabled,
|
||||||
|
WebUiCustomHttpHeaders = changed.WebUiCustomHttpHeaders,
|
||||||
|
WebUiDomainList = changed.WebUiDomainList,
|
||||||
|
WebUiHostHeaderValidationEnabled = changed.WebUiHostHeaderValidationEnabled,
|
||||||
|
WebUiHttpsCertPath = changed.WebUiHttpsCertPath,
|
||||||
|
WebUiHttpsKeyPath = changed.WebUiHttpsKeyPath,
|
||||||
|
WebUiMaxAuthFailCount = changed.WebUiMaxAuthFailCount,
|
||||||
|
WebUiPort = changed.WebUiPort,
|
||||||
|
WebUiReverseProxiesList = changed.WebUiReverseProxiesList,
|
||||||
|
WebUiReverseProxyEnabled = changed.WebUiReverseProxyEnabled,
|
||||||
|
WebUiSecureCookieEnabled = changed.WebUiSecureCookieEnabled,
|
||||||
|
WebUiSessionTimeout = changed.WebUiSessionTimeout,
|
||||||
|
WebUiUpnp = changed.WebUiUpnp,
|
||||||
|
WebUiUseCustomHttpHeadersEnabled = changed.WebUiUseCustomHttpHeadersEnabled,
|
||||||
|
WebUiUsername = changed.WebUiUsername,
|
||||||
|
WebUiPassword = changed.WebUiPassword,
|
||||||
|
ConfirmTorrentDeletion = changed.ConfirmTorrentDeletion,
|
||||||
|
ConfirmTorrentRecheck = changed.ConfirmTorrentRecheck,
|
||||||
|
StatusBarExternalIp = changed.StatusBarExternalIp
|
||||||
|
};
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
|
||||||
|
original.AddToTopOfQueue = changed.AddToTopOfQueue ?? original.AddToTopOfQueue;
|
||||||
|
original.AddStoppedEnabled = changed.AddStoppedEnabled ?? original.AddStoppedEnabled;
|
||||||
|
original.AddTrackers = changed.AddTrackers ?? original.AddTrackers;
|
||||||
|
original.AddTrackersEnabled = changed.AddTrackersEnabled ?? original.AddTrackersEnabled;
|
||||||
|
original.AltDlLimit = changed.AltDlLimit ?? original.AltDlLimit;
|
||||||
|
original.AltUpLimit = changed.AltUpLimit ?? original.AltUpLimit;
|
||||||
|
original.AlternativeWebuiEnabled = changed.AlternativeWebuiEnabled ?? original.AlternativeWebuiEnabled;
|
||||||
|
original.AlternativeWebuiPath = changed.AlternativeWebuiPath ?? original.AlternativeWebuiPath;
|
||||||
|
original.AnnounceIp = changed.AnnounceIp ?? original.AnnounceIp;
|
||||||
|
original.AnnouncePort = changed.AnnouncePort ?? original.AnnouncePort;
|
||||||
|
original.AnnounceToAllTiers = changed.AnnounceToAllTiers ?? original.AnnounceToAllTiers;
|
||||||
|
original.AnnounceToAllTrackers = changed.AnnounceToAllTrackers ?? original.AnnounceToAllTrackers;
|
||||||
|
original.AnonymousMode = changed.AnonymousMode ?? original.AnonymousMode;
|
||||||
|
original.AppInstanceName = changed.AppInstanceName ?? original.AppInstanceName;
|
||||||
|
original.AsyncIoThreads = changed.AsyncIoThreads ?? original.AsyncIoThreads;
|
||||||
|
original.AutoDeleteMode = changed.AutoDeleteMode ?? original.AutoDeleteMode;
|
||||||
|
original.AutoTmmEnabled = changed.AutoTmmEnabled ?? original.AutoTmmEnabled;
|
||||||
|
original.AutorunEnabled = changed.AutorunEnabled ?? original.AutorunEnabled;
|
||||||
|
original.AutorunOnTorrentAddedEnabled = changed.AutorunOnTorrentAddedEnabled ?? original.AutorunOnTorrentAddedEnabled;
|
||||||
|
original.AutorunOnTorrentAddedProgram = changed.AutorunOnTorrentAddedProgram ?? original.AutorunOnTorrentAddedProgram;
|
||||||
|
original.AutorunProgram = changed.AutorunProgram ?? original.AutorunProgram;
|
||||||
|
original.BannedIPs = changed.BannedIPs ?? original.BannedIPs;
|
||||||
|
original.BdecodeDepthLimit = changed.BdecodeDepthLimit ?? original.BdecodeDepthLimit;
|
||||||
|
original.BdecodeTokenLimit = changed.BdecodeTokenLimit ?? original.BdecodeTokenLimit;
|
||||||
|
original.BittorrentProtocol = changed.BittorrentProtocol ?? original.BittorrentProtocol;
|
||||||
|
original.BlockPeersOnPrivilegedPorts = changed.BlockPeersOnPrivilegedPorts ?? original.BlockPeersOnPrivilegedPorts;
|
||||||
|
original.BypassAuthSubnetWhitelist = changed.BypassAuthSubnetWhitelist ?? original.BypassAuthSubnetWhitelist;
|
||||||
|
original.BypassAuthSubnetWhitelistEnabled = changed.BypassAuthSubnetWhitelistEnabled ?? original.BypassAuthSubnetWhitelistEnabled;
|
||||||
|
original.BypassLocalAuth = changed.BypassLocalAuth ?? original.BypassLocalAuth;
|
||||||
|
original.CategoryChangedTmmEnabled = changed.CategoryChangedTmmEnabled ?? original.CategoryChangedTmmEnabled;
|
||||||
|
original.CheckingMemoryUse = changed.CheckingMemoryUse ?? original.CheckingMemoryUse;
|
||||||
|
original.ConnectionSpeed = changed.ConnectionSpeed ?? original.ConnectionSpeed;
|
||||||
|
original.CurrentInterfaceAddress = changed.CurrentInterfaceAddress ?? original.CurrentInterfaceAddress;
|
||||||
|
original.CurrentInterfaceName = changed.CurrentInterfaceName ?? original.CurrentInterfaceName;
|
||||||
|
original.CurrentNetworkInterface = changed.CurrentNetworkInterface ?? original.CurrentNetworkInterface;
|
||||||
|
original.Dht = changed.Dht ?? original.Dht;
|
||||||
|
original.DhtBootstrapNodes = changed.DhtBootstrapNodes ?? original.DhtBootstrapNodes;
|
||||||
|
original.DiskCache = changed.DiskCache ?? original.DiskCache;
|
||||||
|
original.DiskCacheTtl = changed.DiskCacheTtl ?? original.DiskCacheTtl;
|
||||||
|
original.DiskIoReadMode = changed.DiskIoReadMode ?? original.DiskIoReadMode;
|
||||||
|
original.DiskIoType = changed.DiskIoType ?? original.DiskIoType;
|
||||||
|
original.DiskIoWriteMode = changed.DiskIoWriteMode ?? original.DiskIoWriteMode;
|
||||||
|
original.DiskQueueSize = changed.DiskQueueSize ?? original.DiskQueueSize;
|
||||||
|
original.DlLimit = changed.DlLimit ?? original.DlLimit;
|
||||||
|
original.DontCountSlowTorrents = changed.DontCountSlowTorrents ?? original.DontCountSlowTorrents;
|
||||||
|
original.DyndnsDomain = changed.DyndnsDomain ?? original.DyndnsDomain;
|
||||||
|
original.DyndnsEnabled = changed.DyndnsEnabled ?? original.DyndnsEnabled;
|
||||||
|
original.DyndnsPassword = changed.DyndnsPassword ?? original.DyndnsPassword;
|
||||||
|
original.DyndnsService = changed.DyndnsService ?? original.DyndnsService;
|
||||||
|
original.DyndnsUsername = changed.DyndnsUsername ?? original.DyndnsUsername;
|
||||||
|
original.EmbeddedTrackerPort = changed.EmbeddedTrackerPort ?? original.EmbeddedTrackerPort;
|
||||||
|
original.EmbeddedTrackerPortForwarding = changed.EmbeddedTrackerPortForwarding ?? original.EmbeddedTrackerPortForwarding;
|
||||||
|
original.EnableCoalesceReadWrite = changed.EnableCoalesceReadWrite ?? original.EnableCoalesceReadWrite;
|
||||||
|
original.EnableEmbeddedTracker = changed.EnableEmbeddedTracker ?? original.EnableEmbeddedTracker;
|
||||||
|
original.EnableMultiConnectionsFromSameIp = changed.EnableMultiConnectionsFromSameIp ?? original.EnableMultiConnectionsFromSameIp;
|
||||||
|
original.EnablePieceExtentAffinity = changed.EnablePieceExtentAffinity ?? original.EnablePieceExtentAffinity;
|
||||||
|
original.EnableUploadSuggestions = changed.EnableUploadSuggestions ?? original.EnableUploadSuggestions;
|
||||||
|
original.Encryption = changed.Encryption ?? original.Encryption;
|
||||||
|
original.ExcludedFileNames = changed.ExcludedFileNames ?? original.ExcludedFileNames;
|
||||||
|
original.ExcludedFileNamesEnabled = changed.ExcludedFileNamesEnabled ?? original.ExcludedFileNamesEnabled;
|
||||||
|
original.ExportDir = changed.ExportDir ?? original.ExportDir;
|
||||||
|
original.ExportDirFin = changed.ExportDirFin ?? original.ExportDirFin;
|
||||||
|
original.FileLogAge = changed.FileLogAge ?? original.FileLogAge;
|
||||||
|
original.FileLogAgeType = changed.FileLogAgeType ?? original.FileLogAgeType;
|
||||||
|
original.FileLogBackupEnabled = changed.FileLogBackupEnabled ?? original.FileLogBackupEnabled;
|
||||||
|
original.FileLogDeleteOld = changed.FileLogDeleteOld ?? original.FileLogDeleteOld;
|
||||||
|
original.FileLogEnabled = changed.FileLogEnabled ?? original.FileLogEnabled;
|
||||||
|
original.FileLogMaxSize = changed.FileLogMaxSize ?? original.FileLogMaxSize;
|
||||||
|
original.FileLogPath = changed.FileLogPath ?? original.FileLogPath;
|
||||||
|
original.FilePoolSize = changed.FilePoolSize ?? original.FilePoolSize;
|
||||||
|
original.HashingThreads = changed.HashingThreads ?? original.HashingThreads;
|
||||||
|
original.I2pAddress = changed.I2pAddress ?? original.I2pAddress;
|
||||||
|
original.I2pEnabled = changed.I2pEnabled ?? original.I2pEnabled;
|
||||||
|
original.I2pInboundLength = changed.I2pInboundLength ?? original.I2pInboundLength;
|
||||||
|
original.I2pInboundQuantity = changed.I2pInboundQuantity ?? original.I2pInboundQuantity;
|
||||||
|
original.I2pMixedMode = changed.I2pMixedMode ?? original.I2pMixedMode;
|
||||||
|
original.I2pOutboundLength = changed.I2pOutboundLength ?? original.I2pOutboundLength;
|
||||||
|
original.I2pOutboundQuantity = changed.I2pOutboundQuantity ?? original.I2pOutboundQuantity;
|
||||||
|
original.I2pPort = changed.I2pPort ?? original.I2pPort;
|
||||||
|
original.IdnSupportEnabled = changed.IdnSupportEnabled ?? original.IdnSupportEnabled;
|
||||||
|
original.IncompleteFilesExt = changed.IncompleteFilesExt ?? original.IncompleteFilesExt;
|
||||||
|
original.UseUnwantedFolder = changed.UseUnwantedFolder ?? original.UseUnwantedFolder;
|
||||||
|
original.IpFilterEnabled = changed.IpFilterEnabled ?? original.IpFilterEnabled;
|
||||||
|
original.IpFilterPath = changed.IpFilterPath ?? original.IpFilterPath;
|
||||||
|
original.IpFilterTrackers = changed.IpFilterTrackers ?? original.IpFilterTrackers;
|
||||||
|
original.LimitLanPeers = changed.LimitLanPeers ?? original.LimitLanPeers;
|
||||||
|
original.LimitTcpOverhead = changed.LimitTcpOverhead ?? original.LimitTcpOverhead;
|
||||||
|
original.LimitUtpRate = changed.LimitUtpRate ?? original.LimitUtpRate;
|
||||||
|
original.ListenPort = changed.ListenPort ?? original.ListenPort;
|
||||||
|
original.SslEnabled = changed.SslEnabled ?? original.SslEnabled;
|
||||||
|
original.SslListenPort = changed.SslListenPort ?? original.SslListenPort;
|
||||||
|
original.Locale = changed.Locale ?? original.Locale;
|
||||||
|
original.Lsd = changed.Lsd ?? original.Lsd;
|
||||||
|
original.MailNotificationAuthEnabled = changed.MailNotificationAuthEnabled ?? original.MailNotificationAuthEnabled;
|
||||||
|
original.MailNotificationEmail = changed.MailNotificationEmail ?? original.MailNotificationEmail;
|
||||||
|
original.MailNotificationEnabled = changed.MailNotificationEnabled ?? original.MailNotificationEnabled;
|
||||||
|
original.MailNotificationPassword = changed.MailNotificationPassword ?? original.MailNotificationPassword;
|
||||||
|
original.MailNotificationSender = changed.MailNotificationSender ?? original.MailNotificationSender;
|
||||||
|
original.MailNotificationSmtp = changed.MailNotificationSmtp ?? original.MailNotificationSmtp;
|
||||||
|
original.MailNotificationSslEnabled = changed.MailNotificationSslEnabled ?? original.MailNotificationSslEnabled;
|
||||||
|
original.MailNotificationUsername = changed.MailNotificationUsername ?? original.MailNotificationUsername;
|
||||||
|
original.MarkOfTheWeb = changed.MarkOfTheWeb ?? original.MarkOfTheWeb;
|
||||||
|
original.MaxActiveCheckingTorrents = changed.MaxActiveCheckingTorrents ?? original.MaxActiveCheckingTorrents;
|
||||||
|
original.MaxActiveDownloads = changed.MaxActiveDownloads ?? original.MaxActiveDownloads;
|
||||||
|
original.MaxActiveTorrents = changed.MaxActiveTorrents ?? original.MaxActiveTorrents;
|
||||||
|
original.MaxActiveUploads = changed.MaxActiveUploads ?? original.MaxActiveUploads;
|
||||||
|
original.MaxConcurrentHttpAnnounces = changed.MaxConcurrentHttpAnnounces ?? original.MaxConcurrentHttpAnnounces;
|
||||||
|
original.MaxConnec = changed.MaxConnec ?? original.MaxConnec;
|
||||||
|
original.MaxConnecPerTorrent = changed.MaxConnecPerTorrent ?? original.MaxConnecPerTorrent;
|
||||||
|
original.MaxInactiveSeedingTime = changed.MaxInactiveSeedingTime ?? original.MaxInactiveSeedingTime;
|
||||||
|
original.MaxInactiveSeedingTimeEnabled = changed.MaxInactiveSeedingTimeEnabled ?? original.MaxInactiveSeedingTimeEnabled;
|
||||||
|
original.MaxRatio = changed.MaxRatio ?? original.MaxRatio;
|
||||||
|
original.MaxRatioAct = changed.MaxRatioAct ?? original.MaxRatioAct;
|
||||||
|
original.MaxRatioEnabled = changed.MaxRatioEnabled ?? original.MaxRatioEnabled;
|
||||||
|
original.MaxSeedingTime = changed.MaxSeedingTime ?? original.MaxSeedingTime;
|
||||||
|
original.MaxSeedingTimeEnabled = changed.MaxSeedingTimeEnabled ?? original.MaxSeedingTimeEnabled;
|
||||||
|
original.MaxUploads = changed.MaxUploads ?? original.MaxUploads;
|
||||||
|
original.MaxUploadsPerTorrent = changed.MaxUploadsPerTorrent ?? original.MaxUploadsPerTorrent;
|
||||||
|
original.MemoryWorkingSetLimit = changed.MemoryWorkingSetLimit ?? original.MemoryWorkingSetLimit;
|
||||||
|
original.MergeTrackers = changed.MergeTrackers ?? original.MergeTrackers;
|
||||||
|
original.OutgoingPortsMax = changed.OutgoingPortsMax ?? original.OutgoingPortsMax;
|
||||||
|
original.OutgoingPortsMin = changed.OutgoingPortsMin ?? original.OutgoingPortsMin;
|
||||||
|
original.PeerTos = changed.PeerTos ?? original.PeerTos;
|
||||||
|
original.PeerTurnover = changed.PeerTurnover ?? original.PeerTurnover;
|
||||||
|
original.PeerTurnoverCutoff = changed.PeerTurnoverCutoff ?? original.PeerTurnoverCutoff;
|
||||||
|
original.PeerTurnoverInterval = changed.PeerTurnoverInterval ?? original.PeerTurnoverInterval;
|
||||||
|
original.PerformanceWarning = changed.PerformanceWarning ?? original.PerformanceWarning;
|
||||||
|
original.Pex = changed.Pex ?? original.Pex;
|
||||||
|
original.PreallocateAll = changed.PreallocateAll ?? original.PreallocateAll;
|
||||||
|
original.ProxyAuthEnabled = changed.ProxyAuthEnabled ?? original.ProxyAuthEnabled;
|
||||||
|
original.ProxyBittorrent = changed.ProxyBittorrent ?? original.ProxyBittorrent;
|
||||||
|
original.ProxyHostnameLookup = changed.ProxyHostnameLookup ?? original.ProxyHostnameLookup;
|
||||||
|
original.ProxyIp = changed.ProxyIp ?? original.ProxyIp;
|
||||||
|
original.ProxyMisc = changed.ProxyMisc ?? original.ProxyMisc;
|
||||||
|
original.ProxyPassword = changed.ProxyPassword ?? original.ProxyPassword;
|
||||||
|
original.ProxyPeerConnections = changed.ProxyPeerConnections ?? original.ProxyPeerConnections;
|
||||||
|
original.ProxyPort = changed.ProxyPort ?? original.ProxyPort;
|
||||||
|
original.ProxyRss = changed.ProxyRss ?? original.ProxyRss;
|
||||||
|
original.ProxyType = changed.ProxyType ?? original.ProxyType;
|
||||||
|
original.ProxyUsername = changed.ProxyUsername ?? original.ProxyUsername;
|
||||||
|
original.PythonExecutablePath = changed.PythonExecutablePath ?? original.PythonExecutablePath;
|
||||||
|
original.QueueingEnabled = changed.QueueingEnabled ?? original.QueueingEnabled;
|
||||||
|
original.RandomPort = changed.RandomPort ?? original.RandomPort;
|
||||||
|
original.ReannounceWhenAddressChanged = changed.ReannounceWhenAddressChanged ?? original.ReannounceWhenAddressChanged;
|
||||||
|
original.RecheckCompletedTorrents = changed.RecheckCompletedTorrents ?? original.RecheckCompletedTorrents;
|
||||||
|
original.RefreshInterval = changed.RefreshInterval ?? original.RefreshInterval;
|
||||||
|
original.RequestQueueSize = changed.RequestQueueSize ?? original.RequestQueueSize;
|
||||||
|
original.ResolvePeerCountries = changed.ResolvePeerCountries ?? original.ResolvePeerCountries;
|
||||||
|
original.ResumeDataStorageType = changed.ResumeDataStorageType ?? original.ResumeDataStorageType;
|
||||||
|
original.RssAutoDownloadingEnabled = changed.RssAutoDownloadingEnabled ?? original.RssAutoDownloadingEnabled;
|
||||||
|
original.RssDownloadRepackProperEpisodes = changed.RssDownloadRepackProperEpisodes ?? original.RssDownloadRepackProperEpisodes;
|
||||||
|
original.RssFetchDelay = changed.RssFetchDelay ?? original.RssFetchDelay;
|
||||||
|
original.RssMaxArticlesPerFeed = changed.RssMaxArticlesPerFeed ?? original.RssMaxArticlesPerFeed;
|
||||||
|
original.RssProcessingEnabled = changed.RssProcessingEnabled ?? original.RssProcessingEnabled;
|
||||||
|
original.RssRefreshInterval = changed.RssRefreshInterval ?? original.RssRefreshInterval;
|
||||||
|
original.RssSmartEpisodeFilters = changed.RssSmartEpisodeFilters ?? original.RssSmartEpisodeFilters;
|
||||||
|
original.SavePath = changed.SavePath ?? original.SavePath;
|
||||||
|
original.SavePathChangedTmmEnabled = changed.SavePathChangedTmmEnabled ?? original.SavePathChangedTmmEnabled;
|
||||||
|
original.SaveResumeDataInterval = changed.SaveResumeDataInterval ?? original.SaveResumeDataInterval;
|
||||||
|
original.SaveStatisticsInterval = changed.SaveStatisticsInterval ?? original.SaveStatisticsInterval;
|
||||||
|
original.ScanDirs = changed.ScanDirs ?? original.ScanDirs;
|
||||||
|
original.ScheduleFromHour = changed.ScheduleFromHour ?? original.ScheduleFromHour;
|
||||||
|
original.ScheduleFromMin = changed.ScheduleFromMin ?? original.ScheduleFromMin;
|
||||||
|
original.ScheduleToHour = changed.ScheduleToHour ?? original.ScheduleToHour;
|
||||||
|
original.ScheduleToMin = changed.ScheduleToMin ?? original.ScheduleToMin;
|
||||||
|
original.SchedulerDays = changed.SchedulerDays ?? original.SchedulerDays;
|
||||||
|
original.SchedulerEnabled = changed.SchedulerEnabled ?? original.SchedulerEnabled;
|
||||||
|
original.SendBufferLowWatermark = changed.SendBufferLowWatermark ?? original.SendBufferLowWatermark;
|
||||||
|
original.SendBufferWatermark = changed.SendBufferWatermark ?? original.SendBufferWatermark;
|
||||||
|
original.SendBufferWatermarkFactor = changed.SendBufferWatermarkFactor ?? original.SendBufferWatermarkFactor;
|
||||||
|
original.SlowTorrentDlRateThreshold = changed.SlowTorrentDlRateThreshold ?? original.SlowTorrentDlRateThreshold;
|
||||||
|
original.SlowTorrentInactiveTimer = changed.SlowTorrentInactiveTimer ?? original.SlowTorrentInactiveTimer;
|
||||||
|
original.SlowTorrentUlRateThreshold = changed.SlowTorrentUlRateThreshold ?? original.SlowTorrentUlRateThreshold;
|
||||||
|
original.SocketBacklogSize = changed.SocketBacklogSize ?? original.SocketBacklogSize;
|
||||||
|
original.SocketReceiveBufferSize = changed.SocketReceiveBufferSize ?? original.SocketReceiveBufferSize;
|
||||||
|
original.SocketSendBufferSize = changed.SocketSendBufferSize ?? original.SocketSendBufferSize;
|
||||||
|
original.SsrfMitigation = changed.SsrfMitigation ?? original.SsrfMitigation;
|
||||||
|
original.StopTrackerTimeout = changed.StopTrackerTimeout ?? original.StopTrackerTimeout;
|
||||||
|
original.TempPath = changed.TempPath ?? original.TempPath;
|
||||||
|
original.TempPathEnabled = changed.TempPathEnabled ?? original.TempPathEnabled;
|
||||||
|
original.TorrentChangedTmmEnabled = changed.TorrentChangedTmmEnabled ?? original.TorrentChangedTmmEnabled;
|
||||||
|
original.TorrentContentLayout = changed.TorrentContentLayout ?? original.TorrentContentLayout;
|
||||||
|
original.TorrentContentRemoveOption = changed.TorrentContentRemoveOption ?? original.TorrentContentRemoveOption;
|
||||||
|
original.TorrentFileSizeLimit = changed.TorrentFileSizeLimit ?? original.TorrentFileSizeLimit;
|
||||||
|
original.TorrentStopCondition = changed.TorrentStopCondition ?? original.TorrentStopCondition;
|
||||||
|
original.UpLimit = changed.UpLimit ?? original.UpLimit;
|
||||||
|
original.UploadChokingAlgorithm = changed.UploadChokingAlgorithm ?? original.UploadChokingAlgorithm;
|
||||||
|
original.UploadSlotsBehavior = changed.UploadSlotsBehavior ?? original.UploadSlotsBehavior;
|
||||||
|
original.Upnp = changed.Upnp ?? original.Upnp;
|
||||||
|
original.UpnpLeaseDuration = changed.UpnpLeaseDuration ?? original.UpnpLeaseDuration;
|
||||||
|
original.UseCategoryPathsInManualMode = changed.UseCategoryPathsInManualMode ?? original.UseCategoryPathsInManualMode;
|
||||||
|
original.UseHttps = changed.UseHttps ?? original.UseHttps;
|
||||||
|
original.IgnoreSslErrors = changed.IgnoreSslErrors ?? original.IgnoreSslErrors;
|
||||||
|
original.UseSubcategories = changed.UseSubcategories ?? original.UseSubcategories;
|
||||||
|
original.UtpTcpMixedMode = changed.UtpTcpMixedMode ?? original.UtpTcpMixedMode;
|
||||||
|
original.ValidateHttpsTrackerCertificate = changed.ValidateHttpsTrackerCertificate ?? original.ValidateHttpsTrackerCertificate;
|
||||||
|
original.WebUiAddress = changed.WebUiAddress ?? original.WebUiAddress;
|
||||||
|
original.WebUiApiKey = changed.WebUiApiKey ?? original.WebUiApiKey;
|
||||||
|
original.WebUiBanDuration = changed.WebUiBanDuration ?? original.WebUiBanDuration;
|
||||||
|
original.WebUiClickjackingProtectionEnabled = changed.WebUiClickjackingProtectionEnabled ?? original.WebUiClickjackingProtectionEnabled;
|
||||||
|
original.WebUiCsrfProtectionEnabled = changed.WebUiCsrfProtectionEnabled ?? original.WebUiCsrfProtectionEnabled;
|
||||||
|
original.WebUiCustomHttpHeaders = changed.WebUiCustomHttpHeaders ?? original.WebUiCustomHttpHeaders;
|
||||||
|
original.WebUiDomainList = changed.WebUiDomainList ?? original.WebUiDomainList;
|
||||||
|
original.WebUiHostHeaderValidationEnabled = changed.WebUiHostHeaderValidationEnabled ?? original.WebUiHostHeaderValidationEnabled;
|
||||||
|
original.WebUiHttpsCertPath = changed.WebUiHttpsCertPath ?? original.WebUiHttpsCertPath;
|
||||||
|
original.WebUiHttpsKeyPath = changed.WebUiHttpsKeyPath ?? original.WebUiHttpsKeyPath;
|
||||||
|
original.WebUiMaxAuthFailCount = changed.WebUiMaxAuthFailCount ?? original.WebUiMaxAuthFailCount;
|
||||||
|
original.WebUiPort = changed.WebUiPort ?? original.WebUiPort;
|
||||||
|
original.WebUiReverseProxiesList = changed.WebUiReverseProxiesList ?? original.WebUiReverseProxiesList;
|
||||||
|
original.WebUiReverseProxyEnabled = changed.WebUiReverseProxyEnabled ?? original.WebUiReverseProxyEnabled;
|
||||||
|
original.WebUiSecureCookieEnabled = changed.WebUiSecureCookieEnabled ?? original.WebUiSecureCookieEnabled;
|
||||||
|
original.WebUiSessionTimeout = changed.WebUiSessionTimeout ?? original.WebUiSessionTimeout;
|
||||||
|
original.WebUiUpnp = changed.WebUiUpnp ?? original.WebUiUpnp;
|
||||||
|
original.WebUiUseCustomHttpHeadersEnabled = changed.WebUiUseCustomHttpHeadersEnabled ?? original.WebUiUseCustomHttpHeadersEnabled;
|
||||||
|
original.WebUiUsername = changed.WebUiUsername ?? original.WebUiUsername;
|
||||||
|
original.WebUiPassword = changed.WebUiPassword ?? original.WebUiPassword;
|
||||||
|
original.ConfirmTorrentDeletion = changed.ConfirmTorrentDeletion ?? original.ConfirmTorrentDeletion;
|
||||||
|
original.ConfirmTorrentRecheck = changed.ConfirmTorrentRecheck ?? original.ConfirmTorrentRecheck;
|
||||||
|
original.StatusBarExternalIp = changed.StatusBarExternalIp ?? original.StatusBarExternalIp;
|
||||||
|
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Lantean.QBTMud/Services/RssDataManager.cs
Normal file
40
Lantean.QBTMud/Services/RssDataManager.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using Lantean.QBTMud.Models;
|
||||||
|
|
||||||
|
namespace Lantean.QBTMud.Services
|
||||||
|
{
|
||||||
|
public class RssDataManager : IRssDataManager
|
||||||
|
{
|
||||||
|
public RssList CreateRssList(IReadOnlyDictionary<string, QBitTorrentClient.Models.RssItem> rssItems)
|
||||||
|
{
|
||||||
|
var articles = new List<RssArticle>();
|
||||||
|
var feeds = new Dictionary<string, RssFeed>();
|
||||||
|
foreach (var (key, rssItem) in rssItems)
|
||||||
|
{
|
||||||
|
feeds.Add(key, new RssFeed(rssItem.HasError, rssItem.IsLoading, rssItem.LastBuildDate, rssItem.Title, rssItem.Uid, rssItem.Url));
|
||||||
|
if (rssItem.Articles is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach (var rssArticle in rssItem.Articles)
|
||||||
|
{
|
||||||
|
var article = new RssArticle(
|
||||||
|
key,
|
||||||
|
rssArticle.Category,
|
||||||
|
rssArticle.Comments,
|
||||||
|
rssArticle.Date!,
|
||||||
|
rssArticle.Description,
|
||||||
|
rssArticle.Id!,
|
||||||
|
rssArticle.Link,
|
||||||
|
rssArticle.Thumbnail,
|
||||||
|
rssArticle.Title!,
|
||||||
|
rssArticle.TorrentURL!,
|
||||||
|
rssArticle.IsRead);
|
||||||
|
|
||||||
|
articles.Add(article);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RssList(feeds, articles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +1,15 @@
|
|||||||
using Lantean.QBTMud.Helpers;
|
using Lantean.QBTMud.Helpers;
|
||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
|
using ShareLimitAction = Lantean.QBitTorrentClient.Models.ShareLimitAction;
|
||||||
|
|
||||||
namespace Lantean.QBTMud.Services
|
namespace Lantean.QBTMud.Services
|
||||||
{
|
{
|
||||||
public class DataManager : IDataManager
|
public class TorrentDataManager : ITorrentDataManager
|
||||||
{
|
{
|
||||||
private static Status[]? _statusArray = null;
|
private static Status[]? _statusArray = null;
|
||||||
|
|
||||||
public PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers)
|
public MainData CreateMainData(QBitTorrentClient.Models.MainData mainData)
|
||||||
{
|
{
|
||||||
var peers = new Dictionary<string, Peer>();
|
|
||||||
if (torrentPeers.Peers is not null)
|
|
||||||
{
|
|
||||||
foreach (var (key, peer) in torrentPeers.Peers)
|
|
||||||
{
|
|
||||||
var newPeer = CreatePeer(key, peer);
|
|
||||||
|
|
||||||
peers[key] = newPeer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var peerList = new PeerList(peers);
|
|
||||||
|
|
||||||
return peerList;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MainData CreateMainData(QBitTorrentClient.Models.MainData mainData, string version)
|
|
||||||
{
|
|
||||||
var majorVersion = VersionHelper.GetMajorVersion(version);
|
|
||||||
var torrents = new Dictionary<string, Torrent>(mainData.Torrents?.Count ?? 0);
|
var torrents = new Dictionary<string, Torrent>(mainData.Torrents?.Count ?? 0);
|
||||||
if (mainData.Torrents is not null)
|
if (mainData.Torrents is not null)
|
||||||
{
|
{
|
||||||
@@ -95,7 +77,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
categoriesState.Add(category, torrents.Values.Where(t => FilterHelper.FilterCategory(t, category, serverState.UseSubcategories)).ToHashesHashSet());
|
categoriesState.Add(category, torrents.Values.Where(t => FilterHelper.FilterCategory(t, category, serverState.UseSubcategories)).ToHashesHashSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
var statuses = GetStatuses(majorVersion).ToArray();
|
var statuses = GetStatuses().ToArray();
|
||||||
var statusState = new Dictionary<string, HashSet<string>>(statuses.Length + 2);
|
var statusState = new Dictionary<string, HashSet<string>>(statuses.Length + 2);
|
||||||
foreach (var status in statuses)
|
foreach (var status in statuses)
|
||||||
{
|
{
|
||||||
@@ -110,7 +92,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
trackersState.Add(tracker, torrents.Values.Where(t => FilterHelper.FilterTracker(t, tracker)).ToHashesHashSet());
|
trackersState.Add(tracker, torrents.Values.Where(t => FilterHelper.FilterTracker(t, tracker)).ToHashesHashSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
var torrentList = new MainData(torrents, tags, categories, trackers, serverState, tagState, categoriesState, statusState, trackersState, majorVersion);
|
var torrentList = new MainData(torrents, tags, categories, trackers, serverState, tagState, categoriesState, statusState, trackersState);
|
||||||
|
|
||||||
return torrentList;
|
return torrentList;
|
||||||
}
|
}
|
||||||
@@ -146,7 +128,9 @@ namespace Lantean.QBTMud.Services
|
|||||||
serverState.UploadRateLimit.GetValueOrDefault(),
|
serverState.UploadRateLimit.GetValueOrDefault(),
|
||||||
serverState.UseAltSpeedLimits.GetValueOrDefault(),
|
serverState.UseAltSpeedLimits.GetValueOrDefault(),
|
||||||
serverState.UseSubcategories.GetValueOrDefault(),
|
serverState.UseSubcategories.GetValueOrDefault(),
|
||||||
serverState.WriteCacheOverload.GetValueOrDefault());
|
serverState.WriteCacheOverload.GetValueOrDefault(),
|
||||||
|
serverState.LastExternalAddressV4 ?? string.Empty,
|
||||||
|
serverState.LastExternalAddressV6 ?? string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList, out bool filterChanged)
|
public bool MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList, out bool filterChanged)
|
||||||
@@ -184,7 +168,6 @@ namespace Lantean.QBTMud.Services
|
|||||||
{
|
{
|
||||||
filterChanged = true;
|
filterChanged = true;
|
||||||
}
|
}
|
||||||
torrentList.TagState.Remove(normalizedTag);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,9 +191,14 @@ namespace Lantean.QBTMud.Services
|
|||||||
{
|
{
|
||||||
foreach (var hash in mainData.TorrentsRemoved)
|
foreach (var hash in mainData.TorrentsRemoved)
|
||||||
{
|
{
|
||||||
if (torrentList.Torrents.Remove(hash))
|
if (torrentList.Torrents.TryGetValue(hash, out var existing))
|
||||||
{
|
{
|
||||||
RemoveTorrentFromStates(torrentList, hash);
|
var snapshot = CreateSnapshot(existing);
|
||||||
|
torrentList.Torrents.Remove(hash);
|
||||||
|
|
||||||
|
// remove from all filter sets using the captured snapshot
|
||||||
|
RemoveTorrentFromStates(torrentList, hash, snapshot);
|
||||||
|
|
||||||
dataChanged = true;
|
dataChanged = true;
|
||||||
filterChanged = true;
|
filterChanged = true;
|
||||||
}
|
}
|
||||||
@@ -284,7 +272,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
{
|
{
|
||||||
var newTorrent = CreateTorrent(hash, torrent);
|
var newTorrent = CreateTorrent(hash, torrent);
|
||||||
torrentList.Torrents.Add(hash, newTorrent);
|
torrentList.Torrents.Add(hash, newTorrent);
|
||||||
AddTorrentToStates(torrentList, hash, torrentList.MajorVersion);
|
AddTorrentToStates(torrentList, hash);
|
||||||
dataChanged = true;
|
dataChanged = true;
|
||||||
filterChanged = true;
|
filterChanged = true;
|
||||||
}
|
}
|
||||||
@@ -316,7 +304,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
return dataChanged;
|
return dataChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddTorrentToStates(MainData torrentList, string hash, int version)
|
private static void AddTorrentToStates(MainData torrentList, string hash)
|
||||||
{
|
{
|
||||||
if (!torrentList.Torrents.TryGetValue(hash, out var torrent))
|
if (!torrentList.Torrents.TryGetValue(hash, out var torrent))
|
||||||
{
|
{
|
||||||
@@ -329,7 +317,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
torrentList.CategoriesState[FilterHelper.CATEGORY_ALL].Add(hash);
|
torrentList.CategoriesState[FilterHelper.CATEGORY_ALL].Add(hash);
|
||||||
UpdateCategoryState(torrentList, torrent, hash, previousCategory: null);
|
UpdateCategoryState(torrentList, torrent, hash, previousCategory: null);
|
||||||
|
|
||||||
foreach (var status in GetStatuses(version))
|
foreach (var status in GetStatuses())
|
||||||
{
|
{
|
||||||
if (!torrentList.StatusState.TryGetValue(status.ToString(), out var statusSet))
|
if (!torrentList.StatusState.TryGetValue(status.ToString(), out var statusSet))
|
||||||
{
|
{
|
||||||
@@ -346,21 +334,14 @@ namespace Lantean.QBTMud.Services
|
|||||||
UpdateTrackerState(torrentList, torrent, hash, previousTracker: null);
|
UpdateTrackerState(torrentList, torrent, hash, previousTracker: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Status[] GetStatuses(int version)
|
private static Status[] GetStatuses()
|
||||||
{
|
{
|
||||||
if (_statusArray is not null)
|
if (_statusArray is not null)
|
||||||
{
|
{
|
||||||
return _statusArray;
|
return _statusArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (version == 5)
|
_statusArray = Enum.GetValues<Status>();
|
||||||
{
|
|
||||||
_statusArray = Enum.GetValues<Status>().Where(s => s != Status.Paused).ToArray();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_statusArray = Enum.GetValues<Status>().Where(s => s != Status.Stopped).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _statusArray;
|
return _statusArray;
|
||||||
}
|
}
|
||||||
@@ -373,22 +354,15 @@ namespace Lantean.QBTMud.Services
|
|||||||
UpdateTrackerState(torrentList, updatedTorrent, hash, previousSnapshot.Tracker);
|
UpdateTrackerState(torrentList, updatedTorrent, hash, previousSnapshot.Tracker);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RemoveTorrentFromStates(MainData torrentList, string hash)
|
private static void RemoveTorrentFromStates(MainData torrentList, string hash, TorrentSnapshot snapshot)
|
||||||
{
|
{
|
||||||
if (!torrentList.Torrents.TryGetValue(hash, out var torrent))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var snapshot = CreateSnapshot(torrent);
|
|
||||||
|
|
||||||
torrentList.TagState[FilterHelper.TAG_ALL].Remove(hash);
|
torrentList.TagState[FilterHelper.TAG_ALL].Remove(hash);
|
||||||
UpdateTagStateForRemoval(torrentList, hash, snapshot.Tags);
|
UpdateTagStateForRemoval(torrentList, hash, snapshot.Tags);
|
||||||
|
|
||||||
torrentList.CategoriesState[FilterHelper.CATEGORY_ALL].Remove(hash);
|
torrentList.CategoriesState[FilterHelper.CATEGORY_ALL].Remove(hash);
|
||||||
UpdateCategoryStateForRemoval(torrentList, hash, snapshot.Category);
|
UpdateCategoryStateForRemoval(torrentList, hash, snapshot.Category);
|
||||||
|
|
||||||
foreach (var status in GetStatuses(torrentList.MajorVersion))
|
foreach (var status in GetStatuses())
|
||||||
{
|
{
|
||||||
if (!torrentList.StatusState.TryGetValue(status.ToString(), out var statusState))
|
if (!torrentList.StatusState.TryGetValue(status.ToString(), out var statusState))
|
||||||
{
|
{
|
||||||
@@ -559,83 +533,26 @@ namespace Lantean.QBTMud.Services
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (serverState.LastExternalAddressV4 is not null && existingServerState.LastExternalAddressV4 != serverState.LastExternalAddressV4)
|
||||||
|
{
|
||||||
|
existingServerState.LastExternalAddressV4 = serverState.LastExternalAddressV4;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverState.LastExternalAddressV6 is not null && existingServerState.LastExternalAddressV6 != serverState.LastExternalAddressV6)
|
||||||
|
{
|
||||||
|
existingServerState.LastExternalAddressV6 = serverState.LastExternalAddressV6;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MergeTorrentPeers(QBitTorrentClient.Models.TorrentPeers torrentPeers, PeerList peerList)
|
|
||||||
{
|
|
||||||
if (torrentPeers.PeersRemoved is not null)
|
|
||||||
{
|
|
||||||
foreach (var key in torrentPeers.PeersRemoved)
|
|
||||||
{
|
|
||||||
peerList.Peers.Remove(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (torrentPeers.Peers is not null)
|
|
||||||
{
|
|
||||||
foreach (var (key, peer) in torrentPeers.Peers)
|
|
||||||
{
|
|
||||||
if (!peerList.Peers.TryGetValue(key, out var existingPeer))
|
|
||||||
{
|
|
||||||
var newPeer = CreatePeer(key, peer);
|
|
||||||
peerList.Peers.Add(key, newPeer);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UpdatePeer(existingPeer, peer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void UpdatePeer(Peer existingPeer, QBitTorrentClient.Models.Peer peer)
|
|
||||||
{
|
|
||||||
existingPeer.Client = peer.Client ?? existingPeer.Client;
|
|
||||||
existingPeer.ClientId = peer.ClientId ?? existingPeer.ClientId;
|
|
||||||
existingPeer.Connection = peer.Connection ?? existingPeer.Connection;
|
|
||||||
existingPeer.Country = peer.Country ?? existingPeer.Country;
|
|
||||||
existingPeer.CountryCode = peer.CountryCode ?? existingPeer.CountryCode;
|
|
||||||
existingPeer.Downloaded = peer.Downloaded ?? existingPeer.Downloaded;
|
|
||||||
existingPeer.DownloadSpeed = peer.DownloadSpeed ?? existingPeer.DownloadSpeed;
|
|
||||||
existingPeer.Files = peer.Files ?? existingPeer.Files;
|
|
||||||
existingPeer.Flags = peer.Flags ?? existingPeer.Flags;
|
|
||||||
existingPeer.FlagsDescription = peer.FlagsDescription ?? existingPeer.FlagsDescription;
|
|
||||||
existingPeer.IPAddress = peer.IPAddress ?? existingPeer.IPAddress;
|
|
||||||
existingPeer.Port = peer.Port ?? existingPeer.Port;
|
|
||||||
existingPeer.Progress = peer.Progress ?? existingPeer.Progress;
|
|
||||||
existingPeer.Relevance = peer.Relevance ?? existingPeer.Relevance;
|
|
||||||
existingPeer.Uploaded = peer.Uploaded ?? existingPeer.Uploaded;
|
|
||||||
existingPeer.UploadSpeed = peer.UploadSpeed ?? existingPeer.UploadSpeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Category CreateCategory(QBitTorrentClient.Models.Category category)
|
private static Category CreateCategory(QBitTorrentClient.Models.Category category)
|
||||||
{
|
{
|
||||||
return new Category(category.Name, category.SavePath!);
|
return new Category(category.Name, category.SavePath!);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Peer CreatePeer(string key, QBitTorrentClient.Models.Peer peer)
|
|
||||||
{
|
|
||||||
return new Peer(
|
|
||||||
key,
|
|
||||||
peer.Client!,
|
|
||||||
peer.ClientId!,
|
|
||||||
peer.Connection!,
|
|
||||||
peer.Country,
|
|
||||||
peer.CountryCode,
|
|
||||||
peer.Downloaded.GetValueOrDefault(),
|
|
||||||
peer.DownloadSpeed.GetValueOrDefault(),
|
|
||||||
peer.Files!,
|
|
||||||
peer.Flags!,
|
|
||||||
peer.FlagsDescription!,
|
|
||||||
peer.IPAddress!,
|
|
||||||
peer.Port.GetValueOrDefault(),
|
|
||||||
peer.Progress.GetValueOrDefault(),
|
|
||||||
peer.Relevance.GetValueOrDefault(),
|
|
||||||
peer.Uploaded.GetValueOrDefault(),
|
|
||||||
peer.UploadSpeed.GetValueOrDefault());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent)
|
public Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent)
|
||||||
{
|
{
|
||||||
var normalizedTags = torrent.Tags?
|
var normalizedTags = torrent.Tags?
|
||||||
@@ -694,10 +611,16 @@ namespace Lantean.QBTMud.Services
|
|||||||
torrent.UploadSpeed.GetValueOrDefault(),
|
torrent.UploadSpeed.GetValueOrDefault(),
|
||||||
torrent.Reannounce ?? 0,
|
torrent.Reannounce ?? 0,
|
||||||
torrent.InactiveSeedingTimeLimit.GetValueOrDefault(),
|
torrent.InactiveSeedingTimeLimit.GetValueOrDefault(),
|
||||||
torrent.MaxInactiveSeedingTime.GetValueOrDefault());
|
torrent.MaxInactiveSeedingTime.GetValueOrDefault(),
|
||||||
|
torrent.Popularity.GetValueOrDefault(),
|
||||||
|
torrent.DownloadPath ?? string.Empty,
|
||||||
|
torrent.RootPath ?? string.Empty,
|
||||||
|
torrent.IsPrivate.GetValueOrDefault(),
|
||||||
|
torrent.ShareLimitAction ?? ShareLimitAction.Default,
|
||||||
|
torrent.Comment ?? string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NormalizeTag(string? tag)
|
internal static string NormalizeTag(string? tag)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(tag))
|
if (string.IsNullOrEmpty(tag))
|
||||||
{
|
{
|
||||||
@@ -710,7 +633,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
return normalized.Trim();
|
return normalized.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TorrentSnapshot CreateSnapshot(Torrent torrent)
|
internal static TorrentSnapshot CreateSnapshot(Torrent torrent)
|
||||||
{
|
{
|
||||||
return new TorrentSnapshot(
|
return new TorrentSnapshot(
|
||||||
string.IsNullOrEmpty(torrent.Category) ? null : torrent.Category,
|
string.IsNullOrEmpty(torrent.Category) ? null : torrent.Category,
|
||||||
@@ -720,7 +643,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
torrent.UploadSpeed);
|
torrent.UploadSpeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly struct TorrentSnapshot
|
internal readonly struct TorrentSnapshot
|
||||||
{
|
{
|
||||||
public TorrentSnapshot(string? category, List<string> tags, string tracker, string state, long uploadSpeed)
|
public TorrentSnapshot(string? category, List<string> tags, string tracker, string state, long uploadSpeed)
|
||||||
{
|
{
|
||||||
@@ -742,7 +665,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
public long UploadSpeed { get; }
|
public long UploadSpeed { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UpdateTagStateForAddition(MainData torrentList, Torrent torrent, string hash)
|
internal static void UpdateTagStateForAddition(MainData torrentList, Torrent torrent, string hash)
|
||||||
{
|
{
|
||||||
if (torrent.Tags.Count == 0)
|
if (torrent.Tags.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -762,7 +685,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UpdateTagStateForUpdate(MainData torrentList, string hash, IReadOnlyList<string> previousTags, IList<string> newTags)
|
internal static void UpdateTagStateForUpdate(MainData torrentList, string hash, IReadOnlyList<string> previousTags, IList<string> newTags)
|
||||||
{
|
{
|
||||||
UpdateTagStateForRemoval(torrentList, hash, previousTags);
|
UpdateTagStateForRemoval(torrentList, hash, previousTags);
|
||||||
|
|
||||||
@@ -784,7 +707,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UpdateTagStateForRemoval(MainData torrentList, string hash, IReadOnlyList<string> previousTags)
|
internal static void UpdateTagStateForRemoval(MainData torrentList, string hash, IReadOnlyList<string> previousTags)
|
||||||
{
|
{
|
||||||
torrentList.TagState[FilterHelper.TAG_UNTAGGED].Remove(hash);
|
torrentList.TagState[FilterHelper.TAG_UNTAGGED].Remove(hash);
|
||||||
|
|
||||||
@@ -802,7 +725,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UpdateCategoryState(MainData torrentList, Torrent updatedTorrent, string hash, string? previousCategory)
|
internal static void UpdateCategoryState(MainData torrentList, Torrent updatedTorrent, string hash, string? previousCategory)
|
||||||
{
|
{
|
||||||
var useSubcategories = torrentList.ServerState.UseSubcategories;
|
var useSubcategories = torrentList.ServerState.UseSubcategories;
|
||||||
|
|
||||||
@@ -833,7 +756,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UpdateCategoryStateForRemoval(MainData torrentList, string hash, string? previousCategory)
|
internal static void UpdateCategoryStateForRemoval(MainData torrentList, string hash, string? previousCategory)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(previousCategory))
|
if (string.IsNullOrEmpty(previousCategory))
|
||||||
{
|
{
|
||||||
@@ -850,9 +773,9 @@ namespace Lantean.QBTMud.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UpdateStatusState(MainData torrentList, string hash, string previousState, long previousUploadSpeed, string newState, long newUploadSpeed)
|
internal static void UpdateStatusState(MainData torrentList, string hash, string previousState, long previousUploadSpeed, string newState, long newUploadSpeed)
|
||||||
{
|
{
|
||||||
foreach (var status in GetStatuses(torrentList.MajorVersion))
|
foreach (var status in GetStatuses())
|
||||||
{
|
{
|
||||||
if (!torrentList.StatusState.TryGetValue(status.ToString(), out var statusSet))
|
if (!torrentList.StatusState.TryGetValue(status.ToString(), out var statusSet))
|
||||||
{
|
{
|
||||||
@@ -879,7 +802,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UpdateTrackerState(MainData torrentList, Torrent updatedTorrent, string hash, string? previousTracker)
|
internal static void UpdateTrackerState(MainData torrentList, Torrent updatedTorrent, string hash, string? previousTracker)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(previousTracker))
|
if (!string.IsNullOrEmpty(previousTracker))
|
||||||
{
|
{
|
||||||
@@ -904,7 +827,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
GetOrCreateTrackerSet(torrentList, tracker).Add(hash);
|
GetOrCreateTrackerSet(torrentList, tracker).Add(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UpdateTrackerStateForRemoval(MainData torrentList, string hash, string? previousTracker)
|
internal static void UpdateTrackerStateForRemoval(MainData torrentList, string hash, string? previousTracker)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(previousTracker))
|
if (string.IsNullOrEmpty(previousTracker))
|
||||||
{
|
{
|
||||||
@@ -918,7 +841,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<string> EnumerateCategoryKeys(string category, bool useSubcategories)
|
internal static IEnumerable<string> EnumerateCategoryKeys(string category, bool useSubcategories)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(category))
|
if (string.IsNullOrEmpty(category))
|
||||||
{
|
{
|
||||||
@@ -946,7 +869,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HashSet<string> GetOrCreateTagSet(MainData torrentList, string tag)
|
internal static HashSet<string> GetOrCreateTagSet(MainData torrentList, string tag)
|
||||||
{
|
{
|
||||||
if (!torrentList.TagState.TryGetValue(tag, out var set))
|
if (!torrentList.TagState.TryGetValue(tag, out var set))
|
||||||
{
|
{
|
||||||
@@ -957,7 +880,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
return set;
|
return set;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HashSet<string> GetOrCreateCategorySet(MainData torrentList, string category)
|
internal static HashSet<string> GetOrCreateCategorySet(MainData torrentList, string category)
|
||||||
{
|
{
|
||||||
if (!torrentList.CategoriesState.TryGetValue(category, out var set))
|
if (!torrentList.CategoriesState.TryGetValue(category, out var set))
|
||||||
{
|
{
|
||||||
@@ -968,7 +891,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
return set;
|
return set;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HashSet<string> GetOrCreateTrackerSet(MainData torrentList, string tracker)
|
internal static HashSet<string> GetOrCreateTrackerSet(MainData torrentList, string tracker)
|
||||||
{
|
{
|
||||||
if (!torrentList.TrackersState.TryGetValue(tracker, out var set))
|
if (!torrentList.TrackersState.TryGetValue(tracker, out var set))
|
||||||
{
|
{
|
||||||
@@ -979,7 +902,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
return set;
|
return set;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool UpdateCategory(Category existingCategory, QBitTorrentClient.Models.Category category)
|
internal static bool UpdateCategory(Category existingCategory, QBitTorrentClient.Models.Category category)
|
||||||
{
|
{
|
||||||
if (category.SavePath is not null && existingCategory.SavePath != category.SavePath)
|
if (category.SavePath is not null && existingCategory.SavePath != category.SavePath)
|
||||||
{
|
{
|
||||||
@@ -990,7 +913,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly struct TorrentUpdateResult
|
internal readonly struct TorrentUpdateResult
|
||||||
{
|
{
|
||||||
public TorrentUpdateResult(bool dataChanged, bool filterChanged)
|
public TorrentUpdateResult(bool dataChanged, bool filterChanged)
|
||||||
{
|
{
|
||||||
@@ -1003,7 +926,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
public bool FilterChanged { get; }
|
public bool FilterChanged { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TorrentUpdateResult UpdateTorrent(Torrent existingTorrent, QBitTorrentClient.Models.Torrent torrent)
|
internal static TorrentUpdateResult UpdateTorrent(Torrent existingTorrent, QBitTorrentClient.Models.Torrent torrent)
|
||||||
{
|
{
|
||||||
var dataChanged = false;
|
var dataChanged = false;
|
||||||
var filterChanged = false;
|
var filterChanged = false;
|
||||||
@@ -1320,6 +1243,41 @@ namespace Lantean.QBTMud.Services
|
|||||||
dataChanged = true;
|
dataChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (torrent.Popularity.HasValue && existingTorrent.Popularity != torrent.Popularity.Value)
|
||||||
|
{
|
||||||
|
existingTorrent.Popularity = torrent.Popularity.Value;
|
||||||
|
dataChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrent.DownloadPath is not null && !string.Equals(existingTorrent.DownloadPath, torrent.DownloadPath, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
existingTorrent.DownloadPath = torrent.DownloadPath;
|
||||||
|
dataChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrent.RootPath is not null && !string.Equals(existingTorrent.RootPath, torrent.RootPath, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
existingTorrent.RootPath = torrent.RootPath;
|
||||||
|
dataChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrent.IsPrivate.HasValue && existingTorrent.IsPrivate != torrent.IsPrivate.Value)
|
||||||
|
{
|
||||||
|
existingTorrent.IsPrivate = torrent.IsPrivate.Value;
|
||||||
|
dataChanged = true;
|
||||||
|
}
|
||||||
|
if (torrent.ShareLimitAction.HasValue && existingTorrent.ShareLimitAction != torrent.ShareLimitAction.Value)
|
||||||
|
{
|
||||||
|
existingTorrent.ShareLimitAction = torrent.ShareLimitAction.Value;
|
||||||
|
dataChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrent.Comment is not null && !string.Equals(existingTorrent.Comment, torrent.Comment, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
existingTorrent.Comment = torrent.Comment;
|
||||||
|
dataChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
return new TorrentUpdateResult(dataChanged, filterChanged);
|
return new TorrentUpdateResult(dataChanged, filterChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1445,7 +1403,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool UpdateContentItem(ContentItem destination, ContentItem source)
|
internal static bool UpdateContentItem(ContentItem destination, ContentItem source)
|
||||||
{
|
{
|
||||||
const float floatTolerance = 0.0001f;
|
const float floatTolerance = 0.0001f;
|
||||||
var changed = false;
|
var changed = false;
|
||||||
@@ -1456,7 +1414,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (System.Math.Abs(destination.Progress - source.Progress) > floatTolerance)
|
if (Math.Abs(destination.Progress - source.Progress) > floatTolerance)
|
||||||
{
|
{
|
||||||
destination.Progress = source.Progress;
|
destination.Progress = source.Progress;
|
||||||
changed = true;
|
changed = true;
|
||||||
@@ -1468,7 +1426,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (System.Math.Abs(destination.Availability - source.Availability) > floatTolerance)
|
if (Math.Abs(destination.Availability - source.Availability) > floatTolerance)
|
||||||
{
|
{
|
||||||
destination.Availability = source.Availability;
|
destination.Availability = source.Availability;
|
||||||
changed = true;
|
changed = true;
|
||||||
@@ -1566,425 +1524,6 @@ namespace Lantean.QBTMud.Services
|
|||||||
public Dictionary<string, ContentTreeNode> Children { get; }
|
public Dictionary<string, ContentTreeNode> Children { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public QBitTorrentClient.Models.UpdatePreferences MergePreferences(QBitTorrentClient.Models.UpdatePreferences? original, QBitTorrentClient.Models.UpdatePreferences changed)
|
|
||||||
{
|
|
||||||
if (original is null)
|
|
||||||
{
|
|
||||||
original = new QBitTorrentClient.Models.UpdatePreferences
|
|
||||||
{
|
|
||||||
AddToTopOfQueue = changed.AddToTopOfQueue,
|
|
||||||
AddTrackers = changed.AddTrackers,
|
|
||||||
AddTrackersEnabled = changed.AddTrackersEnabled,
|
|
||||||
AltDlLimit = changed.AltDlLimit,
|
|
||||||
AltUpLimit = changed.AltUpLimit,
|
|
||||||
AlternativeWebuiEnabled = changed.AlternativeWebuiEnabled,
|
|
||||||
AlternativeWebuiPath = changed.AlternativeWebuiPath,
|
|
||||||
AnnounceIp = changed.AnnounceIp,
|
|
||||||
AnnounceToAllTiers = changed.AnnounceToAllTiers,
|
|
||||||
AnnounceToAllTrackers = changed.AnnounceToAllTrackers,
|
|
||||||
AnonymousMode = changed.AnonymousMode,
|
|
||||||
AsyncIoThreads = changed.AsyncIoThreads,
|
|
||||||
AutoDeleteMode = changed.AutoDeleteMode,
|
|
||||||
AutoTmmEnabled = changed.AutoTmmEnabled,
|
|
||||||
AutorunEnabled = changed.AutorunEnabled,
|
|
||||||
AutorunOnTorrentAddedEnabled = changed.AutorunOnTorrentAddedEnabled,
|
|
||||||
AutorunOnTorrentAddedProgram = changed.AutorunOnTorrentAddedProgram,
|
|
||||||
AutorunProgram = changed.AutorunProgram,
|
|
||||||
BannedIPs = changed.BannedIPs,
|
|
||||||
BdecodeDepthLimit = changed.BdecodeDepthLimit,
|
|
||||||
BdecodeTokenLimit = changed.BdecodeTokenLimit,
|
|
||||||
BittorrentProtocol = changed.BittorrentProtocol,
|
|
||||||
BlockPeersOnPrivilegedPorts = changed.BlockPeersOnPrivilegedPorts,
|
|
||||||
BypassAuthSubnetWhitelist = changed.BypassAuthSubnetWhitelist,
|
|
||||||
BypassAuthSubnetWhitelistEnabled = changed.BypassAuthSubnetWhitelistEnabled,
|
|
||||||
BypassLocalAuth = changed.BypassLocalAuth,
|
|
||||||
CategoryChangedTmmEnabled = changed.CategoryChangedTmmEnabled,
|
|
||||||
CheckingMemoryUse = changed.CheckingMemoryUse,
|
|
||||||
ConnectionSpeed = changed.ConnectionSpeed,
|
|
||||||
CurrentInterfaceAddress = changed.CurrentInterfaceAddress,
|
|
||||||
CurrentInterfaceName = changed.CurrentInterfaceName,
|
|
||||||
CurrentNetworkInterface = changed.CurrentNetworkInterface,
|
|
||||||
Dht = changed.Dht,
|
|
||||||
DiskCache = changed.DiskCache,
|
|
||||||
DiskCacheTtl = changed.DiskCacheTtl,
|
|
||||||
DiskIoReadMode = changed.DiskIoReadMode,
|
|
||||||
DiskIoType = changed.DiskIoType,
|
|
||||||
DiskIoWriteMode = changed.DiskIoWriteMode,
|
|
||||||
DiskQueueSize = changed.DiskQueueSize,
|
|
||||||
DlLimit = changed.DlLimit,
|
|
||||||
DontCountSlowTorrents = changed.DontCountSlowTorrents,
|
|
||||||
DyndnsDomain = changed.DyndnsDomain,
|
|
||||||
DyndnsEnabled = changed.DyndnsEnabled,
|
|
||||||
DyndnsPassword = changed.DyndnsPassword,
|
|
||||||
DyndnsService = changed.DyndnsService,
|
|
||||||
DyndnsUsername = changed.DyndnsUsername,
|
|
||||||
EmbeddedTrackerPort = changed.EmbeddedTrackerPort,
|
|
||||||
EmbeddedTrackerPortForwarding = changed.EmbeddedTrackerPortForwarding,
|
|
||||||
EnableCoalesceReadWrite = changed.EnableCoalesceReadWrite,
|
|
||||||
EnableEmbeddedTracker = changed.EnableEmbeddedTracker,
|
|
||||||
EnableMultiConnectionsFromSameIp = changed.EnableMultiConnectionsFromSameIp,
|
|
||||||
EnablePieceExtentAffinity = changed.EnablePieceExtentAffinity,
|
|
||||||
EnableUploadSuggestions = changed.EnableUploadSuggestions,
|
|
||||||
Encryption = changed.Encryption,
|
|
||||||
ExcludedFileNames = changed.ExcludedFileNames,
|
|
||||||
ExcludedFileNamesEnabled = changed.ExcludedFileNamesEnabled,
|
|
||||||
ExportDir = changed.ExportDir,
|
|
||||||
ExportDirFin = changed.ExportDirFin,
|
|
||||||
FileLogAge = changed.FileLogAge,
|
|
||||||
FileLogAgeType = changed.FileLogAgeType,
|
|
||||||
FileLogBackupEnabled = changed.FileLogBackupEnabled,
|
|
||||||
FileLogDeleteOld = changed.FileLogDeleteOld,
|
|
||||||
FileLogEnabled = changed.FileLogEnabled,
|
|
||||||
FileLogMaxSize = changed.FileLogMaxSize,
|
|
||||||
FileLogPath = changed.FileLogPath,
|
|
||||||
FilePoolSize = changed.FilePoolSize,
|
|
||||||
HashingThreads = changed.HashingThreads,
|
|
||||||
I2pAddress = changed.I2pAddress,
|
|
||||||
I2pEnabled = changed.I2pEnabled,
|
|
||||||
I2pInboundLength = changed.I2pInboundLength,
|
|
||||||
I2pInboundQuantity = changed.I2pInboundQuantity,
|
|
||||||
I2pMixedMode = changed.I2pMixedMode,
|
|
||||||
I2pOutboundLength = changed.I2pOutboundLength,
|
|
||||||
I2pOutboundQuantity = changed.I2pOutboundQuantity,
|
|
||||||
I2pPort = changed.I2pPort,
|
|
||||||
IdnSupportEnabled = changed.IdnSupportEnabled,
|
|
||||||
IncompleteFilesExt = changed.IncompleteFilesExt,
|
|
||||||
IpFilterEnabled = changed.IpFilterEnabled,
|
|
||||||
IpFilterPath = changed.IpFilterPath,
|
|
||||||
IpFilterTrackers = changed.IpFilterTrackers,
|
|
||||||
LimitLanPeers = changed.LimitLanPeers,
|
|
||||||
LimitTcpOverhead = changed.LimitTcpOverhead,
|
|
||||||
LimitUtpRate = changed.LimitUtpRate,
|
|
||||||
ListenPort = changed.ListenPort,
|
|
||||||
Locale = changed.Locale,
|
|
||||||
Lsd = changed.Lsd,
|
|
||||||
MailNotificationAuthEnabled = changed.MailNotificationAuthEnabled,
|
|
||||||
MailNotificationEmail = changed.MailNotificationEmail,
|
|
||||||
MailNotificationEnabled = changed.MailNotificationEnabled,
|
|
||||||
MailNotificationPassword = changed.MailNotificationPassword,
|
|
||||||
MailNotificationSender = changed.MailNotificationSender,
|
|
||||||
MailNotificationSmtp = changed.MailNotificationSmtp,
|
|
||||||
MailNotificationSslEnabled = changed.MailNotificationSslEnabled,
|
|
||||||
MailNotificationUsername = changed.MailNotificationUsername,
|
|
||||||
MaxActiveCheckingTorrents = changed.MaxActiveCheckingTorrents,
|
|
||||||
MaxActiveDownloads = changed.MaxActiveDownloads,
|
|
||||||
MaxActiveTorrents = changed.MaxActiveTorrents,
|
|
||||||
MaxActiveUploads = changed.MaxActiveUploads,
|
|
||||||
MaxConcurrentHttpAnnounces = changed.MaxConcurrentHttpAnnounces,
|
|
||||||
MaxConnec = changed.MaxConnec,
|
|
||||||
MaxConnecPerTorrent = changed.MaxConnecPerTorrent,
|
|
||||||
MaxInactiveSeedingTime = changed.MaxInactiveSeedingTime,
|
|
||||||
MaxInactiveSeedingTimeEnabled = changed.MaxInactiveSeedingTimeEnabled,
|
|
||||||
MaxRatio = changed.MaxRatio,
|
|
||||||
MaxRatioAct = changed.MaxRatioAct,
|
|
||||||
MaxRatioEnabled = changed.MaxRatioEnabled,
|
|
||||||
MaxSeedingTime = changed.MaxSeedingTime,
|
|
||||||
MaxSeedingTimeEnabled = changed.MaxSeedingTimeEnabled,
|
|
||||||
MaxUploads = changed.MaxUploads,
|
|
||||||
MaxUploadsPerTorrent = changed.MaxUploadsPerTorrent,
|
|
||||||
MemoryWorkingSetLimit = changed.MemoryWorkingSetLimit,
|
|
||||||
MergeTrackers = changed.MergeTrackers,
|
|
||||||
OutgoingPortsMax = changed.OutgoingPortsMax,
|
|
||||||
OutgoingPortsMin = changed.OutgoingPortsMin,
|
|
||||||
PeerTos = changed.PeerTos,
|
|
||||||
PeerTurnover = changed.PeerTurnover,
|
|
||||||
PeerTurnoverCutoff = changed.PeerTurnoverCutoff,
|
|
||||||
PeerTurnoverInterval = changed.PeerTurnoverInterval,
|
|
||||||
PerformanceWarning = changed.PerformanceWarning,
|
|
||||||
Pex = changed.Pex,
|
|
||||||
PreallocateAll = changed.PreallocateAll,
|
|
||||||
ProxyAuthEnabled = changed.ProxyAuthEnabled,
|
|
||||||
ProxyBittorrent = changed.ProxyBittorrent,
|
|
||||||
ProxyHostnameLookup = changed.ProxyHostnameLookup,
|
|
||||||
ProxyIp = changed.ProxyIp,
|
|
||||||
ProxyMisc = changed.ProxyMisc,
|
|
||||||
ProxyPassword = changed.ProxyPassword,
|
|
||||||
ProxyPeerConnections = changed.ProxyPeerConnections,
|
|
||||||
ProxyPort = changed.ProxyPort,
|
|
||||||
ProxyRss = changed.ProxyRss,
|
|
||||||
ProxyType = changed.ProxyType,
|
|
||||||
ProxyUsername = changed.ProxyUsername,
|
|
||||||
QueueingEnabled = changed.QueueingEnabled,
|
|
||||||
RandomPort = changed.RandomPort,
|
|
||||||
ReannounceWhenAddressChanged = changed.ReannounceWhenAddressChanged,
|
|
||||||
RecheckCompletedTorrents = changed.RecheckCompletedTorrents,
|
|
||||||
RefreshInterval = changed.RefreshInterval,
|
|
||||||
RequestQueueSize = changed.RequestQueueSize,
|
|
||||||
ResolvePeerCountries = changed.ResolvePeerCountries,
|
|
||||||
ResumeDataStorageType = changed.ResumeDataStorageType,
|
|
||||||
RssAutoDownloadingEnabled = changed.RssAutoDownloadingEnabled,
|
|
||||||
RssDownloadRepackProperEpisodes = changed.RssDownloadRepackProperEpisodes,
|
|
||||||
RssMaxArticlesPerFeed = changed.RssMaxArticlesPerFeed,
|
|
||||||
RssProcessingEnabled = changed.RssProcessingEnabled,
|
|
||||||
RssRefreshInterval = changed.RssRefreshInterval,
|
|
||||||
RssSmartEpisodeFilters = changed.RssSmartEpisodeFilters,
|
|
||||||
SavePath = changed.SavePath,
|
|
||||||
SavePathChangedTmmEnabled = changed.SavePathChangedTmmEnabled,
|
|
||||||
SaveResumeDataInterval = changed.SaveResumeDataInterval,
|
|
||||||
ScanDirs = changed.ScanDirs,
|
|
||||||
ScheduleFromHour = changed.ScheduleFromHour,
|
|
||||||
ScheduleFromMin = changed.ScheduleFromMin,
|
|
||||||
ScheduleToHour = changed.ScheduleToHour,
|
|
||||||
ScheduleToMin = changed.ScheduleToMin,
|
|
||||||
SchedulerDays = changed.SchedulerDays,
|
|
||||||
SchedulerEnabled = changed.SchedulerEnabled,
|
|
||||||
SendBufferLowWatermark = changed.SendBufferLowWatermark,
|
|
||||||
SendBufferWatermark = changed.SendBufferWatermark,
|
|
||||||
SendBufferWatermarkFactor = changed.SendBufferWatermarkFactor,
|
|
||||||
SlowTorrentDlRateThreshold = changed.SlowTorrentDlRateThreshold,
|
|
||||||
SlowTorrentInactiveTimer = changed.SlowTorrentInactiveTimer,
|
|
||||||
SlowTorrentUlRateThreshold = changed.SlowTorrentUlRateThreshold,
|
|
||||||
SocketBacklogSize = changed.SocketBacklogSize,
|
|
||||||
SocketReceiveBufferSize = changed.SocketReceiveBufferSize,
|
|
||||||
SocketSendBufferSize = changed.SocketSendBufferSize,
|
|
||||||
SsrfMitigation = changed.SsrfMitigation,
|
|
||||||
StartPausedEnabled = changed.StartPausedEnabled,
|
|
||||||
StopTrackerTimeout = changed.StopTrackerTimeout,
|
|
||||||
TempPath = changed.TempPath,
|
|
||||||
TempPathEnabled = changed.TempPathEnabled,
|
|
||||||
TorrentChangedTmmEnabled = changed.TorrentChangedTmmEnabled,
|
|
||||||
TorrentContentLayout = changed.TorrentContentLayout,
|
|
||||||
TorrentFileSizeLimit = changed.TorrentFileSizeLimit,
|
|
||||||
TorrentStopCondition = changed.TorrentStopCondition,
|
|
||||||
UpLimit = changed.UpLimit,
|
|
||||||
UploadChokingAlgorithm = changed.UploadChokingAlgorithm,
|
|
||||||
UploadSlotsBehavior = changed.UploadSlotsBehavior,
|
|
||||||
Upnp = changed.Upnp,
|
|
||||||
UpnpLeaseDuration = changed.UpnpLeaseDuration,
|
|
||||||
UseCategoryPathsInManualMode = changed.UseCategoryPathsInManualMode,
|
|
||||||
UseHttps = changed.UseHttps,
|
|
||||||
UseSubcategories = changed.UseSubcategories,
|
|
||||||
UtpTcpMixedMode = changed.UtpTcpMixedMode,
|
|
||||||
ValidateHttpsTrackerCertificate = changed.ValidateHttpsTrackerCertificate,
|
|
||||||
WebUiAddress = changed.WebUiAddress,
|
|
||||||
WebUiBanDuration = changed.WebUiBanDuration,
|
|
||||||
WebUiClickjackingProtectionEnabled = changed.WebUiClickjackingProtectionEnabled,
|
|
||||||
WebUiCsrfProtectionEnabled = changed.WebUiCsrfProtectionEnabled,
|
|
||||||
WebUiCustomHttpHeaders = changed.WebUiCustomHttpHeaders,
|
|
||||||
WebUiDomainList = changed.WebUiDomainList,
|
|
||||||
WebUiHostHeaderValidationEnabled = changed.WebUiHostHeaderValidationEnabled,
|
|
||||||
WebUiHttpsCertPath = changed.WebUiHttpsCertPath,
|
|
||||||
WebUiHttpsKeyPath = changed.WebUiHttpsKeyPath,
|
|
||||||
WebUiMaxAuthFailCount = changed.WebUiMaxAuthFailCount,
|
|
||||||
WebUiPort = changed.WebUiPort,
|
|
||||||
WebUiReverseProxiesList = changed.WebUiReverseProxiesList,
|
|
||||||
WebUiReverseProxyEnabled = changed.WebUiReverseProxyEnabled,
|
|
||||||
WebUiSecureCookieEnabled = changed.WebUiSecureCookieEnabled,
|
|
||||||
WebUiSessionTimeout = changed.WebUiSessionTimeout,
|
|
||||||
WebUiUpnp = changed.WebUiUpnp,
|
|
||||||
WebUiUseCustomHttpHeadersEnabled = changed.WebUiUseCustomHttpHeadersEnabled,
|
|
||||||
WebUiUsername = changed.WebUiUsername
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
original.AddToTopOfQueue = changed.AddToTopOfQueue ?? original.AddToTopOfQueue;
|
|
||||||
original.AddTrackers = changed.AddTrackers ?? original.AddTrackers;
|
|
||||||
original.AddTrackersEnabled = changed.AddTrackersEnabled ?? original.AddTrackersEnabled;
|
|
||||||
original.AltDlLimit = changed.AltDlLimit ?? original.AltDlLimit;
|
|
||||||
original.AltUpLimit = changed.AltUpLimit ?? original.AltUpLimit;
|
|
||||||
original.AlternativeWebuiEnabled = changed.AlternativeWebuiEnabled ?? original.AlternativeWebuiEnabled;
|
|
||||||
original.AlternativeWebuiPath = changed.AlternativeWebuiPath ?? original.AlternativeWebuiPath;
|
|
||||||
original.AnnounceIp = changed.AnnounceIp ?? original.AnnounceIp;
|
|
||||||
original.AnnounceToAllTiers = changed.AnnounceToAllTiers ?? original.AnnounceToAllTiers;
|
|
||||||
original.AnnounceToAllTrackers = changed.AnnounceToAllTrackers ?? original.AnnounceToAllTrackers;
|
|
||||||
original.AnonymousMode = changed.AnonymousMode ?? original.AnonymousMode;
|
|
||||||
original.AsyncIoThreads = changed.AsyncIoThreads ?? original.AsyncIoThreads;
|
|
||||||
original.AutoDeleteMode = changed.AutoDeleteMode ?? original.AutoDeleteMode;
|
|
||||||
original.AutoTmmEnabled = changed.AutoTmmEnabled ?? original.AutoTmmEnabled;
|
|
||||||
original.AutorunEnabled = changed.AutorunEnabled ?? original.AutorunEnabled;
|
|
||||||
original.AutorunOnTorrentAddedEnabled = changed.AutorunOnTorrentAddedEnabled ?? original.AutorunOnTorrentAddedEnabled;
|
|
||||||
original.AutorunOnTorrentAddedProgram = changed.AutorunOnTorrentAddedProgram ?? original.AutorunOnTorrentAddedProgram;
|
|
||||||
original.AutorunProgram = changed.AutorunProgram ?? original.AutorunProgram;
|
|
||||||
original.BannedIPs = changed.BannedIPs ?? original.BannedIPs;
|
|
||||||
original.BdecodeDepthLimit = changed.BdecodeDepthLimit ?? original.BdecodeDepthLimit;
|
|
||||||
original.BdecodeTokenLimit = changed.BdecodeTokenLimit ?? original.BdecodeTokenLimit;
|
|
||||||
original.BittorrentProtocol = changed.BittorrentProtocol ?? original.BittorrentProtocol;
|
|
||||||
original.BlockPeersOnPrivilegedPorts = changed.BlockPeersOnPrivilegedPorts ?? original.BlockPeersOnPrivilegedPorts;
|
|
||||||
original.BypassAuthSubnetWhitelist = changed.BypassAuthSubnetWhitelist ?? original.BypassAuthSubnetWhitelist;
|
|
||||||
original.BypassAuthSubnetWhitelistEnabled = changed.BypassAuthSubnetWhitelistEnabled ?? original.BypassAuthSubnetWhitelistEnabled;
|
|
||||||
original.BypassLocalAuth = changed.BypassLocalAuth ?? original.BypassLocalAuth;
|
|
||||||
original.CategoryChangedTmmEnabled = changed.CategoryChangedTmmEnabled ?? original.CategoryChangedTmmEnabled;
|
|
||||||
original.CheckingMemoryUse = changed.CheckingMemoryUse ?? original.CheckingMemoryUse;
|
|
||||||
original.ConnectionSpeed = changed.ConnectionSpeed ?? original.ConnectionSpeed;
|
|
||||||
original.CurrentInterfaceAddress = changed.CurrentInterfaceAddress ?? original.CurrentInterfaceAddress;
|
|
||||||
original.CurrentInterfaceName = changed.CurrentInterfaceName ?? original.CurrentInterfaceName;
|
|
||||||
original.CurrentNetworkInterface = changed.CurrentNetworkInterface ?? original.CurrentNetworkInterface;
|
|
||||||
original.Dht = changed.Dht ?? original.Dht;
|
|
||||||
original.DiskCache = changed.DiskCache ?? original.DiskCache;
|
|
||||||
original.DiskCacheTtl = changed.DiskCacheTtl ?? original.DiskCacheTtl;
|
|
||||||
original.DiskIoReadMode = changed.DiskIoReadMode ?? original.DiskIoReadMode;
|
|
||||||
original.DiskIoType = changed.DiskIoType ?? original.DiskIoType;
|
|
||||||
original.DiskIoWriteMode = changed.DiskIoWriteMode ?? original.DiskIoWriteMode;
|
|
||||||
original.DiskQueueSize = changed.DiskQueueSize ?? original.DiskQueueSize;
|
|
||||||
original.DlLimit = changed.DlLimit ?? original.DlLimit;
|
|
||||||
original.DontCountSlowTorrents = changed.DontCountSlowTorrents ?? original.DontCountSlowTorrents;
|
|
||||||
original.DyndnsDomain = changed.DyndnsDomain ?? original.DyndnsDomain;
|
|
||||||
original.DyndnsEnabled = changed.DyndnsEnabled ?? original.DyndnsEnabled;
|
|
||||||
original.DyndnsPassword = changed.DyndnsPassword ?? original.DyndnsPassword;
|
|
||||||
original.DyndnsService = changed.DyndnsService ?? original.DyndnsService;
|
|
||||||
original.DyndnsUsername = changed.DyndnsUsername ?? original.DyndnsUsername;
|
|
||||||
original.EmbeddedTrackerPort = changed.EmbeddedTrackerPort ?? original.EmbeddedTrackerPort;
|
|
||||||
original.EmbeddedTrackerPortForwarding = changed.EmbeddedTrackerPortForwarding ?? original.EmbeddedTrackerPortForwarding;
|
|
||||||
original.EnableCoalesceReadWrite = changed.EnableCoalesceReadWrite ?? original.EnableCoalesceReadWrite;
|
|
||||||
original.EnableEmbeddedTracker = changed.EnableEmbeddedTracker ?? original.EnableEmbeddedTracker;
|
|
||||||
original.EnableMultiConnectionsFromSameIp = changed.EnableMultiConnectionsFromSameIp ?? original.EnableMultiConnectionsFromSameIp;
|
|
||||||
original.EnablePieceExtentAffinity = changed.EnablePieceExtentAffinity ?? original.EnablePieceExtentAffinity;
|
|
||||||
original.EnableUploadSuggestions = changed.EnableUploadSuggestions ?? original.EnableUploadSuggestions;
|
|
||||||
original.Encryption = changed.Encryption ?? original.Encryption;
|
|
||||||
original.ExcludedFileNames = changed.ExcludedFileNames ?? original.ExcludedFileNames;
|
|
||||||
original.ExcludedFileNamesEnabled = changed.ExcludedFileNamesEnabled ?? original.ExcludedFileNamesEnabled;
|
|
||||||
original.ExportDir = changed.ExportDir ?? original.ExportDir;
|
|
||||||
original.ExportDirFin = changed.ExportDirFin ?? original.ExportDirFin;
|
|
||||||
original.FileLogAge = changed.FileLogAge ?? original.FileLogAge;
|
|
||||||
original.FileLogAgeType = changed.FileLogAgeType ?? original.FileLogAgeType;
|
|
||||||
original.FileLogBackupEnabled = changed.FileLogBackupEnabled ?? original.FileLogBackupEnabled;
|
|
||||||
original.FileLogDeleteOld = changed.FileLogDeleteOld ?? original.FileLogDeleteOld;
|
|
||||||
original.FileLogEnabled = changed.FileLogEnabled ?? original.FileLogEnabled;
|
|
||||||
original.FileLogMaxSize = changed.FileLogMaxSize ?? original.FileLogMaxSize;
|
|
||||||
original.FileLogPath = changed.FileLogPath ?? original.FileLogPath;
|
|
||||||
original.FilePoolSize = changed.FilePoolSize ?? original.FilePoolSize;
|
|
||||||
original.HashingThreads = changed.HashingThreads ?? original.HashingThreads;
|
|
||||||
original.I2pAddress = changed.I2pAddress ?? original.I2pAddress;
|
|
||||||
original.I2pEnabled = changed.I2pEnabled ?? original.I2pEnabled;
|
|
||||||
original.I2pInboundLength = changed.I2pInboundLength ?? original.I2pInboundLength;
|
|
||||||
original.I2pInboundQuantity = changed.I2pInboundQuantity ?? original.I2pInboundQuantity;
|
|
||||||
original.I2pMixedMode = changed.I2pMixedMode ?? original.I2pMixedMode;
|
|
||||||
original.I2pOutboundLength = changed.I2pOutboundLength ?? original.I2pOutboundLength;
|
|
||||||
original.I2pOutboundQuantity = changed.I2pOutboundQuantity ?? original.I2pOutboundQuantity;
|
|
||||||
original.I2pPort = changed.I2pPort ?? original.I2pPort;
|
|
||||||
original.IdnSupportEnabled = changed.IdnSupportEnabled ?? original.IdnSupportEnabled;
|
|
||||||
original.IncompleteFilesExt = changed.IncompleteFilesExt ?? original.IncompleteFilesExt;
|
|
||||||
original.IpFilterEnabled = changed.IpFilterEnabled ?? original.IpFilterEnabled;
|
|
||||||
original.IpFilterPath = changed.IpFilterPath ?? original.IpFilterPath;
|
|
||||||
original.IpFilterTrackers = changed.IpFilterTrackers ?? original.IpFilterTrackers;
|
|
||||||
original.LimitLanPeers = changed.LimitLanPeers ?? original.LimitLanPeers;
|
|
||||||
original.LimitTcpOverhead = changed.LimitTcpOverhead ?? original.LimitTcpOverhead;
|
|
||||||
original.LimitUtpRate = changed.LimitUtpRate ?? original.LimitUtpRate;
|
|
||||||
original.ListenPort = changed.ListenPort ?? original.ListenPort;
|
|
||||||
original.Locale = changed.Locale ?? original.Locale;
|
|
||||||
original.Lsd = changed.Lsd ?? original.Lsd;
|
|
||||||
original.MailNotificationAuthEnabled = changed.MailNotificationAuthEnabled ?? original.MailNotificationAuthEnabled;
|
|
||||||
original.MailNotificationEmail = changed.MailNotificationEmail ?? original.MailNotificationEmail;
|
|
||||||
original.MailNotificationEnabled = changed.MailNotificationEnabled ?? original.MailNotificationEnabled;
|
|
||||||
original.MailNotificationPassword = changed.MailNotificationPassword ?? original.MailNotificationPassword;
|
|
||||||
original.MailNotificationSender = changed.MailNotificationSender ?? original.MailNotificationSender;
|
|
||||||
original.MailNotificationSmtp = changed.MailNotificationSmtp ?? original.MailNotificationSmtp;
|
|
||||||
original.MailNotificationSslEnabled = changed.MailNotificationSslEnabled ?? original.MailNotificationSslEnabled;
|
|
||||||
original.MailNotificationUsername = changed.MailNotificationUsername ?? original.MailNotificationUsername;
|
|
||||||
original.MaxActiveCheckingTorrents = changed.MaxActiveCheckingTorrents ?? original.MaxActiveCheckingTorrents;
|
|
||||||
original.MaxActiveDownloads = changed.MaxActiveDownloads ?? original.MaxActiveDownloads;
|
|
||||||
original.MaxActiveTorrents = changed.MaxActiveTorrents ?? original.MaxActiveTorrents;
|
|
||||||
original.MaxActiveUploads = changed.MaxActiveUploads ?? original.MaxActiveUploads;
|
|
||||||
original.MaxConcurrentHttpAnnounces = changed.MaxConcurrentHttpAnnounces ?? original.MaxConcurrentHttpAnnounces;
|
|
||||||
original.MaxConnec = changed.MaxConnec ?? original.MaxConnec;
|
|
||||||
original.MaxConnecPerTorrent = changed.MaxConnecPerTorrent ?? original.MaxConnecPerTorrent;
|
|
||||||
original.MaxInactiveSeedingTime = changed.MaxInactiveSeedingTime ?? original.MaxInactiveSeedingTime;
|
|
||||||
original.MaxInactiveSeedingTimeEnabled = changed.MaxInactiveSeedingTimeEnabled ?? original.MaxInactiveSeedingTimeEnabled;
|
|
||||||
original.MaxRatio = changed.MaxRatio ?? original.MaxRatio;
|
|
||||||
original.MaxRatioAct = changed.MaxRatioAct ?? original.MaxRatioAct;
|
|
||||||
original.MaxRatioEnabled = changed.MaxRatioEnabled ?? original.MaxRatioEnabled;
|
|
||||||
original.MaxSeedingTime = changed.MaxSeedingTime ?? original.MaxSeedingTime;
|
|
||||||
original.MaxSeedingTimeEnabled = changed.MaxSeedingTimeEnabled ?? original.MaxSeedingTimeEnabled;
|
|
||||||
original.MaxUploads = changed.MaxUploads ?? original.MaxUploads;
|
|
||||||
original.MaxUploadsPerTorrent = changed.MaxUploadsPerTorrent ?? original.MaxUploadsPerTorrent;
|
|
||||||
original.MemoryWorkingSetLimit = changed.MemoryWorkingSetLimit ?? original.MemoryWorkingSetLimit;
|
|
||||||
original.MergeTrackers = changed.MergeTrackers ?? original.MergeTrackers;
|
|
||||||
original.OutgoingPortsMax = changed.OutgoingPortsMax ?? original.OutgoingPortsMax;
|
|
||||||
original.OutgoingPortsMin = changed.OutgoingPortsMin ?? original.OutgoingPortsMin;
|
|
||||||
original.PeerTos = changed.PeerTos ?? original.PeerTos;
|
|
||||||
original.PeerTurnover = changed.PeerTurnover ?? original.PeerTurnover;
|
|
||||||
original.PeerTurnoverCutoff = changed.PeerTurnoverCutoff ?? original.PeerTurnoverCutoff;
|
|
||||||
original.PeerTurnoverInterval = changed.PeerTurnoverInterval ?? original.PeerTurnoverInterval;
|
|
||||||
original.PerformanceWarning = changed.PerformanceWarning ?? original.PerformanceWarning;
|
|
||||||
original.Pex = changed.Pex ?? original.Pex;
|
|
||||||
original.PreallocateAll = changed.PreallocateAll ?? original.PreallocateAll;
|
|
||||||
original.ProxyAuthEnabled = changed.ProxyAuthEnabled ?? original.ProxyAuthEnabled;
|
|
||||||
original.ProxyBittorrent = changed.ProxyBittorrent ?? original.ProxyBittorrent;
|
|
||||||
original.ProxyHostnameLookup = changed.ProxyHostnameLookup ?? original.ProxyHostnameLookup;
|
|
||||||
original.ProxyIp = changed.ProxyIp ?? original.ProxyIp;
|
|
||||||
original.ProxyMisc = changed.ProxyMisc ?? original.ProxyMisc;
|
|
||||||
original.ProxyPassword = changed.ProxyPassword ?? original.ProxyPassword;
|
|
||||||
original.ProxyPeerConnections = changed.ProxyPeerConnections ?? original.ProxyPeerConnections;
|
|
||||||
original.ProxyPort = changed.ProxyPort ?? original.ProxyPort;
|
|
||||||
original.ProxyRss = changed.ProxyRss ?? original.ProxyRss;
|
|
||||||
original.ProxyType = changed.ProxyType ?? original.ProxyType;
|
|
||||||
original.ProxyUsername = changed.ProxyUsername ?? original.ProxyUsername;
|
|
||||||
original.QueueingEnabled = changed.QueueingEnabled ?? original.QueueingEnabled;
|
|
||||||
original.RandomPort = changed.RandomPort ?? original.RandomPort;
|
|
||||||
original.ReannounceWhenAddressChanged = changed.ReannounceWhenAddressChanged ?? original.ReannounceWhenAddressChanged;
|
|
||||||
original.RecheckCompletedTorrents = changed.RecheckCompletedTorrents ?? original.RecheckCompletedTorrents;
|
|
||||||
original.RefreshInterval = changed.RefreshInterval ?? original.RefreshInterval;
|
|
||||||
original.RequestQueueSize = changed.RequestQueueSize ?? original.RequestQueueSize;
|
|
||||||
original.ResolvePeerCountries = changed.ResolvePeerCountries ?? original.ResolvePeerCountries;
|
|
||||||
original.ResumeDataStorageType = changed.ResumeDataStorageType ?? original.ResumeDataStorageType;
|
|
||||||
original.RssAutoDownloadingEnabled = changed.RssAutoDownloadingEnabled ?? original.RssAutoDownloadingEnabled;
|
|
||||||
original.RssDownloadRepackProperEpisodes = changed.RssDownloadRepackProperEpisodes ?? original.RssDownloadRepackProperEpisodes;
|
|
||||||
original.RssMaxArticlesPerFeed = changed.RssMaxArticlesPerFeed ?? original.RssMaxArticlesPerFeed;
|
|
||||||
original.RssProcessingEnabled = changed.RssProcessingEnabled ?? original.RssProcessingEnabled;
|
|
||||||
original.RssRefreshInterval = changed.RssRefreshInterval ?? original.RssRefreshInterval;
|
|
||||||
original.RssSmartEpisodeFilters = changed.RssSmartEpisodeFilters ?? original.RssSmartEpisodeFilters;
|
|
||||||
original.SavePath = changed.SavePath ?? original.SavePath;
|
|
||||||
original.SavePathChangedTmmEnabled = changed.SavePathChangedTmmEnabled ?? original.SavePathChangedTmmEnabled;
|
|
||||||
original.SaveResumeDataInterval = changed.SaveResumeDataInterval ?? original.SaveResumeDataInterval;
|
|
||||||
original.ScanDirs = changed.ScanDirs ?? original.ScanDirs;
|
|
||||||
original.ScheduleFromHour = changed.ScheduleFromHour ?? original.ScheduleFromHour;
|
|
||||||
original.ScheduleFromMin = changed.ScheduleFromMin ?? original.ScheduleFromMin;
|
|
||||||
original.ScheduleToHour = changed.ScheduleToHour ?? original.ScheduleToHour;
|
|
||||||
original.ScheduleToMin = changed.ScheduleToMin ?? original.ScheduleToMin;
|
|
||||||
original.SchedulerDays = changed.SchedulerDays ?? original.SchedulerDays;
|
|
||||||
original.SchedulerEnabled = changed.SchedulerEnabled ?? original.SchedulerEnabled;
|
|
||||||
original.SendBufferLowWatermark = changed.SendBufferLowWatermark ?? original.SendBufferLowWatermark;
|
|
||||||
original.SendBufferWatermark = changed.SendBufferWatermark ?? original.SendBufferWatermark;
|
|
||||||
original.SendBufferWatermarkFactor = changed.SendBufferWatermarkFactor ?? original.SendBufferWatermarkFactor;
|
|
||||||
original.SlowTorrentDlRateThreshold = changed.SlowTorrentDlRateThreshold ?? original.SlowTorrentDlRateThreshold;
|
|
||||||
original.SlowTorrentInactiveTimer = changed.SlowTorrentInactiveTimer ?? original.SlowTorrentInactiveTimer;
|
|
||||||
original.SlowTorrentUlRateThreshold = changed.SlowTorrentUlRateThreshold ?? original.SlowTorrentUlRateThreshold;
|
|
||||||
original.SocketBacklogSize = changed.SocketBacklogSize ?? original.SocketBacklogSize;
|
|
||||||
original.SocketReceiveBufferSize = changed.SocketReceiveBufferSize ?? original.SocketReceiveBufferSize;
|
|
||||||
original.SocketSendBufferSize = changed.SocketSendBufferSize ?? original.SocketSendBufferSize;
|
|
||||||
original.SsrfMitigation = changed.SsrfMitigation ?? original.SsrfMitigation;
|
|
||||||
original.StartPausedEnabled = changed.StartPausedEnabled ?? original.StartPausedEnabled;
|
|
||||||
original.StopTrackerTimeout = changed.StopTrackerTimeout ?? original.StopTrackerTimeout;
|
|
||||||
original.TempPath = changed.TempPath ?? original.TempPath;
|
|
||||||
original.TempPathEnabled = changed.TempPathEnabled ?? original.TempPathEnabled;
|
|
||||||
original.TorrentChangedTmmEnabled = changed.TorrentChangedTmmEnabled ?? original.TorrentChangedTmmEnabled;
|
|
||||||
original.TorrentContentLayout = changed.TorrentContentLayout ?? original.TorrentContentLayout;
|
|
||||||
original.TorrentFileSizeLimit = changed.TorrentFileSizeLimit ?? original.TorrentFileSizeLimit;
|
|
||||||
original.TorrentStopCondition = changed.TorrentStopCondition ?? original.TorrentStopCondition;
|
|
||||||
original.UpLimit = changed.UpLimit ?? original.UpLimit;
|
|
||||||
original.UploadChokingAlgorithm = changed.UploadChokingAlgorithm ?? original.UploadChokingAlgorithm;
|
|
||||||
original.UploadSlotsBehavior = changed.UploadSlotsBehavior ?? original.UploadSlotsBehavior;
|
|
||||||
original.Upnp = changed.Upnp ?? original.Upnp;
|
|
||||||
original.UpnpLeaseDuration = changed.UpnpLeaseDuration ?? original.UpnpLeaseDuration;
|
|
||||||
original.UseCategoryPathsInManualMode = changed.UseCategoryPathsInManualMode ?? original.UseCategoryPathsInManualMode;
|
|
||||||
original.UseHttps = changed.UseHttps ?? original.UseHttps;
|
|
||||||
original.UseSubcategories = changed.UseSubcategories ?? original.UseSubcategories;
|
|
||||||
original.UtpTcpMixedMode = changed.UtpTcpMixedMode ?? original.UtpTcpMixedMode;
|
|
||||||
original.ValidateHttpsTrackerCertificate = changed.ValidateHttpsTrackerCertificate ?? original.ValidateHttpsTrackerCertificate;
|
|
||||||
original.WebUiAddress = changed.WebUiAddress ?? original.WebUiAddress;
|
|
||||||
original.WebUiBanDuration = changed.WebUiBanDuration ?? original.WebUiBanDuration;
|
|
||||||
original.WebUiClickjackingProtectionEnabled = changed.WebUiClickjackingProtectionEnabled ?? original.WebUiClickjackingProtectionEnabled;
|
|
||||||
original.WebUiCsrfProtectionEnabled = changed.WebUiCsrfProtectionEnabled ?? original.WebUiCsrfProtectionEnabled;
|
|
||||||
original.WebUiCustomHttpHeaders = changed.WebUiCustomHttpHeaders ?? original.WebUiCustomHttpHeaders;
|
|
||||||
original.WebUiDomainList = changed.WebUiDomainList ?? original.WebUiDomainList;
|
|
||||||
original.WebUiHostHeaderValidationEnabled = changed.WebUiHostHeaderValidationEnabled ?? original.WebUiHostHeaderValidationEnabled;
|
|
||||||
original.WebUiHttpsCertPath = changed.WebUiHttpsCertPath ?? original.WebUiHttpsCertPath;
|
|
||||||
original.WebUiHttpsKeyPath = changed.WebUiHttpsKeyPath ?? original.WebUiHttpsKeyPath;
|
|
||||||
original.WebUiMaxAuthFailCount = changed.WebUiMaxAuthFailCount ?? original.WebUiMaxAuthFailCount;
|
|
||||||
original.WebUiPort = changed.WebUiPort ?? original.WebUiPort;
|
|
||||||
original.WebUiReverseProxiesList = changed.WebUiReverseProxiesList ?? original.WebUiReverseProxiesList;
|
|
||||||
original.WebUiReverseProxyEnabled = changed.WebUiReverseProxyEnabled ?? original.WebUiReverseProxyEnabled;
|
|
||||||
original.WebUiSecureCookieEnabled = changed.WebUiSecureCookieEnabled ?? original.WebUiSecureCookieEnabled;
|
|
||||||
original.WebUiSessionTimeout = changed.WebUiSessionTimeout ?? original.WebUiSessionTimeout;
|
|
||||||
original.WebUiUpnp = changed.WebUiUpnp ?? original.WebUiUpnp;
|
|
||||||
original.WebUiUseCustomHttpHeadersEnabled = changed.WebUiUseCustomHttpHeadersEnabled ?? original.WebUiUseCustomHttpHeadersEnabled;
|
|
||||||
original.WebUiUsername = changed.WebUiUsername ?? original.WebUiUsername;
|
|
||||||
}
|
|
||||||
|
|
||||||
return original;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool MergeContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files, Dictionary<string, ContentItem> contents)
|
public bool MergeContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files, Dictionary<string, ContentItem> contents)
|
||||||
{
|
{
|
||||||
if (files.Count == 0)
|
if (files.Count == 0)
|
||||||
@@ -2006,7 +1545,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
? int.MaxValue
|
? int.MaxValue
|
||||||
: contents.Values.Min(c => c.Index);
|
: contents.Values.Min(c => c.Index);
|
||||||
var minFileIndex = files.Min(f => f.Index);
|
var minFileIndex = files.Min(f => f.Index);
|
||||||
var nextFolderIndex = System.Math.Min(minExistingIndex, minFileIndex) - 1;
|
var nextFolderIndex = Math.Min(minExistingIndex, minFileIndex) - 1;
|
||||||
|
|
||||||
foreach (var file in files)
|
foreach (var file in files)
|
||||||
{
|
{
|
||||||
@@ -2100,38 +1639,5 @@ namespace Lantean.QBTMud.Services
|
|||||||
|
|
||||||
return hasChanges;
|
return hasChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RssList CreateRssList(IReadOnlyDictionary<string, QBitTorrentClient.Models.RssItem> rssItems)
|
|
||||||
{
|
|
||||||
var articles = new List<RssArticle>();
|
|
||||||
var feeds = new Dictionary<string, RssFeed>();
|
|
||||||
foreach (var (key, rssItem) in rssItems)
|
|
||||||
{
|
|
||||||
feeds.Add(key, new RssFeed(rssItem.HasError, rssItem.IsLoading, rssItem.LastBuildDate, rssItem.Title, rssItem.Uid, rssItem.Url));
|
|
||||||
if (rssItem.Articles is null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
foreach (var rssArticle in rssItem.Articles)
|
|
||||||
{
|
|
||||||
var article = new RssArticle(
|
|
||||||
key,
|
|
||||||
rssArticle.Category,
|
|
||||||
rssArticle.Comments,
|
|
||||||
rssArticle.Date!,
|
|
||||||
rssArticle.Description,
|
|
||||||
rssArticle.Id!,
|
|
||||||
rssArticle.Link,
|
|
||||||
rssArticle.Thumbnail,
|
|
||||||
rssArticle.Title!,
|
|
||||||
rssArticle.TorrentURL!,
|
|
||||||
rssArticle.IsRead);
|
|
||||||
|
|
||||||
articles.Add(article);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RssList(feeds, articles);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class ApiClientAddTorrentAndMetadataTests
|
||||||
|
{
|
||||||
|
private readonly ApiClient _target;
|
||||||
|
private readonly StubHttpMessageHandler _handler;
|
||||||
|
|
||||||
|
public ApiClientAddTorrentAndMetadataTests()
|
||||||
|
{
|
||||||
|
_handler = new StubHttpMessageHandler();
|
||||||
|
var http = new HttpClient(_handler) { BaseAddress = new Uri("http://localhost/") };
|
||||||
|
_target = new ApiClient(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_OnlyUrls_WHEN_AddTorrent_THEN_ShouldPostMultipartWithUrlsNewlineSeparated()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/add");
|
||||||
|
req.Content.Should().BeOfType<MultipartFormDataContent>();
|
||||||
|
|
||||||
|
var parts = (req.Content as MultipartFormDataContent)!.ToList();
|
||||||
|
parts.Count.Should().Be(1);
|
||||||
|
|
||||||
|
var urlsPart = parts.Single();
|
||||||
|
urlsPart.Headers.ContentDisposition!.Name.Should().Be("urls");
|
||||||
|
(await urlsPart.ReadAsStringAsync()).Should().Be("u1\nu2");
|
||||||
|
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var p = new AddTorrentParams
|
||||||
|
{
|
||||||
|
Urls = new[] { "u1", "u2" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.AddTorrent(p);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_FilesAndOptions_WHEN_AddTorrent_THEN_ShouldIncludeAllExpectedParts()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/add");
|
||||||
|
req.Content.Should().BeOfType<MultipartFormDataContent>();
|
||||||
|
|
||||||
|
var parts = (req.Content as MultipartFormDataContent)!.ToList();
|
||||||
|
|
||||||
|
string Read(string name) =>
|
||||||
|
parts.Single(p => p.Headers.ContentDisposition!.Name == name)
|
||||||
|
.ReadAsStringAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
parts.Any(p => p.Headers.ContentDisposition!.Name == "torrents" &&
|
||||||
|
p.Headers.ContentDisposition!.FileName == "a.torrent").Should().BeTrue();
|
||||||
|
parts.Any(p => p.Headers.ContentDisposition!.Name == "torrents" &&
|
||||||
|
p.Headers.ContentDisposition!.FileName == "b.torrent").Should().BeTrue();
|
||||||
|
|
||||||
|
Read("skip_checking").Should().Be("true");
|
||||||
|
Read("sequentialDownload").Should().Be("false");
|
||||||
|
Read("firstLastPiecePrio").Should().Be("true");
|
||||||
|
Read("addToTopOfQueue").Should().Be("true");
|
||||||
|
Read("forced").Should().Be("false");
|
||||||
|
Read("stopped").Should().Be("true");
|
||||||
|
Read("savepath").Should().Be("/save");
|
||||||
|
Read("downloadPath").Should().Be("/dl");
|
||||||
|
Read("useDownloadPath").Should().Be("true");
|
||||||
|
Read("category").Should().Be("Movies");
|
||||||
|
Read("tags").Should().Be("one,two");
|
||||||
|
Read("rename").Should().Be("renamed");
|
||||||
|
Read("upLimit").Should().Be("123");
|
||||||
|
Read("dlLimit").Should().Be("456");
|
||||||
|
Read("downloader").Should().Be("curl");
|
||||||
|
Read("filePriorities").Should().Be("0,1");
|
||||||
|
Read("ssl_certificate").Should().Be("cert");
|
||||||
|
Read("ssl_private_key").Should().Be("key");
|
||||||
|
Read("ssl_dh_params").Should().Be("dh");
|
||||||
|
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
using var s1 = new MemoryStream(Encoding.UTF8.GetBytes("a"));
|
||||||
|
using var s2 = new MemoryStream(Encoding.UTF8.GetBytes("b"));
|
||||||
|
|
||||||
|
var p = new AddTorrentParams
|
||||||
|
{
|
||||||
|
Urls = null,
|
||||||
|
Torrents = new Dictionary<string, Stream> { { "a.torrent", (Stream)s1 }, { "b.torrent", (Stream)s2 } },
|
||||||
|
SkipChecking = true,
|
||||||
|
SequentialDownload = false,
|
||||||
|
FirstLastPiecePriority = true,
|
||||||
|
AddToTopOfQueue = true,
|
||||||
|
Forced = false,
|
||||||
|
Stopped = true,
|
||||||
|
SavePath = "/save",
|
||||||
|
DownloadPath = "/dl",
|
||||||
|
UseDownloadPath = true,
|
||||||
|
Category = "Movies",
|
||||||
|
Tags = new[] { "one", "two" },
|
||||||
|
RenameTorrent = "renamed",
|
||||||
|
UploadLimit = 123,
|
||||||
|
DownloadLimit = 456,
|
||||||
|
Downloader = "curl",
|
||||||
|
FilePriorities = new[] { (Priority)0, (Priority)1 },
|
||||||
|
SslCertificate = "cert",
|
||||||
|
SslPrivateKey = "key",
|
||||||
|
SslDhParams = "dh"
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.AddTorrent(p);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_ConflictAndEmptyMessage_WHEN_AddTorrent_THEN_ShouldThrowWithDefaultConflictMessage()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Conflict)
|
||||||
|
{
|
||||||
|
Content = new StringContent(string.Empty)
|
||||||
|
});
|
||||||
|
|
||||||
|
var p = new AddTorrentParams { Urls = new[] { "u" } };
|
||||||
|
|
||||||
|
var act = async () => await _target.AddTorrent(p);
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
||||||
|
ex.Which.Message.Should().Be("All torrents failed to add.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_ConflictWithMessage_WHEN_AddTorrent_THEN_ShouldThrowWithServerMessage()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Conflict)
|
||||||
|
{
|
||||||
|
Content = new StringContent("some failed")
|
||||||
|
});
|
||||||
|
|
||||||
|
var p = new AddTorrentParams { Urls = new[] { "u" } };
|
||||||
|
|
||||||
|
var act = async () => await _target.AddTorrent(p);
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
||||||
|
ex.Which.Message.Should().Be("some failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_SuccessAndEmptyBody_WHEN_AddTorrent_THEN_ShouldReturnDefaultResultObject()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent(string.Empty)
|
||||||
|
});
|
||||||
|
|
||||||
|
var p = new AddTorrentParams { Urls = new[] { "u" } };
|
||||||
|
|
||||||
|
var result = await _target.AddTorrent(p);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_BaseAddressAndHash_WHEN_GetExportUrl_THEN_ShouldReturnFormattedUrl()
|
||||||
|
{
|
||||||
|
var result = await _target.GetExportUrl("abc123");
|
||||||
|
|
||||||
|
result.Should().Be("http://localhost/torrents/export?hash=abc123");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_SourceOnly_WHEN_FetchMetadata_THEN_ShouldPostFormAndReturnNullOnEmpty()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/fetchMetadata");
|
||||||
|
var form = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
Uri.UnescapeDataString(form).Should().Be("source=magnet:?xt=urn:btih:abc");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent(string.Empty)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.FetchMetadata("magnet:?xt=urn:btih:abc");
|
||||||
|
|
||||||
|
result.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_SourceAndDownloader_WHEN_FetchMetadata_THEN_ShouldIncludeDownloaderAndDeserialize()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
var decoded = Uri.UnescapeDataString(await req.Content!.ReadAsStringAsync(ct));
|
||||||
|
decoded.Should().Be("source=src&downloader=aria2");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.FetchMetadata("src", "aria2");
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_TorrentStreams_WHEN_ParseMetadata_THEN_ShouldPostMultipartAndReturnList()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/parseMetadata");
|
||||||
|
|
||||||
|
var parts = (req.Content as MultipartFormDataContent)!.ToList();
|
||||||
|
parts.Count.Should().Be(2);
|
||||||
|
parts.All(p => p.Headers.ContentDisposition!.Name == "torrents").Should().BeTrue();
|
||||||
|
parts.Any(p => p.Headers.ContentDisposition!.FileName == "a.torrent").Should().BeTrue();
|
||||||
|
parts.Any(p => p.Headers.ContentDisposition!.FileName == "b.torrent").Should().BeTrue();
|
||||||
|
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[]")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
using var s1 = new MemoryStream(Encoding.UTF8.GetBytes("a"));
|
||||||
|
using var s2 = new MemoryStream(Encoding.UTF8.GetBytes("b"));
|
||||||
|
|
||||||
|
var list = await _target.ParseMetadata(new[] { ("a.torrent", (Stream)s1), ("b.torrent", (Stream)s2) });
|
||||||
|
|
||||||
|
list.Should().NotBeNull();
|
||||||
|
list.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_BadJson_WHEN_ParseMetadata_THEN_ShouldReturnEmptyList()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("oops")
|
||||||
|
});
|
||||||
|
|
||||||
|
var list = await _target.ParseMetadata(Array.Empty<(string, Stream)>());
|
||||||
|
|
||||||
|
list.Should().NotBeNull();
|
||||||
|
list.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_ParseMetadata_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.ParseMetadata(Array.Empty<(string, Stream)>());
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
|
ex.Which.Message.Should().Be("bad");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Source_WHEN_SaveMetadata_THEN_ShouldPostFormAndReturnBytes()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/saveMetadata");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
Uri.UnescapeDataString(body).Should().Be("source=magnet");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new ByteArrayContent(new byte[] { 1, 2, 3 })
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var bytes = await _target.SaveMetadata("magnet");
|
||||||
|
|
||||||
|
bytes.Should().Equal(new byte[] { 1, 2, 3 });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_SaveMetadata_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
Content = new StringContent("no")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.SaveMetadata("x");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||||
|
ex.Which.Message.Should().Be("no");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
348
Lantean.QBitTorrentClient.Test/ApiClientApplicationTests.cs
Normal file
348
Lantean.QBitTorrentClient.Test/ApiClientApplicationTests.cs
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
using AwesomeAssertions;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class ApiClientApplicationTests
|
||||||
|
{
|
||||||
|
private readonly ApiClient _target;
|
||||||
|
private readonly StubHttpMessageHandler _handler;
|
||||||
|
|
||||||
|
public ApiClientApplicationTests()
|
||||||
|
{
|
||||||
|
_handler = new StubHttpMessageHandler();
|
||||||
|
var http = new HttpClient(_handler)
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri("http://localhost/")
|
||||||
|
};
|
||||||
|
_target = new ApiClient(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_OK_WHEN_GetApplicationVersion_THEN_ShouldReturnRawBody()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/app/version");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("4.6.0")
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.GetApplicationVersion();
|
||||||
|
|
||||||
|
result.Should().Be("4.6.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetApplicationVersion_THEN_ShouldThrowWithStatusAndMessage()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadGateway)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetApplicationVersion();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadGateway);
|
||||||
|
ex.Which.Message.Should().Be("bad");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_OK_WHEN_GetAPIVersion_THEN_ShouldReturnRawBody()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("2.10")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetAPIVersion();
|
||||||
|
|
||||||
|
result.Should().Be("2.10");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetAPIVersion_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
||||||
|
{
|
||||||
|
Content = new StringContent("no")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetAPIVersion();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
|
||||||
|
ex.Which.Message.Should().Be("no");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_OKAndJson_WHEN_GetBuildInfo_THEN_ShouldDeserialize()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetBuildInfo();
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetBuildInfo_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
Content = new StringContent("missing")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetBuildInfo();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||||
|
ex.Which.Message.Should().Be("missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_OK_WHEN_Shutdown_THEN_ShouldPostAndNotThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/app/shutdown");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_Shutdown_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)
|
||||||
|
{
|
||||||
|
Content = new StringContent("busy")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.Shutdown();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.ServiceUnavailable);
|
||||||
|
ex.Which.Message.Should().Be("busy");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_OKAndJson_WHEN_GetApplicationPreferences_THEN_ShouldDeserialize()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetApplicationPreferences();
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetApplicationPreferences_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad prefs")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetApplicationPreferences();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
|
ex.Which.Message.Should().Be("bad prefs");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Preferences_WHEN_SetApplicationPreferences_THEN_ShouldPostJsonFormAndSucceed()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/app/setPreferences");
|
||||||
|
req.Content!.Headers.ContentType!.MediaType.Should().Be("application/x-www-form-urlencoded");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().StartWith("json=");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
var prefs = new UpdatePreferences();
|
||||||
|
await _target.SetApplicationPreferences(prefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_SetApplicationPreferences_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Conflict)
|
||||||
|
{
|
||||||
|
Content = new StringContent("conflict")
|
||||||
|
});
|
||||||
|
|
||||||
|
var prefs = new UpdatePreferences();
|
||||||
|
var act = async () => await _target.SetApplicationPreferences(prefs);
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
||||||
|
ex.Which.Message.Should().Be("conflict");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_OKAndJsonList_WHEN_GetApplicationCookies_THEN_ShouldReturnListOrEmptyOnBadJson()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("not json")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetApplicationCookies();
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_ListOfCookies_WHEN_SetApplicationCookies_THEN_ShouldPostJsonArrayInForm()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/app/setCookies");
|
||||||
|
req.Content!.Headers.ContentType!.MediaType.Should().Be("application/x-www-form-urlencoded");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().StartWith("cookies=");
|
||||||
|
body.Should().Contain("%5B"); // '[' encoded
|
||||||
|
body.Should().Contain("%5D"); // ']' encoded
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
var cookies = new List<ApplicationCookie>();
|
||||||
|
await _target.SetApplicationCookies(cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_OKEmptyBody_WHEN_RotateApiKey_THEN_ShouldReturnEmptyString()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent(string.Empty)
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.RotateApiKey();
|
||||||
|
|
||||||
|
result.Should().Be(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_ObjectWithApiKey_WHEN_RotateApiKey_THEN_ShouldReturnApiKeyValue()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{\"apiKey\":\"abcd1234\"}")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.RotateApiKey();
|
||||||
|
|
||||||
|
result.Should().Be("abcd1234");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_ObjectWithoutApiKey_WHEN_RotateApiKey_THEN_ShouldReturnEmptyString()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.RotateApiKey();
|
||||||
|
|
||||||
|
result.Should().Be(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonObjectJson_WHEN_RotateApiKey_THEN_ShouldReturnEmptyString()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[]")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.RotateApiKey();
|
||||||
|
|
||||||
|
result.Should().Be(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_OK_WHEN_GetDefaultSavePath_THEN_ShouldReturnRawBody()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("/data/downloads")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetDefaultSavePath();
|
||||||
|
|
||||||
|
result.Should().Be("/data/downloads");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetDefaultSavePath_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
Content = new StringContent("nope")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetDefaultSavePath();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||||
|
ex.Which.Message.Should().Be("nope");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_BadJson_WHEN_GetNetworkInterfaces_THEN_ShouldReturnEmptyList()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("not json")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetNetworkInterfaces();
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_OKJsonArrayOfStrings_WHEN_GetNetworkInterfaceAddressList_THEN_ShouldDeserializeStrings()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/app/networkInterfaceAddressList?iface=eth0");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[\"192.168.1.10\",\"fe80::1\"]")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.GetNetworkInterfaceAddressList("eth0");
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(2);
|
||||||
|
result[0].Should().Be("192.168.1.10");
|
||||||
|
result[1].Should().Be("fe80::1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
144
Lantean.QBitTorrentClient.Test/ApiClientAuthenticationTests.cs
Normal file
144
Lantean.QBitTorrentClient.Test/ApiClientAuthenticationTests.cs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
using System.Net;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public partial class ApiClientAuthenticationTests
|
||||||
|
{
|
||||||
|
private readonly ApiClient _target;
|
||||||
|
private readonly StubHttpMessageHandler _handler;
|
||||||
|
|
||||||
|
public ApiClientAuthenticationTests()
|
||||||
|
{
|
||||||
|
_handler = new StubHttpMessageHandler();
|
||||||
|
var http = new HttpClient(_handler)
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri("http://localhost/")
|
||||||
|
};
|
||||||
|
_target = new ApiClient(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_ServerReturnsOK_WHEN_CheckAuthState_THEN_ShouldBeTrue()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/app/version");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.CheckAuthState();
|
||||||
|
|
||||||
|
result.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_ServerReturnsNonOK_WHEN_CheckAuthState_THEN_ShouldBeFalse()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.CheckAuthState();
|
||||||
|
|
||||||
|
result.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_HandlerThrows_WHEN_CheckAuthState_THEN_ShouldBeFalse()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => throw new HttpRequestException("boom", null, HttpStatusCode.BadGateway);
|
||||||
|
|
||||||
|
var result = await _target.CheckAuthState();
|
||||||
|
|
||||||
|
result.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_ValidCredentialsAndSuccessStatus_WHEN_Login_THEN_ShouldPostFormAndNotThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/auth/login");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("username=user&password=pass");
|
||||||
|
req.Content!.Headers.ContentType!.MediaType.Should().Be("application/x-www-form-urlencoded");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("Ok")
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.Login("user", "pass");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_SuccessStatusButFailsBody_WHEN_Login_THEN_ShouldThrowBadRequest()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("Fails.")
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = async () => await _target.Login("user", "pass");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccessStatus_WHEN_Login_THEN_ShouldThrowWithStatusAndMessage()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
Content = new StringContent("Nope")
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = async () => await _target.Login("user", "pass");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||||
|
ex.Which.Message.Should().Be("Nope");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Success_WHEN_Logout_THEN_ShouldPostAndNotThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/auth/logout");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.Logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_Logout_THEN_ShouldThrowWithStatusAndMessage()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
||||||
|
{
|
||||||
|
Content = new StringContent("fail")
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = async () => await _target.Logout();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
|
||||||
|
ex.Which.Message.Should().Be("fail");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
143
Lantean.QBitTorrentClient.Test/ApiClientClientDataTests.cs
Normal file
143
Lantean.QBitTorrentClient.Test/ApiClientClientDataTests.cs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class ApiClientClientDataTests
|
||||||
|
{
|
||||||
|
private readonly ApiClient _target;
|
||||||
|
private readonly StubHttpMessageHandler _handler;
|
||||||
|
|
||||||
|
public ApiClientClientDataTests()
|
||||||
|
{
|
||||||
|
_handler = new StubHttpMessageHandler();
|
||||||
|
var http = new HttpClient(_handler)
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri("http://localhost/")
|
||||||
|
};
|
||||||
|
_target = new ApiClient(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NoKeys_WHEN_LoadClientData_THEN_ShouldGETAndReturnDictionary()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/clientdata/load");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{\"k1\":1,\"k2\":2}")
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.LoadClientData();
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(2);
|
||||||
|
result["k1"].GetInt32().Should().Be(1);
|
||||||
|
result["k2"].GetInt32().Should().Be(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Keys_WHEN_LoadClientData_THEN_ShouldEncodeKeysAsJsonQuery()
|
||||||
|
{
|
||||||
|
var keys = new[] { "alpha", "beta gamma" };
|
||||||
|
var expectedJson = JsonSerializer.Serialize(keys);
|
||||||
|
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.AbsolutePath.Should().Be("/clientdata/load");
|
||||||
|
var query = req.RequestUri!.Query.TrimStart('?');
|
||||||
|
query.Should().StartWith("keys=");
|
||||||
|
|
||||||
|
var encoded = query.Substring("keys=".Length);
|
||||||
|
var decoded = Uri.UnescapeDataString(encoded);
|
||||||
|
decoded.Should().Be(expectedJson);
|
||||||
|
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.LoadClientData(keys);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_LoadClientData_THEN_ShouldThrowWithStatusAndMessage()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
||||||
|
{
|
||||||
|
Content = new StringContent("err")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.LoadClientData();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
|
||||||
|
ex.Which.Message.Should().Be("err");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_InvalidJson_WHEN_LoadClientData_THEN_ShouldReturnEmptyDictionary()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("not json")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.LoadClientData();
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Data_WHEN_StoreClientData_THEN_ShouldPostFormWithJson()
|
||||||
|
{
|
||||||
|
var data = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>("{\"a\":1,\"b\":\"x\"}")!;
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/clientdata/store");
|
||||||
|
req.Content!.Headers.ContentType!.MediaType.Should().Be("application/x-www-form-urlencoded");
|
||||||
|
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().StartWith("data=");
|
||||||
|
|
||||||
|
var encoded = body.Substring("data=".Length);
|
||||||
|
var json = Uri.UnescapeDataString(encoded);
|
||||||
|
var roundTrip = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json)!;
|
||||||
|
|
||||||
|
roundTrip["a"].GetInt32().Should().Be(1);
|
||||||
|
roundTrip["b"].GetString().Should().Be("x");
|
||||||
|
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.StoreClientData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_StoreClientData_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
var data = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>("{\"a\":1}")!;
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.StoreClientData(data);
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
|
ex.Which.Message.Should().Be("bad");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
145
Lantean.QBitTorrentClient.Test/ApiClientLogTests.cs
Normal file
145
Lantean.QBitTorrentClient.Test/ApiClientLogTests.cs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
using System.Net;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class ApiClientLogTests
|
||||||
|
{
|
||||||
|
private readonly ApiClient _target;
|
||||||
|
private readonly StubHttpMessageHandler _handler;
|
||||||
|
|
||||||
|
public ApiClientLogTests()
|
||||||
|
{
|
||||||
|
_handler = new StubHttpMessageHandler();
|
||||||
|
var http = new HttpClient(_handler)
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri("http://localhost/")
|
||||||
|
};
|
||||||
|
_target = new ApiClient(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NoFilters_WHEN_GetLog_THEN_ShouldGETWithoutQueryAndReturnList()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.AbsolutePath.Should().Be("/log/main");
|
||||||
|
req.RequestUri!.Query.Should().BeEmpty();
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[]")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.GetLog();
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_AllFilters_WHEN_GetLog_THEN_ShouldIncludeAllQueryParams()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.AbsolutePath.Should().Be("/log/main");
|
||||||
|
req.RequestUri!.Query.Should().Be("?normal=true&info=false&warning=true&critical=false&last_known_id=123");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[]")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.GetLog(normal: true, info: false, warning: true, critical: false, lastKnownId: 123);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_BadJson_WHEN_GetLog_THEN_ShouldReturnEmptyList()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("not json")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetLog();
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetLog_THEN_ShouldThrowWithStatusAndMessage()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
||||||
|
{
|
||||||
|
Content = new StringContent("boom")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetLog();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
|
||||||
|
ex.Which.Message.Should().Be("boom");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NoLastKnownId_WHEN_GetPeerLog_THEN_ShouldGETWithoutQueryAndReturnList()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.AbsolutePath.Should().Be("/log/peers");
|
||||||
|
req.RequestUri!.Query.Should().BeEmpty();
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[]")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.GetPeerLog();
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_LastKnownId_WHEN_GetPeerLog_THEN_ShouldIncludeQueryParam()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.AbsolutePath.Should().Be("/log/peers");
|
||||||
|
req.RequestUri!.Query.Should().Be("?last_known_id=77");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[]")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.GetPeerLog(77);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetPeerLog_THEN_ShouldThrowWithStatusAndMessage()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadGateway)
|
||||||
|
{
|
||||||
|
Content = new StringContent("fail")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetPeerLog();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadGateway);
|
||||||
|
ex.Which.Message.Should().Be("fail");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
333
Lantean.QBitTorrentClient.Test/ApiClientRssTests.cs
Normal file
333
Lantean.QBitTorrentClient.Test/ApiClientRssTests.cs
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
using System.Net;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class ApiClientRssTests
|
||||||
|
{
|
||||||
|
private readonly ApiClient _target;
|
||||||
|
private readonly StubHttpMessageHandler _handler;
|
||||||
|
|
||||||
|
public ApiClientRssTests()
|
||||||
|
{
|
||||||
|
_handler = new StubHttpMessageHandler();
|
||||||
|
var http = new HttpClient(_handler) { BaseAddress = new Uri("http://localhost/") };
|
||||||
|
_target = new ApiClient(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Path_WHEN_AddRssFolder_THEN_ShouldPOSTForm()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/rss/addFolder");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("path=%2Ffeeds%2Ftv");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.AddRssFolder("/feeds/tv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_AddRssFolder_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Conflict)
|
||||||
|
{
|
||||||
|
Content = new StringContent("exists")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.AddRssFolder("/x");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
||||||
|
ex.Which.Message.Should().Be("exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_UrlOnly_WHEN_AddRssFeed_THEN_ShouldPOSTUrlWithoutPath()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/rss/addFeed");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("url=http%3A%2F%2Ffeed");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.AddRssFeed("http://feed");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_UrlAndPath_WHEN_AddRssFeed_THEN_ShouldIncludeBoth()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
var decoded = Uri.UnescapeDataString(await req.Content!.ReadAsStringAsync(ct));
|
||||||
|
decoded.Should().Be("url=http://feed&path=/podcasts");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.AddRssFeed("http://feed", "/podcasts");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_AddRssFeed_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.AddRssFeed("u");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
|
ex.Which.Message.Should().Be("bad");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Path_WHEN_RemoveRssItem_THEN_ShouldPOSTForm()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/rss/removeItem");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("path=%2Ffeeds%2Ftv");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.RemoveRssItem("/feeds/tv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_ItemAndDest_WHEN_MoveRssItem_THEN_ShouldPOSTBoth()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/rss/moveItem");
|
||||||
|
var decoded = Uri.UnescapeDataString(await req.Content!.ReadAsStringAsync(ct));
|
||||||
|
decoded.Should().Be("itemPath=/feeds/tv&destPath=/feeds/news");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.MoveRssItem("/feeds/tv", "/feeds/news");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_MoveRssItem_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
Content = new StringContent("nope")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.MoveRssItem("/a", "/b");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||||
|
ex.Which.Message.Should().Be("nope");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NoFlag_WHEN_GetAllRssItems_THEN_ShouldGETWithoutQueryAndReturnDict()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.AbsolutePath.Should().Be("/rss/items");
|
||||||
|
req.RequestUri!.Query.Should().BeEmpty();
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var dict = await _target.GetAllRssItems();
|
||||||
|
|
||||||
|
dict.Should().NotBeNull();
|
||||||
|
dict.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_WithDataTrue_WHEN_GetAllRssItems_THEN_ShouldQueryWithTrueCapitalized()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/rss/items?withData=True");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var dict = await _target.GetAllRssItems(true);
|
||||||
|
|
||||||
|
dict.Should().NotBeNull();
|
||||||
|
dict.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetAllRssItems_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
||||||
|
{
|
||||||
|
Content = new StringContent("err")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetAllRssItems();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
|
||||||
|
ex.Which.Message.Should().Be("err");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_ItemPathOnly_WHEN_MarkRssItemAsRead_THEN_ShouldPOSTOnlyItemPath()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/rss/markAsRead");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("itemPath=%2Ffeeds%2Ftv");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.MarkRssItemAsRead("/feeds/tv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_ArticleId_WHEN_MarkRssItemAsRead_THEN_ShouldIncludeArticleId()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
var decoded = Uri.UnescapeDataString(await req.Content!.ReadAsStringAsync(ct));
|
||||||
|
decoded.Should().Be("itemPath=/feeds/tv&articleId=a1");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.MarkRssItemAsRead("/feeds/tv", "a1");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_ItemPath_WHEN_RefreshRssItem_THEN_ShouldPOSTForm()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/rss/refreshItem");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("itemPath=%2Ffeeds%2Ftv");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.RefreshRssItem("/feeds/tv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Rule_WHEN_SetRssAutoDownloadingRule_THEN_ShouldPOSTRuleNameAndRuleDefJson()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/rss/setRule");
|
||||||
|
|
||||||
|
var decoded = Uri.UnescapeDataString(await req.Content!.ReadAsStringAsync(ct));
|
||||||
|
decoded.Should().StartWith("ruleName=r1&ruleDef=");
|
||||||
|
|
||||||
|
var json = decoded.Substring("ruleName=r1&ruleDef=".Length);
|
||||||
|
var expectedJson = System.Text.Json.JsonSerializer.Serialize(new AutoDownloadingRule());
|
||||||
|
|
||||||
|
json.Should().Be(expectedJson);
|
||||||
|
|
||||||
|
return new HttpResponseMessage(System.Net.HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.SetRssAutoDownloadingRule("r1", new AutoDownloadingRule());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_RuleNames_WHEN_RenameRssAutoDownloadingRule_THEN_ShouldPOSTBothNames()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/rss/renameRule");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("ruleName=old&newRuleName=new");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.RenameRssAutoDownloadingRule("old", "new");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_RuleName_WHEN_RemoveRssAutoDownloadingRule_THEN_ShouldPOSTName()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/rss/removeRule");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("ruleName=dead");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.RemoveRssAutoDownloadingRule("dead");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_OKOrBadJson_WHEN_GetAllRssAutoDownloadingRules_THEN_ShouldDeserializeOrReturnEmpty()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
});
|
||||||
|
|
||||||
|
var dict = await _target.GetAllRssAutoDownloadingRules();
|
||||||
|
dict.Should().NotBeNull();
|
||||||
|
dict.Count.Should().Be(0);
|
||||||
|
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var empty = await _target.GetAllRssAutoDownloadingRules();
|
||||||
|
empty.Should().NotBeNull();
|
||||||
|
empty.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_RuleName_WHEN_GetRssMatchingArticles_THEN_ShouldGETAndReturnDictionaryOfLists()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/rss/matchingArticles?ruleName=myrule");
|
||||||
|
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{\"feed1\":[\"a\",\"b\"]}")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var dict = await _target.GetRssMatchingArticles("myrule");
|
||||||
|
|
||||||
|
dict.Should().NotBeNull();
|
||||||
|
dict.Count.Should().Be(1);
|
||||||
|
dict["feed1"].Count.Should().Be(2);
|
||||||
|
dict["feed1"][0].Should().Be("a");
|
||||||
|
dict["feed1"][1].Should().Be("b");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetRssMatchingArticles_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadGateway)
|
||||||
|
{
|
||||||
|
Content = new StringContent("fail")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetRssMatchingArticles("x");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadGateway);
|
||||||
|
ex.Which.Message.Should().Be("fail");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
373
Lantean.QBitTorrentClient.Test/ApiClientSearchTests.cs
Normal file
373
Lantean.QBitTorrentClient.Test/ApiClientSearchTests.cs
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
using System.Net;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class ApiClientSearchTests
|
||||||
|
{
|
||||||
|
private readonly ApiClient _target;
|
||||||
|
private readonly StubHttpMessageHandler _handler;
|
||||||
|
|
||||||
|
public ApiClientSearchTests()
|
||||||
|
{
|
||||||
|
_handler = new StubHttpMessageHandler();
|
||||||
|
var http = new HttpClient(_handler) { BaseAddress = new Uri("http://localhost/") };
|
||||||
|
_target = new ApiClient(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_PatternAndPlugins_WHEN_StartSearch_THEN_ShouldPOSTFormAndReturnId()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/search/start");
|
||||||
|
req.Content!.Headers.ContentType!.MediaType.Should().Be("application/x-www-form-urlencoded");
|
||||||
|
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("pattern=My+pattern&plugins=a%7Cb%7Cc&category=all");
|
||||||
|
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{\"id\":123}")
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var id = await _target.StartSearch("My pattern", new[] { "a", "b", "c" });
|
||||||
|
|
||||||
|
id.Should().Be(123);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_StartSearch_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.StartSearch("p", new[] { "x" });
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
|
ex.Which.Message.Should().Be("bad");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Id_WHEN_StopSearch_THEN_ShouldPOSTFormWithId()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/search/stop");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("id=77");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.StopSearch(77);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Id_WHEN_GetSearchStatus_THEN_ShouldGETWithIdAndReturnNullOnEmpty()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/search/status?id=5");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[]")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var status = await _target.GetSearchStatus(5);
|
||||||
|
|
||||||
|
status.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NotFound_WHEN_GetSearchStatus_THEN_ShouldReturnNull()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
|
||||||
|
|
||||||
|
var status = await _target.GetSearchStatus(1);
|
||||||
|
|
||||||
|
status.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetSearchStatus_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
Content = new StringContent("nope")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetSearchStatus(2);
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||||
|
ex.Which.Message.Should().Be("nope");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Request_WHEN_GetSearchesStatus_THEN_ShouldGETAndReturnListOrEmptyOnBadJson()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[]")
|
||||||
|
});
|
||||||
|
|
||||||
|
var list = await _target.GetSearchesStatus();
|
||||||
|
list.Should().NotBeNull();
|
||||||
|
list.Count.Should().Be(0);
|
||||||
|
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("oops")
|
||||||
|
});
|
||||||
|
|
||||||
|
var empty = await _target.GetSearchesStatus();
|
||||||
|
empty.Should().NotBeNull();
|
||||||
|
empty.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_IdOnly_WHEN_GetSearchResults_THEN_ShouldGETWithIdOnly()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/search/results?id=9");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var results = await _target.GetSearchResults(9);
|
||||||
|
|
||||||
|
results.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_LimitAndOffset_WHEN_GetSearchResults_THEN_ShouldGETWithAllParamsInOrder()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/search/results?id=9&limit=50&offset=100");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var results = await _target.GetSearchResults(9, limit: 50, offset: 100);
|
||||||
|
|
||||||
|
results.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetSearchResults_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
||||||
|
{
|
||||||
|
Content = new StringContent("err")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetSearchResults(1);
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
|
||||||
|
ex.Which.Message.Should().Be("err");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Id_WHEN_DeleteSearch_THEN_ShouldPOSTFormWithId()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/search/delete");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("id=3");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.DeleteSearch(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_DeleteSearch_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.DeleteSearch(3);
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
|
ex.Which.Message.Should().Be("bad");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Request_WHEN_GetSearchPlugins_THEN_ShouldGETAndReturnListOrEmptyOnBadJson()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[]")
|
||||||
|
});
|
||||||
|
|
||||||
|
var list = await _target.GetSearchPlugins();
|
||||||
|
list.Should().NotBeNull();
|
||||||
|
list.Count.Should().Be(0);
|
||||||
|
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var empty = await _target.GetSearchPlugins();
|
||||||
|
empty.Should().NotBeNull();
|
||||||
|
empty.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Sources_WHEN_InstallSearchPlugins_THEN_ShouldPOSTPipeSeparatedSources()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/search/installPlugin");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("sources=s1%7Cs2");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.InstallSearchPlugins("s1", "s2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_InstallSearchPlugins_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Conflict)
|
||||||
|
{
|
||||||
|
Content = new StringContent("conflict")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.InstallSearchPlugins("s");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
||||||
|
ex.Which.Message.Should().Be("conflict");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Names_WHEN_UninstallSearchPlugins_THEN_ShouldPOSTPipeSeparatedNames()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/search/uninstallPlugin");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("names=p1%7Cp2");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.UninstallSearchPlugins("p1", "p2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_UninstallSearchPlugins_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
Content = new StringContent("nope")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.UninstallSearchPlugins("p");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||||
|
ex.Which.Message.Should().Be("nope");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Names_WHEN_EnableSearchPlugins_THEN_ShouldPOSTNamesAndEnableTrue()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/search/enablePlugin");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("names=p1%7Cp2&enable=true");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.EnableSearchPlugins("p1", "p2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Names_WHEN_DisableSearchPlugins_THEN_ShouldPOSTNamesAndEnableFalse()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/search/enablePlugin");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("names=p1%7Cp2&enable=false");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.DisableSearchPlugins("p1", "p2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_EnableOrDisableSearchPlugins_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
||||||
|
{
|
||||||
|
Content = new StringContent("err")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act1 = async () => await _target.EnableSearchPlugins("p");
|
||||||
|
var ex1 = await act1.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex1.Which.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
|
||||||
|
ex1.Which.Message.Should().Be("err");
|
||||||
|
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
||||||
|
{
|
||||||
|
Content = new StringContent("err2")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act2 = async () => await _target.DisableSearchPlugins("p");
|
||||||
|
var ex2 = await act2.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex2.Which.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
|
||||||
|
ex2.Which.Message.Should().Be("err2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Request_WHEN_UpdateSearchPlugins_THEN_ShouldPOSTAndNotThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/search/updatePlugins");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.UpdateSearchPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_UpdateSearchPlugins_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadGateway)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.UpdateSearchPlugins();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadGateway);
|
||||||
|
ex.Which.Message.Should().Be("bad");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
113
Lantean.QBitTorrentClient.Test/ApiClientSyncTests.cs
Normal file
113
Lantean.QBitTorrentClient.Test/ApiClientSyncTests.cs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
using System.Net;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class ApiClientSyncTests
|
||||||
|
{
|
||||||
|
private readonly ApiClient _target;
|
||||||
|
private readonly StubHttpMessageHandler _handler;
|
||||||
|
|
||||||
|
public ApiClientSyncTests()
|
||||||
|
{
|
||||||
|
_handler = new StubHttpMessageHandler();
|
||||||
|
var http = new HttpClient(_handler)
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri("http://localhost/")
|
||||||
|
};
|
||||||
|
_target = new ApiClient(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_RequestId_WHEN_GetMainData_THEN_ShouldGETWithRidAndDeserialize()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/sync/maindata?rid=123");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.GetMainData(123);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetMainData_THEN_ShouldThrowWithStatusAndMessage()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadGateway)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetMainData(1);
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadGateway);
|
||||||
|
ex.Which.Message.Should().Be("bad");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NullJsonBody_WHEN_GetMainData_THEN_ShouldThrowInvalidOperation()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("null")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetMainData(5);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<InvalidOperationException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_HashAndRid_WHEN_GetTorrentPeersData_THEN_ShouldGETWithParamsAndDeserialize()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/sync/torrentPeers?hash=abcdef&rid=7");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.GetTorrentPeersData("abcdef", 7);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetTorrentPeersData_THEN_ShouldThrowWithStatusAndMessage()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
Content = new StringContent("missing")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetTorrentPeersData("abc", 1);
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||||
|
ex.Which.Message.Should().Be("missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NullJsonBody_WHEN_GetTorrentPeersData_THEN_ShouldThrowInvalidOperation()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("null")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetTorrentPeersData("abc", 1);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<InvalidOperationException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
using System.Net;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class ApiClientTorrentAutoAndRenameTests
|
||||||
|
{
|
||||||
|
private readonly ApiClient _target;
|
||||||
|
private readonly StubHttpMessageHandler _handler;
|
||||||
|
|
||||||
|
public ApiClientTorrentAutoAndRenameTests()
|
||||||
|
{
|
||||||
|
_handler = new StubHttpMessageHandler();
|
||||||
|
var http = new HttpClient(_handler) { BaseAddress = new Uri("http://localhost/") };
|
||||||
|
_target = new ApiClient(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_EnableTrueAndHashes_WHEN_SetAutomaticTorrentManagement_THEN_ShouldPostEnableAndHashes()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/setAutoManagement");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hashes=h1%7Ch2&enable=true");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.SetAutomaticTorrentManagement(true, false, "h1", "h2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_AllTrue_WHEN_ToggleSequentialDownload_THEN_ShouldOnlyPostHashesAll()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/toggleSequentialDownload");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("hashes=all");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.ToggleSequentialDownload(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Hashes_WHEN_SetFirstLastPiecePriority_THEN_ShouldOnlyPostHashes()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/toggleFirstLastPiecePrio");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("hashes=h");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.SetFirstLastPiecePriority(false, "h");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_ValueTrue_WHEN_SetForceStart_THEN_ShouldPostValueAndHashes()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/setForceStart");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hashes=h1%7Ch2&value=true");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.SetForceStart(true, false, "h1", "h2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_ValueFalse_WHEN_SetSuperSeeding_THEN_ShouldPostValueAndAll()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/setSuperSeeding");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hashes=all&value=false");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.SetSuperSeeding(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_RenameFile_WHEN_RenameFile_THEN_ShouldPostHashAndPaths()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/renameFile");
|
||||||
|
var decoded = Uri.UnescapeDataString(await req.Content!.ReadAsStringAsync(ct));
|
||||||
|
decoded.Should().Be("hash=h&oldPath=old/name&newPath=new/name");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.RenameFile("h", "old/name", "new/name");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_RenameFolder_WHEN_RenameFolder_THEN_ShouldPostHashAndPaths()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/renameFolder");
|
||||||
|
var decoded = Uri.UnescapeDataString(await req.Content!.ReadAsStringAsync(ct));
|
||||||
|
decoded.Should().Be("hash=h&oldPath=old/folder&newPath=new/folder");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.RenameFolder("h", "old/folder", "new/folder");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
using System.Net;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class ApiClientTorrentBasicActionsTests
|
||||||
|
{
|
||||||
|
private readonly ApiClient _target;
|
||||||
|
private readonly StubHttpMessageHandler _handler;
|
||||||
|
|
||||||
|
public ApiClientTorrentBasicActionsTests()
|
||||||
|
{
|
||||||
|
_handler = new StubHttpMessageHandler();
|
||||||
|
var http = new HttpClient(_handler)
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri("http://localhost/")
|
||||||
|
};
|
||||||
|
_target = new ApiClient(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_LocationAndHashes_WHEN_SetTorrentLocation_THEN_ShouldPOSTForm()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/setLocation");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hashes=h1%7Ch2&location=%2Fdata%2Fdl");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.SetTorrentLocation("/data/dl", false, "h1", "h2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_SetTorrentLocation_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.SetTorrentLocation("/x");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
|
ex.Which.Message.Should().Be("bad");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NameAndHash_WHEN_SetTorrentName_THEN_ShouldPOSTForm()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/rename");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hash=hx&name=My+Torrent"); // spaces => '+'
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.SetTorrentName("My Torrent", "hx");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_SetTorrentName_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Conflict)
|
||||||
|
{
|
||||||
|
Content = new StringContent("exists")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.SetTorrentName("n", "h");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
||||||
|
ex.Which.Message.Should().Be("exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_HashesAndComment_WHEN_SetTorrentComment_THEN_ShouldPOSTForm()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/setComment");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hashes=h1%7Ch2&comment=hello+world");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.SetTorrentComment(new[] { "h1", "h2" }, "hello world");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_SetTorrentComment_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
Content = new StringContent("forbidden")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.SetTorrentComment(Array.Empty<string>(), "x");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||||
|
ex.Which.Message.Should().Be("forbidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_CategoryAndHashes_WHEN_SetTorrentCategory_THEN_ShouldPOSTForm()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/setCategory");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hashes=h1%7Ch2&category=Movies");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.SetTorrentCategory("Movies", false, "h1", "h2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_SetTorrentCategory_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadGateway)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.SetTorrentCategory("c");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadGateway);
|
||||||
|
ex.Which.Message.Should().Be("bad");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
using System.Net;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class ApiClientTorrentCategoriesAndTagsTests
|
||||||
|
{
|
||||||
|
private readonly ApiClient _target;
|
||||||
|
private readonly StubHttpMessageHandler _handler;
|
||||||
|
|
||||||
|
public ApiClientTorrentCategoriesAndTagsTests()
|
||||||
|
{
|
||||||
|
_handler = new StubHttpMessageHandler();
|
||||||
|
var http = new HttpClient(_handler) { BaseAddress = new Uri("http://localhost/") };
|
||||||
|
_target = new ApiClient(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_OKJson_WHEN_GetAllCategories_THEN_ShouldDeserializeOrEmptyOnBadJson()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetAllCategories();
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var empty = await _target.GetAllCategories();
|
||||||
|
empty.Should().NotBeNull();
|
||||||
|
empty.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_CategoryAndPath_WHEN_AddCategory_THEN_ShouldPOSTForm()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/createCategory");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("category=Movies&savePath=%2Fdata");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.AddCategory("Movies", "/data");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_CategoryAndPath_WHEN_EditCategory_THEN_ShouldPOSTForm()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/editCategory");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("category=Shows&savePath=%2Ftv");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.EditCategory("Shows", "/tv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Categories_WHEN_RemoveCategories_THEN_ShouldPOSTNewlineSeparated()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/removeCategories");
|
||||||
|
var decoded = Uri.UnescapeDataString(await req.Content!.ReadAsStringAsync(ct));
|
||||||
|
decoded.Should().Be("categories=a\nb\nc");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.RemoveCategories("a", "b", "c");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_TagsAndHashes_WHEN_AddTorrentTags_THEN_ShouldCSVAndEncode()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/addTags");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hashes=h1%7Ch2&tags=one%2Ctwo%2Cthree");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.AddTorrentTags(new[] { "one", "two", "three" }, false, "h1", "h2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_TagsAndAllTrue_WHEN_RemoveTorrentTags_THEN_ShouldCSVAndAll()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/removeTags");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hashes=all&tags=a%2Cb");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.RemoveTorrentTags(new[] { "a", "b" }, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_OKJson_WHEN_GetAllTags_THEN_ShouldDeserializeList()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[\"x\",\"y\"]")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetAllTags();
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(2);
|
||||||
|
result[0].Should().Be("x");
|
||||||
|
result[1].Should().Be("y");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Tags_WHEN_CreateTags_THEN_ShouldCSV()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/createTags");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("tags=a%2Cb%2Cc");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.CreateTags(new[] { "a", "b", "c" });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Tags_WHEN_DeleteTags_THEN_ShouldCSV()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/deleteTags");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("tags=a%2Cb");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.DeleteTags("a", "b");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
288
Lantean.QBitTorrentClient.Test/ApiClientTorrentCreatorTests.cs
Normal file
288
Lantean.QBitTorrentClient.Test/ApiClientTorrentCreatorTests.cs
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
using System.Net;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class ApiClientTorrentCreatorTests
|
||||||
|
{
|
||||||
|
private readonly ApiClient _target;
|
||||||
|
private readonly StubHttpMessageHandler _handler;
|
||||||
|
|
||||||
|
public ApiClientTorrentCreatorTests()
|
||||||
|
{
|
||||||
|
_handler = new StubHttpMessageHandler();
|
||||||
|
var http = new HttpClient(_handler) { BaseAddress = new Uri("http://localhost/") };
|
||||||
|
_target = new ApiClient(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NullRequest_WHEN_AddTorrentCreationTask_THEN_ShouldThrowArgumentNullException()
|
||||||
|
{
|
||||||
|
var act = async () => await _target.AddTorrentCreationTask(null!);
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<ArgumentNullException>();
|
||||||
|
ex.Which.ParamName.Should().Be("request");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_EmptySourcePath_WHEN_AddTorrentCreationTask_THEN_ShouldThrowArgumentException()
|
||||||
|
{
|
||||||
|
var act = async () => await _target.AddTorrentCreationTask(new TorrentCreationTaskRequest
|
||||||
|
{
|
||||||
|
SourcePath = " "
|
||||||
|
});
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<ArgumentException>();
|
||||||
|
ex.Which.ParamName.Should().Be("request");
|
||||||
|
ex.Which.Message.Should().Contain("SourcePath is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_MinimalRequest_WHEN_AddTorrentCreationTask_THEN_ShouldPOSTOnlySourcePathAndReturnEmptyOnEmptyBody()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrentcreator/addTask");
|
||||||
|
req.Content!.Headers.ContentType!.MediaType.Should().Be("application/x-www-form-urlencoded");
|
||||||
|
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("sourcePath=%2Fsrc");
|
||||||
|
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent(string.Empty)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var id = await _target.AddTorrentCreationTask(new TorrentCreationTaskRequest
|
||||||
|
{
|
||||||
|
SourcePath = "/src"
|
||||||
|
});
|
||||||
|
|
||||||
|
id.Should().Be(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_AllFields_WHEN_AddTorrentCreationTask_THEN_ShouldIncludeEveryParameterAndReturnTaskId()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrentcreator/addTask");
|
||||||
|
|
||||||
|
var form = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
var parts = form.Split('&')
|
||||||
|
.Select(p => p.Split('='))
|
||||||
|
.ToDictionary(a => a[0], a => Uri.UnescapeDataString(a.Length > 1 ? a[1] : string.Empty));
|
||||||
|
|
||||||
|
parts["sourcePath"].Should().Be("/src");
|
||||||
|
parts["torrentFilePath"].Should().Be("/out.torrent");
|
||||||
|
parts["pieceSize"].Should().Be("512");
|
||||||
|
parts["private"].Should().Be("true");
|
||||||
|
parts["startSeeding"].Should().Be("false");
|
||||||
|
parts["comment"].Should().Be("hello");
|
||||||
|
parts["source"].Should().Be("mysrc");
|
||||||
|
parts["trackers"].Should().Be("t1|t2");
|
||||||
|
parts["urlSeeds"].Should().Be("u1|u2");
|
||||||
|
parts["format"].Should().Be("v2");
|
||||||
|
parts["optimizeAlignment"].Should().Be("true");
|
||||||
|
parts["paddedFileSizeLimit"].Should().Be("4096");
|
||||||
|
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{\"taskID\":\"task-123\"}")
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var request = new TorrentCreationTaskRequest
|
||||||
|
{
|
||||||
|
SourcePath = "/src",
|
||||||
|
TorrentFilePath = "/out.torrent",
|
||||||
|
PieceSize = 512,
|
||||||
|
Private = true,
|
||||||
|
StartSeeding = false,
|
||||||
|
Comment = "hello",
|
||||||
|
Source = "mysrc",
|
||||||
|
Trackers = new[] { "t1", "t2" },
|
||||||
|
UrlSeeds = new[] { "u1", "u2" },
|
||||||
|
Format = "v2",
|
||||||
|
OptimizeAlignment = true,
|
||||||
|
PaddedFileSizeLimit = 4096
|
||||||
|
};
|
||||||
|
|
||||||
|
var id = await _target.AddTorrentCreationTask(request);
|
||||||
|
|
||||||
|
id.Should().Be("task-123");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_OKButNoTaskIdInJson_WHEN_AddTorrentCreationTask_THEN_ShouldReturnEmptyString()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
});
|
||||||
|
|
||||||
|
var id = await _target.AddTorrentCreationTask(new TorrentCreationTaskRequest
|
||||||
|
{
|
||||||
|
SourcePath = "/src"
|
||||||
|
});
|
||||||
|
|
||||||
|
id.Should().Be(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_AddTorrentCreationTask_THEN_ShouldThrowWithStatusAndMessage()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad req")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.AddTorrentCreationTask(new TorrentCreationTaskRequest { SourcePath = "/src" });
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
|
ex.Which.Message.Should().Be("bad req");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NoTaskId_WHEN_GetTorrentCreationTasks_THEN_ShouldGETWithoutQueryAndReturnList()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.AbsolutePath.Should().Be("/torrentcreator/status");
|
||||||
|
req.RequestUri!.Query.Should().BeEmpty();
|
||||||
|
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[]")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var list = await _target.GetTorrentCreationTasks();
|
||||||
|
|
||||||
|
list.Should().NotBeNull();
|
||||||
|
list.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_TaskId_WHEN_GetTorrentCreationTasks_THEN_ShouldGETWithQuery()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrentcreator/status?taskID=task-1");
|
||||||
|
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[]")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var list = await _target.GetTorrentCreationTasks("task-1");
|
||||||
|
|
||||||
|
list.Should().NotBeNull();
|
||||||
|
list.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_BadJson_WHEN_GetTorrentCreationTasks_THEN_ShouldReturnEmptyList()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("oops")
|
||||||
|
});
|
||||||
|
|
||||||
|
var list = await _target.GetTorrentCreationTasks();
|
||||||
|
|
||||||
|
list.Should().NotBeNull();
|
||||||
|
list.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetTorrentCreationTasks_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)
|
||||||
|
{
|
||||||
|
Content = new StringContent("busy")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetTorrentCreationTasks();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.ServiceUnavailable);
|
||||||
|
ex.Which.Message.Should().Be("busy");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_TaskId_WHEN_GetTorrentCreationTaskFile_THEN_ShouldGETWithQueryAndReturnBytes()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrentcreator/torrentFile?taskID=abc");
|
||||||
|
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new ByteArrayContent(new byte[] { 1, 2 })
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var bytes = await _target.GetTorrentCreationTaskFile("abc");
|
||||||
|
|
||||||
|
bytes.Should().Equal(new byte[] { 1, 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetTorrentCreationTaskFile_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
Content = new StringContent("missing")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetTorrentCreationTaskFile("x");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||||
|
ex.Which.Message.Should().Be("missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_TaskId_WHEN_DeleteTorrentCreationTask_THEN_ShouldPOSTForm()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrentcreator/deleteTask");
|
||||||
|
req.Content!.Headers.ContentType!.MediaType.Should().Be("application/x-www-form-urlencoded");
|
||||||
|
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("taskID=abc");
|
||||||
|
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.DeleteTorrentCreationTask("abc");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_DeleteTorrentCreationTask_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
Content = new StringContent("nope")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.DeleteTorrentCreationTask("abc");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||||
|
ex.Which.Message.Should().Be("nope");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
using System.Net;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class ApiClientTorrentListAndGettersTests
|
||||||
|
{
|
||||||
|
private readonly ApiClient _target;
|
||||||
|
private readonly StubHttpMessageHandler _handler;
|
||||||
|
|
||||||
|
public ApiClientTorrentListAndGettersTests()
|
||||||
|
{
|
||||||
|
_handler = new StubHttpMessageHandler();
|
||||||
|
var http = new HttpClient(_handler)
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri("http://localhost/")
|
||||||
|
};
|
||||||
|
_target = new ApiClient(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NoFilters_WHEN_GetTorrentList_THEN_ShouldGETWithoutQueryAndReturnList()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.AbsolutePath.Should().Be("/torrents/info");
|
||||||
|
req.RequestUri!.Query.Should().BeEmpty();
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[]")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.GetTorrentList();
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_AllFilters_WHEN_GetTorrentList_THEN_ShouldIncludeAllParamsInOrder()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.AbsolutePath.Should().Be("/torrents/info");
|
||||||
|
req.RequestUri!.Query.Should().Be("?filter=active&category=Movies&tag=HD&sort=name&reverse=true&limit=50&offset=5&hashes=a%7Cb%7Cc&private=true&includeFiles=false");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[]")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.GetTorrentList(
|
||||||
|
filter: "active",
|
||||||
|
category: "Movies",
|
||||||
|
tag: "HD",
|
||||||
|
sort: "name",
|
||||||
|
reverse: true,
|
||||||
|
limit: 50,
|
||||||
|
offset: 5,
|
||||||
|
isPrivate: true,
|
||||||
|
includeFiles: false,
|
||||||
|
"a", "b", "c"
|
||||||
|
);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_BadJson_WHEN_GetTorrentList_THEN_ShouldReturnEmptyList()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("not json")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetTorrentList();
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetTorrentList_THEN_ShouldThrowWithStatusAndMessage()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
Content = new StringContent("no")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetTorrentList();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||||
|
ex.Which.Message.Should().Be("no");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Hash_WHEN_GetTorrentProperties_THEN_ShouldGETWithHashAndDeserialize()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/properties?hash=abc");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.GetTorrentProperties("abc");
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetTorrentProperties_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
Content = new StringContent("missing")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetTorrentProperties("abc");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||||
|
ex.Which.Message.Should().Be("missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Hash_WHEN_GetTorrentTrackers_THEN_ShouldGETAndReturnList()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/trackers?hash=xyz");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[]")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.GetTorrentTrackers("xyz");
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Hash_WHEN_GetTorrentWebSeeds_THEN_ShouldGETAndReturnList()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/webseeds?hash=h1");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[]")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.GetTorrentWebSeeds("h1");
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_HashOnly_WHEN_GetTorrentContents_THEN_ShouldGETWithHashOnly()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.AbsolutePath.Should().Be("/torrents/files");
|
||||||
|
req.RequestUri!.Query.Should().Be("?hash=abc");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[]")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.GetTorrentContents("abc");
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Indexes_WHEN_GetTorrentContents_THEN_ShouldGETWithIndexesPipeSeparated()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.AbsolutePath.Should().Be("/torrents/files");
|
||||||
|
req.RequestUri!.Query.Should().Be("?hash=abc&indexes=1%7C2%7C3");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[]")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.GetTorrentContents("abc", 1, 2, 3);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_BadJson_WHEN_GetTorrentContents_THEN_ShouldReturnEmptyList()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("oops")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetTorrentContents("abc");
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetTorrentContents_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetTorrentContents("abc");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
|
ex.Which.Message.Should().Be("bad");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Hash_WHEN_GetTorrentPieceStates_THEN_ShouldGETAndReturnListOrEmptyOnBadJson()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/pieceStates?hash=abc");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("not json")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.GetTorrentPieceStates("abc");
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetTorrentPieceStates_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
Content = new StringContent("missing")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetTorrentPieceStates("abc");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||||
|
ex.Which.Message.Should().Be("missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Hash_WHEN_GetTorrentPieceHashes_THEN_ShouldGETAndReturnStrings()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Get);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/pieceHashes?hash=abc");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("[\"h1\",\"h2\"]")
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _target.GetTorrentPieceHashes("abc");
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(2);
|
||||||
|
result[0].Should().Be("h1");
|
||||||
|
result[1].Should().Be("h2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_BadJson_WHEN_GetTorrentPieceHashes_THEN_ShouldReturnEmptyList()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetTorrentPieceHashes("abc");
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetTorrentPieceHashes_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
||||||
|
{
|
||||||
|
Content = new StringContent("err")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetTorrentPieceHashes("abc");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
|
||||||
|
ex.Which.Message.Should().Be("err");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
using System.Net;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class ApiClientTorrentPriorityAndLimitsTests
|
||||||
|
{
|
||||||
|
private readonly ApiClient _target;
|
||||||
|
private readonly StubHttpMessageHandler _handler;
|
||||||
|
|
||||||
|
public ApiClientTorrentPriorityAndLimitsTests()
|
||||||
|
{
|
||||||
|
_handler = new StubHttpMessageHandler();
|
||||||
|
var http = new HttpClient(_handler) { BaseAddress = new Uri("http://localhost/") };
|
||||||
|
_target = new ApiClient(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Hashes_WHEN_IncreaseTorrentPriority_THEN_ShouldPostHashes()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/increasePrio");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("hashes=h1%7Ch2");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.IncreaseTorrentPriority(false, "h1", "h2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_AllTrue_WHEN_DecreaseTorrentPriority_THEN_ShouldPostAll()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/decreasePrio");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("hashes=all");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.DecreaseTorrentPriority(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Hashes_WHEN_MaxTorrentPriority_THEN_ShouldPostHashes()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/topPrio");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("hashes=h");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.MaxTorrentPriority(false, "h");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Hashes_WHEN_MinTorrentPriority_THEN_ShouldPostHashes()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/bottomPrio");
|
||||||
|
(await req.Content!.ReadAsStringAsync(ct)).Should().Be("hashes=h1%7Ch2%7Ch3");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.MinTorrentPriority(false, "h1", "h2", "h3");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_FileIdsAndPriority_WHEN_SetFilePriority_THEN_ShouldPostIdsAndPriorityInt()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/filePrio");
|
||||||
|
var body = Uri.UnescapeDataString(await req.Content!.ReadAsStringAsync(ct));
|
||||||
|
body.Should().Be("hash=h1&id=1|2|3&priority=7");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.SetFilePriority("h1", new[] { 1, 2, 3 }, (Priority)7);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Hashes_WHEN_GetTorrentDownloadLimit_THEN_ShouldReturnDictionary()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{\"h1\":1000,\"h2\":0}")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetTorrentDownloadLimit(false, "h1", "h2");
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(2);
|
||||||
|
result["h1"].Should().Be(1000);
|
||||||
|
result["h2"].Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_BadJson_WHEN_GetTorrentDownloadLimit_THEN_ShouldReturnEmptyDict()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("oops")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetTorrentDownloadLimit();
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_LimitAndHashes_WHEN_SetTorrentDownloadLimit_THEN_ShouldPostLimitAndHashes()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/setDownloadLimit");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hashes=h%7Ci&limit=500");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.SetTorrentDownloadLimit(500, false, "h", "i");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Ratios_WHEN_SetTorrentShareLimit_THEN_ShouldPostAllValuesAndOptionalAction()
|
||||||
|
{
|
||||||
|
var ratio = 1.5f.ToString();
|
||||||
|
var seed = 2.25f.ToString();
|
||||||
|
var inactive = 0.75f.ToString();
|
||||||
|
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
var form = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
var parts = form.Split('&').ToDictionary(
|
||||||
|
s => s.Split('=')[0],
|
||||||
|
s => Uri.UnescapeDataString(s.Split('=')[1])
|
||||||
|
);
|
||||||
|
|
||||||
|
parts["hashes"].Should().Be("h1|h2");
|
||||||
|
parts["ratioLimit"].Should().Be(ratio);
|
||||||
|
parts["seedingTimeLimit"].Should().Be(seed);
|
||||||
|
parts["inactiveSeedingTimeLimit"].Should().Be(inactive);
|
||||||
|
|
||||||
|
// enum is serialized as its NAME, not numeric value
|
||||||
|
parts["shareLimitAction"].Should().Be(ShareLimitAction.Remove.ToString());
|
||||||
|
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.SetTorrentShareLimit(
|
||||||
|
ratioLimit: 1.5f,
|
||||||
|
seedingTimeLimit: 2.25f,
|
||||||
|
inactiveSeedingTimeLimit: 0.75f,
|
||||||
|
shareLimitAction: ShareLimitAction.Remove, // <-- changed from Delete
|
||||||
|
all: false,
|
||||||
|
hashes: new[] { "h1", "h2" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NoAction_WHEN_SetTorrentShareLimit_THEN_ShouldOmitActionField()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
var form = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
form.Should().Contain("ratioLimit=");
|
||||||
|
form.Should().Contain("seedingTimeLimit=");
|
||||||
|
form.Should().Contain("inactiveSeedingTimeLimit=");
|
||||||
|
form.Should().NotContain("shareLimitAction=");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.SetTorrentShareLimit(1, 2, 3, shareLimitAction: null, all: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Hashes_WHEN_GetTorrentUploadLimit_THEN_ShouldReturnDictionary()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{\"x\":10}")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetTorrentUploadLimit(false, "x");
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(1);
|
||||||
|
result["x"].Should().Be(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_BadJson_WHEN_GetTorrentUploadLimit_THEN_ShouldReturnEmptyDict()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetTorrentUploadLimit();
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_LimitAndHashes_WHEN_SetTorrentUploadLimit_THEN_ShouldPostLimitAndHashes()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/setUploadLimit");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hashes=h1&limit=42");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.SetTorrentUploadLimit(42, false, "h1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
using System.Net;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class ApiClientTorrentWebSeedsAndLifecycleTests
|
||||||
|
{
|
||||||
|
private readonly ApiClient _target;
|
||||||
|
private readonly StubHttpMessageHandler _handler;
|
||||||
|
|
||||||
|
public ApiClientTorrentWebSeedsAndLifecycleTests()
|
||||||
|
{
|
||||||
|
_handler = new StubHttpMessageHandler();
|
||||||
|
var http = new HttpClient(_handler)
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri("http://localhost/")
|
||||||
|
};
|
||||||
|
_target = new ApiClient(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Urls_WHEN_AddTorrentWebSeeds_THEN_ShouldPOSTFormWithPipeSeparatedUrls()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/addWebSeeds");
|
||||||
|
req.Content!.Headers.ContentType!.MediaType.Should().Be("application/x-www-form-urlencoded");
|
||||||
|
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hash=h123&urls=a%7Cb%7Cc");
|
||||||
|
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.AddTorrentWebSeeds("h123", new[] { "a", "b", "c" });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_AddTorrentWebSeeds_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.AddTorrentWebSeeds("h", new[] { "u" });
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
|
ex.Which.Message.Should().Be("bad");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Urls_WHEN_RemoveTorrentWebSeeds_THEN_ShouldPOSTFormWithPipeSeparatedUrls()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/removeWebSeeds");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hash=h1&urls=http%3A%2F%2Fe1%7Chttp%3A%2F%2Fe2");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.RemoveTorrentWebSeeds("h1", new[] { "http://e1", "http://e2" });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_RemoveTorrentWebSeeds_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Conflict)
|
||||||
|
{
|
||||||
|
Content = new StringContent("conflict")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.RemoveTorrentWebSeeds("h", new[] { "u" });
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
||||||
|
ex.Which.Message.Should().Be("conflict");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_EditParams_WHEN_EditTorrentWebSeed_THEN_ShouldPOSTFormWithAllFields()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/editWebSeed");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hash=hx&origUrl=old%2Furl&newUrl=new%2Furl");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.EditTorrentWebSeed("hx", "old/url", "new/url");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_EditTorrentWebSeed_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
Content = new StringContent("missing")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.EditTorrentWebSeed("h", "o", "n");
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||||
|
ex.Which.Message.Should().Be("missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NoArgs_WHEN_StopTorrents_THEN_ShouldPOSTWithEmptyHashesValue()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/stop");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hashes=");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.StopTorrents();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_AllTrue_WHEN_StopTorrents_THEN_ShouldSendAllLiteral()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hashes=all");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.StopTorrents(all: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Hashes_WHEN_StartTorrents_THEN_ShouldPipeSeparate()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/start");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hashes=a%7Cb%7Cc");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.StartTorrents(false, "a", "b", "c");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_DeleteFilesTrue_WHEN_DeleteTorrents_THEN_ShouldIncludeDeleteFlag()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/delete");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hashes=a%7Cb&deleteFiles=true");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.DeleteTorrents(false, true, "a", "b");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_AllTrueAndDefaultDeleteFlag_WHEN_DeleteTorrents_THEN_ShouldSendFalseAndAll()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hashes=all&deleteFiles=false");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.DeleteTorrents(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_DeleteTorrents_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.DeleteTorrents();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
|
ex.Which.Message.Should().Be("bad");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Hashes_WHEN_RecheckTorrents_THEN_ShouldPOST()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/recheck");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hashes=h1%7Ch2");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.RecheckTorrents(false, "h1", "h2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_AllTrueAndNoTrackers_WHEN_ReannounceTorrents_THEN_ShouldOnlySendHashes()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/torrents/reannounce");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("hashes=all");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.ReannounceTorrents(true, trackers: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Trackers_WHEN_ReannounceTorrents_THEN_ShouldIncludeUrlsJoinedByPipe()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
// Decode to assert actual values
|
||||||
|
var parts = body.Split('&').ToDictionary(
|
||||||
|
s => s.Split('=')[0],
|
||||||
|
s => Uri.UnescapeDataString(s.Split('=')[1])
|
||||||
|
);
|
||||||
|
|
||||||
|
parts["hashes"].Should().Be("h1|h2");
|
||||||
|
parts["urls"].Should().Be("http://t1|http://t2");
|
||||||
|
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.ReannounceTorrents(false, new[] { "http://t1", "http://t2" }, "h1", "h2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_ReannounceTorrents_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
Content = new StringContent("nope")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.ReannounceTorrents();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||||
|
ex.Which.Message.Should().Be("nope");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
264
Lantean.QBitTorrentClient.Test/ApiClientTransferInfoTests.cs
Normal file
264
Lantean.QBitTorrentClient.Test/ApiClientTransferInfoTests.cs
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
using System.Net;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class ApiClientTransferInfoTests
|
||||||
|
{
|
||||||
|
private readonly ApiClient _target;
|
||||||
|
private readonly StubHttpMessageHandler _handler;
|
||||||
|
|
||||||
|
public ApiClientTransferInfoTests()
|
||||||
|
{
|
||||||
|
_handler = new StubHttpMessageHandler();
|
||||||
|
var http = new HttpClient(_handler)
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri("http://localhost/")
|
||||||
|
};
|
||||||
|
_target = new ApiClient(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_OKJson_WHEN_GetGlobalTransferInfo_THEN_ShouldDeserialize()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetGlobalTransferInfo();
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetGlobalTransferInfo_THEN_ShouldThrowWithStatusAndMessage()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadGateway)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetGlobalTransferInfo();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadGateway);
|
||||||
|
ex.Which.Message.Should().Be("bad");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_ResponseIsOne_WHEN_GetAlternativeSpeedLimitsState_THEN_ShouldBeTrue()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("1")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetAlternativeSpeedLimitsState();
|
||||||
|
|
||||||
|
result.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_ResponseIsZero_WHEN_GetAlternativeSpeedLimitsState_THEN_ShouldBeFalse()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("0")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetAlternativeSpeedLimitsState();
|
||||||
|
|
||||||
|
result.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_GetAlternativeSpeedLimitsState_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
Content = new StringContent("no")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetAlternativeSpeedLimitsState();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||||
|
ex.Which.Message.Should().Be("no");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_OK_WHEN_ToggleAlternativeSpeedLimits_THEN_ShouldPOSTAndNotThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (req, _) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/transfer/toggleSpeedLimitsMode");
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.ToggleAlternativeSpeedLimits();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_ToggleAlternativeSpeedLimits_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
||||||
|
{
|
||||||
|
Content = new StringContent("err")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.ToggleAlternativeSpeedLimits();
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
|
||||||
|
ex.Which.Message.Should().Be("err");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Digits_WHEN_GetGlobalDownloadLimit_THEN_ShouldParseLong()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("1234567890")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetGlobalDownloadLimit();
|
||||||
|
|
||||||
|
result.Should().Be(1234567890);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_InvalidNumber_WHEN_GetGlobalDownloadLimit_THEN_ShouldThrowFormatException()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("oops")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetGlobalDownloadLimit();
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<FormatException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Limit_WHEN_SetGlobalDownloadLimit_THEN_ShouldPOSTFormWithLimit()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/transfer/setDownloadLimit");
|
||||||
|
req.Content!.Headers.ContentType!.MediaType.Should().Be("application/x-www-form-urlencoded");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("limit=5000");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.SetGlobalDownloadLimit(5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_SetGlobalDownloadLimit_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||||
|
{
|
||||||
|
Content = new StringContent("bad")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.SetGlobalDownloadLimit(1);
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
|
ex.Which.Message.Should().Be("bad");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Digits_WHEN_GetGlobalUploadLimit_THEN_ShouldParseLong()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("4321")
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await _target.GetGlobalUploadLimit();
|
||||||
|
|
||||||
|
result.Should().Be(4321);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_InvalidNumber_WHEN_GetGlobalUploadLimit_THEN_ShouldThrowFormatException()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Content = new StringContent("NaN")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.GetGlobalUploadLimit();
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<FormatException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Limit_WHEN_SetGlobalUploadLimit_THEN_ShouldPOSTFormWithLimit()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/transfer/setUploadLimit");
|
||||||
|
req.Content!.Headers.ContentType!.MediaType.Should().Be("application/x-www-form-urlencoded");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("limit=9001");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.SetGlobalUploadLimit(9001);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_SetGlobalUploadLimit_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
Content = new StringContent("nope")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.SetGlobalUploadLimit(1);
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||||
|
ex.Which.Message.Should().Be("nope");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_EmptyPeers_WHEN_BanPeers_THEN_ShouldPOSTFormWithEmptyPeersValue()
|
||||||
|
{
|
||||||
|
_handler.Responder = async (req, ct) =>
|
||||||
|
{
|
||||||
|
req.Method.Should().Be(HttpMethod.Post);
|
||||||
|
req.RequestUri!.ToString().Should().Be("http://localhost/transfer/banPeers");
|
||||||
|
req.Content!.Headers.ContentType!.MediaType.Should().Be("application/x-www-form-urlencoded");
|
||||||
|
var body = await req.Content!.ReadAsStringAsync(ct);
|
||||||
|
body.Should().Be("peers=");
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _target.BanPeers(Array.Empty<PeerId>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonSuccess_WHEN_BanPeers_THEN_ShouldThrow()
|
||||||
|
{
|
||||||
|
_handler.Responder = (_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Conflict)
|
||||||
|
{
|
||||||
|
Content = new StringContent("conflict")
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = async () => await _target.BanPeers(Array.Empty<PeerId>());
|
||||||
|
|
||||||
|
var ex = await act.Should().ThrowAsync<HttpRequestException>();
|
||||||
|
ex.Which.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
||||||
|
ex.Which.Message.Should().Be("conflict");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
using Lantean.QBitTorrentClient.Converters;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test.Converters
|
||||||
|
{
|
||||||
|
public class CommaSeparatedJsonConverterTests
|
||||||
|
{
|
||||||
|
private static JsonSerializerOptions CreateOptions()
|
||||||
|
{
|
||||||
|
var o = new JsonSerializerOptions();
|
||||||
|
o.Converters.Add(new CommaSeparatedJsonConverter());
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_EmptyString_WHEN_Read_THEN_ShouldReturnEmptyReadOnlyList()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var json = "\"\"";
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<IReadOnlyList<string>>(json, options);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result!.Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_CommaSeparatedWithSpacesAndEmpties_WHEN_Read_THEN_ShouldSplitTrimAndRemoveEmpties()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
// contains spaces and empty segments between commas
|
||||||
|
var json = "\" alpha , , beta ,, , gamma \"";
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<IReadOnlyList<string>>(json, options);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result!.Count.Should().Be(3);
|
||||||
|
result[0].Should().Be("alpha");
|
||||||
|
result[1].Should().Be("beta");
|
||||||
|
result[2].Should().Be("gamma");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_NonStringToken_WHEN_Read_THEN_ShouldThrowJsonException()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var json = "123"; // number token, not a string
|
||||||
|
|
||||||
|
var act = () => JsonSerializer.Deserialize<IReadOnlyList<string>>(json, options)!;
|
||||||
|
|
||||||
|
var ex = act.Should().Throw<JsonException>();
|
||||||
|
ex.Which.Message.Should().Be("Must be of type string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_List_WHEN_Write_THEN_ShouldOutputSingleJsonStringCommaJoined()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
IReadOnlyList<string> value = new[] { "a", "b", "c" };
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(value, options);
|
||||||
|
|
||||||
|
json.Should().Be("\"a,b,c\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_EmptyList_WHEN_Write_THEN_ShouldOutputEmptyJsonString()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
IReadOnlyList<string> value = Array.Empty<string>();
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(value, options);
|
||||||
|
|
||||||
|
json.Should().Be("\"\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_ReadResult_WHEN_AttemptToMutate_THEN_ShouldThrowNotSupportedException()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var json = "\"x,y\"";
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<IReadOnlyList<string>>(json, options)!;
|
||||||
|
|
||||||
|
// Converter returns list.AsReadOnly() -> ReadOnlyCollection<string>
|
||||||
|
var asList = (IList<string>)result;
|
||||||
|
var act = () => asList.Add("z");
|
||||||
|
|
||||||
|
act.Should().Throw<NotSupportedException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
using Lantean.QBitTorrentClient.Converters;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test.Converters
|
||||||
|
{
|
||||||
|
public class DownloadPathOptionJsonConverterTests
|
||||||
|
{
|
||||||
|
private static JsonSerializerOptions CreateOptions()
|
||||||
|
{
|
||||||
|
var o = new JsonSerializerOptions();
|
||||||
|
o.Converters.Add(new DownloadPathOptionJsonConverter());
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Read --------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_JsonNull_WHEN_Read_THEN_ShouldReturnNull()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var json = "null";
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<DownloadPathOption>(json, options);
|
||||||
|
|
||||||
|
result.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_JsonFalse_WHEN_Read_THEN_ShouldReturnDisabledWithNullPath()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var json = "false";
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<DownloadPathOption>(json, options);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result!.Enabled.Should().BeFalse();
|
||||||
|
result.Path.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_JsonTrue_WHEN_Read_THEN_ShouldReturnEnabledWithNullPath()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var json = "true";
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<DownloadPathOption>(json, options);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result!.Enabled.Should().BeTrue();
|
||||||
|
result.Path.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_JsonString_WHEN_Read_THEN_ShouldReturnEnabledWithThatPath()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var json = "\"/downloads\"";
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<DownloadPathOption>(json, options);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result!.Enabled.Should().BeTrue();
|
||||||
|
result.Path.Should().Be("/downloads");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_UnexpectedToken_WHEN_Read_THEN_ShouldThrowJsonException()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var json = "123"; // number token, not supported
|
||||||
|
|
||||||
|
var act = () => JsonSerializer.Deserialize<DownloadPathOption>(json, options)!;
|
||||||
|
|
||||||
|
var ex = act.Should().Throw<JsonException>();
|
||||||
|
ex.Which.Message.Should().Contain("Unexpected token");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Write --------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_NullValue_WHEN_Write_THEN_ShouldEmitJsonNull()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
DownloadPathOption? value = null;
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(value, options);
|
||||||
|
|
||||||
|
json.Should().Be("null");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_Disabled_WHEN_Write_THEN_ShouldEmitFalse()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var value = new DownloadPathOption(false, null);
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(value, options);
|
||||||
|
|
||||||
|
json.Should().Be("false");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_EnabledWithNullPath_WHEN_Write_THEN_ShouldEmitTrue()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var value = new DownloadPathOption(true, null);
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(value, options);
|
||||||
|
|
||||||
|
json.Should().Be("true");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_EnabledWithWhitespacePath_WHEN_Write_THEN_ShouldEmitTrue()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var value = new DownloadPathOption(true, " ");
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(value, options);
|
||||||
|
|
||||||
|
json.Should().Be("true");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_EnabledWithPath_WHEN_Write_THEN_ShouldEmitThatString()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var value = new DownloadPathOption(true, "/dl/path");
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(value, options);
|
||||||
|
|
||||||
|
json.Should().Be("\"/dl/path\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Round-trip sanity --------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_PathString_WHEN_RoundTrip_THEN_ShouldPreserveEnabledTrueAndPath()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var original = new DownloadPathOption(true, "/data");
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(original, options);
|
||||||
|
var round = JsonSerializer.Deserialize<DownloadPathOption>(json, options)!;
|
||||||
|
|
||||||
|
round.Enabled.Should().BeTrue();
|
||||||
|
round.Path.Should().Be("/data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
using Lantean.QBitTorrentClient.Converters;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test.Converters
|
||||||
|
{
|
||||||
|
public class SaveLocationJsonConverterTests
|
||||||
|
{
|
||||||
|
private static JsonSerializerOptions CreateOptions()
|
||||||
|
{
|
||||||
|
var o = new JsonSerializerOptions();
|
||||||
|
o.Converters.Add(new SaveLocationJsonConverter());
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Read --------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_String_WHEN_Read_THEN_ShouldReturnCustomPath()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var json = "\"/downloads\"";
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<SaveLocation>(json, options);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.SavePath.Should().Be("/downloads");
|
||||||
|
result.IsDefaultFolder.Should().BeFalse();
|
||||||
|
result.IsWatchedFolder.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_NumberZero_WHEN_Read_THEN_ShouldReturnWatchedFolder()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var json = "0";
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<SaveLocation>(json, options);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.IsWatchedFolder.Should().BeTrue();
|
||||||
|
result.IsDefaultFolder.Should().BeFalse();
|
||||||
|
result.SavePath.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_NumberOne_WHEN_Read_THEN_ShouldReturnDefaultFolder()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var json = "1";
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<SaveLocation>(json, options);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.IsDefaultFolder.Should().BeTrue();
|
||||||
|
result.IsWatchedFolder.Should().BeFalse();
|
||||||
|
result.SavePath.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_UnsupportedToken_WHEN_Read_THEN_ShouldThrowJsonException()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var json = "true"; // bool token is not supported
|
||||||
|
|
||||||
|
var act = () => JsonSerializer.Deserialize<SaveLocation>(json, options)!;
|
||||||
|
|
||||||
|
var ex = act.Should().Throw<JsonException>();
|
||||||
|
ex.Which.Message.Should().Contain("Unsupported token type");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Write --------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_WatchedFolder_WHEN_Write_THEN_ShouldEmitZero()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var value = SaveLocation.Create(0);
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(value, options);
|
||||||
|
|
||||||
|
json.Should().Be("0");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_DefaultFolder_WHEN_Write_THEN_ShouldEmitOne()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var value = SaveLocation.Create(1);
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(value, options);
|
||||||
|
|
||||||
|
json.Should().Be("1");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_CustomPath_WHEN_Write_THEN_ShouldEmitJsonString()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var value = SaveLocation.Create("/data/films");
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(value, options);
|
||||||
|
|
||||||
|
json.Should().Be("\"/data/films\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Round-trip sanity --------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_PathString_WHEN_RoundTrip_THEN_ShouldPreserveCustomPath()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var original = SaveLocation.Create("/data");
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(original, options);
|
||||||
|
var round = JsonSerializer.Deserialize<SaveLocation>(json, options)!;
|
||||||
|
|
||||||
|
round.SavePath.Should().Be("/data");
|
||||||
|
round.IsDefaultFolder.Should().BeFalse();
|
||||||
|
round.IsWatchedFolder.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_Zero_WHEN_RoundTrip_THEN_ShouldStayWatchedFolder()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var original = SaveLocation.Create(0);
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(original, options);
|
||||||
|
var round = JsonSerializer.Deserialize<SaveLocation>(json, options)!;
|
||||||
|
|
||||||
|
round.IsWatchedFolder.Should().BeTrue();
|
||||||
|
round.IsDefaultFolder.Should().BeFalse();
|
||||||
|
round.SavePath.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_One_WHEN_RoundTrip_THEN_ShouldStayDefaultFolder()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var original = SaveLocation.Create(1);
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(original, options);
|
||||||
|
var round = JsonSerializer.Deserialize<SaveLocation>(json, options)!;
|
||||||
|
|
||||||
|
round.IsDefaultFolder.Should().BeTrue();
|
||||||
|
round.IsWatchedFolder.Should().BeFalse();
|
||||||
|
round.SavePath.Should().BeNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using AwesomeAssertions;
|
||||||
|
using Lantean.QBitTorrentClient.Converters;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test.Converters
|
||||||
|
{
|
||||||
|
public class StringFloatJsonConverterTests
|
||||||
|
{
|
||||||
|
private static JsonSerializerOptions CreateOptions()
|
||||||
|
{
|
||||||
|
var o = new JsonSerializerOptions();
|
||||||
|
o.Converters.Add(new StringFloatJsonConverter());
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Read --------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_StringNumber_WHEN_Read_THEN_ShouldParseFloat()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var json = "\"42\"";
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<float>(json, options);
|
||||||
|
|
||||||
|
result.Should().Be(42f);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_StringInvalid_WHEN_Read_THEN_ShouldReturnZero()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var json = "\"not-a-number\"";
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<float>(json, options);
|
||||||
|
|
||||||
|
result.Should().Be(0f);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NumberToken_WHEN_Read_THEN_ShouldReadSingle()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var json = "123.0";
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<float>(json, options);
|
||||||
|
|
||||||
|
result.Should().Be(123f);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NullToken_WHEN_Read_THEN_ShouldReturnZero()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var json = "null";
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<float>(json, options);
|
||||||
|
|
||||||
|
result.Should().Be(0f);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_UnsupportedToken_WHEN_Read_THEN_ShouldReturnZero()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
var json = "true"; // bool token -> converter returns 0
|
||||||
|
|
||||||
|
var result = JsonSerializer.Deserialize<float>(json, options);
|
||||||
|
|
||||||
|
result.Should().Be(0f);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Write --------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_IntegerValue_WHEN_Write_THEN_ShouldEmitJsonStringContainingThatValue()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
float value = 42f;
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(value, options);
|
||||||
|
|
||||||
|
// Should be a JSON string (quotes)
|
||||||
|
json.Should().StartWith("\"").And.EndWith("\"");
|
||||||
|
|
||||||
|
// Remove quotes and ensure it parses back to the original value
|
||||||
|
var inner = json.Trim('"');
|
||||||
|
float.TryParse(inner, out var parsed).Should().BeTrue();
|
||||||
|
parsed.Should().Be(42f);
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_FractionalValue_WHEN_Write_THEN_ShouldEmitJsonStringParsableToSameValue()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
float value = 1.5f; // exactly representable
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(value, options);
|
||||||
|
|
||||||
|
json.Should().StartWith("\"").And.EndWith("\"");
|
||||||
|
var inner = json.Trim('"');
|
||||||
|
|
||||||
|
float.TryParse(inner, out var parsed).Should().BeTrue();
|
||||||
|
parsed.Should().Be(1.5f);
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Round-trip --------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Value_WHEN_RoundTrip_THEN_ShouldPreserveValue()
|
||||||
|
{
|
||||||
|
var options = CreateOptions();
|
||||||
|
float original = 1.5f;
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(original, options);
|
||||||
|
var round = JsonSerializer.Deserialize<float>(json, options);
|
||||||
|
|
||||||
|
round.Should().Be(1.5f);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
using AwesomeAssertions;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class FormUrlEncodedBuilderExtensionsTests
|
||||||
|
{
|
||||||
|
private readonly FormUrlEncodedBuilder _target;
|
||||||
|
|
||||||
|
public FormUrlEncodedBuilderExtensionsTests()
|
||||||
|
{
|
||||||
|
_target = new FormUrlEncodedBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_BoolTrue_WHEN_Add_THEN_ShouldSerializeAsTrue()
|
||||||
|
{
|
||||||
|
var returned = _target.Add("flag", true);
|
||||||
|
|
||||||
|
ReferenceEquals(_target, returned).Should().BeTrue();
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("flag");
|
||||||
|
parameters[0].Value.Should().Be("true");
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
(await content.ReadAsStringAsync()).Should().Be("flag=true");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_BoolFalse_WHEN_Add_THEN_ShouldSerializeAsFalse()
|
||||||
|
{
|
||||||
|
_target.Add("flag", false);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("flag");
|
||||||
|
parameters[0].Value.Should().Be("false");
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
(await content.ReadAsStringAsync()).Should().Be("flag=false");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Int_WHEN_Add_THEN_ShouldSerializeAsDigits()
|
||||||
|
{
|
||||||
|
_target.Add("count", 123);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("count");
|
||||||
|
parameters[0].Value.Should().Be("123");
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
(await content.ReadAsStringAsync()).Should().Be("count=123");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Long_WHEN_Add_THEN_ShouldSerializeAsDigits()
|
||||||
|
{
|
||||||
|
_target.Add("size", 9223372036854775807);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("size");
|
||||||
|
parameters[0].Value.Should().Be("9223372036854775807");
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
(await content.ReadAsStringAsync()).Should().Be("size=9223372036854775807");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_DateTimeOffsetSeconds_WHEN_Add_THEN_ShouldUseUnixSeconds()
|
||||||
|
{
|
||||||
|
var when = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||||
|
|
||||||
|
_target.Add("epoch", when);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("epoch");
|
||||||
|
parameters[0].Value.Should().Be("946684800");
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
(await content.ReadAsStringAsync()).Should().Be("epoch=946684800");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_DateTimeOffsetMilliseconds_WHEN_Add_THEN_ShouldUseUnixMilliseconds()
|
||||||
|
{
|
||||||
|
var when = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||||
|
|
||||||
|
_target.Add("epochMs", when, useSeconds: false);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("epochMs");
|
||||||
|
parameters[0].Value.Should().Be("946684800000");
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
(await content.ReadAsStringAsync()).Should().Be("epochMs=946684800000");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Float_WHEN_Add_THEN_ShouldUseCurrentCultureToString()
|
||||||
|
{
|
||||||
|
_target.Add("ratio", 42f);
|
||||||
|
|
||||||
|
var expected = 42f.ToString();
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("ratio");
|
||||||
|
parameters[0].Value.Should().Be(expected);
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
(await content.ReadAsStringAsync()).Should().Be($"ratio={expected}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_GenericByte_WHEN_Add_Generic_THEN_ShouldSerializeAsInt32String()
|
||||||
|
{
|
||||||
|
_target.Add<byte>("b", 7);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("b");
|
||||||
|
parameters[0].Value.Should().Be("7");
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
(await content.ReadAsStringAsync()).Should().Be("b=7");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_GenericEnum_WHEN_Add_Generic_THEN_ShouldSerializeUnderlyingInt32String()
|
||||||
|
{
|
||||||
|
_target.Add("day", DayOfWeek.Friday);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("day");
|
||||||
|
parameters[0].Value.Should().Be("5");
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
(await content.ReadAsStringAsync()).Should().Be("day=5");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_AllTrue_WHEN_AddAllOrPipeSeparated_THEN_ShouldUseAllLiteral()
|
||||||
|
{
|
||||||
|
var returned = _target.AddAllOrPipeSeparated("list", all: true, "a", "b");
|
||||||
|
|
||||||
|
ReferenceEquals(_target, returned).Should().BeTrue();
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("list");
|
||||||
|
parameters[0].Value.Should().Be("all");
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
(await content.ReadAsStringAsync()).Should().Be("list=all");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_AllNullOrFalse_WHEN_AddAllOrPipeSeparated_THEN_ShouldJoinWithPipes()
|
||||||
|
{
|
||||||
|
_target.AddAllOrPipeSeparated("list", null, "a", "b c", "d|e");
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("list");
|
||||||
|
parameters[0].Value.Should().Be("a|b c|d|e");
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
(await content.ReadAsStringAsync()).Should().Be("list=a%7Cb+c%7Cd%7Ce");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NoValues_WHEN_AddAllOrPipeSeparatedWithFalse_THEN_ShouldYieldEmptyValue()
|
||||||
|
{
|
||||||
|
_target.AddAllOrPipeSeparated("list", false);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("list");
|
||||||
|
parameters[0].Value.Should().Be(string.Empty);
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
(await content.ReadAsStringAsync()).Should().Be("list=");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_PipeSeparatedValues_WHEN_AddPipeSeparated_THEN_ShouldJoinAndEncodeProperly()
|
||||||
|
{
|
||||||
|
_target.AddPipeSeparated("ids", new[] { "a", "b c", "d|e" });
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("ids");
|
||||||
|
parameters[0].Value.Should().Be("a|b c|d|e");
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
(await content.ReadAsStringAsync()).Should().Be("ids=a%7Cb+c%7Cd%7Ce");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_CommaSeparatedValues_WHEN_AddCommaSeparated_THEN_ShouldJoinAndEncodeProperly()
|
||||||
|
{
|
||||||
|
_target.AddCommaSeparated("items", new[] { 1, 2, 3 });
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("items");
|
||||||
|
parameters[0].Value.Should().Be("1,2,3");
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
(await content.ReadAsStringAsync()).Should().Be("items=1%2C2%2C3");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_MultipleAdds_WHEN_Chained_THEN_ShouldPreserveOrder()
|
||||||
|
{
|
||||||
|
var returned = _target
|
||||||
|
.Add("flag", true)
|
||||||
|
.Add("count", 2)
|
||||||
|
.Add("epoch", new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero))
|
||||||
|
.Add<byte>("b", 7);
|
||||||
|
|
||||||
|
ReferenceEquals(_target, returned).Should().BeTrue();
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(4);
|
||||||
|
parameters[0].Key.Should().Be("flag");
|
||||||
|
parameters[0].Value.Should().Be("true");
|
||||||
|
parameters[1].Key.Should().Be("count");
|
||||||
|
parameters[1].Value.Should().Be("2");
|
||||||
|
parameters[2].Key.Should().Be("epoch");
|
||||||
|
parameters[2].Value.Should().Be("946684800");
|
||||||
|
parameters[3].Key.Should().Be("b");
|
||||||
|
parameters[3].Value.Should().Be("7");
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
(await content.ReadAsStringAsync()).Should().Be("flag=true&count=2&epoch=946684800&b=7");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
176
Lantean.QBitTorrentClient.Test/FormUrlEncodedBuilderTests.cs
Normal file
176
Lantean.QBitTorrentClient.Test/FormUrlEncodedBuilderTests.cs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
using AwesomeAssertions;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class FormUrlEncodedBuilderTests
|
||||||
|
{
|
||||||
|
private readonly FormUrlEncodedBuilder _target;
|
||||||
|
|
||||||
|
public FormUrlEncodedBuilderTests()
|
||||||
|
{
|
||||||
|
_target = new FormUrlEncodedBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NoParameters_WHEN_ToFormUrlEncodedContent_THEN_ShouldBeEmptyString()
|
||||||
|
{
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
|
var payload = await content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
payload.Should().Be(string.Empty);
|
||||||
|
_target.GetParameters().Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_SingleParameter_WHEN_ToFormUrlEncodedContent_THEN_ShouldEncodeAndContainPair()
|
||||||
|
{
|
||||||
|
_target.Add("first", "one");
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
var payload = await content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
payload.Should().Be("first=one");
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("first");
|
||||||
|
parameters[0].Value.Should().Be("one");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_MultipleParameters_WHEN_ToFormUrlEncodedContent_THEN_ShouldPreserveOrderWithAmpersand()
|
||||||
|
{
|
||||||
|
_target.Add("a", "1").Add("b", "2").Add("c", "3");
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
var payload = await content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
payload.Should().Be("a=1&b=2&c=3");
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(3);
|
||||||
|
parameters[0].Key.Should().Be("a");
|
||||||
|
parameters[0].Value.Should().Be("1");
|
||||||
|
parameters[1].Key.Should().Be("b");
|
||||||
|
parameters[1].Value.Should().Be("2");
|
||||||
|
parameters[2].Key.Should().Be("c");
|
||||||
|
parameters[2].Value.Should().Be("3");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_SpecialCharacters_WHEN_ToFormUrlEncodedContent_THEN_ShouldBeProperlyEncoded()
|
||||||
|
{
|
||||||
|
_target.Add("a b", "c+d&=");
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
var payload = await content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
((payload.StartsWith("a%20b=") || payload.StartsWith("a+b="))).Should().BeTrue();
|
||||||
|
payload.EndsWith("c%2Bd%26%3D").Should().BeTrue();
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("a b");
|
||||||
|
parameters[0].Value.Should().Be("c+d&=");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NonEmptyString_WHEN_AddIfNotNullOrEmpty_THEN_ShouldAddPair()
|
||||||
|
{
|
||||||
|
_target.AddIfNotNullOrEmpty("key", "value");
|
||||||
|
|
||||||
|
_target.GetParameters().Count.Should().Be(1);
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
var payload = await content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
payload.Should().Be("key=value");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_EmptyOrNullString_WHEN_AddIfNotNullOrEmpty_THEN_ShouldNotAddPair()
|
||||||
|
{
|
||||||
|
_target.AddIfNotNullOrEmpty("k1", "");
|
||||||
|
_target.AddIfNotNullOrEmpty("k2", null);
|
||||||
|
|
||||||
|
_target.GetParameters().Count.Should().Be(0);
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
var payload = await content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
payload.Should().Be(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NullableValueHasNoValue_WHEN_AddIfNotNullOrEmpty_Generic_THEN_ShouldNotAddPair()
|
||||||
|
{
|
||||||
|
int? value = null;
|
||||||
|
|
||||||
|
_target.AddIfNotNullOrEmpty("count", value);
|
||||||
|
|
||||||
|
_target.GetParameters().Count.Should().Be(0);
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
var payload = await content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
payload.Should().Be(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_NullableValueHasValue_WHEN_AddIfNotNullOrEmpty_Generic_THEN_ShouldAddPairUsingToString()
|
||||||
|
{
|
||||||
|
int? value = 42;
|
||||||
|
|
||||||
|
_target.AddIfNotNullOrEmpty("count", value);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("count");
|
||||||
|
parameters[0].Value.Should().Be("42");
|
||||||
|
|
||||||
|
using var content = _target.ToFormUrlEncodedContent();
|
||||||
|
var payload = await content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
payload.Should().Be("count=42");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_FluentAdd_WHEN_Used_THEN_ShouldReturnSameInstance()
|
||||||
|
{
|
||||||
|
var returned = _target.Add("x", "y");
|
||||||
|
|
||||||
|
ReferenceEquals(_target, returned).Should().BeTrue();
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("x");
|
||||||
|
parameters[0].Value.Should().Be("y");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_CustomParameterList_WHEN_ConstructedWithList_THEN_ShouldUseInjectedList()
|
||||||
|
{
|
||||||
|
var backing = new List<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>("a", "1")
|
||||||
|
};
|
||||||
|
|
||||||
|
var builder = new FormUrlEncodedBuilder(backing);
|
||||||
|
builder.Add("b", "2");
|
||||||
|
|
||||||
|
var observed = builder.GetParameters();
|
||||||
|
ReferenceEquals(backing, observed).Should().BeTrue();
|
||||||
|
observed.Count.Should().Be(2);
|
||||||
|
observed[0].Key.Should().Be("a");
|
||||||
|
observed[0].Value.Should().Be("1");
|
||||||
|
observed[1].Key.Should().Be("b");
|
||||||
|
observed[1].Value.Should().Be("2");
|
||||||
|
|
||||||
|
using var content = builder.ToFormUrlEncodedContent();
|
||||||
|
var payload = await content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
payload.Should().Be("a=1&b=2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AwesomeAssertions" Version="9.2.1" />
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Lantean.QBitTorrentClient\Lantean.QBitTorrentClient.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
using AwesomeAssertions;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class MultipartFormDataContentExtensionsTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly MultipartFormDataContent _target;
|
||||||
|
|
||||||
|
public MultipartFormDataContentExtensionsTests()
|
||||||
|
{
|
||||||
|
_target = new MultipartFormDataContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_String_WHEN_AddString_THEN_ShouldAddWithNameAndValue()
|
||||||
|
{
|
||||||
|
_target.AddString("name", "value");
|
||||||
|
|
||||||
|
var part = _target.ToList().Single();
|
||||||
|
part.Headers.ContentDisposition!.Name.Should().Be("name");
|
||||||
|
(await part.ReadAsStringAsync()).Should().Be("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_BoolTrue_WHEN_AddString_THEN_ShouldStoreTrue()
|
||||||
|
{
|
||||||
|
_target.AddString("flag", true);
|
||||||
|
|
||||||
|
var part = _target.ToList().Single();
|
||||||
|
part.Headers.ContentDisposition!.Name.Should().Be("flag");
|
||||||
|
(await part.ReadAsStringAsync()).Should().Be("true");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_BoolFalse_WHEN_AddString_THEN_ShouldStoreFalse()
|
||||||
|
{
|
||||||
|
_target.AddString("flag", false);
|
||||||
|
|
||||||
|
var part = _target.ToList().Single();
|
||||||
|
part.Headers.ContentDisposition!.Name.Should().Be("flag");
|
||||||
|
(await part.ReadAsStringAsync()).Should().Be("false");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Int_WHEN_AddString_THEN_ShouldStoreNumericString()
|
||||||
|
{
|
||||||
|
_target.AddString("count", 123);
|
||||||
|
|
||||||
|
var part = _target.ToList().Single();
|
||||||
|
part.Headers.ContentDisposition!.Name.Should().Be("count");
|
||||||
|
(await part.ReadAsStringAsync()).Should().Be("123");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Long_WHEN_AddString_THEN_ShouldStoreNumericString()
|
||||||
|
{
|
||||||
|
_target.AddString("size", 9223372036854775807);
|
||||||
|
|
||||||
|
var part = _target.ToList().Single();
|
||||||
|
part.Headers.ContentDisposition!.Name.Should().Be("size");
|
||||||
|
(await part.ReadAsStringAsync()).Should().Be("9223372036854775807");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Float_WHEN_AddString_THEN_ShouldUseCurrentCultureToString()
|
||||||
|
{
|
||||||
|
_target.AddString("ratio", 42f);
|
||||||
|
|
||||||
|
var part = _target.ToList().Single();
|
||||||
|
part.Headers.ContentDisposition!.Name.Should().Be("ratio");
|
||||||
|
(await part.ReadAsStringAsync()).Should().Be(42f.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_Enum_WHEN_AddString_THEN_ShouldStoreEnumName()
|
||||||
|
{
|
||||||
|
_target.AddString("day", DayOfWeek.Monday);
|
||||||
|
|
||||||
|
var part = _target.ToList().Single();
|
||||||
|
part.Headers.ContentDisposition!.Name.Should().Be("day");
|
||||||
|
(await part.ReadAsStringAsync()).Should().Be("Monday");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_DateTimeOffsetSeconds_WHEN_AddString_THEN_ShouldUseUnixSeconds()
|
||||||
|
{
|
||||||
|
var when = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||||
|
|
||||||
|
_target.AddString("epoch", when);
|
||||||
|
|
||||||
|
var part = _target.ToList().Single();
|
||||||
|
part.Headers.ContentDisposition!.Name.Should().Be("epoch");
|
||||||
|
(await part.ReadAsStringAsync()).Should().Be("946684800");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GIVEN_DateTimeOffsetMilliseconds_WHEN_AddString_THEN_ShouldUseUnixMilliseconds()
|
||||||
|
{
|
||||||
|
var when = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||||
|
|
||||||
|
_target.AddString("epochMs", when, useSeconds: false);
|
||||||
|
|
||||||
|
var part = _target.ToList().Single();
|
||||||
|
part.Headers.ContentDisposition!.Name.Should().Be("epochMs");
|
||||||
|
(await part.ReadAsStringAsync()).Should().Be("946684800000");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_target.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
157
Lantean.QBitTorrentClient.Test/QueryBuilderExtensionsTests.cs
Normal file
157
Lantean.QBitTorrentClient.Test/QueryBuilderExtensionsTests.cs
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
using AwesomeAssertions;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class QueryBuilderExtensionsTests
|
||||||
|
{
|
||||||
|
private readonly QueryBuilder _target;
|
||||||
|
|
||||||
|
public QueryBuilderExtensionsTests()
|
||||||
|
{
|
||||||
|
_target = new QueryBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_BoolTrue_WHEN_Add_THEN_ShouldStoreTrueString()
|
||||||
|
{
|
||||||
|
var returned = _target.Add("flag", true);
|
||||||
|
|
||||||
|
ReferenceEquals(_target, returned).Should().BeTrue();
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("flag");
|
||||||
|
parameters[0].Value.Should().Be("true");
|
||||||
|
|
||||||
|
_target.ToQueryString().Should().Be("?flag=true");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_BoolFalse_WHEN_Add_THEN_ShouldStoreFalseString()
|
||||||
|
{
|
||||||
|
_target.Add("flag", false);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("flag");
|
||||||
|
parameters[0].Value.Should().Be("false");
|
||||||
|
|
||||||
|
_target.ToQueryString().Should().Be("?flag=false");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_Int_WHEN_Add_THEN_ShouldStoreNumericString()
|
||||||
|
{
|
||||||
|
_target.Add("count", 123);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("count");
|
||||||
|
parameters[0].Value.Should().Be("123");
|
||||||
|
|
||||||
|
_target.ToQueryString().Should().Be("?count=123");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_Long_WHEN_Add_THEN_ShouldStoreNumericString()
|
||||||
|
{
|
||||||
|
_target.Add("size", 9223372036854775807);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("size");
|
||||||
|
parameters[0].Value.Should().Be("9223372036854775807");
|
||||||
|
|
||||||
|
_target.ToQueryString().Should().Be("?size=9223372036854775807");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_DateTimeOffsetSeconds_WHEN_Add_THEN_ShouldUseUnixSeconds()
|
||||||
|
{
|
||||||
|
var when = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||||
|
|
||||||
|
_target.Add("epoch", when);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("epoch");
|
||||||
|
parameters[0].Value.Should().Be("946684800");
|
||||||
|
|
||||||
|
_target.ToQueryString().Should().Be("?epoch=946684800");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_DateTimeOffsetMilliseconds_WHEN_Add_THEN_ShouldUseUnixMilliseconds()
|
||||||
|
{
|
||||||
|
var when = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||||
|
|
||||||
|
_target.Add("epochMs", when, useSeconds: false);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("epochMs");
|
||||||
|
parameters[0].Value.Should().Be("946684800000");
|
||||||
|
|
||||||
|
_target.ToQueryString().Should().Be("?epochMs=946684800000");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_Enum_WHEN_Add_THEN_ShouldUseEnumNameString()
|
||||||
|
{
|
||||||
|
_target.Add("day", DayOfWeek.Monday);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("day");
|
||||||
|
parameters[0].Value.Should().Be("Monday");
|
||||||
|
|
||||||
|
_target.ToQueryString().Should().Be("?day=Monday");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_PipeSeparatedValues_WHEN_AddPipeSeparated_THEN_ShouldJoinWithPipeAndEscapeInQuery()
|
||||||
|
{
|
||||||
|
_target.AddPipeSeparated("list", new[] { "a", "b c", "d|e" });
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("list");
|
||||||
|
parameters[0].Value.Should().Be("a|b c|d|e");
|
||||||
|
|
||||||
|
_target.ToQueryString().Should().Be("?list=a%7Cb%20c%7Cd%7Ce");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_CommaSeparatedValues_WHEN_AddCommaSeparated_THEN_ShouldJoinWithCommaAndEscapeInQuery()
|
||||||
|
{
|
||||||
|
_target.AddCommaSeparated("items", new[] { 1, 2, 3 });
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("items");
|
||||||
|
parameters[0].Value.Should().Be("1,2,3");
|
||||||
|
|
||||||
|
_target.ToQueryString().Should().Be("?items=1%2C2%2C3");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_MultipleExtensionAdds_WHEN_Chained_THEN_ShouldPreserveOrderInQuery()
|
||||||
|
{
|
||||||
|
_target
|
||||||
|
.Add("flag", true)
|
||||||
|
.Add("count", 2)
|
||||||
|
.Add("day", DayOfWeek.Friday);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(3);
|
||||||
|
parameters[0].Key.Should().Be("flag");
|
||||||
|
parameters[0].Value.Should().Be("true");
|
||||||
|
parameters[1].Key.Should().Be("count");
|
||||||
|
parameters[1].Value.Should().Be("2");
|
||||||
|
parameters[2].Key.Should().Be("day");
|
||||||
|
parameters[2].Value.Should().Be("Friday");
|
||||||
|
|
||||||
|
_target.ToQueryString().Should().Be("?flag=true&count=2&day=Friday");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
159
Lantean.QBitTorrentClient.Test/QueryBuilderTests.cs
Normal file
159
Lantean.QBitTorrentClient.Test/QueryBuilderTests.cs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
using AwesomeAssertions;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class QueryBuilderTests
|
||||||
|
{
|
||||||
|
private readonly QueryBuilder _target;
|
||||||
|
|
||||||
|
public QueryBuilderTests()
|
||||||
|
{
|
||||||
|
_target = new QueryBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_NoParameters_WHEN_ToQueryString_THEN_ShouldBeEmptyString()
|
||||||
|
{
|
||||||
|
var result = _target.ToQueryString();
|
||||||
|
|
||||||
|
result.Should().Be(string.Empty);
|
||||||
|
_target.GetParameters().Count.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_MultipleParameters_WHEN_ToQueryString_THEN_ShouldStartWithQuestionAndUseAmpersandBetweenPairs()
|
||||||
|
{
|
||||||
|
_target.Add("first", "one");
|
||||||
|
_target.Add("second", "two");
|
||||||
|
|
||||||
|
var result = _target.ToQueryString();
|
||||||
|
|
||||||
|
result.Should().Be("?first=one&second=two");
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(2);
|
||||||
|
parameters[0].Key.Should().Be("first");
|
||||||
|
parameters[0].Value.Should().Be("one");
|
||||||
|
parameters[1].Key.Should().Be("second");
|
||||||
|
parameters[1].Value.Should().Be("two");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_SpecialChars_WHEN_ToQueryString_THEN_ShouldBeUriEscaped()
|
||||||
|
{
|
||||||
|
_target.Add("a b", "c+d&");
|
||||||
|
_target.Add("こんにちは", "é l'œ");
|
||||||
|
|
||||||
|
var result = _target.ToQueryString();
|
||||||
|
|
||||||
|
result.Should().Be("?a%20b=c%2Bd%26&%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF=%C3%A9%20l%27%C5%93");
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(2);
|
||||||
|
parameters[0].Key.Should().Be("a b");
|
||||||
|
parameters[0].Value.Should().Be("c+d&");
|
||||||
|
parameters[1].Key.Should().Be("こんにちは");
|
||||||
|
parameters[1].Value.Should().Be("é l'œ");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_NonEmptyString_WHEN_AddIfNotNullOrEmpty_THEN_ShouldAddPair()
|
||||||
|
{
|
||||||
|
_target.AddIfNotNullOrEmpty("key", "value");
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("key");
|
||||||
|
parameters[0].Value.Should().Be("value");
|
||||||
|
|
||||||
|
_target.ToQueryString().Should().Be("?key=value");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_EmptyOrNullString_WHEN_AddIfNotNullOrEmpty_THEN_ShouldNotAddPair()
|
||||||
|
{
|
||||||
|
_target.AddIfNotNullOrEmpty("k1", "");
|
||||||
|
_target.AddIfNotNullOrEmpty("k2", null);
|
||||||
|
|
||||||
|
_target.GetParameters().Count.Should().Be(0);
|
||||||
|
_target.ToQueryString().Should().Be(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_NullableValueHasNoValue_WHEN_AddIfNotNullOrEmpty_THEN_ShouldNotAddPair()
|
||||||
|
{
|
||||||
|
int? value = null;
|
||||||
|
|
||||||
|
_target.AddIfNotNullOrEmpty("count", value);
|
||||||
|
|
||||||
|
_target.GetParameters().Count.Should().Be(0);
|
||||||
|
_target.ToQueryString().Should().Be(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_NullableValueHasValue_WHEN_AddIfNotNullOrEmpty_THEN_ShouldAddPairUsingToString()
|
||||||
|
{
|
||||||
|
int? value = 42;
|
||||||
|
|
||||||
|
_target.AddIfNotNullOrEmpty("count", value);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("count");
|
||||||
|
parameters[0].Value.Should().Be("42");
|
||||||
|
|
||||||
|
_target.ToQueryString().Should().Be("?count=42");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_FluentAdd_WHEN_Used_THEN_ShouldReturnSameInstanceAndAppendParameter()
|
||||||
|
{
|
||||||
|
var returned = _target.Add("x", "y");
|
||||||
|
|
||||||
|
ReferenceEquals(_target, returned).Should().BeTrue();
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("x");
|
||||||
|
parameters[0].Value.Should().Be("y");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_CustomParameterList_WHEN_ConstructedWithList_THEN_ShouldUseInjectedList()
|
||||||
|
{
|
||||||
|
var backing = new List<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>("a", "1")
|
||||||
|
};
|
||||||
|
|
||||||
|
var builder = new QueryBuilder(backing);
|
||||||
|
builder.Add("b", "2");
|
||||||
|
|
||||||
|
var observed = builder.GetParameters();
|
||||||
|
ReferenceEquals(backing, observed).Should().BeTrue();
|
||||||
|
observed.Count.Should().Be(2);
|
||||||
|
observed[0].Key.Should().Be("a");
|
||||||
|
observed[0].Value.Should().Be("1");
|
||||||
|
observed[1].Key.Should().Be("b");
|
||||||
|
observed[1].Value.Should().Be("2");
|
||||||
|
|
||||||
|
builder.ToQueryString().Should().Be("?a=1&b=2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GIVEN_ToStringCalled_WHEN_ParametersExist_THEN_ShouldReturnSameAsToQueryString()
|
||||||
|
{
|
||||||
|
_target.Add("p", "q");
|
||||||
|
|
||||||
|
var qs = _target.ToQueryString();
|
||||||
|
var result = _target.ToString();
|
||||||
|
|
||||||
|
result.Should().Be(qs);
|
||||||
|
|
||||||
|
var parameters = _target.GetParameters();
|
||||||
|
parameters.Count.Should().Be(1);
|
||||||
|
parameters[0].Key.Should().Be("p");
|
||||||
|
parameters[0].Value.Should().Be("q");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Lantean.QBitTorrentClient.Test/StubHttpMessageHandler.cs
Normal file
15
Lantean.QBitTorrentClient.Test/StubHttpMessageHandler.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using AwesomeAssertions;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
internal sealed class StubHttpMessageHandler : HttpMessageHandler
|
||||||
|
{
|
||||||
|
public Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>>? Responder { get; set; }
|
||||||
|
|
||||||
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Responder.Should().NotBeNull();
|
||||||
|
return Responder!(request, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Lantean.QBitTorrentClient.Models;
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
using System.Globalization;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -105,6 +106,8 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
public async Task SetApplicationPreferences(UpdatePreferences preferences)
|
public async Task SetApplicationPreferences(UpdatePreferences preferences)
|
||||||
{
|
{
|
||||||
|
preferences.Validate();
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(preferences, _options);
|
var json = JsonSerializer.Serialize(preferences, _options);
|
||||||
|
|
||||||
var content = new FormUrlEncodedBuilder()
|
var content = new FormUrlEncodedBuilder()
|
||||||
@@ -116,6 +119,49 @@ namespace Lantean.QBitTorrentClient
|
|||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<ApplicationCookie>> GetApplicationCookies()
|
||||||
|
{
|
||||||
|
var response = await _httpClient.GetAsync("app/cookies");
|
||||||
|
|
||||||
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
|
|
||||||
|
return await GetJsonList<ApplicationCookie>(response.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetApplicationCookies(IEnumerable<ApplicationCookie> cookies)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(cookies, _options);
|
||||||
|
|
||||||
|
var content = new FormUrlEncodedBuilder()
|
||||||
|
.Add("cookies", json)
|
||||||
|
.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync("app/setCookies", content);
|
||||||
|
|
||||||
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> RotateApiKey()
|
||||||
|
{
|
||||||
|
var response = await _httpClient.PostAsync("app/rotateAPIKey", null);
|
||||||
|
|
||||||
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
|
|
||||||
|
var payload = await response.Content.ReadAsStringAsync();
|
||||||
|
if (string.IsNullOrWhiteSpace(payload))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JsonSerializer.Deserialize<JsonElement>(payload, _options);
|
||||||
|
if (json.ValueKind == JsonValueKind.Object && json.TryGetProperty("apiKey", out var apiKeyElement))
|
||||||
|
{
|
||||||
|
return apiKeyElement.GetString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<string> GetDefaultSavePath()
|
public async Task<string> GetDefaultSavePath()
|
||||||
{
|
{
|
||||||
var response = await _httpClient.GetAsync("app/defaultSavePath");
|
var response = await _httpClient.GetAsync("app/defaultSavePath");
|
||||||
@@ -145,6 +191,43 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
#endregion Application
|
#endregion Application
|
||||||
|
|
||||||
|
#region Client data
|
||||||
|
|
||||||
|
public async Task<IReadOnlyDictionary<string, JsonElement>> LoadClientData(IEnumerable<string>? keys = null)
|
||||||
|
{
|
||||||
|
HttpResponseMessage response;
|
||||||
|
if (keys is null)
|
||||||
|
{
|
||||||
|
response = await _httpClient.GetAsync("clientdata/load");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var query = new QueryBuilder()
|
||||||
|
.Add("keys", JsonSerializer.Serialize(keys, _options));
|
||||||
|
|
||||||
|
response = await _httpClient.GetAsync("clientdata/load", query);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
|
|
||||||
|
return await GetJsonDictionary<string, JsonElement>(response.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StoreClientData(IReadOnlyDictionary<string, JsonElement> data)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(data, _options);
|
||||||
|
|
||||||
|
var content = new FormUrlEncodedBuilder()
|
||||||
|
.Add("data", json)
|
||||||
|
.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync("clientdata/store", content);
|
||||||
|
|
||||||
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Client data
|
||||||
|
|
||||||
#region Log
|
#region Log
|
||||||
|
|
||||||
public async Task<IReadOnlyList<Log>> GetLog(bool? normal = null, bool? info = null, bool? warning = null, bool? critical = null, int? lastKnownId = null)
|
public async Task<IReadOnlyList<Log>> GetLog(bool? normal = null, bool? info = null, bool? warning = null, bool? critical = null, int? lastKnownId = null)
|
||||||
@@ -305,7 +388,7 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
#region Torrent management
|
#region Torrent management
|
||||||
|
|
||||||
public async Task<IReadOnlyList<Torrent>> GetTorrentList(string? filter = null, string? category = null, string? tag = null, string? sort = null, bool? reverse = null, int? limit = null, int? offset = null, bool? isPrivate = null, params string[] hashes)
|
public async Task<IReadOnlyList<Torrent>> GetTorrentList(string? filter = null, string? category = null, string? tag = null, string? sort = null, bool? reverse = null, int? limit = null, int? offset = null, bool? isPrivate = null, bool? includeFiles = null, params string[] hashes)
|
||||||
{
|
{
|
||||||
var query = new QueryBuilder();
|
var query = new QueryBuilder();
|
||||||
if (filter is not null)
|
if (filter is not null)
|
||||||
@@ -344,6 +427,10 @@ namespace Lantean.QBitTorrentClient
|
|||||||
{
|
{
|
||||||
query.Add("private", isPrivate.Value ? "true" : "false");
|
query.Add("private", isPrivate.Value ? "true" : "false");
|
||||||
}
|
}
|
||||||
|
if (includeFiles is not null)
|
||||||
|
{
|
||||||
|
query.Add("includeFiles", includeFiles.Value ? "true" : "false");
|
||||||
|
}
|
||||||
|
|
||||||
var response = await _httpClient.GetAsync("torrents/info", query);
|
var response = await _httpClient.GetAsync("torrents/info", query);
|
||||||
|
|
||||||
@@ -379,6 +466,43 @@ namespace Lantean.QBitTorrentClient
|
|||||||
return await GetJsonList<WebSeed>(response.Content);
|
return await GetJsonList<WebSeed>(response.Content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task AddTorrentWebSeeds(string hash, IEnumerable<string> urls)
|
||||||
|
{
|
||||||
|
var content = new FormUrlEncodedBuilder()
|
||||||
|
.Add("hash", hash)
|
||||||
|
.Add("urls", string.Join('|', urls))
|
||||||
|
.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync("torrents/addWebSeeds", content);
|
||||||
|
|
||||||
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EditTorrentWebSeed(string hash, string originalUrl, string newUrl)
|
||||||
|
{
|
||||||
|
var content = new FormUrlEncodedBuilder()
|
||||||
|
.Add("hash", hash)
|
||||||
|
.Add("origUrl", originalUrl)
|
||||||
|
.Add("newUrl", newUrl)
|
||||||
|
.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync("torrents/editWebSeed", content);
|
||||||
|
|
||||||
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveTorrentWebSeeds(string hash, IEnumerable<string> urls)
|
||||||
|
{
|
||||||
|
var content = new FormUrlEncodedBuilder()
|
||||||
|
.Add("hash", hash)
|
||||||
|
.Add("urls", string.Join('|', urls))
|
||||||
|
.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync("torrents/removeWebSeeds", content);
|
||||||
|
|
||||||
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<FileData>> GetTorrentContents(string hash, params int[] indexes)
|
public async Task<IReadOnlyList<FileData>> GetTorrentContents(string hash, params int[] indexes)
|
||||||
{
|
{
|
||||||
var query = new QueryBuilder();
|
var query = new QueryBuilder();
|
||||||
@@ -411,18 +535,6 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
return await GetJsonList<string>(response.Content);
|
return await GetJsonList<string>(response.Content);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PauseTorrents(bool? all = null, params string[] hashes)
|
|
||||||
{
|
|
||||||
var content = new FormUrlEncodedBuilder()
|
|
||||||
.AddAllOrPipeSeparated("hashes", all, hashes)
|
|
||||||
.ToFormUrlEncodedContent();
|
|
||||||
|
|
||||||
var response = await _httpClient.PostAsync("torrents/pause", content);
|
|
||||||
|
|
||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task StopTorrents(bool? all = null, params string[] hashes)
|
public async Task StopTorrents(bool? all = null, params string[] hashes)
|
||||||
{
|
{
|
||||||
var content = new FormUrlEncodedBuilder()
|
var content = new FormUrlEncodedBuilder()
|
||||||
@@ -433,18 +545,6 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ResumeTorrents(bool? all = null, params string[] hashes)
|
|
||||||
{
|
|
||||||
var content = new FormUrlEncodedBuilder()
|
|
||||||
.AddAllOrPipeSeparated("hashes", all, hashes)
|
|
||||||
.ToFormUrlEncodedContent();
|
|
||||||
|
|
||||||
var response = await _httpClient.PostAsync("torrents/resume", content);
|
|
||||||
|
|
||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task StartTorrents(bool? all = null, params string[] hashes)
|
public async Task StartTorrents(bool? all = null, params string[] hashes)
|
||||||
{
|
{
|
||||||
var content = new FormUrlEncodedBuilder()
|
var content = new FormUrlEncodedBuilder()
|
||||||
@@ -479,10 +579,11 @@ namespace Lantean.QBitTorrentClient
|
|||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ReannounceTorrents(bool? all = null, params string[] hashes)
|
public async Task ReannounceTorrents(bool? all = null, IEnumerable<string>? trackers = null, params string[] hashes)
|
||||||
{
|
{
|
||||||
var content = new FormUrlEncodedBuilder()
|
var content = new FormUrlEncodedBuilder()
|
||||||
.AddAllOrPipeSeparated("hashes", all, hashes)
|
.AddAllOrPipeSeparated("hashes", all, hashes)
|
||||||
|
.AddIfNotNullOrEmpty("urls", trackers is null ? null : string.Join('|', trackers))
|
||||||
.ToFormUrlEncodedContent();
|
.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
var response = await _httpClient.PostAsync("torrents/reannounce", content);
|
var response = await _httpClient.PostAsync("torrents/reannounce", content);
|
||||||
@@ -490,13 +591,15 @@ namespace Lantean.QBitTorrentClient
|
|||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddTorrent(AddTorrentParams addTorrentParams)
|
public async Task<AddTorrentResult> AddTorrent(AddTorrentParams addTorrentParams)
|
||||||
{
|
{
|
||||||
var content = new MultipartFormDataContent();
|
var content = new MultipartFormDataContent();
|
||||||
if (addTorrentParams.Urls is not null)
|
|
||||||
|
if (addTorrentParams.Urls?.Any() == true)
|
||||||
{
|
{
|
||||||
content.AddString("urls", string.Join('\n', addTorrentParams.Urls));
|
content.AddString("urls", string.Join('\n', addTorrentParams.Urls));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addTorrentParams.Torrents is not null)
|
if (addTorrentParams.Torrents is not null)
|
||||||
{
|
{
|
||||||
foreach (var (name, stream) in addTorrentParams.Torrents)
|
foreach (var (name, stream) in addTorrentParams.Torrents)
|
||||||
@@ -504,6 +607,7 @@ namespace Lantean.QBitTorrentClient
|
|||||||
content.Add(new StreamContent(stream), "torrents", name);
|
content.Add(new StreamContent(stream), "torrents", name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addTorrentParams.SkipChecking is not null)
|
if (addTorrentParams.SkipChecking is not null)
|
||||||
{
|
{
|
||||||
content.AddString("skip_checking", addTorrentParams.SkipChecking.Value);
|
content.AddString("skip_checking", addTorrentParams.SkipChecking.Value);
|
||||||
@@ -520,12 +624,10 @@ namespace Lantean.QBitTorrentClient
|
|||||||
{
|
{
|
||||||
content.AddString("addToTopOfQueue", addTorrentParams.AddToTopOfQueue.Value);
|
content.AddString("addToTopOfQueue", addTorrentParams.AddToTopOfQueue.Value);
|
||||||
}
|
}
|
||||||
// v4
|
if (addTorrentParams.Forced is not null)
|
||||||
if (addTorrentParams.Paused is not null)
|
|
||||||
{
|
{
|
||||||
content.AddString("paused", addTorrentParams.Paused.Value);
|
content.AddString("forced", addTorrentParams.Forced.Value);
|
||||||
}
|
}
|
||||||
// v5
|
|
||||||
if (addTorrentParams.Stopped is not null)
|
if (addTorrentParams.Stopped is not null)
|
||||||
{
|
{
|
||||||
content.AddString("stopped", addTorrentParams.Stopped.Value);
|
content.AddString("stopped", addTorrentParams.Stopped.Value);
|
||||||
@@ -590,21 +692,61 @@ namespace Lantean.QBitTorrentClient
|
|||||||
{
|
{
|
||||||
content.AddString("contentLayout", addTorrentParams.ContentLayout.Value);
|
content.AddString("contentLayout", addTorrentParams.ContentLayout.Value);
|
||||||
}
|
}
|
||||||
|
if (addTorrentParams.Downloader is not null)
|
||||||
if (addTorrentParams.Cookie is not null)
|
|
||||||
{
|
{
|
||||||
content.AddString("cookie", addTorrentParams.Cookie);
|
content.AddString("downloader", addTorrentParams.Downloader);
|
||||||
|
}
|
||||||
|
if (addTorrentParams.FilePriorities is not null)
|
||||||
|
{
|
||||||
|
var priorities = string.Join(',', addTorrentParams.FilePriorities.Select(priority => ((int)priority).ToString(CultureInfo.InvariantCulture)));
|
||||||
|
content.AddString("filePriorities", priorities);
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(addTorrentParams.SslCertificate))
|
||||||
|
{
|
||||||
|
content.AddString("ssl_certificate", addTorrentParams.SslCertificate!);
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(addTorrentParams.SslPrivateKey))
|
||||||
|
{
|
||||||
|
content.AddString("ssl_private_key", addTorrentParams.SslPrivateKey!);
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(addTorrentParams.SslDhParams))
|
||||||
|
{
|
||||||
|
content.AddString("ssl_dh_params", addTorrentParams.SslDhParams!);
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await _httpClient.PostAsync("torrents/add", content);
|
var response = await _httpClient.PostAsync("torrents/add", content);
|
||||||
|
|
||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
if (response.StatusCode == HttpStatusCode.Conflict)
|
||||||
|
{
|
||||||
|
var conflictMessage = await response.Content.ReadAsStringAsync();
|
||||||
|
if (string.IsNullOrWhiteSpace(conflictMessage))
|
||||||
|
{
|
||||||
|
conflictMessage = "All torrents failed to add.";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddTrackersToTorrent(string hash, IEnumerable<string> urls)
|
throw new HttpRequestException(conflictMessage, null, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
|
|
||||||
|
var payload = await response.Content.ReadAsStringAsync();
|
||||||
|
if (string.IsNullOrWhiteSpace(payload))
|
||||||
{
|
{
|
||||||
|
return new AddTorrentResult(0, 0, 0, Array.Empty<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<AddTorrentResult>(payload, _options) ?? new AddTorrentResult(0, 0, 0, Array.Empty<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddTrackersToTorrent(IEnumerable<string> urls, bool? all = null, params string[] hashes)
|
||||||
|
{
|
||||||
|
if (all is not true && (hashes is null || hashes.Length == 0))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Specify at least one torrent hash or set all=true.", nameof(hashes));
|
||||||
|
}
|
||||||
|
|
||||||
var content = new FormUrlEncodedBuilder()
|
var content = new FormUrlEncodedBuilder()
|
||||||
.Add("hash", hash)
|
.AddAllOrPipeSeparated("hash", all, hashes ?? Array.Empty<string>())
|
||||||
.Add("urls", string.Join('\n', urls))
|
.Add("urls", string.Join('\n', urls))
|
||||||
.ToFormUrlEncodedContent();
|
.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
@@ -613,23 +755,42 @@ namespace Lantean.QBitTorrentClient
|
|||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task EditTracker(string hash, string originalUrl, string newUrl)
|
public async Task EditTracker(string hash, string url, string? newUrl = null, int? tier = null)
|
||||||
{
|
{
|
||||||
|
if ((newUrl is null) && (tier is null))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Must specify at least one of newUrl or tier.");
|
||||||
|
}
|
||||||
|
|
||||||
var content = new FormUrlEncodedBuilder()
|
var content = new FormUrlEncodedBuilder()
|
||||||
.Add("hash", hash)
|
.Add("hash", hash)
|
||||||
.Add("originalUrl", originalUrl)
|
.Add("url", url);
|
||||||
.Add("newUrl", newUrl)
|
|
||||||
.ToFormUrlEncodedContent();
|
|
||||||
|
|
||||||
var response = await _httpClient.PostAsync("torrents/editTracker", content);
|
if (!string.IsNullOrEmpty(newUrl))
|
||||||
|
{
|
||||||
|
content.Add("newUrl", newUrl!);
|
||||||
|
}
|
||||||
|
if (tier is not null)
|
||||||
|
{
|
||||||
|
content.Add("tier", tier.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var form = content.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync("torrents/editTracker", form);
|
||||||
|
|
||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RemoveTrackers(string hash, IEnumerable<string> urls)
|
public async Task RemoveTrackers(IEnumerable<string> urls, bool? all = null, params string[] hashes)
|
||||||
{
|
{
|
||||||
|
if (all is not true && (hashes is null || hashes.Length == 0))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Specify at least one torrent hash or set all=true.", nameof(hashes));
|
||||||
|
}
|
||||||
|
|
||||||
var content = new FormUrlEncodedBuilder()
|
var content = new FormUrlEncodedBuilder()
|
||||||
.Add("hash", hash)
|
.AddAllOrPipeSeparated("hash", all, hashes ?? Array.Empty<string>())
|
||||||
.AddPipeSeparated("urls", urls)
|
.AddPipeSeparated("urls", urls)
|
||||||
.ToFormUrlEncodedContent();
|
.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
@@ -732,13 +893,14 @@ namespace Lantean.QBitTorrentClient
|
|||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetTorrentShareLimit(float ratioLimit, float seedingTimeLimit, float inactiveSeedingTimeLimit, bool? all = null, params string[] hashes)
|
public async Task SetTorrentShareLimit(float ratioLimit, float seedingTimeLimit, float inactiveSeedingTimeLimit, ShareLimitAction? shareLimitAction = null, bool? all = null, params string[] hashes)
|
||||||
{
|
{
|
||||||
var content = new FormUrlEncodedBuilder()
|
var content = new FormUrlEncodedBuilder()
|
||||||
.AddAllOrPipeSeparated("hashes", all, hashes)
|
.AddAllOrPipeSeparated("hashes", all, hashes)
|
||||||
.Add("ratioLimit", ratioLimit)
|
.Add("ratioLimit", ratioLimit)
|
||||||
.Add("seedingTimeLimit", seedingTimeLimit)
|
.Add("seedingTimeLimit", seedingTimeLimit)
|
||||||
.Add("inactiveSeedingTimeLimit", inactiveSeedingTimeLimit)
|
.Add("inactiveSeedingTimeLimit", inactiveSeedingTimeLimit)
|
||||||
|
.AddIfNotNullOrEmpty("shareLimitAction", shareLimitAction)
|
||||||
.ToFormUrlEncodedContent();
|
.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
var response = await _httpClient.PostAsync("torrents/setShareLimits", content);
|
var response = await _httpClient.PostAsync("torrents/setShareLimits", content);
|
||||||
@@ -795,6 +957,18 @@ namespace Lantean.QBitTorrentClient
|
|||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetTorrentComment(IEnumerable<string> hashes, string comment)
|
||||||
|
{
|
||||||
|
var content = new FormUrlEncodedBuilder()
|
||||||
|
.Add("hashes", string.Join('|', hashes))
|
||||||
|
.Add("comment", comment)
|
||||||
|
.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync("torrents/setComment", content);
|
||||||
|
|
||||||
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task SetTorrentCategory(string category, bool? all = null, params string[] hashes)
|
public async Task SetTorrentCategory(string category, bool? all = null, params string[] hashes)
|
||||||
{
|
{
|
||||||
var content = new FormUrlEncodedBuilder()
|
var content = new FormUrlEncodedBuilder()
|
||||||
@@ -995,8 +1169,180 @@ namespace Lantean.QBitTorrentClient
|
|||||||
return Task.FromResult($"{_httpClient.BaseAddress}torrents/export?hash={hash}");
|
return Task.FromResult($"{_httpClient.BaseAddress}torrents/export?hash={hash}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<TorrentMetadata?> FetchMetadata(string source, string? downloader = null)
|
||||||
|
{
|
||||||
|
var builder = new FormUrlEncodedBuilder()
|
||||||
|
.Add("source", source);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(downloader))
|
||||||
|
{
|
||||||
|
builder.Add("downloader", downloader!);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync("torrents/fetchMetadata", builder.ToFormUrlEncodedContent());
|
||||||
|
|
||||||
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
|
|
||||||
|
var payload = await response.Content.ReadAsStringAsync();
|
||||||
|
if (string.IsNullOrWhiteSpace(payload))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<TorrentMetadata>(payload, _options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<TorrentMetadata>> ParseMetadata(IEnumerable<(string FileName, Stream Content)> torrents)
|
||||||
|
{
|
||||||
|
var content = new MultipartFormDataContent();
|
||||||
|
foreach (var (fileName, stream) in torrents)
|
||||||
|
{
|
||||||
|
content.Add(new StreamContent(stream), "torrents", fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync("torrents/parseMetadata", content);
|
||||||
|
|
||||||
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
|
|
||||||
|
return await GetJsonList<TorrentMetadata>(response.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]> SaveMetadata(string source)
|
||||||
|
{
|
||||||
|
var content = new FormUrlEncodedBuilder()
|
||||||
|
.Add("source", source)
|
||||||
|
.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync("torrents/saveMetadata", content);
|
||||||
|
|
||||||
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
|
|
||||||
|
return await response.Content.ReadAsByteArrayAsync();
|
||||||
|
}
|
||||||
|
|
||||||
#endregion Torrent management
|
#endregion Torrent management
|
||||||
|
|
||||||
|
#region Torrent creator
|
||||||
|
|
||||||
|
public async Task<string> AddTorrentCreationTask(TorrentCreationTaskRequest request)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(request.SourcePath))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("SourcePath is required.", nameof(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = new FormUrlEncodedBuilder()
|
||||||
|
.Add("sourcePath", request.SourcePath);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.TorrentFilePath))
|
||||||
|
{
|
||||||
|
builder.Add("torrentFilePath", request.TorrentFilePath!);
|
||||||
|
}
|
||||||
|
if (request.PieceSize.HasValue)
|
||||||
|
{
|
||||||
|
builder.Add("pieceSize", request.PieceSize.Value);
|
||||||
|
}
|
||||||
|
if (request.Private.HasValue)
|
||||||
|
{
|
||||||
|
builder.Add("private", request.Private.Value);
|
||||||
|
}
|
||||||
|
if (request.StartSeeding.HasValue)
|
||||||
|
{
|
||||||
|
builder.Add("startSeeding", request.StartSeeding.Value);
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.Comment))
|
||||||
|
{
|
||||||
|
builder.Add("comment", request.Comment!);
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.Source))
|
||||||
|
{
|
||||||
|
builder.Add("source", request.Source!);
|
||||||
|
}
|
||||||
|
if (request.Trackers is not null)
|
||||||
|
{
|
||||||
|
builder.Add("trackers", string.Join('|', request.Trackers));
|
||||||
|
}
|
||||||
|
if (request.UrlSeeds is not null)
|
||||||
|
{
|
||||||
|
builder.Add("urlSeeds", string.Join('|', request.UrlSeeds));
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.Format))
|
||||||
|
{
|
||||||
|
builder.Add("format", request.Format!);
|
||||||
|
}
|
||||||
|
if (request.OptimizeAlignment.HasValue)
|
||||||
|
{
|
||||||
|
builder.Add("optimizeAlignment", request.OptimizeAlignment.Value);
|
||||||
|
}
|
||||||
|
if (request.PaddedFileSizeLimit.HasValue)
|
||||||
|
{
|
||||||
|
builder.Add("paddedFileSizeLimit", request.PaddedFileSizeLimit.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync("torrentcreator/addTask", builder.ToFormUrlEncodedContent());
|
||||||
|
|
||||||
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
|
|
||||||
|
var payload = await response.Content.ReadAsStringAsync();
|
||||||
|
if (string.IsNullOrWhiteSpace(payload))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JsonSerializer.Deserialize<JsonElement>(payload, _options);
|
||||||
|
if (json.ValueKind == JsonValueKind.Object && json.TryGetProperty("taskID", out var idElement))
|
||||||
|
{
|
||||||
|
return idElement.GetString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<TorrentCreationTaskStatus>> GetTorrentCreationTasks(string? taskId = null)
|
||||||
|
{
|
||||||
|
HttpResponseMessage response;
|
||||||
|
if (string.IsNullOrWhiteSpace(taskId))
|
||||||
|
{
|
||||||
|
response = await _httpClient.GetAsync("torrentcreator/status");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var query = new QueryBuilder()
|
||||||
|
.Add("taskID", taskId);
|
||||||
|
|
||||||
|
response = await _httpClient.GetAsync("torrentcreator/status", query);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
|
|
||||||
|
return await GetJsonList<TorrentCreationTaskStatus>(response.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]> GetTorrentCreationTaskFile(string taskId)
|
||||||
|
{
|
||||||
|
var query = new QueryBuilder()
|
||||||
|
.Add("taskID", taskId);
|
||||||
|
|
||||||
|
var response = await _httpClient.GetAsync("torrentcreator/torrentFile", query);
|
||||||
|
|
||||||
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
|
|
||||||
|
return await response.Content.ReadAsByteArrayAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteTorrentCreationTask(string taskId)
|
||||||
|
{
|
||||||
|
var content = new FormUrlEncodedBuilder()
|
||||||
|
.Add("taskID", taskId)
|
||||||
|
.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync("torrentcreator/deleteTask", content);
|
||||||
|
|
||||||
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Torrent creator
|
||||||
|
|
||||||
#region RSS
|
#region RSS
|
||||||
|
|
||||||
public async Task AddRssFolder(string path)
|
public async Task AddRssFolder(string path)
|
||||||
|
|||||||
@@ -1,24 +1,10 @@
|
|||||||
using Lantean.QBitTorrentClient.Models;
|
using System.Linq;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
|
||||||
namespace Lantean.QBitTorrentClient
|
namespace Lantean.QBitTorrentClient
|
||||||
{
|
{
|
||||||
public static class ApiClientExtensions
|
public static class ApiClientExtensions
|
||||||
{
|
{
|
||||||
public static Task PauseTorrent(this IApiClient apiClient, string hash)
|
|
||||||
{
|
|
||||||
return apiClient.PauseTorrents(null, hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task PauseTorrents(this IApiClient apiClient, IEnumerable<string> hashes)
|
|
||||||
{
|
|
||||||
return apiClient.PauseTorrents(null, hashes.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task PauseAllTorrents(this IApiClient apiClient)
|
|
||||||
{
|
|
||||||
return apiClient.PauseTorrents(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task StopTorrent(this IApiClient apiClient, string hash)
|
public static Task StopTorrent(this IApiClient apiClient, string hash)
|
||||||
{
|
{
|
||||||
return apiClient.StopTorrents(null, hash);
|
return apiClient.StopTorrents(null, hash);
|
||||||
@@ -34,21 +20,6 @@ namespace Lantean.QBitTorrentClient
|
|||||||
return apiClient.StopTorrents(true);
|
return apiClient.StopTorrents(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task ResumeTorrent(this IApiClient apiClient, string hash)
|
|
||||||
{
|
|
||||||
return apiClient.ResumeTorrents(null, hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task ResumeTorrents(this IApiClient apiClient, IEnumerable<string> hashes)
|
|
||||||
{
|
|
||||||
return apiClient.ResumeTorrents(null, hashes.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task ResumeAllTorrents(this IApiClient apiClient)
|
|
||||||
{
|
|
||||||
return apiClient.ResumeTorrents(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task StartTorrent(this IApiClient apiClient, string hash)
|
public static Task StartTorrent(this IApiClient apiClient, string hash)
|
||||||
{
|
{
|
||||||
return apiClient.StartTorrents(null, hash);
|
return apiClient.StartTorrents(null, hash);
|
||||||
@@ -158,7 +129,7 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
public static Task ReannounceTorrent(this IApiClient apiClient, string hash)
|
public static Task ReannounceTorrent(this IApiClient apiClient, string hash)
|
||||||
{
|
{
|
||||||
return apiClient.ReannounceTorrents(null, hash);
|
return apiClient.ReannounceTorrents(null, null, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<IEnumerable<string>> RemoveUnusedCategories(this IApiClient apiClient)
|
public static async Task<IEnumerable<string>> RemoveUnusedCategories(this IApiClient apiClient)
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Converters
|
||||||
|
{
|
||||||
|
internal sealed class DownloadPathOptionJsonConverter : JsonConverter<DownloadPathOption>
|
||||||
|
{
|
||||||
|
public override DownloadPathOption? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
return reader.TokenType switch
|
||||||
|
{
|
||||||
|
JsonTokenType.Null => null,
|
||||||
|
JsonTokenType.False => new DownloadPathOption(false, null),
|
||||||
|
JsonTokenType.True => new DownloadPathOption(true, null),
|
||||||
|
JsonTokenType.String => new DownloadPathOption(true, reader.GetString()),
|
||||||
|
_ => throw new JsonException($"Unexpected token {reader.TokenType} when parsing download_path.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, DownloadPathOption? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
writer.WriteNullValue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value.Enabled)
|
||||||
|
{
|
||||||
|
writer.WriteBooleanValue(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(value.Path))
|
||||||
|
{
|
||||||
|
writer.WriteBooleanValue(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteStringValue(value.Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace Lantean.QBitTorrentClient.Converters
|
namespace Lantean.QBitTorrentClient.Converters
|
||||||
{
|
{
|
||||||
public class SaveLocationJsonConverter : JsonConverter<SaveLocation>
|
internal class SaveLocationJsonConverter : JsonConverter<SaveLocation>
|
||||||
{
|
{
|
||||||
public override SaveLocation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override SaveLocation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -44,5 +44,10 @@
|
|||||||
{
|
{
|
||||||
return new FormUrlEncodedContent(_parameters);
|
return new FormUrlEncodedContent(_parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal IList<KeyValuePair<string, string>> GetParameters()
|
||||||
|
{
|
||||||
|
return _parameters;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
using Lantean.QBitTorrentClient.Models;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
|
||||||
namespace Lantean.QBitTorrentClient
|
namespace Lantean.QBitTorrentClient
|
||||||
{
|
{
|
||||||
@@ -28,6 +32,12 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
Task SetApplicationPreferences(UpdatePreferences preferences);
|
Task SetApplicationPreferences(UpdatePreferences preferences);
|
||||||
|
|
||||||
|
Task<IReadOnlyList<ApplicationCookie>> GetApplicationCookies();
|
||||||
|
|
||||||
|
Task SetApplicationCookies(IEnumerable<ApplicationCookie> cookies);
|
||||||
|
|
||||||
|
Task<string> RotateApiKey();
|
||||||
|
|
||||||
Task<string> GetDefaultSavePath();
|
Task<string> GetDefaultSavePath();
|
||||||
|
|
||||||
Task<IReadOnlyList<NetworkInterface>> GetNetworkInterfaces();
|
Task<IReadOnlyList<NetworkInterface>> GetNetworkInterfaces();
|
||||||
@@ -36,6 +46,14 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
#endregion Application
|
#endregion Application
|
||||||
|
|
||||||
|
#region Client data
|
||||||
|
|
||||||
|
Task<IReadOnlyDictionary<string, JsonElement>> LoadClientData(IEnumerable<string>? keys = null);
|
||||||
|
|
||||||
|
Task StoreClientData(IReadOnlyDictionary<string, JsonElement> data);
|
||||||
|
|
||||||
|
#endregion Client data
|
||||||
|
|
||||||
#region Log
|
#region Log
|
||||||
|
|
||||||
Task<IReadOnlyList<Log>> GetLog(bool? normal = null, bool? info = null, bool? warning = null, bool? critical = null, int? lastKnownId = null);
|
Task<IReadOnlyList<Log>> GetLog(bool? normal = null, bool? info = null, bool? warning = null, bool? critical = null, int? lastKnownId = null);
|
||||||
@@ -74,7 +92,7 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
#region Torrent management
|
#region Torrent management
|
||||||
|
|
||||||
Task<IReadOnlyList<Torrent>> GetTorrentList(string? filter = null, string? category = null, string? tag = null, string? sort = null, bool? reverse = null, int? limit = null, int? offset = null, bool? isPrivate = null, params string[] hashes);
|
Task<IReadOnlyList<Torrent>> GetTorrentList(string? filter = null, string? category = null, string? tag = null, string? sort = null, bool? reverse = null, int? limit = null, int? offset = null, bool? isPrivate = null, bool? includeFiles = null, params string[] hashes);
|
||||||
|
|
||||||
Task<TorrentProperties> GetTorrentProperties(string hash);
|
Task<TorrentProperties> GetTorrentProperties(string hash);
|
||||||
|
|
||||||
@@ -82,16 +100,18 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
Task<IReadOnlyList<WebSeed>> GetTorrentWebSeeds(string hash);
|
Task<IReadOnlyList<WebSeed>> GetTorrentWebSeeds(string hash);
|
||||||
|
|
||||||
|
Task AddTorrentWebSeeds(string hash, IEnumerable<string> urls);
|
||||||
|
|
||||||
|
Task EditTorrentWebSeed(string hash, string originalUrl, string newUrl);
|
||||||
|
|
||||||
|
Task RemoveTorrentWebSeeds(string hash, IEnumerable<string> urls);
|
||||||
|
|
||||||
Task<IReadOnlyList<FileData>> GetTorrentContents(string hash, params int[] indexes);
|
Task<IReadOnlyList<FileData>> GetTorrentContents(string hash, params int[] indexes);
|
||||||
|
|
||||||
Task<IReadOnlyList<PieceState>> GetTorrentPieceStates(string hash);
|
Task<IReadOnlyList<PieceState>> GetTorrentPieceStates(string hash);
|
||||||
|
|
||||||
Task<IReadOnlyList<string>> GetTorrentPieceHashes(string hash);
|
Task<IReadOnlyList<string>> GetTorrentPieceHashes(string hash);
|
||||||
|
|
||||||
Task PauseTorrents(bool? all = null, params string[] hashes);
|
|
||||||
|
|
||||||
Task ResumeTorrents(bool? all = null, params string[] hashes);
|
|
||||||
|
|
||||||
Task StartTorrents(bool? all = null, params string[] hashes);
|
Task StartTorrents(bool? all = null, params string[] hashes);
|
||||||
|
|
||||||
Task StopTorrents(bool? all = null, params string[] hashes);
|
Task StopTorrents(bool? all = null, params string[] hashes);
|
||||||
@@ -100,15 +120,15 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
Task RecheckTorrents(bool? all = null, params string[] hashes);
|
Task RecheckTorrents(bool? all = null, params string[] hashes);
|
||||||
|
|
||||||
Task ReannounceTorrents(bool? all = null, params string[] hashes);
|
Task ReannounceTorrents(bool? all = null, IEnumerable<string>? trackers = null, params string[] hashes);
|
||||||
|
|
||||||
Task AddTorrent(AddTorrentParams addTorrentParams);
|
Task<AddTorrentResult> AddTorrent(AddTorrentParams addTorrentParams);
|
||||||
|
|
||||||
Task AddTrackersToTorrent(string hash, IEnumerable<string> urls);
|
Task AddTrackersToTorrent(IEnumerable<string> urls, bool? all = null, params string[] hashes);
|
||||||
|
|
||||||
Task EditTracker(string hash, string originalUrl, string newUrl);
|
Task EditTracker(string hash, string url, string? newUrl = null, int? tier = null);
|
||||||
|
|
||||||
Task RemoveTrackers(string hash, IEnumerable<string> urls);
|
Task RemoveTrackers(IEnumerable<string> urls, bool? all = null, params string[] hashes);
|
||||||
|
|
||||||
Task AddPeers(IEnumerable<string> hashes, IEnumerable<PeerId> peers);
|
Task AddPeers(IEnumerable<string> hashes, IEnumerable<PeerId> peers);
|
||||||
|
|
||||||
@@ -126,7 +146,7 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
Task SetTorrentDownloadLimit(long limit, bool? all = null, params string[] hashes);
|
Task SetTorrentDownloadLimit(long limit, bool? all = null, params string[] hashes);
|
||||||
|
|
||||||
Task SetTorrentShareLimit(float ratioLimit, float seedingTimeLimit, float inactiveSeedingTimeLimit, bool? all = null, params string[] hashes);
|
Task SetTorrentShareLimit(float ratioLimit, float seedingTimeLimit, float inactiveSeedingTimeLimit, ShareLimitAction? shareLimitAction = null, bool? all = null, params string[] hashes);
|
||||||
|
|
||||||
Task<IReadOnlyDictionary<string, long>> GetTorrentUploadLimit(bool? all = null, params string[] hashes);
|
Task<IReadOnlyDictionary<string, long>> GetTorrentUploadLimit(bool? all = null, params string[] hashes);
|
||||||
|
|
||||||
@@ -136,6 +156,8 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
Task SetTorrentName(string name, string hash);
|
Task SetTorrentName(string name, string hash);
|
||||||
|
|
||||||
|
Task SetTorrentComment(IEnumerable<string> hashes, string comment);
|
||||||
|
|
||||||
Task SetTorrentCategory(string category, bool? all = null, params string[] hashes);
|
Task SetTorrentCategory(string category, bool? all = null, params string[] hashes);
|
||||||
|
|
||||||
Task<IReadOnlyDictionary<string, Category>> GetAllCategories();
|
Task<IReadOnlyDictionary<string, Category>> GetAllCategories();
|
||||||
@@ -172,8 +194,26 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
Task<string> GetExportUrl(string hash);
|
Task<string> GetExportUrl(string hash);
|
||||||
|
|
||||||
|
Task<TorrentMetadata?> FetchMetadata(string source, string? downloader = null);
|
||||||
|
|
||||||
|
Task<IReadOnlyList<TorrentMetadata>> ParseMetadata(IEnumerable<(string FileName, Stream Content)> torrents);
|
||||||
|
|
||||||
|
Task<byte[]> SaveMetadata(string source);
|
||||||
|
|
||||||
#endregion Torrent management
|
#endregion Torrent management
|
||||||
|
|
||||||
|
#region Torrent creator
|
||||||
|
|
||||||
|
Task<string> AddTorrentCreationTask(TorrentCreationTaskRequest request);
|
||||||
|
|
||||||
|
Task<IReadOnlyList<TorrentCreationTaskStatus>> GetTorrentCreationTasks(string? taskId = null);
|
||||||
|
|
||||||
|
Task<byte[]> GetTorrentCreationTaskFile(string taskId);
|
||||||
|
|
||||||
|
Task DeleteTorrentCreationTask(string taskId);
|
||||||
|
|
||||||
|
#endregion Torrent creator
|
||||||
|
|
||||||
#region RSS
|
#region RSS
|
||||||
|
|
||||||
Task AddRssFolder(string path);
|
Task AddRssFolder(string path);
|
||||||
|
|||||||
@@ -7,4 +7,8 @@
|
|||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="Lantean.QBitTorrentClient.Test" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -12,9 +12,8 @@
|
|||||||
|
|
||||||
public bool? AddToTopOfQueue { get; set; }
|
public bool? AddToTopOfQueue { get; set; }
|
||||||
|
|
||||||
// v4
|
public bool? Forced { get; set; }
|
||||||
public bool? Paused { get; set; }
|
|
||||||
// v5
|
|
||||||
public bool? Stopped { get; set; }
|
public bool? Stopped { get; set; }
|
||||||
|
|
||||||
public string? SavePath { get; set; }
|
public string? SavePath { get; set; }
|
||||||
@@ -47,7 +46,15 @@
|
|||||||
|
|
||||||
public TorrentContentLayout? ContentLayout { get; set; }
|
public TorrentContentLayout? ContentLayout { get; set; }
|
||||||
|
|
||||||
public string? Cookie { get; set; }
|
public IEnumerable<Priority>? FilePriorities { get; set; }
|
||||||
|
|
||||||
|
public string? Downloader { get; set; }
|
||||||
|
|
||||||
|
public string? SslCertificate { get; set; }
|
||||||
|
|
||||||
|
public string? SslPrivateKey { get; set; }
|
||||||
|
|
||||||
|
public string? SslDhParams { get; set; }
|
||||||
|
|
||||||
public Dictionary<string, Stream>? Torrents { get; set; }
|
public Dictionary<string, Stream>? Torrents { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
30
Lantean.QBitTorrentClient/Models/AddTorrentResult.cs
Normal file
30
Lantean.QBitTorrentClient/Models/AddTorrentResult.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Models
|
||||||
|
{
|
||||||
|
public record AddTorrentResult
|
||||||
|
{
|
||||||
|
[JsonConstructor]
|
||||||
|
public AddTorrentResult(int successCount, int failureCount, int pendingCount, IReadOnlyList<string>? addedTorrentIds)
|
||||||
|
{
|
||||||
|
SuccessCount = successCount;
|
||||||
|
FailureCount = failureCount;
|
||||||
|
PendingCount = pendingCount;
|
||||||
|
AddedTorrentIds = addedTorrentIds ?? Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("success_count")]
|
||||||
|
public int SuccessCount { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("failure_count")]
|
||||||
|
public int FailureCount { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("pending_count")]
|
||||||
|
public int PendingCount { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("added_torrent_ids")]
|
||||||
|
public IReadOnlyList<string> AddedTorrentIds { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Lantean.QBitTorrentClient/Models/ApplicationCookie.cs
Normal file
33
Lantean.QBitTorrentClient/Models/ApplicationCookie.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Models
|
||||||
|
{
|
||||||
|
public record ApplicationCookie
|
||||||
|
{
|
||||||
|
[JsonConstructor]
|
||||||
|
public ApplicationCookie(string name, string? domain, string? path, string? value, long? expirationDate)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Domain = domain;
|
||||||
|
Path = path;
|
||||||
|
Value = value;
|
||||||
|
ExpirationDate = expirationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("domain")]
|
||||||
|
public string? Domain { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("path")]
|
||||||
|
public string? Path { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("value")]
|
||||||
|
public string? Value { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("expirationDate")]
|
||||||
|
public long? ExpirationDate { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json.Serialization;
|
using Lantean.QBitTorrentClient.Converters;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Lantean.QBitTorrentClient.Models
|
namespace Lantean.QBitTorrentClient.Models
|
||||||
{
|
{
|
||||||
@@ -7,10 +8,12 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
public Category(
|
public Category(
|
||||||
string name,
|
string name,
|
||||||
string? savePath)
|
string? savePath,
|
||||||
|
DownloadPathOption? downloadPath)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
SavePath = savePath;
|
SavePath = savePath;
|
||||||
|
DownloadPath = downloadPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName("name")]
|
[JsonPropertyName("name")]
|
||||||
@@ -18,5 +21,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
|
|
||||||
[JsonPropertyName("savePath")]
|
[JsonPropertyName("savePath")]
|
||||||
public string? SavePath { get; }
|
public string? SavePath { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("download_path")]
|
||||||
|
[JsonConverter(typeof(DownloadPathOptionJsonConverter))]
|
||||||
|
public DownloadPathOption? DownloadPath { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
15
Lantean.QBitTorrentClient/Models/DownloadPathOption.cs
Normal file
15
Lantean.QBitTorrentClient/Models/DownloadPathOption.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Lantean.QBitTorrentClient.Models
|
||||||
|
{
|
||||||
|
public record DownloadPathOption
|
||||||
|
{
|
||||||
|
public DownloadPathOption(bool enabled, string? path)
|
||||||
|
{
|
||||||
|
Enabled = enabled;
|
||||||
|
Path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Enabled { get; }
|
||||||
|
|
||||||
|
public string? Path { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,8 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
IReadOnlyList<string>? categoriesRemoved,
|
IReadOnlyList<string>? categoriesRemoved,
|
||||||
IReadOnlyList<string>? tags,
|
IReadOnlyList<string>? tags,
|
||||||
IReadOnlyList<string>? tagsRemoved,
|
IReadOnlyList<string>? tagsRemoved,
|
||||||
IReadOnlyDictionary<string, IReadOnlyList<string>> trackers,
|
IReadOnlyDictionary<string, IReadOnlyList<string>>? trackers,
|
||||||
|
IReadOnlyList<string>? trackersRemoved,
|
||||||
ServerState? serverState)
|
ServerState? serverState)
|
||||||
{
|
{
|
||||||
ResponseId = responseId;
|
ResponseId = responseId;
|
||||||
@@ -26,6 +27,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
Tags = tags;
|
Tags = tags;
|
||||||
TagsRemoved = tagsRemoved;
|
TagsRemoved = tagsRemoved;
|
||||||
Trackers = trackers;
|
Trackers = trackers;
|
||||||
|
TrackersRemoved = trackersRemoved;
|
||||||
ServerState = serverState;
|
ServerState = serverState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
string? flags,
|
string? flags,
|
||||||
string? flagsDescription,
|
string? flagsDescription,
|
||||||
string? iPAddress,
|
string? iPAddress,
|
||||||
|
string? i2pDestination,
|
||||||
string? clientId,
|
string? clientId,
|
||||||
int? port,
|
int? port,
|
||||||
float? progress,
|
float? progress,
|
||||||
@@ -33,6 +34,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
Flags = flags;
|
Flags = flags;
|
||||||
FlagsDescription = flagsDescription;
|
FlagsDescription = flagsDescription;
|
||||||
IPAddress = iPAddress;
|
IPAddress = iPAddress;
|
||||||
|
I2pDestination = i2pDestination;
|
||||||
ClientId = clientId;
|
ClientId = clientId;
|
||||||
Port = port;
|
Port = port;
|
||||||
Progress = progress;
|
Progress = progress;
|
||||||
@@ -71,6 +73,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("ip")]
|
[JsonPropertyName("ip")]
|
||||||
public string? IPAddress { get; }
|
public string? IPAddress { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("i2p_dest")]
|
||||||
|
public string? I2pDestination { get; }
|
||||||
|
|
||||||
[JsonPropertyName("peer_id_client")]
|
[JsonPropertyName("peer_id_client")]
|
||||||
public string? ClientId { get; }
|
public string? ClientId { get; }
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
public Preferences(
|
public Preferences(
|
||||||
bool addToTopOfQueue,
|
bool addToTopOfQueue,
|
||||||
|
bool addStoppedEnabled,
|
||||||
string addTrackers,
|
string addTrackers,
|
||||||
bool addTrackersEnabled,
|
bool addTrackersEnabled,
|
||||||
int altDlLimit,
|
int altDlLimit,
|
||||||
@@ -14,6 +15,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
bool alternativeWebuiEnabled,
|
bool alternativeWebuiEnabled,
|
||||||
string alternativeWebuiPath,
|
string alternativeWebuiPath,
|
||||||
string announceIp,
|
string announceIp,
|
||||||
|
int announcePort,
|
||||||
bool announceToAllTiers,
|
bool announceToAllTiers,
|
||||||
bool announceToAllTrackers,
|
bool announceToAllTrackers,
|
||||||
bool anonymousMode,
|
bool anonymousMode,
|
||||||
@@ -85,6 +87,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
int i2pPort,
|
int i2pPort,
|
||||||
bool idnSupportEnabled,
|
bool idnSupportEnabled,
|
||||||
bool incompleteFilesExt,
|
bool incompleteFilesExt,
|
||||||
|
bool useUnwantedFolder,
|
||||||
bool ipFilterEnabled,
|
bool ipFilterEnabled,
|
||||||
string ipFilterPath,
|
string ipFilterPath,
|
||||||
bool ipFilterTrackers,
|
bool ipFilterTrackers,
|
||||||
@@ -92,6 +95,8 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
bool limitTcpOverhead,
|
bool limitTcpOverhead,
|
||||||
bool limitUtpRate,
|
bool limitUtpRate,
|
||||||
int listenPort,
|
int listenPort,
|
||||||
|
bool sslEnabled,
|
||||||
|
int sslListenPort,
|
||||||
string locale,
|
string locale,
|
||||||
bool lsd,
|
bool lsd,
|
||||||
bool mailNotificationAuthEnabled,
|
bool mailNotificationAuthEnabled,
|
||||||
@@ -160,6 +165,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
string savePath,
|
string savePath,
|
||||||
bool savePathChangedTmmEnabled,
|
bool savePathChangedTmmEnabled,
|
||||||
int saveResumeDataInterval,
|
int saveResumeDataInterval,
|
||||||
|
int saveStatisticsInterval,
|
||||||
Dictionary<string, SaveLocation> scanDirs,
|
Dictionary<string, SaveLocation> scanDirs,
|
||||||
int scheduleFromHour,
|
int scheduleFromHour,
|
||||||
int scheduleFromMin,
|
int scheduleFromMin,
|
||||||
@@ -177,12 +183,12 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
int socketReceiveBufferSize,
|
int socketReceiveBufferSize,
|
||||||
int socketSendBufferSize,
|
int socketSendBufferSize,
|
||||||
bool ssrfMitigation,
|
bool ssrfMitigation,
|
||||||
bool startPausedEnabled,
|
|
||||||
int stopTrackerTimeout,
|
int stopTrackerTimeout,
|
||||||
string tempPath,
|
string tempPath,
|
||||||
bool tempPathEnabled,
|
bool tempPathEnabled,
|
||||||
bool torrentChangedTmmEnabled,
|
bool torrentChangedTmmEnabled,
|
||||||
string torrentContentLayout,
|
string torrentContentLayout,
|
||||||
|
string torrentContentRemoveOption,
|
||||||
int torrentFileSizeLimit,
|
int torrentFileSizeLimit,
|
||||||
string torrentStopCondition,
|
string torrentStopCondition,
|
||||||
int upLimit,
|
int upLimit,
|
||||||
@@ -192,10 +198,12 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
int upnpLeaseDuration,
|
int upnpLeaseDuration,
|
||||||
bool useCategoryPathsInManualMode,
|
bool useCategoryPathsInManualMode,
|
||||||
bool useHttps,
|
bool useHttps,
|
||||||
|
bool ignoreSslErrors,
|
||||||
bool useSubcategories,
|
bool useSubcategories,
|
||||||
int utpTcpMixedMode,
|
int utpTcpMixedMode,
|
||||||
bool validateHttpsTrackerCertificate,
|
bool validateHttpsTrackerCertificate,
|
||||||
string webUiAddress,
|
string webUiAddress,
|
||||||
|
string webUiApiKey,
|
||||||
int webUiBanDuration,
|
int webUiBanDuration,
|
||||||
bool webUiClickjackingProtectionEnabled,
|
bool webUiClickjackingProtectionEnabled,
|
||||||
bool webUiCsrfProtectionEnabled,
|
bool webUiCsrfProtectionEnabled,
|
||||||
@@ -213,10 +221,14 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
bool webUiUpnp,
|
bool webUiUpnp,
|
||||||
bool webUiUseCustomHttpHeadersEnabled,
|
bool webUiUseCustomHttpHeadersEnabled,
|
||||||
string webUiUsername,
|
string webUiUsername,
|
||||||
string webUiPassword
|
string webUiPassword,
|
||||||
|
bool confirmTorrentDeletion,
|
||||||
|
bool confirmTorrentRecheck,
|
||||||
|
bool statusBarExternalIp
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
AddToTopOfQueue = addToTopOfQueue;
|
AddToTopOfQueue = addToTopOfQueue;
|
||||||
|
AddStoppedEnabled = addStoppedEnabled;
|
||||||
AddTrackers = addTrackers;
|
AddTrackers = addTrackers;
|
||||||
AddTrackersEnabled = addTrackersEnabled;
|
AddTrackersEnabled = addTrackersEnabled;
|
||||||
AltDlLimit = altDlLimit;
|
AltDlLimit = altDlLimit;
|
||||||
@@ -224,6 +236,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
AlternativeWebuiEnabled = alternativeWebuiEnabled;
|
AlternativeWebuiEnabled = alternativeWebuiEnabled;
|
||||||
AlternativeWebuiPath = alternativeWebuiPath;
|
AlternativeWebuiPath = alternativeWebuiPath;
|
||||||
AnnounceIp = announceIp;
|
AnnounceIp = announceIp;
|
||||||
|
AnnouncePort = announcePort;
|
||||||
AnnounceToAllTiers = announceToAllTiers;
|
AnnounceToAllTiers = announceToAllTiers;
|
||||||
AnnounceToAllTrackers = announceToAllTrackers;
|
AnnounceToAllTrackers = announceToAllTrackers;
|
||||||
AnonymousMode = anonymousMode;
|
AnonymousMode = anonymousMode;
|
||||||
@@ -295,6 +308,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
I2pPort = i2pPort;
|
I2pPort = i2pPort;
|
||||||
IdnSupportEnabled = idnSupportEnabled;
|
IdnSupportEnabled = idnSupportEnabled;
|
||||||
IncompleteFilesExt = incompleteFilesExt;
|
IncompleteFilesExt = incompleteFilesExt;
|
||||||
|
UseUnwantedFolder = useUnwantedFolder;
|
||||||
IpFilterEnabled = ipFilterEnabled;
|
IpFilterEnabled = ipFilterEnabled;
|
||||||
IpFilterPath = ipFilterPath;
|
IpFilterPath = ipFilterPath;
|
||||||
IpFilterTrackers = ipFilterTrackers;
|
IpFilterTrackers = ipFilterTrackers;
|
||||||
@@ -302,6 +316,8 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
LimitTcpOverhead = limitTcpOverhead;
|
LimitTcpOverhead = limitTcpOverhead;
|
||||||
LimitUtpRate = limitUtpRate;
|
LimitUtpRate = limitUtpRate;
|
||||||
ListenPort = listenPort;
|
ListenPort = listenPort;
|
||||||
|
SslEnabled = sslEnabled;
|
||||||
|
SslListenPort = sslListenPort;
|
||||||
Locale = locale;
|
Locale = locale;
|
||||||
Lsd = lsd;
|
Lsd = lsd;
|
||||||
MailNotificationAuthEnabled = mailNotificationAuthEnabled;
|
MailNotificationAuthEnabled = mailNotificationAuthEnabled;
|
||||||
@@ -370,6 +386,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
SavePath = savePath;
|
SavePath = savePath;
|
||||||
SavePathChangedTmmEnabled = savePathChangedTmmEnabled;
|
SavePathChangedTmmEnabled = savePathChangedTmmEnabled;
|
||||||
SaveResumeDataInterval = saveResumeDataInterval;
|
SaveResumeDataInterval = saveResumeDataInterval;
|
||||||
|
SaveStatisticsInterval = saveStatisticsInterval;
|
||||||
ScanDirs = scanDirs;
|
ScanDirs = scanDirs;
|
||||||
ScheduleFromHour = scheduleFromHour;
|
ScheduleFromHour = scheduleFromHour;
|
||||||
ScheduleFromMin = scheduleFromMin;
|
ScheduleFromMin = scheduleFromMin;
|
||||||
@@ -387,12 +404,12 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
SocketReceiveBufferSize = socketReceiveBufferSize;
|
SocketReceiveBufferSize = socketReceiveBufferSize;
|
||||||
SocketSendBufferSize = socketSendBufferSize;
|
SocketSendBufferSize = socketSendBufferSize;
|
||||||
SsrfMitigation = ssrfMitigation;
|
SsrfMitigation = ssrfMitigation;
|
||||||
StartPausedEnabled = startPausedEnabled;
|
|
||||||
StopTrackerTimeout = stopTrackerTimeout;
|
StopTrackerTimeout = stopTrackerTimeout;
|
||||||
TempPath = tempPath;
|
TempPath = tempPath;
|
||||||
TempPathEnabled = tempPathEnabled;
|
TempPathEnabled = tempPathEnabled;
|
||||||
TorrentChangedTmmEnabled = torrentChangedTmmEnabled;
|
TorrentChangedTmmEnabled = torrentChangedTmmEnabled;
|
||||||
TorrentContentLayout = torrentContentLayout;
|
TorrentContentLayout = torrentContentLayout;
|
||||||
|
TorrentContentRemoveOption = torrentContentRemoveOption;
|
||||||
TorrentFileSizeLimit = torrentFileSizeLimit;
|
TorrentFileSizeLimit = torrentFileSizeLimit;
|
||||||
TorrentStopCondition = torrentStopCondition;
|
TorrentStopCondition = torrentStopCondition;
|
||||||
UpLimit = upLimit;
|
UpLimit = upLimit;
|
||||||
@@ -402,10 +419,12 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
UpnpLeaseDuration = upnpLeaseDuration;
|
UpnpLeaseDuration = upnpLeaseDuration;
|
||||||
UseCategoryPathsInManualMode = useCategoryPathsInManualMode;
|
UseCategoryPathsInManualMode = useCategoryPathsInManualMode;
|
||||||
UseHttps = useHttps;
|
UseHttps = useHttps;
|
||||||
|
IgnoreSslErrors = ignoreSslErrors;
|
||||||
UseSubcategories = useSubcategories;
|
UseSubcategories = useSubcategories;
|
||||||
UtpTcpMixedMode = utpTcpMixedMode;
|
UtpTcpMixedMode = utpTcpMixedMode;
|
||||||
ValidateHttpsTrackerCertificate = validateHttpsTrackerCertificate;
|
ValidateHttpsTrackerCertificate = validateHttpsTrackerCertificate;
|
||||||
WebUiAddress = webUiAddress;
|
WebUiAddress = webUiAddress;
|
||||||
|
WebUiApiKey = webUiApiKey;
|
||||||
WebUiBanDuration = webUiBanDuration;
|
WebUiBanDuration = webUiBanDuration;
|
||||||
WebUiClickjackingProtectionEnabled = webUiClickjackingProtectionEnabled;
|
WebUiClickjackingProtectionEnabled = webUiClickjackingProtectionEnabled;
|
||||||
WebUiCsrfProtectionEnabled = webUiCsrfProtectionEnabled;
|
WebUiCsrfProtectionEnabled = webUiCsrfProtectionEnabled;
|
||||||
@@ -424,11 +443,17 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
WebUiUseCustomHttpHeadersEnabled = webUiUseCustomHttpHeadersEnabled;
|
WebUiUseCustomHttpHeadersEnabled = webUiUseCustomHttpHeadersEnabled;
|
||||||
WebUiUsername = webUiUsername;
|
WebUiUsername = webUiUsername;
|
||||||
WebUiPassword = webUiPassword;
|
WebUiPassword = webUiPassword;
|
||||||
|
ConfirmTorrentDeletion = confirmTorrentDeletion;
|
||||||
|
ConfirmTorrentRecheck = confirmTorrentRecheck;
|
||||||
|
StatusBarExternalIp = statusBarExternalIp;
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName("add_to_top_of_queue")]
|
[JsonPropertyName("add_to_top_of_queue")]
|
||||||
public bool AddToTopOfQueue { get; }
|
public bool AddToTopOfQueue { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("add_stopped_enabled")]
|
||||||
|
public bool AddStoppedEnabled { get; }
|
||||||
|
|
||||||
[JsonPropertyName("add_trackers")]
|
[JsonPropertyName("add_trackers")]
|
||||||
public string AddTrackers { get; }
|
public string AddTrackers { get; }
|
||||||
|
|
||||||
@@ -450,6 +475,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("announce_ip")]
|
[JsonPropertyName("announce_ip")]
|
||||||
public string AnnounceIp { get; }
|
public string AnnounceIp { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("announce_port")]
|
||||||
|
public int AnnouncePort { get; }
|
||||||
|
|
||||||
[JsonPropertyName("announce_to_all_tiers")]
|
[JsonPropertyName("announce_to_all_tiers")]
|
||||||
public bool AnnounceToAllTiers { get; }
|
public bool AnnounceToAllTiers { get; }
|
||||||
|
|
||||||
@@ -663,6 +691,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("incomplete_files_ext")]
|
[JsonPropertyName("incomplete_files_ext")]
|
||||||
public bool IncompleteFilesExt { get; }
|
public bool IncompleteFilesExt { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("use_unwanted_folder")]
|
||||||
|
public bool UseUnwantedFolder { get; }
|
||||||
|
|
||||||
[JsonPropertyName("ip_filter_enabled")]
|
[JsonPropertyName("ip_filter_enabled")]
|
||||||
public bool IpFilterEnabled { get; }
|
public bool IpFilterEnabled { get; }
|
||||||
|
|
||||||
@@ -684,6 +715,12 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("listen_port")]
|
[JsonPropertyName("listen_port")]
|
||||||
public int ListenPort { get; }
|
public int ListenPort { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("ssl_enabled")]
|
||||||
|
public bool SslEnabled { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("ssl_listen_port")]
|
||||||
|
public int SslListenPort { get; }
|
||||||
|
|
||||||
[JsonPropertyName("locale")]
|
[JsonPropertyName("locale")]
|
||||||
public string Locale { get; }
|
public string Locale { get; }
|
||||||
|
|
||||||
@@ -888,6 +925,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("save_resume_data_interval")]
|
[JsonPropertyName("save_resume_data_interval")]
|
||||||
public int SaveResumeDataInterval { get; }
|
public int SaveResumeDataInterval { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("save_statistics_interval")]
|
||||||
|
public int SaveStatisticsInterval { get; }
|
||||||
|
|
||||||
[JsonPropertyName("scan_dirs")]
|
[JsonPropertyName("scan_dirs")]
|
||||||
public Dictionary<string, SaveLocation> ScanDirs { get; }
|
public Dictionary<string, SaveLocation> ScanDirs { get; }
|
||||||
|
|
||||||
@@ -939,9 +979,6 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("ssrf_mitigation")]
|
[JsonPropertyName("ssrf_mitigation")]
|
||||||
public bool SsrfMitigation { get; }
|
public bool SsrfMitigation { get; }
|
||||||
|
|
||||||
[JsonPropertyName("start_paused_enabled")]
|
|
||||||
public bool StartPausedEnabled { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("stop_tracker_timeout")]
|
[JsonPropertyName("stop_tracker_timeout")]
|
||||||
public int StopTrackerTimeout { get; }
|
public int StopTrackerTimeout { get; }
|
||||||
|
|
||||||
@@ -957,6 +994,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("torrent_content_layout")]
|
[JsonPropertyName("torrent_content_layout")]
|
||||||
public string TorrentContentLayout { get; }
|
public string TorrentContentLayout { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("torrent_content_remove_option")]
|
||||||
|
public string TorrentContentRemoveOption { get; }
|
||||||
|
|
||||||
[JsonPropertyName("torrent_file_size_limit")]
|
[JsonPropertyName("torrent_file_size_limit")]
|
||||||
public int TorrentFileSizeLimit { get; }
|
public int TorrentFileSizeLimit { get; }
|
||||||
|
|
||||||
@@ -984,6 +1024,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("use_https")]
|
[JsonPropertyName("use_https")]
|
||||||
public bool UseHttps { get; }
|
public bool UseHttps { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("ignore_ssl_errors")]
|
||||||
|
public bool IgnoreSslErrors { get; }
|
||||||
|
|
||||||
[JsonPropertyName("use_subcategories")]
|
[JsonPropertyName("use_subcategories")]
|
||||||
public bool UseSubcategories { get; }
|
public bool UseSubcategories { get; }
|
||||||
|
|
||||||
@@ -996,6 +1039,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("web_ui_address")]
|
[JsonPropertyName("web_ui_address")]
|
||||||
public string WebUiAddress { get; }
|
public string WebUiAddress { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("web_ui_api_key")]
|
||||||
|
public string WebUiApiKey { get; }
|
||||||
|
|
||||||
[JsonPropertyName("web_ui_ban_duration")]
|
[JsonPropertyName("web_ui_ban_duration")]
|
||||||
public int WebUiBanDuration { get; }
|
public int WebUiBanDuration { get; }
|
||||||
|
|
||||||
@@ -1049,5 +1095,14 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
|
|
||||||
[JsonPropertyName("web_ui_password")]
|
[JsonPropertyName("web_ui_password")]
|
||||||
public string WebUiPassword { get; }
|
public string WebUiPassword { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("confirm_torrent_deletion")]
|
||||||
|
public bool ConfirmTorrentDeletion { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("confirm_torrent_recheck")]
|
||||||
|
public bool ConfirmTorrentRecheck { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("status_bar_external_ip")]
|
||||||
|
public bool StatusBarExternalIp { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,31 +12,51 @@
|
|||||||
{
|
{
|
||||||
if (value is int intValue)
|
if (value is int intValue)
|
||||||
{
|
{
|
||||||
if (intValue == 0)
|
return Create(intValue);
|
||||||
|
}
|
||||||
|
else if (value is string stringValue)
|
||||||
|
{
|
||||||
|
return Create(stringValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SaveLocation Create(int value)
|
||||||
|
{
|
||||||
|
if (value == 0)
|
||||||
{
|
{
|
||||||
return new SaveLocation
|
return new SaveLocation
|
||||||
{
|
{
|
||||||
IsWatchedFolder = true
|
IsWatchedFolder = true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if (intValue == 1)
|
else if (value == 1)
|
||||||
{
|
{
|
||||||
return new SaveLocation
|
return new SaveLocation
|
||||||
{
|
{
|
||||||
IsDefaultFolder = true
|
IsDefaultFolder = true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(value));
|
||||||
}
|
}
|
||||||
else if (value is string stringValue)
|
|
||||||
|
public static SaveLocation Create(string? value)
|
||||||
{
|
{
|
||||||
if (stringValue == "0")
|
if (value is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == "0")
|
||||||
{
|
{
|
||||||
return new SaveLocation
|
return new SaveLocation
|
||||||
{
|
{
|
||||||
IsWatchedFolder = true
|
IsWatchedFolder = true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if (stringValue == "1")
|
else if (value == "1")
|
||||||
{
|
{
|
||||||
return new SaveLocation
|
return new SaveLocation
|
||||||
{
|
{
|
||||||
@@ -47,14 +67,11 @@
|
|||||||
{
|
{
|
||||||
return new SaveLocation
|
return new SaveLocation
|
||||||
{
|
{
|
||||||
SavePath = stringValue
|
SavePath = value
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ToValue()
|
public object ToValue()
|
||||||
{
|
{
|
||||||
if (IsWatchedFolder)
|
if (IsWatchedFolder)
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
long? uploadRateLimit,
|
long? uploadRateLimit,
|
||||||
bool? useAltSpeedLimits,
|
bool? useAltSpeedLimits,
|
||||||
bool? useSubcategories,
|
bool? useSubcategories,
|
||||||
float? writeCacheOverload) : base(connectionStatus, dHTNodes, downloadInfoData, downloadInfoSpeed, downloadRateLimit, uploadInfoData, uploadInfoSpeed, uploadRateLimit)
|
float? writeCacheOverload,
|
||||||
|
string? lastExternalAddressV4 = null,
|
||||||
|
string? lastExternalAddressV6 = null) : base(connectionStatus, dHTNodes, downloadInfoData, downloadInfoSpeed, downloadRateLimit, uploadInfoData, uploadInfoSpeed, uploadRateLimit)
|
||||||
{
|
{
|
||||||
AllTimeDownloaded = allTimeDownloaded;
|
AllTimeDownloaded = allTimeDownloaded;
|
||||||
AllTimeUploaded = allTimeUploaded;
|
AllTimeUploaded = allTimeUploaded;
|
||||||
@@ -49,6 +51,8 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
UseAltSpeedLimits = useAltSpeedLimits;
|
UseAltSpeedLimits = useAltSpeedLimits;
|
||||||
UseSubcategories = useSubcategories;
|
UseSubcategories = useSubcategories;
|
||||||
WriteCacheOverload = writeCacheOverload;
|
WriteCacheOverload = writeCacheOverload;
|
||||||
|
LastExternalAddressV4 = lastExternalAddressV4;
|
||||||
|
LastExternalAddressV6 = lastExternalAddressV6;
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName("alltime_dl")]
|
[JsonPropertyName("alltime_dl")]
|
||||||
@@ -101,5 +105,11 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
|
|
||||||
[JsonPropertyName("write_cache_overload")]
|
[JsonPropertyName("write_cache_overload")]
|
||||||
public float? WriteCacheOverload { get; }
|
public float? WriteCacheOverload { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("last_external_address_v4")]
|
||||||
|
public string? LastExternalAddressV4 { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("last_external_address_v6")]
|
||||||
|
public string? LastExternalAddressV6 { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,264 +1,219 @@
|
|||||||
using Lantean.QBitTorrentClient.Converters;
|
using Lantean.QBitTorrentClient.Converters;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Lantean.QBitTorrentClient.Models
|
namespace Lantean.QBitTorrentClient.Models
|
||||||
{
|
{
|
||||||
public record Torrent
|
public record Torrent
|
||||||
{
|
{
|
||||||
[JsonConstructor]
|
|
||||||
public Torrent(
|
|
||||||
long? addedOn,
|
|
||||||
long? amountLeft,
|
|
||||||
bool? automaticTorrentManagement,
|
|
||||||
float? availability,
|
|
||||||
string? category,
|
|
||||||
long? completed,
|
|
||||||
long? completionOn,
|
|
||||||
string? contentPath,
|
|
||||||
long? downloadLimit,
|
|
||||||
long? downloadSpeed,
|
|
||||||
long? downloaded,
|
|
||||||
long? downloadedSession,
|
|
||||||
long? estimatedTimeOfArrival,
|
|
||||||
bool? firstLastPiecePriority,
|
|
||||||
bool? forceStart,
|
|
||||||
string hash,
|
|
||||||
string? infoHashV1,
|
|
||||||
string? infoHashV2,
|
|
||||||
long? lastActivity,
|
|
||||||
string? magnetUri,
|
|
||||||
float? maxRatio,
|
|
||||||
int? maxSeedingTime,
|
|
||||||
string? name,
|
|
||||||
int? numberComplete,
|
|
||||||
int? numberIncomplete,
|
|
||||||
int? numberLeeches,
|
|
||||||
int? numberSeeds,
|
|
||||||
int? priority,
|
|
||||||
float? progress,
|
|
||||||
float? ratio,
|
|
||||||
float? ratioLimit,
|
|
||||||
string? savePath,
|
|
||||||
long? seedingTime,
|
|
||||||
int? seedingTimeLimit,
|
|
||||||
long? seenComplete,
|
|
||||||
bool? sequentialDownload,
|
|
||||||
long? size,
|
|
||||||
string? state,
|
|
||||||
bool? superSeeding,
|
|
||||||
IReadOnlyList<string>? tags,
|
|
||||||
int? timeActive,
|
|
||||||
long? totalSize,
|
|
||||||
string? tracker,
|
|
||||||
long? uploadLimit,
|
|
||||||
long? uploaded,
|
|
||||||
long? uploadedSession,
|
|
||||||
long? uploadSpeed,
|
|
||||||
long? reannounce,
|
|
||||||
float? inactiveSeedingTimeLimit,
|
|
||||||
float? maxInactiveSeedingTime)
|
|
||||||
{
|
|
||||||
AddedOn = addedOn;
|
|
||||||
AmountLeft = amountLeft;
|
|
||||||
AutomaticTorrentManagement = automaticTorrentManagement;
|
|
||||||
Availability = availability;
|
|
||||||
Category = category;
|
|
||||||
Completed = completed;
|
|
||||||
CompletionOn = completionOn;
|
|
||||||
ContentPath = contentPath;
|
|
||||||
DownloadLimit = downloadLimit;
|
|
||||||
DownloadSpeed = downloadSpeed;
|
|
||||||
Downloaded = downloaded;
|
|
||||||
DownloadedSession = downloadedSession;
|
|
||||||
EstimatedTimeOfArrival = estimatedTimeOfArrival;
|
|
||||||
FirstLastPiecePriority = firstLastPiecePriority;
|
|
||||||
ForceStart = forceStart;
|
|
||||||
Hash = hash;
|
|
||||||
InfoHashV1 = infoHashV1;
|
|
||||||
InfoHashV2 = infoHashV2;
|
|
||||||
LastActivity = lastActivity;
|
|
||||||
MagnetUri = magnetUri;
|
|
||||||
MaxRatio = maxRatio;
|
|
||||||
MaxSeedingTime = maxSeedingTime;
|
|
||||||
Name = name;
|
|
||||||
NumberComplete = numberComplete;
|
|
||||||
NumberIncomplete = numberIncomplete;
|
|
||||||
NumberLeeches = numberLeeches;
|
|
||||||
NumberSeeds = numberSeeds;
|
|
||||||
Priority = priority;
|
|
||||||
Progress = progress;
|
|
||||||
Ratio = ratio;
|
|
||||||
RatioLimit = ratioLimit;
|
|
||||||
SavePath = savePath;
|
|
||||||
SeedingTime = seedingTime;
|
|
||||||
SeedingTimeLimit = seedingTimeLimit;
|
|
||||||
SeenComplete = seenComplete;
|
|
||||||
SequentialDownload = sequentialDownload;
|
|
||||||
Size = size;
|
|
||||||
State = state;
|
|
||||||
SuperSeeding = superSeeding;
|
|
||||||
Tags = tags ?? [];
|
|
||||||
TimeActive = timeActive;
|
|
||||||
TotalSize = totalSize;
|
|
||||||
Tracker = tracker;
|
|
||||||
UploadLimit = uploadLimit;
|
|
||||||
Uploaded = uploaded;
|
|
||||||
UploadedSession = uploadedSession;
|
|
||||||
UploadSpeed = uploadSpeed;
|
|
||||||
Reannounce = reannounce;
|
|
||||||
InactiveSeedingTimeLimit = inactiveSeedingTimeLimit;
|
|
||||||
MaxInactiveSeedingTime = maxInactiveSeedingTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonPropertyName("added_on")]
|
|
||||||
public long? AddedOn { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("amount_left")]
|
|
||||||
public long? AmountLeft { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("auto_tmm")]
|
|
||||||
public bool? AutomaticTorrentManagement { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("availability")]
|
|
||||||
public float? Availability { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("category")]
|
|
||||||
public string? Category { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("completed")]
|
|
||||||
public long? Completed { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("completion_on")]
|
|
||||||
public long? CompletionOn { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("content_path")]
|
|
||||||
public string? ContentPath { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("dl_limit")]
|
|
||||||
public long? DownloadLimit { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("dlspeed")]
|
|
||||||
public long? DownloadSpeed { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("downloaded")]
|
|
||||||
public long? Downloaded { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("downloaded_session")]
|
|
||||||
public long? DownloadedSession { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("eta")]
|
|
||||||
public long? EstimatedTimeOfArrival { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("f_l_piece_prio")]
|
|
||||||
public bool? FirstLastPiecePriority { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("force_start")]
|
|
||||||
public bool? ForceStart { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("hash")]
|
[JsonPropertyName("hash")]
|
||||||
public string Hash { get; }
|
public string Hash { get; init; } = string.Empty;
|
||||||
|
|
||||||
[JsonPropertyName("infohash_v1")]
|
[JsonPropertyName("infohash_v1")]
|
||||||
public string? InfoHashV1 { get; }
|
public string? InfoHashV1 { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("infohash_v2")]
|
[JsonPropertyName("infohash_v2")]
|
||||||
public string? InfoHashV2 { get; }
|
public string? InfoHashV2 { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("last_activity")]
|
|
||||||
public long? LastActivity { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("magnet_uri")]
|
|
||||||
public string? MagnetUri { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("max_ratio")]
|
|
||||||
public float? MaxRatio { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("max_seeding_time")]
|
|
||||||
public int? MaxSeedingTime { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("name")]
|
[JsonPropertyName("name")]
|
||||||
public string? Name { get; }
|
public string? Name { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("num_complete")]
|
[JsonPropertyName("magnet_uri")]
|
||||||
public int? NumberComplete { get; }
|
public string? MagnetUri { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("num_incomplete")]
|
|
||||||
public int? NumberIncomplete { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("num_leechs")]
|
|
||||||
public int? NumberLeeches { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("num_seeds")]
|
|
||||||
public int? NumberSeeds { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("priority")]
|
|
||||||
public int? Priority { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("progress")]
|
|
||||||
public float? Progress { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("ratio")]
|
|
||||||
public float? Ratio { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("ratio_limit")]
|
|
||||||
public float? RatioLimit { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("save_path")]
|
|
||||||
public string? SavePath { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("seeding_time")]
|
|
||||||
public long? SeedingTime { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("seeding_time_limit")]
|
|
||||||
public int? SeedingTimeLimit { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("seen_complete")]
|
|
||||||
public long? SeenComplete { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("seq_dl")]
|
|
||||||
public bool? SequentialDownload { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("size")]
|
[JsonPropertyName("size")]
|
||||||
public long? Size { get; }
|
public long? Size { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("progress")]
|
||||||
|
public float? Progress { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("dlspeed")]
|
||||||
|
public long? DownloadSpeed { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("upspeed")]
|
||||||
|
public long? UploadSpeed { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("priority")]
|
||||||
|
public int? Priority { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("num_seeds")]
|
||||||
|
public int? NumberSeeds { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("num_complete")]
|
||||||
|
public int? NumberComplete { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("num_leechs")]
|
||||||
|
public int? NumberLeeches { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("num_incomplete")]
|
||||||
|
public int? NumberIncomplete { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("ratio")]
|
||||||
|
public float? Ratio { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("popularity")]
|
||||||
|
public float? Popularity { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("eta")]
|
||||||
|
public long? EstimatedTimeOfArrival { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("state")]
|
[JsonPropertyName("state")]
|
||||||
public string? State { get; }
|
public string? State { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("super_seeding")]
|
[JsonPropertyName("seq_dl")]
|
||||||
public bool? SuperSeeding { get; }
|
public bool? SequentialDownload { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("f_l_piece_prio")]
|
||||||
|
public bool? FirstLastPiecePriority { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("category")]
|
||||||
|
public string? Category { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("tags")]
|
[JsonPropertyName("tags")]
|
||||||
[JsonConverter(typeof(CommaSeparatedJsonConverter))]
|
[JsonConverter(typeof(CommaSeparatedJsonConverter))]
|
||||||
public IReadOnlyList<string>? Tags { get; }
|
public IReadOnlyList<string> Tags { get; init; } = Array.Empty<string>();
|
||||||
|
|
||||||
[JsonPropertyName("time_active")]
|
[JsonPropertyName("super_seeding")]
|
||||||
public int? TimeActive { get; }
|
public bool? SuperSeeding { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("total_size")]
|
[JsonPropertyName("force_start")]
|
||||||
public long? TotalSize { get; }
|
public bool? ForceStart { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("save_path")]
|
||||||
|
public string? SavePath { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("download_path")]
|
||||||
|
public string? DownloadPath { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("content_path")]
|
||||||
|
public string? ContentPath { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("root_path")]
|
||||||
|
public string? RootPath { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("added_on")]
|
||||||
|
public long? AddedOn { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("completion_on")]
|
||||||
|
public long? CompletionOn { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("tracker")]
|
[JsonPropertyName("tracker")]
|
||||||
public string? Tracker { get; }
|
public string? Tracker { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("trackers_count")]
|
||||||
|
public int? TrackersCount { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("dl_limit")]
|
||||||
|
public long? DownloadLimit { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("up_limit")]
|
[JsonPropertyName("up_limit")]
|
||||||
public long? UploadLimit { get; }
|
public long? UploadLimit { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("downloaded")]
|
||||||
|
public long? Downloaded { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("uploaded")]
|
[JsonPropertyName("uploaded")]
|
||||||
public long? Uploaded { get; }
|
public long? Uploaded { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("downloaded_session")]
|
||||||
|
public long? DownloadedSession { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("uploaded_session")]
|
[JsonPropertyName("uploaded_session")]
|
||||||
public long? UploadedSession { get; }
|
public long? UploadedSession { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("upspeed")]
|
[JsonPropertyName("amount_left")]
|
||||||
public long? UploadSpeed { get; }
|
public long? AmountLeft { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("reannounce")]
|
[JsonPropertyName("completed")]
|
||||||
public long? Reannounce { get; }
|
public long? Completed { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("inactive_seeding_time_limit")]
|
[JsonPropertyName("connections_count")]
|
||||||
public float? InactiveSeedingTimeLimit { get; }
|
public int? ConnectionsCount { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("connections_limit")]
|
||||||
|
public int? ConnectionsLimit { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("max_ratio")]
|
||||||
|
public float? MaxRatio { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("max_seeding_time")]
|
||||||
|
public int? MaxSeedingTime { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("max_inactive_seeding_time")]
|
[JsonPropertyName("max_inactive_seeding_time")]
|
||||||
public float? MaxInactiveSeedingTime { get; }
|
public float? MaxInactiveSeedingTime { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("ratio_limit")]
|
||||||
|
public float? RatioLimit { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("seeding_time_limit")]
|
||||||
|
public int? SeedingTimeLimit { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("inactive_seeding_time_limit")]
|
||||||
|
public float? InactiveSeedingTimeLimit { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("share_limit_action")]
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public ShareLimitAction? ShareLimitAction { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("seen_complete")]
|
||||||
|
public long? SeenComplete { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("last_activity")]
|
||||||
|
public long? LastActivity { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("total_size")]
|
||||||
|
public long? TotalSize { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("auto_tmm")]
|
||||||
|
public bool? AutomaticTorrentManagement { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("time_active")]
|
||||||
|
public int? TimeActive { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("seeding_time")]
|
||||||
|
public long? SeedingTime { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("availability")]
|
||||||
|
public float? Availability { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("reannounce")]
|
||||||
|
public long? Reannounce { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("comment")]
|
||||||
|
public string? Comment { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("has_metadata")]
|
||||||
|
public bool? HasMetadata { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("created_by")]
|
||||||
|
public string? CreatedBy { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("creation_date")]
|
||||||
|
public long? CreationDate { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("private")]
|
||||||
|
public bool? IsPrivate { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("total_wasted")]
|
||||||
|
public long? TotalWasted { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("pieces_num")]
|
||||||
|
public int? PiecesCount { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("piece_size")]
|
||||||
|
public long? PieceSize { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("pieces_have")]
|
||||||
|
public int? PiecesHave { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("has_tracker_warning")]
|
||||||
|
public bool? HasTrackerWarning { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("has_tracker_error")]
|
||||||
|
public bool? HasTrackerError { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("has_other_announce_error")]
|
||||||
|
public bool? HasOtherAnnounceError { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
131
Lantean.QBitTorrentClient/Models/TorrentCreationTask.cs
Normal file
131
Lantean.QBitTorrentClient/Models/TorrentCreationTask.cs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Models
|
||||||
|
{
|
||||||
|
public class TorrentCreationTaskRequest
|
||||||
|
{
|
||||||
|
public string SourcePath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? TorrentFilePath { get; set; }
|
||||||
|
|
||||||
|
public int? PieceSize { get; set; }
|
||||||
|
|
||||||
|
public bool? Private { get; set; }
|
||||||
|
|
||||||
|
public bool? StartSeeding { get; set; }
|
||||||
|
|
||||||
|
public string? Comment { get; set; }
|
||||||
|
|
||||||
|
public string? Source { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<string>? Trackers { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<string>? UrlSeeds { get; set; }
|
||||||
|
|
||||||
|
public string? Format { get; set; }
|
||||||
|
|
||||||
|
public bool? OptimizeAlignment { get; set; }
|
||||||
|
|
||||||
|
public int? PaddedFileSizeLimit { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record TorrentCreationTaskStatus
|
||||||
|
{
|
||||||
|
[JsonConstructor]
|
||||||
|
public TorrentCreationTaskStatus(
|
||||||
|
string taskID,
|
||||||
|
string? sourcePath,
|
||||||
|
int? pieceSize,
|
||||||
|
bool? @private,
|
||||||
|
string? timeAdded,
|
||||||
|
string? format,
|
||||||
|
bool? optimizeAlignment,
|
||||||
|
int? paddedFileSizeLimit,
|
||||||
|
string? status,
|
||||||
|
string? comment,
|
||||||
|
string? torrentFilePath,
|
||||||
|
string? source,
|
||||||
|
IReadOnlyList<string>? trackers,
|
||||||
|
IReadOnlyList<string>? urlSeeds,
|
||||||
|
string? timeStarted,
|
||||||
|
string? timeFinished,
|
||||||
|
string? errorMessage,
|
||||||
|
double? progress)
|
||||||
|
{
|
||||||
|
TaskId = taskID;
|
||||||
|
SourcePath = sourcePath;
|
||||||
|
PieceSize = pieceSize;
|
||||||
|
Private = @private;
|
||||||
|
TimeAdded = timeAdded;
|
||||||
|
Format = format;
|
||||||
|
OptimizeAlignment = optimizeAlignment;
|
||||||
|
PaddedFileSizeLimit = paddedFileSizeLimit;
|
||||||
|
Status = status;
|
||||||
|
Comment = comment;
|
||||||
|
TorrentFilePath = torrentFilePath;
|
||||||
|
Source = source;
|
||||||
|
Trackers = trackers ?? Array.Empty<string>();
|
||||||
|
UrlSeeds = urlSeeds ?? Array.Empty<string>();
|
||||||
|
TimeStarted = timeStarted;
|
||||||
|
TimeFinished = timeFinished;
|
||||||
|
ErrorMessage = errorMessage;
|
||||||
|
Progress = progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("taskID")]
|
||||||
|
public string TaskId { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("sourcePath")]
|
||||||
|
public string? SourcePath { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("pieceSize")]
|
||||||
|
public int? PieceSize { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("private")]
|
||||||
|
public bool? Private { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("timeAdded")]
|
||||||
|
public string? TimeAdded { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("format")]
|
||||||
|
public string? Format { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("optimizeAlignment")]
|
||||||
|
public bool? OptimizeAlignment { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("paddedFileSizeLimit")]
|
||||||
|
public int? PaddedFileSizeLimit { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public string? Status { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("comment")]
|
||||||
|
public string? Comment { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("torrentFilePath")]
|
||||||
|
public string? TorrentFilePath { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("source")]
|
||||||
|
public string? Source { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("trackers")]
|
||||||
|
public IReadOnlyList<string> Trackers { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("urlSeeds")]
|
||||||
|
public IReadOnlyList<string> UrlSeeds { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("timeStarted")]
|
||||||
|
public string? TimeStarted { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("timeFinished")]
|
||||||
|
public string? TimeFinished { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("errorMessage")]
|
||||||
|
public string? ErrorMessage { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("progress")]
|
||||||
|
public double? Progress { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
105
Lantean.QBitTorrentClient/Models/TorrentMetadata.cs
Normal file
105
Lantean.QBitTorrentClient/Models/TorrentMetadata.cs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Lantean.QBitTorrentClient.Models
|
||||||
|
{
|
||||||
|
public record TorrentMetadata
|
||||||
|
{
|
||||||
|
[JsonConstructor]
|
||||||
|
public TorrentMetadata(
|
||||||
|
string? infoHashV1,
|
||||||
|
string? infoHashV2,
|
||||||
|
string? hash,
|
||||||
|
TorrentMetadataInfo? info,
|
||||||
|
IReadOnlyList<TorrentMetadataTracker>? trackers,
|
||||||
|
IReadOnlyList<string>? webSeeds,
|
||||||
|
string? createdBy,
|
||||||
|
long? creationDate,
|
||||||
|
string? comment)
|
||||||
|
{
|
||||||
|
InfoHashV1 = infoHashV1;
|
||||||
|
InfoHashV2 = infoHashV2;
|
||||||
|
Hash = hash;
|
||||||
|
Info = info;
|
||||||
|
Trackers = trackers ?? Array.Empty<TorrentMetadataTracker>();
|
||||||
|
WebSeeds = webSeeds ?? Array.Empty<string>();
|
||||||
|
CreatedBy = createdBy;
|
||||||
|
CreationDate = creationDate;
|
||||||
|
Comment = comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("infohash_v1")]
|
||||||
|
public string? InfoHashV1 { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("infohash_v2")]
|
||||||
|
public string? InfoHashV2 { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("hash")]
|
||||||
|
public string? Hash { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("info")]
|
||||||
|
public TorrentMetadataInfo? Info { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("trackers")]
|
||||||
|
public IReadOnlyList<TorrentMetadataTracker> Trackers { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("webseeds")]
|
||||||
|
public IReadOnlyList<string> WebSeeds { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("created_by")]
|
||||||
|
public string? CreatedBy { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("creation_date")]
|
||||||
|
public long? CreationDate { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("comment")]
|
||||||
|
public string? Comment { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record TorrentMetadataInfo
|
||||||
|
{
|
||||||
|
[JsonConstructor]
|
||||||
|
public TorrentMetadataInfo(
|
||||||
|
IReadOnlyList<TorrentMetadataFile>? files,
|
||||||
|
long? length,
|
||||||
|
string? name,
|
||||||
|
long? pieceLength,
|
||||||
|
int? piecesCount,
|
||||||
|
bool? @private)
|
||||||
|
{
|
||||||
|
Files = files ?? Array.Empty<TorrentMetadataFile>();
|
||||||
|
Length = length;
|
||||||
|
Name = name;
|
||||||
|
PieceLength = pieceLength;
|
||||||
|
PiecesCount = piecesCount;
|
||||||
|
Private = @private;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("files")]
|
||||||
|
public IReadOnlyList<TorrentMetadataFile> Files { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("length")]
|
||||||
|
public long? Length { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string? Name { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("piece_length")]
|
||||||
|
public long? PieceLength { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("pieces_num")]
|
||||||
|
public int? PiecesCount { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("private")]
|
||||||
|
public bool? Private { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record TorrentMetadataFile(
|
||||||
|
[property: JsonPropertyName("path")] string? Path,
|
||||||
|
[property: JsonPropertyName("length")] long? Length);
|
||||||
|
|
||||||
|
public record TorrentMetadataTracker(
|
||||||
|
[property: JsonPropertyName("url")] string? Url,
|
||||||
|
[property: JsonPropertyName("tier")] int? Tier);
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Lantean.QBitTorrentClient.Models
|
namespace Lantean.QBitTorrentClient.Models
|
||||||
{
|
{
|
||||||
@@ -13,7 +15,10 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
int seeds,
|
int seeds,
|
||||||
int leeches,
|
int leeches,
|
||||||
int downloads,
|
int downloads,
|
||||||
string message)
|
string message,
|
||||||
|
long? nextAnnounce,
|
||||||
|
long? minAnnounce,
|
||||||
|
IReadOnlyList<TrackerEndpoint>? endpoints)
|
||||||
{
|
{
|
||||||
Url = url;
|
Url = url;
|
||||||
Status = status;
|
Status = status;
|
||||||
@@ -23,6 +28,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
Leeches = leeches;
|
Leeches = leeches;
|
||||||
Downloads = downloads;
|
Downloads = downloads;
|
||||||
Message = message;
|
Message = message;
|
||||||
|
NextAnnounce = nextAnnounce;
|
||||||
|
MinAnnounce = minAnnounce;
|
||||||
|
Endpoints = endpoints ?? Array.Empty<TrackerEndpoint>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName("url")]
|
[JsonPropertyName("url")]
|
||||||
@@ -48,5 +56,27 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
|
|
||||||
[JsonPropertyName("msg")]
|
[JsonPropertyName("msg")]
|
||||||
public string Message { get; }
|
public string Message { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("next_announce")]
|
||||||
|
public long? NextAnnounce { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("min_announce")]
|
||||||
|
public long? MinAnnounce { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("endpoints")]
|
||||||
|
public IReadOnlyList<TrackerEndpoint> Endpoints { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record TrackerEndpoint(
|
||||||
|
[property: JsonPropertyName("name")] string? Name,
|
||||||
|
[property: JsonPropertyName("updating")] bool? Updating,
|
||||||
|
[property: JsonPropertyName("status")] TrackerStatus Status,
|
||||||
|
[property: JsonPropertyName("msg")] string? Message,
|
||||||
|
[property: JsonPropertyName("bt_version")] int? BitTorrentVersion,
|
||||||
|
[property: JsonPropertyName("num_peers")] int? Peers,
|
||||||
|
[property: JsonPropertyName("num_seeds")] int? Seeds,
|
||||||
|
[property: JsonPropertyName("num_leeches")] int? Leeches,
|
||||||
|
[property: JsonPropertyName("num_downloaded")] int? Downloads,
|
||||||
|
[property: JsonPropertyName("next_announce")] long? NextAnnounce,
|
||||||
|
[property: JsonPropertyName("min_announce")] long? MinAnnounce);
|
||||||
}
|
}
|
||||||
@@ -7,5 +7,7 @@
|
|||||||
Working = 2,
|
Working = 2,
|
||||||
Updating = 3,
|
Updating = 3,
|
||||||
NotWorking = 4,
|
NotWorking = 4,
|
||||||
|
Error = 5,
|
||||||
|
Unreachable = 6
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user