mirror of
https://github.com/lantean-code/qbtmud.git
synced 2025-10-24 00:24:06 +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
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lantean.QBitTorrentClient.Test", "Lantean.QBitTorrentClient.Test\Lantean.QBitTorrentClient.Test.csproj", "{796E865C-7AA6-4BD9-B12F-394801199A75}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -1,33 +1,50 @@
|
||||
<MudGrid>
|
||||
@using Lantean.QBitTorrentClient.Models
|
||||
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
<MudSwitch Label="Additional Options" @bind-Value="Expanded" LabelPlacement="Placement.End" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
<MudCollapse Expanded="Expanded">
|
||||
<MudGrid>
|
||||
<MudGrid Class="mt-2">
|
||||
<MudItem xs="12">
|
||||
<MudSelect Label="Torrent Management Mode" @bind-Value="TorrentManagementMode" Variant="Variant.Outlined">
|
||||
<MudSelectItem Value="false">Manual</MudSelectItem>
|
||||
<MudSelectItem Value="true">Automatic</MudSelectItem>
|
||||
<MudSelect T="bool" Label="Torrent management mode" Value="@TorrentManagementMode" ValueChanged="@SetTorrentManagementMode" Variant="Variant.Outlined">
|
||||
<MudSelectItem Value="@false">Manual</MudSelectItem>
|
||||
<MudSelectItem Value="@true">Automatic</MudSelectItem>
|
||||
</MudSelect>
|
||||
</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">
|
||||
<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>
|
||||
@if (ShowCookieOption)
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudTextField Label="Cookie" @bind-Value="Cookie" Variant="Variant.Outlined"></MudTextField>
|
||||
<MudTextField Label="Cookie" @bind-Value="Cookie" Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
}
|
||||
<MudItem xs="12">
|
||||
<MudTextField Label="Rename" @bind-Value="RenameTorrent" Variant="Variant.Outlined"></MudTextField>
|
||||
<MudTextField Label="Rename" @bind-Value="RenameTorrent" Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudSelect Label="Category" @bind-Value="Category" Variant="Variant.Outlined">
|
||||
@foreach (var category in Categories)
|
||||
<MudSelect T="string" Label="Category" Value="@Category" ValueChanged="@CategoryChanged" Variant="Variant.Outlined" Clearable="true">
|
||||
<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>
|
||||
</MudItem>
|
||||
@@ -38,7 +55,7 @@
|
||||
<FieldSwitch Label="Add to top of queue" @bind-Value="AddToTopOfQueue" />
|
||||
</MudItem>
|
||||
<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="@("MetadataReceived")">Metadata received</MudSelectItem>
|
||||
<MudSelectItem Value="@("FilesChecked")">Files checked</MudSelectItem>
|
||||
@@ -47,22 +64,58 @@
|
||||
<MudItem xs="12">
|
||||
<FieldSwitch Label="Skip hash check" @bind-Value="SkipHashCheck" />
|
||||
</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="@("Subfolder")">Create subfolder</MudSelectItem>
|
||||
<MudSelectItem Value="@("NoSubfolder")">Don't create subfolder'</MudSelectItem>
|
||||
<MudSelectItem Value="@("NoSubfolder")">Don't create subfolder</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<FieldSwitch Label="Download in sequentual order" @bind-Value="DownloadInSequentialOrder" />
|
||||
<FieldSwitch Label="Download in sequential order" @bind-Value="DownloadInSequentialOrder" />
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<FieldSwitch Label="Download first and last pieces first" @bind-Value="DownloadFirstAndLastPiecesFirst" />
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudNumericField Label="Limit download rate" @bind-Value="DownloadLimit" Variant="Variant.Outlined" Min="0" />
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudNumericField Label="Limit upload rate" @bind-Value="UploadLimit" Variant="Variant.Outlined" Min="0" />
|
||||
</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>
|
||||
</MudCollapse>
|
||||
@@ -1,4 +1,5 @@
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBitTorrentClient.Models;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
@@ -6,6 +7,15 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
{
|
||||
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]
|
||||
protected IApiClient ApiClient { get; set; } = default!;
|
||||
|
||||
@@ -16,15 +26,25 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
|
||||
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? 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;
|
||||
|
||||
@@ -32,41 +52,232 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
|
||||
protected string StopCondition { get; set; } = "None";
|
||||
|
||||
protected bool SkipHashCheck { get; set; } = false;
|
||||
protected bool SkipHashCheck { get; set; }
|
||||
|
||||
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 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()
|
||||
{
|
||||
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();
|
||||
|
||||
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;
|
||||
StopCondition = preferences.TorrentStopCondition;
|
||||
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()
|
||||
{
|
||||
return new TorrentOptions(
|
||||
var options = new TorrentOptions(
|
||||
TorrentManagementMode,
|
||||
SavePath,
|
||||
_manualSavePath,
|
||||
Cookie,
|
||||
RenameTorrent,
|
||||
Category,
|
||||
string.IsNullOrWhiteSpace(Category) ? null : Category,
|
||||
StartTorrent,
|
||||
AddToTopOfQueue,
|
||||
StopCondition,
|
||||
@@ -76,6 +287,154 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
DownloadFirstAndLastPiecesFirst,
|
||||
DownloadLimit,
|
||||
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!;
|
||||
|
||||
[Inject]
|
||||
protected IDataManager DataManager { get; set; } = default!;
|
||||
protected ITorrentDataManager DataManager { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
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" />
|
||||
</MudItem>
|
||||
<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="@("always")">Always</MudSelectItem>
|
||||
<MudSelectItem Value="@("never")">Never</MudSelectItem>
|
||||
|
||||
@@ -114,11 +114,11 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
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)
|
||||
{
|
||||
case "default":
|
||||
@@ -273,15 +273,15 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
switch (SelectedRule.TorrentParams.Stopped)
|
||||
{
|
||||
case null:
|
||||
AddPaused = "default";
|
||||
AddStopped = "default";
|
||||
break;
|
||||
|
||||
case true:
|
||||
AddPaused = "always";
|
||||
AddStopped = "always";
|
||||
break;
|
||||
|
||||
case false:
|
||||
AddPaused = "never";
|
||||
AddStopped = "never";
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@inherits SubmittableDialog
|
||||
@inherits SubmittableDialog
|
||||
@using Lantean.QBitTorrentClient.Models
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@@ -34,6 +35,15 @@
|
||||
<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" />
|
||||
</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>
|
||||
</DialogContent>
|
||||
<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 Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
@@ -16,6 +19,9 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
[Parameter]
|
||||
public ShareRatioMax? Value { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public ShareRatioMax? CurrentValue { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
@@ -33,6 +39,8 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
|
||||
protected int InactiveMinutes { get; set; }
|
||||
|
||||
protected ShareLimitAction SelectedShareLimitAction { get; set; } = ShareLimitAction.Default;
|
||||
|
||||
protected bool CustomEnabled => ShareRatioType == 0;
|
||||
|
||||
protected void RatioEnabledChanged(bool value)
|
||||
@@ -65,40 +73,75 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
InactiveMinutes = value;
|
||||
}
|
||||
|
||||
protected void ShareLimitActionChanged(ShareLimitAction value)
|
||||
{
|
||||
SelectedShareLimitAction = value;
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
return;
|
||||
}
|
||||
|
||||
ShareRatioType = 0;
|
||||
|
||||
if (baseline.RatioLimit >= 0)
|
||||
{
|
||||
RatioEnabled = true;
|
||||
Ratio = baseline.RatioLimit;
|
||||
}
|
||||
else
|
||||
{
|
||||
ShareRatioType = 0;
|
||||
if (Value.RatioLimit >= 0)
|
||||
{
|
||||
RatioEnabled = true;
|
||||
Ratio = Value.RatioLimit;
|
||||
Ratio = 0;
|
||||
}
|
||||
if (Value.SeedingTimeLimit >= 0)
|
||||
|
||||
if (baseline.SeedingTimeLimit >= 0)
|
||||
{
|
||||
TotalMinutesEnabled = true;
|
||||
TotalMinutes = (int)Value.SeedingTimeLimit;
|
||||
TotalMinutes = (int)baseline.SeedingTimeLimit;
|
||||
}
|
||||
if (Value.InactiveSeedingTimeLimit >= 0)
|
||||
else
|
||||
{
|
||||
TotalMinutes = 0;
|
||||
}
|
||||
|
||||
if (baseline.InactiveSeedingTimeLimit >= 0)
|
||||
{
|
||||
InactiveMinutesEnabled = true;
|
||||
InactiveMinutes = (int)Value.InactiveSeedingTimeLimit;
|
||||
InactiveMinutes = (int)baseline.InactiveSeedingTimeLimit;
|
||||
}
|
||||
else
|
||||
{
|
||||
InactiveMinutes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected void ShareRatioTypeChanged(int value)
|
||||
{
|
||||
ShareRatioType = value;
|
||||
if (!CustomEnabled)
|
||||
{
|
||||
RatioEnabled = false;
|
||||
TotalMinutesEnabled = false;
|
||||
InactiveMinutesEnabled = false;
|
||||
SelectedShareLimitAction = ShareLimitAction.Default;
|
||||
}
|
||||
}
|
||||
|
||||
protected void Cancel()
|
||||
@@ -112,16 +155,19 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
if (ShareRatioType == Limits.GlobalLimit)
|
||||
{
|
||||
result.RatioLimit = result.SeedingTimeLimit = result.InactiveSeedingTimeLimit = Limits.GlobalLimit;
|
||||
result.ShareLimitAction = ShareLimitAction.Default;
|
||||
}
|
||||
else if (ShareRatioType == Limits.NoLimit)
|
||||
{
|
||||
result.RatioLimit = result.SeedingTimeLimit = result.InactiveSeedingTimeLimit = Limits.NoLimit;
|
||||
result.ShareLimitAction = ShareLimitAction.Default;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.RatioLimit = RatioEnabled ? Ratio : Limits.NoLimit;
|
||||
result.SeedingTimeLimit = TotalMinutesEnabled ? TotalMinutes : Limits.NoLimit;
|
||||
result.InactiveSeedingTimeLimit = InactiveMinutesEnabled ? InactiveMinutes : Limits.NoLimit;
|
||||
result.ShareLimitAction = SelectedShareLimitAction;
|
||||
}
|
||||
MudDialog.Close(DialogResult.Ok(result));
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Lantean.QBTMud.Components
|
||||
protected ILocalStorageService LocalStorage { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDataManager DataManager { get; set; } = default!;
|
||||
protected ITorrentDataManager DataManager { get; set; } = default!;
|
||||
|
||||
protected HashSet<string> ExpandedNodes { get; set; } = [];
|
||||
|
||||
|
||||
@@ -65,8 +65,8 @@
|
||||
{
|
||||
return __builder =>
|
||||
{
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.PlayArrow" IconColor="Color.Success" OnClick="@(e => ResumeTorrents(type))">Resume torrents</MudMenuItem>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Pause" IconColor="Color.Warning" OnClick="@(e => PauseTorrents(type))">Pause torrents</MudMenuItem>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.PlayArrow" IconColor="Color.Success" OnClick="@(e => StartTorrents(type))">Start 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>
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using MudBlazor;
|
||||
using System.Linq;
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
await ApiClient.PauseTorrents(torrents);
|
||||
await ApiClient.StopTorrents(hashes: torrents.ToArray());
|
||||
}
|
||||
|
||||
protected async Task RemoveTorrents(string type)
|
||||
{
|
||||
var torrents = GetAffectedTorrentHashes(type);
|
||||
|
||||
await DialogService.InvokeDeleteTorrentDialog(ApiClient, [.. torrents]);
|
||||
await DialogService.InvokeDeleteTorrentDialog(ApiClient, Preferences?.ConfirmTorrentDeletion == true, [.. torrents]);
|
||||
}
|
||||
|
||||
private Dictionary<string, int> GetTags()
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Lantean.QBTMud.Components
|
||||
protected IApiClient ApiClient { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDataManager DataManager { get; set; } = default!;
|
||||
protected ITorrentDataManager DataManager { get; set; } = default!;
|
||||
|
||||
protected IReadOnlyList<PieceState> Pieces { get; set; } = [];
|
||||
|
||||
|
||||
@@ -68,6 +68,21 @@
|
||||
</MudCardContent>
|
||||
</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">
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
|
||||
@@ -16,6 +16,8 @@ namespace Lantean.QBTMud.Components.Options
|
||||
protected int SaveResumeDataInterval { get; private set; }
|
||||
protected int TorrentFileSizeLimit { get; private set; }
|
||||
protected bool RecheckCompletedTorrents { get; private set; }
|
||||
|
||||
protected bool ConfirmTorrentRecheck { get; private set; }
|
||||
protected string? AppInstanceName { get; private set; }
|
||||
protected int RefreshInterval { get; private set; }
|
||||
protected bool ResolvePeerCountries { get; private set; }
|
||||
@@ -97,6 +99,7 @@ namespace Lantean.QBTMud.Components.Options
|
||||
SaveResumeDataInterval = Preferences.SaveResumeDataInterval;
|
||||
TorrentFileSizeLimit = Preferences.TorrentFileSizeLimit / 1024 / 1024;
|
||||
RecheckCompletedTorrents = Preferences.RecheckCompletedTorrents;
|
||||
ConfirmTorrentRecheck = Preferences.ConfirmTorrentRecheck;
|
||||
AppInstanceName = Preferences.AppInstanceName;
|
||||
RefreshInterval = Preferences.RefreshInterval;
|
||||
ResolvePeerCountries = Preferences.ResolvePeerCountries;
|
||||
@@ -209,6 +212,13 @@ namespace Lantean.QBTMud.Components.Options
|
||||
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)
|
||||
{
|
||||
AppInstanceName = value;
|
||||
|
||||
@@ -17,6 +17,24 @@
|
||||
</MudCardContent>
|
||||
</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">
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
|
||||
@@ -4,6 +4,10 @@ namespace Lantean.QBTMud.Components.Options
|
||||
{
|
||||
public partial class BehaviourOptions : Options
|
||||
{
|
||||
protected bool ConfirmTorrentDeletion { get; set; }
|
||||
|
||||
protected bool StatusBarExternalIp { get; set; }
|
||||
|
||||
protected bool FileLogEnabled { get; set; }
|
||||
|
||||
protected string? FileLogPath { get; set; }
|
||||
@@ -27,6 +31,8 @@ namespace Lantean.QBTMud.Components.Options
|
||||
return false;
|
||||
}
|
||||
|
||||
ConfirmTorrentDeletion = Preferences.ConfirmTorrentDeletion;
|
||||
StatusBarExternalIp = Preferences.StatusBarExternalIp;
|
||||
FileLogEnabled = Preferences.FileLogEnabled;
|
||||
FileLogPath = Preferences.FileLogPath;
|
||||
FileLogBackupEnabled = Preferences.FileLogBackupEnabled;
|
||||
@@ -39,6 +45,20 @@ namespace Lantean.QBTMud.Components.Options
|
||||
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)
|
||||
{
|
||||
FileLogEnabled = value;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<FieldSwitch Label="Add to top of queue" Value="AddToTopOfQueue" ValueChanged="AddToTopOfQueueChanged" />
|
||||
</MudItem>
|
||||
<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 xs="12">
|
||||
<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 bool AddToTopOfQueue { get; set; }
|
||||
protected bool StartPausedEnabled { get; set; }
|
||||
protected bool AddStoppedEnabled { get; set; }
|
||||
protected string? TorrentStopCondition { get; set; }
|
||||
protected bool AutoDeleteMode { get; set; }
|
||||
protected bool PreallocateAll { get; set; }
|
||||
@@ -51,7 +51,7 @@ namespace Lantean.QBTMud.Components.Options
|
||||
// when adding a torrent
|
||||
TorrentContentLayout = Preferences.TorrentContentLayout;
|
||||
AddToTopOfQueue = Preferences.AddToTopOfQueue;
|
||||
StartPausedEnabled = Preferences.StartPausedEnabled;
|
||||
AddStoppedEnabled = Preferences.AddStoppedEnabled;
|
||||
TorrentStopCondition = Preferences.TorrentStopCondition;
|
||||
AutoDeleteMode = Preferences.AutoDeleteMode == 1;
|
||||
PreallocateAll = Preferences.PreallocateAll;
|
||||
@@ -116,10 +116,10 @@ namespace Lantean.QBTMud.Components.Options
|
||||
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
||||
}
|
||||
|
||||
protected async Task StartPausedEnabledChanged(bool value)
|
||||
protected async Task AddStoppedEnabledChanged(bool value)
|
||||
{
|
||||
StartPausedEnabled = value;
|
||||
UpdatePreferences.StartPausedEnabled = value;
|
||||
AddStoppedEnabled = value;
|
||||
UpdatePreferences.AddStoppedEnabled = value;
|
||||
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Lantean.QBTMud.Components
|
||||
protected IApiClient ApiClient { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDataManager DataManager { get; set; } = default!;
|
||||
protected IPeerDataManager PeerDataManager { get; set; } = default!;
|
||||
|
||||
protected PeerList? PeerList { get; set; }
|
||||
|
||||
@@ -78,11 +78,11 @@ namespace Lantean.QBTMud.Components
|
||||
var peers = await ApiClient.GetTorrentPeersData(Hash, _requestId);
|
||||
if (PeerList is null || peers.FullUpdate)
|
||||
{
|
||||
PeerList = DataManager.CreatePeerList(peers);
|
||||
PeerList = PeerDataManager.CreatePeerList(peers);
|
||||
}
|
||||
else
|
||||
{
|
||||
DataManager.MergeTorrentPeers(peers, PeerList);
|
||||
PeerDataManager.MergeTorrentPeers(peers, PeerList);
|
||||
}
|
||||
_requestId = peers.RequestId;
|
||||
|
||||
@@ -200,11 +200,11 @@ namespace Lantean.QBTMud.Components
|
||||
}
|
||||
if (PeerList is null || peers.FullUpdate)
|
||||
{
|
||||
PeerList = DataManager.CreatePeerList(peers);
|
||||
PeerList = PeerDataManager.CreatePeerList(peers);
|
||||
}
|
||||
else
|
||||
{
|
||||
DataManager.MergeTorrentPeers(peers, PeerList);
|
||||
PeerDataManager.MergeTorrentPeers(peers, PeerList);
|
||||
}
|
||||
|
||||
_requestId = peers.RequestId;
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Lantean.QBTMud.Components
|
||||
public ISnackbar Snackbar { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
public IDataManager DataManager { get; set; } = default!;
|
||||
public ITorrentDataManager DataManager { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
public IJSRuntime JSRuntime { get; set; } = default!;
|
||||
@@ -37,9 +37,6 @@ namespace Lantean.QBTMud.Components
|
||||
[Inject]
|
||||
protected IKeyboardService KeyboardService { get; set; } = default!;
|
||||
|
||||
[CascadingParameter(Name = "Version")]
|
||||
public string? Version { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[EditorRequired]
|
||||
public IEnumerable<string> Hashes { get; set; } = default!;
|
||||
@@ -71,14 +68,12 @@ namespace Lantean.QBTMud.Components
|
||||
|
||||
protected bool OverlayVisible { get; set; }
|
||||
|
||||
protected int MajorVersion => VersionHelper.GetMajorVersion(Version);
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_actions =
|
||||
[
|
||||
new("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Resume)),
|
||||
new("pause", "Pause", MajorVersion < 5 ? Icons.Material.Filled.Pause : Icons.Material.Filled.Stop, Color.Warning, CreateCallback(Pause)),
|
||||
new("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Start)),
|
||||
new("stop", "Stop", Icons.Material.Filled.Stop, Color.Warning, CreateCallback(Stop)),
|
||||
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("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("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("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)),
|
||||
];
|
||||
@@ -146,33 +143,17 @@ namespace Lantean.QBTMud.Components
|
||||
OverlayVisible = value;
|
||||
}
|
||||
|
||||
protected async Task Pause()
|
||||
protected async Task Stop()
|
||||
{
|
||||
if (MajorVersion < 5)
|
||||
{
|
||||
await ApiClient.PauseTorrents(Hashes);
|
||||
Snackbar.Add("Torrent paused.");
|
||||
}
|
||||
else
|
||||
{
|
||||
await ApiClient.StopTorrents(Hashes);
|
||||
await ApiClient.StopTorrents(hashes: Hashes.ToArray());
|
||||
Snackbar.Add("Torrent stopped.");
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task Resume()
|
||||
protected async Task Start()
|
||||
{
|
||||
if (MajorVersion < 5)
|
||||
{
|
||||
await ApiClient.ResumeTorrents(Hashes);
|
||||
Snackbar.Add("Torrent resumed.");
|
||||
}
|
||||
else
|
||||
{
|
||||
await ApiClient.StartTorrents(Hashes);
|
||||
await ApiClient.StartTorrents(hashes: Hashes.ToArray());
|
||||
Snackbar.Add("Torrent started.");
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task ForceStart()
|
||||
{
|
||||
@@ -182,7 +163,7 @@ namespace Lantean.QBTMud.Components
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -278,7 +259,7 @@ namespace Lantean.QBTMud.Components
|
||||
|
||||
protected async Task ForceRecheck()
|
||||
{
|
||||
await ApiClient.RecheckTorrents(null, Hashes.ToArray());
|
||||
await DialogService.ForceRecheckAsync(ApiClient, Hashes, Preferences?.ConfirmTorrentRecheck == true);
|
||||
}
|
||||
|
||||
protected async Task ForceReannounce()
|
||||
@@ -385,8 +366,8 @@ namespace Lantean.QBTMud.Components
|
||||
var allAreFirstLastPiecePrio = true;
|
||||
var thereAreFirstLastPiecePrio = false;
|
||||
var allAreDownloaded = true;
|
||||
var allArePaused = true;
|
||||
var thereArePaused = false;
|
||||
var allAreStopped = true;
|
||||
var thereAreStopped = false;
|
||||
var allAreForceStart = true;
|
||||
var thereAreForceStart = false;
|
||||
var allAreSuperSeeding = true;
|
||||
@@ -424,27 +405,13 @@ namespace Lantean.QBTMud.Components
|
||||
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")
|
||||
{
|
||||
allArePaused = false;
|
||||
allAreStopped = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
thereArePaused = true;
|
||||
}
|
||||
thereAreStopped = true;
|
||||
}
|
||||
|
||||
if (!torrent.ForceStart)
|
||||
@@ -532,7 +499,7 @@ namespace Lantean.QBTMud.Components
|
||||
actionStates["superSeeding"] = ActionState.Hidden;
|
||||
}
|
||||
|
||||
if (allArePaused)
|
||||
if (allAreStopped)
|
||||
{
|
||||
actionStates["pause"] = ActionState.Hidden;
|
||||
}
|
||||
@@ -540,13 +507,11 @@ namespace Lantean.QBTMud.Components
|
||||
{
|
||||
actionStates["forceStart"] = ActionState.Hidden;
|
||||
}
|
||||
else if (!thereArePaused && !thereAreForceStart)
|
||||
else if (!thereAreStopped && !thereAreForceStart)
|
||||
{
|
||||
actionStates["start"] = ActionState.Hidden;
|
||||
}
|
||||
|
||||
if (MajorVersion >= 5)
|
||||
{
|
||||
if (actionStates.TryGetValue("start", out ActionState? startActionState))
|
||||
{
|
||||
startActionState.TextOverride = "Start";
|
||||
@@ -564,7 +529,6 @@ namespace Lantean.QBTMud.Components
|
||||
{
|
||||
actionStates["pause"] = new ActionState { TextOverride = "Stop" };
|
||||
}
|
||||
}
|
||||
|
||||
if (!allAreAutoTmm && thereAreAutoTmm)
|
||||
{
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace Lantean.QBTMud.Components
|
||||
protected IApiClient ApiClient { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDataManager DataManager { get; set; } = default!;
|
||||
protected ITorrentDataManager DataManager { get; set; } = default!;
|
||||
|
||||
protected IReadOnlyList<TorrentTracker>? TrackerList { get; set; }
|
||||
|
||||
@@ -171,7 +171,7 @@ namespace Lantean.QBTMud.Components
|
||||
return;
|
||||
}
|
||||
|
||||
await ApiClient.AddTrackersToTorrent(Hash, trackers);
|
||||
await ApiClient.AddTrackersToTorrent(trackers, hashes: new[] { Hash });
|
||||
}
|
||||
|
||||
protected Task EditTrackerToolbar()
|
||||
@@ -211,7 +211,7 @@ namespace Lantean.QBTMud.Components
|
||||
return;
|
||||
}
|
||||
|
||||
await ApiClient.RemoveTrackers(Hash, [tracker.Url]);
|
||||
await ApiClient.RemoveTrackers([tracker.Url], hashes: new[] { Hash });
|
||||
}
|
||||
|
||||
protected Task CopyTrackerUrlToolbar()
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace Lantean.QBTMud.Components
|
||||
protected IApiClient ApiClient { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDataManager DataManager { get; set; } = default!;
|
||||
protected ITorrentDataManager DataManager { get; set; } = default!;
|
||||
|
||||
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.Filter;
|
||||
using Lantean.QBTMud.Models;
|
||||
@@ -56,7 +57,7 @@ namespace Lantean.QBTMud.Helpers
|
||||
var addTorrentParams = CreateAddTorrentParams(options);
|
||||
addTorrentParams.Torrents = files;
|
||||
|
||||
await apiClient.AddTorrent(addTorrentParams);
|
||||
_ = await apiClient.AddTorrent(addTorrentParams);
|
||||
|
||||
foreach (var stream in streams)
|
||||
{
|
||||
@@ -74,18 +75,19 @@ namespace Lantean.QBTMud.Helpers
|
||||
{
|
||||
addTorrentParams.ContentLayout = Enum.Parse<QBitTorrentClient.Models.TorrentContentLayout>(options.ContentLayout);
|
||||
}
|
||||
if (string.IsNullOrEmpty(options.Cookie))
|
||||
{
|
||||
addTorrentParams.Cookie = options.Cookie;
|
||||
}
|
||||
addTorrentParams.DownloadLimit = options.DownloadLimit;
|
||||
if (!string.IsNullOrWhiteSpace(options.DownloadPath))
|
||||
{
|
||||
addTorrentParams.DownloadPath = options.DownloadPath;
|
||||
}
|
||||
addTorrentParams.FirstLastPiecePriority = options.DownloadFirstAndLastPiecesFirst;
|
||||
addTorrentParams.InactiveSeedingTimeLimit = options.InactiveSeedingTimeLimit;
|
||||
addTorrentParams.Paused = !options.StartTorrent;
|
||||
addTorrentParams.RatioLimit = options.RatioLimit;
|
||||
addTorrentParams.RenameTorrent = options.RenameTorrent;
|
||||
if (!options.TorrentManagementMode)
|
||||
{
|
||||
addTorrentParams.SavePath = options.SavePath;
|
||||
}
|
||||
addTorrentParams.SeedingTimeLimit = options.SeedingTimeLimit;
|
||||
addTorrentParams.SequentialDownload = options.DownloadInSequentialOrder;
|
||||
if (!string.IsNullOrEmpty(options.ShareLimitAction))
|
||||
@@ -100,7 +102,10 @@ namespace Lantean.QBTMud.Helpers
|
||||
addTorrentParams.Stopped = !options.StartTorrent;
|
||||
addTorrentParams.Tags = options.Tags;
|
||||
addTorrentParams.UploadLimit = options.UploadLimit;
|
||||
if (options.UseDownloadPath.HasValue)
|
||||
{
|
||||
addTorrentParams.UseDownloadPath = options.UseDownloadPath;
|
||||
}
|
||||
return addTorrentParams;
|
||||
}
|
||||
|
||||
@@ -123,10 +128,10 @@ namespace Lantean.QBTMud.Helpers
|
||||
var addTorrentParams = CreateAddTorrentParams(options);
|
||||
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)
|
||||
{
|
||||
@@ -138,6 +143,12 @@ namespace Lantean.QBTMud.Helpers
|
||||
{ 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 dialogResult = await reference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
||||
@@ -150,6 +161,28 @@ namespace Lantean.QBTMud.Helpers
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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,
|
||||
MaxInactiveSeedingTime = t.InactiveSeedingTimeLimit,
|
||||
MaxInactiveSeedingTime = t.MaxInactiveSeedingTime,
|
||||
MaxRatio = t.MaxRatio,
|
||||
MaxSeedingTime = t.MaxSeedingTime,
|
||||
RatioLimit = t.RatioLimit,
|
||||
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
|
||||
{
|
||||
{ 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);
|
||||
|
||||
@@ -243,7 +285,7 @@ namespace Lantean.QBTMud.Helpers
|
||||
|
||||
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)
|
||||
@@ -437,3 +479,5 @@ namespace Lantean.QBTMud.Helpers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ByteSizeLib;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Lantean.QBitTorrentClient;
|
||||
using MudBlazor;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
@@ -404,8 +405,6 @@ namespace Lantean.QBTMud.Helpers
|
||||
Status.Downloading => (Icons.Material.Filled.Downloading, Color.Success),
|
||||
Status.Seeding => (Icons.Material.Filled.Upload, Color.Info),
|
||||
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.Active => (Icons.Material.Filled.Sort, Color.Success),
|
||||
Status.Inactive => (Icons.Material.Filled.Sort, Color.Error),
|
||||
@@ -417,5 +416,25 @@ namespace Lantean.QBTMud.Helpers
|
||||
_ => (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;
|
||||
|
||||
case Status.Resumed:
|
||||
if (!state.Contains("resumed"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case Status.Paused:
|
||||
if (!state.Contains("paused") && !state.Contains("stopped"))
|
||||
case Status.Stopped:
|
||||
if (state != "stoppedDL" && state != "stoppedUP")
|
||||
{
|
||||
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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Lantean.QBTMud.Test" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -33,6 +33,14 @@
|
||||
}
|
||||
<MudSpacer />
|
||||
<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" />
|
||||
<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" />
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Lantean.QBTMud.Layout
|
||||
protected IApiClient ApiClient { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDataManager DataManager { get; set; } = default!;
|
||||
protected ITorrentDataManager DataManager { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected NavigationManager NavigationManager { get; set; } = default!;
|
||||
@@ -97,7 +97,7 @@ namespace Lantean.QBTMud.Layout
|
||||
Preferences = await ApiClient.GetApplicationPreferences();
|
||||
Version = await ApiClient.GetApplicationVersion();
|
||||
var data = await ApiClient.GetMainData(_requestId);
|
||||
MainData = DataManager.CreateMainData(data, Version);
|
||||
MainData = DataManager.CreateMainData(data);
|
||||
MarkTorrentsDirty();
|
||||
|
||||
_requestId = data.ResponseId;
|
||||
@@ -145,7 +145,7 @@ namespace Lantean.QBTMud.Layout
|
||||
|
||||
if (MainData is null || data.FullUpdate)
|
||||
{
|
||||
MainData = DataManager.CreateMainData(data, Version);
|
||||
MainData = DataManager.CreateMainData(data);
|
||||
MarkTorrentsDirty();
|
||||
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)
|
||||
{
|
||||
if (Category == category)
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
Dictionary<string, HashSet<string>> tagState,
|
||||
Dictionary<string, HashSet<string>> categoriesState,
|
||||
Dictionary<string, HashSet<string>> statusState,
|
||||
Dictionary<string, HashSet<string>> trackersState,
|
||||
int majorVersion)
|
||||
Dictionary<string, HashSet<string>> trackersState)
|
||||
{
|
||||
Torrents = torrents.ToDictionary();
|
||||
Tags = tags.ToHashSet();
|
||||
@@ -23,7 +22,6 @@
|
||||
CategoriesState = categoriesState;
|
||||
StatusState = statusState;
|
||||
TrackersState = trackersState;
|
||||
MajorVersion = majorVersion;
|
||||
}
|
||||
|
||||
public Dictionary<string, Torrent> Torrents { get; }
|
||||
@@ -38,6 +36,5 @@
|
||||
public Dictionary<string, HashSet<string>> TrackersState { get; }
|
||||
public string? SelectedTorrentHash { get; set; }
|
||||
public bool LostConnection { get; set; }
|
||||
public int MajorVersion { get; }
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,17 @@
|
||||
long uploadRateLimit,
|
||||
bool useAltSpeedLimits,
|
||||
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;
|
||||
AllTimeUploaded = allTimeUploaded;
|
||||
@@ -46,6 +56,8 @@
|
||||
UseAltSpeedLimits = useAltSpeedLimits;
|
||||
UseSubcategories = useSubcategories;
|
||||
WriteCacheOverload = writeCacheOverload;
|
||||
LastExternalAddressV4 = lastExternalAddressV4;
|
||||
LastExternalAddressV6 = lastExternalAddressV6;
|
||||
}
|
||||
|
||||
public ServerState()
|
||||
@@ -85,5 +97,9 @@
|
||||
public bool UseSubcategories { 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 float RatioLimit { get; set; }
|
||||
public float SeedingTimeLimit { get; set; }
|
||||
public float InactiveSeedingTimeLimit { get; set; }
|
||||
public ShareLimitAction? ShareLimitAction { get; set; }
|
||||
}
|
||||
|
||||
public record ShareRatioMax : ShareRatio
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
Downloading,
|
||||
Seeding,
|
||||
Completed,
|
||||
Resumed,
|
||||
Paused,
|
||||
Stopped,
|
||||
Active,
|
||||
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
|
||||
{
|
||||
@@ -52,7 +57,13 @@
|
||||
long uploadSpeed,
|
||||
long reannounce,
|
||||
float inactiveSeedingTimeLimit,
|
||||
float maxInactiveSeedingTime)
|
||||
float maxInactiveSeedingTime,
|
||||
float popularity,
|
||||
string downloadPath,
|
||||
string rootPath,
|
||||
bool isPrivate,
|
||||
ShareLimitAction shareLimitAction,
|
||||
string comment)
|
||||
{
|
||||
Hash = hash;
|
||||
AddedOn = addedOn;
|
||||
@@ -104,21 +115,31 @@
|
||||
Reannounce = reannounce;
|
||||
InactiveSeedingTimeLimit = inactiveSeedingTimeLimit;
|
||||
MaxInactiveSeedingTime = maxInactiveSeedingTime;
|
||||
Popularity = popularity;
|
||||
DownloadPath = downloadPath;
|
||||
RootPath = rootPath;
|
||||
IsPrivate = isPrivate;
|
||||
ShareLimitAction = shareLimitAction;
|
||||
Comment = comment;
|
||||
}
|
||||
|
||||
protected Torrent()
|
||||
{
|
||||
Hash = "";
|
||||
Category = "";
|
||||
ContentPath = "";
|
||||
InfoHashV1 = "";
|
||||
InfoHashV2 = "";
|
||||
MagnetUri = "";
|
||||
Name = "";
|
||||
SavePath = "";
|
||||
State = "";
|
||||
Tags = [];
|
||||
Tracker = "";
|
||||
Hash = string.Empty;
|
||||
Category = string.Empty;
|
||||
ContentPath = string.Empty;
|
||||
InfoHashV1 = string.Empty;
|
||||
InfoHashV2 = string.Empty;
|
||||
MagnetUri = string.Empty;
|
||||
Name = string.Empty;
|
||||
SavePath = string.Empty;
|
||||
DownloadPath = string.Empty;
|
||||
RootPath = string.Empty;
|
||||
State = string.Empty;
|
||||
Tags = new List<string>();
|
||||
Tracker = string.Empty;
|
||||
ShareLimitAction = ShareLimitAction.Default;
|
||||
Comment = string.Empty;
|
||||
}
|
||||
|
||||
public string Hash { get; }
|
||||
@@ -183,8 +204,14 @@
|
||||
|
||||
public float RatioLimit { get; set; }
|
||||
|
||||
public float Popularity { get; set; }
|
||||
|
||||
public string SavePath { get; set; }
|
||||
|
||||
public string DownloadPath { get; set; }
|
||||
|
||||
public string RootPath { get; set; }
|
||||
|
||||
public long SeedingTime { get; set; }
|
||||
|
||||
public int SeedingTimeLimit { get; set; }
|
||||
@@ -221,6 +248,12 @@
|
||||
|
||||
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)
|
||||
{
|
||||
if (obj is null)
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Lantean.QBTMud.Pages
|
||||
protected IApiClient ApiClient { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDataManager DataManager { get; set; } = default!;
|
||||
protected IPreferencesDataManager PreferencesDataManager { get; set; } = default!;
|
||||
|
||||
[CascadingParameter(Name = "DrawerOpen")]
|
||||
public bool DrawerOpen { get; set; }
|
||||
@@ -61,7 +61,7 @@ namespace Lantean.QBTMud.Pages
|
||||
|
||||
protected void PreferencesChanged(UpdatePreferences preferences)
|
||||
{
|
||||
UpdatePreferences = DataManager.MergePreferences(UpdatePreferences, preferences);
|
||||
UpdatePreferences = PreferencesDataManager.MergePreferences(UpdatePreferences, preferences);
|
||||
}
|
||||
|
||||
protected async Task ValidateExit(LocationChangingContext context)
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Lantean.QBTMud.Pages
|
||||
protected NavigationManager NavigationManager { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDataManager DataManager { get; set; } = default!;
|
||||
protected IRssDataManager RssDataManager { get; set; } = default!;
|
||||
|
||||
[CascadingParameter]
|
||||
public MainData? MainData { get; set; }
|
||||
@@ -115,7 +115,7 @@ namespace Lantean.QBTMud.Pages
|
||||
private async Task GetRssList()
|
||||
{
|
||||
var items = await ApiClient.GetAllRssItems(true);
|
||||
RssList = DataManager.CreateRssList(items);
|
||||
RssList = RssDataManager.CreateRssList(items);
|
||||
}
|
||||
|
||||
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>("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>("Popularity", t => t.Popularity, t => t.Popularity.ToString("0.00")),
|
||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Category", t => t.Category),
|
||||
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)),
|
||||
@@ -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>("Save path", t => t.SavePath, 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 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>("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()
|
||||
|
||||
@@ -41,7 +41,11 @@ namespace Lantean.QBTMud
|
||||
builder.Services.AddScoped<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.AddSingleton<IClipboardService, ClipboardService>();
|
||||
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
|
||||
{
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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 ShareLimitAction = Lantean.QBitTorrentClient.Models.ShareLimitAction;
|
||||
|
||||
namespace Lantean.QBTMud.Services
|
||||
{
|
||||
public class DataManager : IDataManager
|
||||
public class TorrentDataManager : ITorrentDataManager
|
||||
{
|
||||
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);
|
||||
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());
|
||||
}
|
||||
|
||||
var statuses = GetStatuses(majorVersion).ToArray();
|
||||
var statuses = GetStatuses().ToArray();
|
||||
var statusState = new Dictionary<string, HashSet<string>>(statuses.Length + 2);
|
||||
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());
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -146,7 +128,9 @@ namespace Lantean.QBTMud.Services
|
||||
serverState.UploadRateLimit.GetValueOrDefault(),
|
||||
serverState.UseAltSpeedLimits.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)
|
||||
@@ -184,7 +168,6 @@ namespace Lantean.QBTMud.Services
|
||||
{
|
||||
filterChanged = true;
|
||||
}
|
||||
torrentList.TagState.Remove(normalizedTag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,9 +191,14 @@ namespace Lantean.QBTMud.Services
|
||||
{
|
||||
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;
|
||||
filterChanged = true;
|
||||
}
|
||||
@@ -284,7 +272,7 @@ namespace Lantean.QBTMud.Services
|
||||
{
|
||||
var newTorrent = CreateTorrent(hash, torrent);
|
||||
torrentList.Torrents.Add(hash, newTorrent);
|
||||
AddTorrentToStates(torrentList, hash, torrentList.MajorVersion);
|
||||
AddTorrentToStates(torrentList, hash);
|
||||
dataChanged = true;
|
||||
filterChanged = true;
|
||||
}
|
||||
@@ -316,7 +304,7 @@ namespace Lantean.QBTMud.Services
|
||||
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))
|
||||
{
|
||||
@@ -329,7 +317,7 @@ namespace Lantean.QBTMud.Services
|
||||
torrentList.CategoriesState[FilterHelper.CATEGORY_ALL].Add(hash);
|
||||
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))
|
||||
{
|
||||
@@ -346,21 +334,14 @@ namespace Lantean.QBTMud.Services
|
||||
UpdateTrackerState(torrentList, torrent, hash, previousTracker: null);
|
||||
}
|
||||
|
||||
private static Status[] GetStatuses(int version)
|
||||
private static Status[] GetStatuses()
|
||||
{
|
||||
if (_statusArray is not null)
|
||||
{
|
||||
return _statusArray;
|
||||
}
|
||||
|
||||
if (version == 5)
|
||||
{
|
||||
_statusArray = Enum.GetValues<Status>().Where(s => s != Status.Paused).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
_statusArray = Enum.GetValues<Status>().Where(s => s != Status.Stopped).ToArray();
|
||||
}
|
||||
_statusArray = Enum.GetValues<Status>();
|
||||
|
||||
return _statusArray;
|
||||
}
|
||||
@@ -373,22 +354,15 @@ namespace Lantean.QBTMud.Services
|
||||
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);
|
||||
UpdateTagStateForRemoval(torrentList, hash, snapshot.Tags);
|
||||
|
||||
torrentList.CategoriesState[FilterHelper.CATEGORY_ALL].Remove(hash);
|
||||
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))
|
||||
{
|
||||
@@ -559,83 +533,26 @@ namespace Lantean.QBTMud.Services
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var normalizedTags = torrent.Tags?
|
||||
@@ -694,10 +611,16 @@ namespace Lantean.QBTMud.Services
|
||||
torrent.UploadSpeed.GetValueOrDefault(),
|
||||
torrent.Reannounce ?? 0,
|
||||
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))
|
||||
{
|
||||
@@ -710,7 +633,7 @@ namespace Lantean.QBTMud.Services
|
||||
return normalized.Trim();
|
||||
}
|
||||
|
||||
private static TorrentSnapshot CreateSnapshot(Torrent torrent)
|
||||
internal static TorrentSnapshot CreateSnapshot(Torrent torrent)
|
||||
{
|
||||
return new TorrentSnapshot(
|
||||
string.IsNullOrEmpty(torrent.Category) ? null : torrent.Category,
|
||||
@@ -720,7 +643,7 @@ namespace Lantean.QBTMud.Services
|
||||
torrent.UploadSpeed);
|
||||
}
|
||||
|
||||
private readonly struct TorrentSnapshot
|
||||
internal readonly struct TorrentSnapshot
|
||||
{
|
||||
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; }
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
@@ -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))
|
||||
{
|
||||
@@ -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))
|
||||
{
|
||||
@@ -904,7 +827,7 @@ namespace Lantean.QBTMud.Services
|
||||
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))
|
||||
{
|
||||
@@ -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))
|
||||
{
|
||||
@@ -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))
|
||||
{
|
||||
@@ -957,7 +880,7 @@ namespace Lantean.QBTMud.Services
|
||||
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))
|
||||
{
|
||||
@@ -968,7 +891,7 @@ namespace Lantean.QBTMud.Services
|
||||
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))
|
||||
{
|
||||
@@ -979,7 +902,7 @@ namespace Lantean.QBTMud.Services
|
||||
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)
|
||||
{
|
||||
@@ -990,7 +913,7 @@ namespace Lantean.QBTMud.Services
|
||||
return false;
|
||||
}
|
||||
|
||||
private readonly struct TorrentUpdateResult
|
||||
internal readonly struct TorrentUpdateResult
|
||||
{
|
||||
public TorrentUpdateResult(bool dataChanged, bool filterChanged)
|
||||
{
|
||||
@@ -1003,7 +926,7 @@ namespace Lantean.QBTMud.Services
|
||||
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 filterChanged = false;
|
||||
@@ -1320,6 +1243,41 @@ namespace Lantean.QBTMud.Services
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1445,7 +1403,7 @@ namespace Lantean.QBTMud.Services
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool UpdateContentItem(ContentItem destination, ContentItem source)
|
||||
internal static bool UpdateContentItem(ContentItem destination, ContentItem source)
|
||||
{
|
||||
const float floatTolerance = 0.0001f;
|
||||
var changed = false;
|
||||
@@ -1456,7 +1414,7 @@ namespace Lantean.QBTMud.Services
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (System.Math.Abs(destination.Progress - source.Progress) > floatTolerance)
|
||||
if (Math.Abs(destination.Progress - source.Progress) > floatTolerance)
|
||||
{
|
||||
destination.Progress = source.Progress;
|
||||
changed = true;
|
||||
@@ -1468,7 +1426,7 @@ namespace Lantean.QBTMud.Services
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (System.Math.Abs(destination.Availability - source.Availability) > floatTolerance)
|
||||
if (Math.Abs(destination.Availability - source.Availability) > floatTolerance)
|
||||
{
|
||||
destination.Availability = source.Availability;
|
||||
changed = true;
|
||||
@@ -1566,425 +1524,6 @@ namespace Lantean.QBTMud.Services
|
||||
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)
|
||||
{
|
||||
if (files.Count == 0)
|
||||
@@ -2006,7 +1545,7 @@ namespace Lantean.QBTMud.Services
|
||||
? int.MaxValue
|
||||
: contents.Values.Min(c => c.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)
|
||||
{
|
||||
@@ -2100,38 +1639,5 @@ namespace Lantean.QBTMud.Services
|
||||
|
||||
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 System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
@@ -105,6 +106,8 @@ namespace Lantean.QBitTorrentClient
|
||||
|
||||
public async Task SetApplicationPreferences(UpdatePreferences preferences)
|
||||
{
|
||||
preferences.Validate();
|
||||
|
||||
var json = JsonSerializer.Serialize(preferences, _options);
|
||||
|
||||
var content = new FormUrlEncodedBuilder()
|
||||
@@ -116,6 +119,49 @@ namespace Lantean.QBitTorrentClient
|
||||
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()
|
||||
{
|
||||
var response = await _httpClient.GetAsync("app/defaultSavePath");
|
||||
@@ -145,6 +191,43 @@ namespace Lantean.QBitTorrentClient
|
||||
|
||||
#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
|
||||
|
||||
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
|
||||
|
||||
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();
|
||||
if (filter is not null)
|
||||
@@ -344,6 +427,10 @@ namespace Lantean.QBitTorrentClient
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -379,6 +466,43 @@ namespace Lantean.QBitTorrentClient
|
||||
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)
|
||||
{
|
||||
var query = new QueryBuilder();
|
||||
@@ -411,18 +535,6 @@ namespace Lantean.QBitTorrentClient
|
||||
|
||||
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)
|
||||
{
|
||||
var content = new FormUrlEncodedBuilder()
|
||||
@@ -433,18 +545,6 @@ namespace Lantean.QBitTorrentClient
|
||||
|
||||
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)
|
||||
{
|
||||
var content = new FormUrlEncodedBuilder()
|
||||
@@ -479,10 +579,11 @@ namespace Lantean.QBitTorrentClient
|
||||
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()
|
||||
.AddAllOrPipeSeparated("hashes", all, hashes)
|
||||
.AddIfNotNullOrEmpty("urls", trackers is null ? null : string.Join('|', trackers))
|
||||
.ToFormUrlEncodedContent();
|
||||
|
||||
var response = await _httpClient.PostAsync("torrents/reannounce", content);
|
||||
@@ -490,13 +591,15 @@ namespace Lantean.QBitTorrentClient
|
||||
await ThrowIfNotSuccessfulStatusCode(response);
|
||||
}
|
||||
|
||||
public async Task AddTorrent(AddTorrentParams addTorrentParams)
|
||||
public async Task<AddTorrentResult> AddTorrent(AddTorrentParams addTorrentParams)
|
||||
{
|
||||
var content = new MultipartFormDataContent();
|
||||
if (addTorrentParams.Urls is not null)
|
||||
|
||||
if (addTorrentParams.Urls?.Any() == true)
|
||||
{
|
||||
content.AddString("urls", string.Join('\n', addTorrentParams.Urls));
|
||||
}
|
||||
|
||||
if (addTorrentParams.Torrents is not null)
|
||||
{
|
||||
foreach (var (name, stream) in addTorrentParams.Torrents)
|
||||
@@ -504,6 +607,7 @@ namespace Lantean.QBitTorrentClient
|
||||
content.Add(new StreamContent(stream), "torrents", name);
|
||||
}
|
||||
}
|
||||
|
||||
if (addTorrentParams.SkipChecking is not null)
|
||||
{
|
||||
content.AddString("skip_checking", addTorrentParams.SkipChecking.Value);
|
||||
@@ -520,12 +624,10 @@ namespace Lantean.QBitTorrentClient
|
||||
{
|
||||
content.AddString("addToTopOfQueue", addTorrentParams.AddToTopOfQueue.Value);
|
||||
}
|
||||
// v4
|
||||
if (addTorrentParams.Paused is not null)
|
||||
if (addTorrentParams.Forced is not null)
|
||||
{
|
||||
content.AddString("paused", addTorrentParams.Paused.Value);
|
||||
content.AddString("forced", addTorrentParams.Forced.Value);
|
||||
}
|
||||
// v5
|
||||
if (addTorrentParams.Stopped is not null)
|
||||
{
|
||||
content.AddString("stopped", addTorrentParams.Stopped.Value);
|
||||
@@ -590,21 +692,61 @@ namespace Lantean.QBitTorrentClient
|
||||
{
|
||||
content.AddString("contentLayout", addTorrentParams.ContentLayout.Value);
|
||||
}
|
||||
|
||||
if (addTorrentParams.Cookie is not null)
|
||||
if (addTorrentParams.Downloader 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);
|
||||
|
||||
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()
|
||||
.Add("hash", hash)
|
||||
.AddAllOrPipeSeparated("hash", all, hashes ?? Array.Empty<string>())
|
||||
.Add("urls", string.Join('\n', urls))
|
||||
.ToFormUrlEncodedContent();
|
||||
|
||||
@@ -613,23 +755,42 @@ namespace Lantean.QBitTorrentClient
|
||||
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()
|
||||
.Add("hash", hash)
|
||||
.Add("originalUrl", originalUrl)
|
||||
.Add("newUrl", newUrl)
|
||||
.ToFormUrlEncodedContent();
|
||||
.Add("url", url);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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()
|
||||
.Add("hash", hash)
|
||||
.AddAllOrPipeSeparated("hash", all, hashes ?? Array.Empty<string>())
|
||||
.AddPipeSeparated("urls", urls)
|
||||
.ToFormUrlEncodedContent();
|
||||
|
||||
@@ -732,13 +893,14 @@ namespace Lantean.QBitTorrentClient
|
||||
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()
|
||||
.AddAllOrPipeSeparated("hashes", all, hashes)
|
||||
.Add("ratioLimit", ratioLimit)
|
||||
.Add("seedingTimeLimit", seedingTimeLimit)
|
||||
.Add("inactiveSeedingTimeLimit", inactiveSeedingTimeLimit)
|
||||
.AddIfNotNullOrEmpty("shareLimitAction", shareLimitAction)
|
||||
.ToFormUrlEncodedContent();
|
||||
|
||||
var response = await _httpClient.PostAsync("torrents/setShareLimits", content);
|
||||
@@ -795,6 +957,18 @@ namespace Lantean.QBitTorrentClient
|
||||
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)
|
||||
{
|
||||
var content = new FormUrlEncodedBuilder()
|
||||
@@ -995,8 +1169,180 @@ namespace Lantean.QBitTorrentClient
|
||||
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
|
||||
|
||||
#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
|
||||
|
||||
public async Task AddRssFolder(string path)
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
using Lantean.QBitTorrentClient.Models;
|
||||
using System.Linq;
|
||||
using Lantean.QBitTorrentClient.Models;
|
||||
|
||||
namespace Lantean.QBitTorrentClient
|
||||
{
|
||||
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)
|
||||
{
|
||||
return apiClient.StopTorrents(null, hash);
|
||||
@@ -34,21 +20,6 @@ namespace Lantean.QBitTorrentClient
|
||||
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)
|
||||
{
|
||||
return apiClient.StartTorrents(null, hash);
|
||||
@@ -158,7 +129,7 @@ namespace Lantean.QBitTorrentClient
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
public class SaveLocationJsonConverter : JsonConverter<SaveLocation>
|
||||
internal class SaveLocationJsonConverter : JsonConverter<SaveLocation>
|
||||
{
|
||||
public override SaveLocation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
|
||||
@@ -44,5 +44,10 @@
|
||||
{
|
||||
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
|
||||
{
|
||||
@@ -28,6 +32,12 @@ namespace Lantean.QBitTorrentClient
|
||||
|
||||
Task SetApplicationPreferences(UpdatePreferences preferences);
|
||||
|
||||
Task<IReadOnlyList<ApplicationCookie>> GetApplicationCookies();
|
||||
|
||||
Task SetApplicationCookies(IEnumerable<ApplicationCookie> cookies);
|
||||
|
||||
Task<string> RotateApiKey();
|
||||
|
||||
Task<string> GetDefaultSavePath();
|
||||
|
||||
Task<IReadOnlyList<NetworkInterface>> GetNetworkInterfaces();
|
||||
@@ -36,6 +46,14 @@ namespace Lantean.QBitTorrentClient
|
||||
|
||||
#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
|
||||
|
||||
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
|
||||
|
||||
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);
|
||||
|
||||
@@ -82,16 +100,18 @@ namespace Lantean.QBitTorrentClient
|
||||
|
||||
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<PieceState>> GetTorrentPieceStates(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 StopTorrents(bool? all = null, params string[] hashes);
|
||||
@@ -100,15 +120,15 @@ namespace Lantean.QBitTorrentClient
|
||||
|
||||
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);
|
||||
|
||||
@@ -126,7 +146,7 @@ namespace Lantean.QBitTorrentClient
|
||||
|
||||
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);
|
||||
|
||||
@@ -136,6 +156,8 @@ namespace Lantean.QBitTorrentClient
|
||||
|
||||
Task SetTorrentName(string name, string hash);
|
||||
|
||||
Task SetTorrentComment(IEnumerable<string> hashes, string comment);
|
||||
|
||||
Task SetTorrentCategory(string category, bool? all = null, params string[] hashes);
|
||||
|
||||
Task<IReadOnlyDictionary<string, Category>> GetAllCategories();
|
||||
@@ -172,8 +194,26 @@ namespace Lantean.QBitTorrentClient
|
||||
|
||||
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
|
||||
|
||||
#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
|
||||
|
||||
Task AddRssFolder(string path);
|
||||
|
||||
@@ -7,4 +7,8 @@
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Lantean.QBitTorrentClient.Test" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -12,9 +12,8 @@
|
||||
|
||||
public bool? AddToTopOfQueue { get; set; }
|
||||
|
||||
// v4
|
||||
public bool? Paused { get; set; }
|
||||
// v5
|
||||
public bool? Forced { get; set; }
|
||||
|
||||
public bool? Stopped { get; set; }
|
||||
|
||||
public string? SavePath { get; set; }
|
||||
@@ -47,7 +46,15 @@
|
||||
|
||||
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; }
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
@@ -7,10 +8,12 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
[JsonConstructor]
|
||||
public Category(
|
||||
string name,
|
||||
string? savePath)
|
||||
string? savePath,
|
||||
DownloadPathOption? downloadPath)
|
||||
{
|
||||
Name = name;
|
||||
SavePath = savePath;
|
||||
DownloadPath = downloadPath;
|
||||
}
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
@@ -18,5 +21,9 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
|
||||
[JsonPropertyName("savePath")]
|
||||
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>? tags,
|
||||
IReadOnlyList<string>? tagsRemoved,
|
||||
IReadOnlyDictionary<string, IReadOnlyList<string>> trackers,
|
||||
IReadOnlyDictionary<string, IReadOnlyList<string>>? trackers,
|
||||
IReadOnlyList<string>? trackersRemoved,
|
||||
ServerState? serverState)
|
||||
{
|
||||
ResponseId = responseId;
|
||||
@@ -26,6 +27,7 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
Tags = tags;
|
||||
TagsRemoved = tagsRemoved;
|
||||
Trackers = trackers;
|
||||
TrackersRemoved = trackersRemoved;
|
||||
ServerState = serverState;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
string? flags,
|
||||
string? flagsDescription,
|
||||
string? iPAddress,
|
||||
string? i2pDestination,
|
||||
string? clientId,
|
||||
int? port,
|
||||
float? progress,
|
||||
@@ -33,6 +34,7 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
Flags = flags;
|
||||
FlagsDescription = flagsDescription;
|
||||
IPAddress = iPAddress;
|
||||
I2pDestination = i2pDestination;
|
||||
ClientId = clientId;
|
||||
Port = port;
|
||||
Progress = progress;
|
||||
@@ -71,6 +73,9 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
[JsonPropertyName("ip")]
|
||||
public string? IPAddress { get; }
|
||||
|
||||
[JsonPropertyName("i2p_dest")]
|
||||
public string? I2pDestination { get; }
|
||||
|
||||
[JsonPropertyName("peer_id_client")]
|
||||
public string? ClientId { get; }
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
[JsonConstructor]
|
||||
public Preferences(
|
||||
bool addToTopOfQueue,
|
||||
bool addStoppedEnabled,
|
||||
string addTrackers,
|
||||
bool addTrackersEnabled,
|
||||
int altDlLimit,
|
||||
@@ -14,6 +15,7 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
bool alternativeWebuiEnabled,
|
||||
string alternativeWebuiPath,
|
||||
string announceIp,
|
||||
int announcePort,
|
||||
bool announceToAllTiers,
|
||||
bool announceToAllTrackers,
|
||||
bool anonymousMode,
|
||||
@@ -85,6 +87,7 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
int i2pPort,
|
||||
bool idnSupportEnabled,
|
||||
bool incompleteFilesExt,
|
||||
bool useUnwantedFolder,
|
||||
bool ipFilterEnabled,
|
||||
string ipFilterPath,
|
||||
bool ipFilterTrackers,
|
||||
@@ -92,6 +95,8 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
bool limitTcpOverhead,
|
||||
bool limitUtpRate,
|
||||
int listenPort,
|
||||
bool sslEnabled,
|
||||
int sslListenPort,
|
||||
string locale,
|
||||
bool lsd,
|
||||
bool mailNotificationAuthEnabled,
|
||||
@@ -160,6 +165,7 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
string savePath,
|
||||
bool savePathChangedTmmEnabled,
|
||||
int saveResumeDataInterval,
|
||||
int saveStatisticsInterval,
|
||||
Dictionary<string, SaveLocation> scanDirs,
|
||||
int scheduleFromHour,
|
||||
int scheduleFromMin,
|
||||
@@ -177,12 +183,12 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
int socketReceiveBufferSize,
|
||||
int socketSendBufferSize,
|
||||
bool ssrfMitigation,
|
||||
bool startPausedEnabled,
|
||||
int stopTrackerTimeout,
|
||||
string tempPath,
|
||||
bool tempPathEnabled,
|
||||
bool torrentChangedTmmEnabled,
|
||||
string torrentContentLayout,
|
||||
string torrentContentRemoveOption,
|
||||
int torrentFileSizeLimit,
|
||||
string torrentStopCondition,
|
||||
int upLimit,
|
||||
@@ -192,10 +198,12 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
int upnpLeaseDuration,
|
||||
bool useCategoryPathsInManualMode,
|
||||
bool useHttps,
|
||||
bool ignoreSslErrors,
|
||||
bool useSubcategories,
|
||||
int utpTcpMixedMode,
|
||||
bool validateHttpsTrackerCertificate,
|
||||
string webUiAddress,
|
||||
string webUiApiKey,
|
||||
int webUiBanDuration,
|
||||
bool webUiClickjackingProtectionEnabled,
|
||||
bool webUiCsrfProtectionEnabled,
|
||||
@@ -213,10 +221,14 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
bool webUiUpnp,
|
||||
bool webUiUseCustomHttpHeadersEnabled,
|
||||
string webUiUsername,
|
||||
string webUiPassword
|
||||
string webUiPassword,
|
||||
bool confirmTorrentDeletion,
|
||||
bool confirmTorrentRecheck,
|
||||
bool statusBarExternalIp
|
||||
)
|
||||
{
|
||||
AddToTopOfQueue = addToTopOfQueue;
|
||||
AddStoppedEnabled = addStoppedEnabled;
|
||||
AddTrackers = addTrackers;
|
||||
AddTrackersEnabled = addTrackersEnabled;
|
||||
AltDlLimit = altDlLimit;
|
||||
@@ -224,6 +236,7 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
AlternativeWebuiEnabled = alternativeWebuiEnabled;
|
||||
AlternativeWebuiPath = alternativeWebuiPath;
|
||||
AnnounceIp = announceIp;
|
||||
AnnouncePort = announcePort;
|
||||
AnnounceToAllTiers = announceToAllTiers;
|
||||
AnnounceToAllTrackers = announceToAllTrackers;
|
||||
AnonymousMode = anonymousMode;
|
||||
@@ -295,6 +308,7 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
I2pPort = i2pPort;
|
||||
IdnSupportEnabled = idnSupportEnabled;
|
||||
IncompleteFilesExt = incompleteFilesExt;
|
||||
UseUnwantedFolder = useUnwantedFolder;
|
||||
IpFilterEnabled = ipFilterEnabled;
|
||||
IpFilterPath = ipFilterPath;
|
||||
IpFilterTrackers = ipFilterTrackers;
|
||||
@@ -302,6 +316,8 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
LimitTcpOverhead = limitTcpOverhead;
|
||||
LimitUtpRate = limitUtpRate;
|
||||
ListenPort = listenPort;
|
||||
SslEnabled = sslEnabled;
|
||||
SslListenPort = sslListenPort;
|
||||
Locale = locale;
|
||||
Lsd = lsd;
|
||||
MailNotificationAuthEnabled = mailNotificationAuthEnabled;
|
||||
@@ -370,6 +386,7 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
SavePath = savePath;
|
||||
SavePathChangedTmmEnabled = savePathChangedTmmEnabled;
|
||||
SaveResumeDataInterval = saveResumeDataInterval;
|
||||
SaveStatisticsInterval = saveStatisticsInterval;
|
||||
ScanDirs = scanDirs;
|
||||
ScheduleFromHour = scheduleFromHour;
|
||||
ScheduleFromMin = scheduleFromMin;
|
||||
@@ -387,12 +404,12 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
SocketReceiveBufferSize = socketReceiveBufferSize;
|
||||
SocketSendBufferSize = socketSendBufferSize;
|
||||
SsrfMitigation = ssrfMitigation;
|
||||
StartPausedEnabled = startPausedEnabled;
|
||||
StopTrackerTimeout = stopTrackerTimeout;
|
||||
TempPath = tempPath;
|
||||
TempPathEnabled = tempPathEnabled;
|
||||
TorrentChangedTmmEnabled = torrentChangedTmmEnabled;
|
||||
TorrentContentLayout = torrentContentLayout;
|
||||
TorrentContentRemoveOption = torrentContentRemoveOption;
|
||||
TorrentFileSizeLimit = torrentFileSizeLimit;
|
||||
TorrentStopCondition = torrentStopCondition;
|
||||
UpLimit = upLimit;
|
||||
@@ -402,10 +419,12 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
UpnpLeaseDuration = upnpLeaseDuration;
|
||||
UseCategoryPathsInManualMode = useCategoryPathsInManualMode;
|
||||
UseHttps = useHttps;
|
||||
IgnoreSslErrors = ignoreSslErrors;
|
||||
UseSubcategories = useSubcategories;
|
||||
UtpTcpMixedMode = utpTcpMixedMode;
|
||||
ValidateHttpsTrackerCertificate = validateHttpsTrackerCertificate;
|
||||
WebUiAddress = webUiAddress;
|
||||
WebUiApiKey = webUiApiKey;
|
||||
WebUiBanDuration = webUiBanDuration;
|
||||
WebUiClickjackingProtectionEnabled = webUiClickjackingProtectionEnabled;
|
||||
WebUiCsrfProtectionEnabled = webUiCsrfProtectionEnabled;
|
||||
@@ -424,11 +443,17 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
WebUiUseCustomHttpHeadersEnabled = webUiUseCustomHttpHeadersEnabled;
|
||||
WebUiUsername = webUiUsername;
|
||||
WebUiPassword = webUiPassword;
|
||||
ConfirmTorrentDeletion = confirmTorrentDeletion;
|
||||
ConfirmTorrentRecheck = confirmTorrentRecheck;
|
||||
StatusBarExternalIp = statusBarExternalIp;
|
||||
}
|
||||
|
||||
[JsonPropertyName("add_to_top_of_queue")]
|
||||
public bool AddToTopOfQueue { get; }
|
||||
|
||||
[JsonPropertyName("add_stopped_enabled")]
|
||||
public bool AddStoppedEnabled { get; }
|
||||
|
||||
[JsonPropertyName("add_trackers")]
|
||||
public string AddTrackers { get; }
|
||||
|
||||
@@ -450,6 +475,9 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
[JsonPropertyName("announce_ip")]
|
||||
public string AnnounceIp { get; }
|
||||
|
||||
[JsonPropertyName("announce_port")]
|
||||
public int AnnouncePort { get; }
|
||||
|
||||
[JsonPropertyName("announce_to_all_tiers")]
|
||||
public bool AnnounceToAllTiers { get; }
|
||||
|
||||
@@ -663,6 +691,9 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
[JsonPropertyName("incomplete_files_ext")]
|
||||
public bool IncompleteFilesExt { get; }
|
||||
|
||||
[JsonPropertyName("use_unwanted_folder")]
|
||||
public bool UseUnwantedFolder { get; }
|
||||
|
||||
[JsonPropertyName("ip_filter_enabled")]
|
||||
public bool IpFilterEnabled { get; }
|
||||
|
||||
@@ -684,6 +715,12 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
[JsonPropertyName("listen_port")]
|
||||
public int ListenPort { get; }
|
||||
|
||||
[JsonPropertyName("ssl_enabled")]
|
||||
public bool SslEnabled { get; }
|
||||
|
||||
[JsonPropertyName("ssl_listen_port")]
|
||||
public int SslListenPort { get; }
|
||||
|
||||
[JsonPropertyName("locale")]
|
||||
public string Locale { get; }
|
||||
|
||||
@@ -888,6 +925,9 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
[JsonPropertyName("save_resume_data_interval")]
|
||||
public int SaveResumeDataInterval { get; }
|
||||
|
||||
[JsonPropertyName("save_statistics_interval")]
|
||||
public int SaveStatisticsInterval { get; }
|
||||
|
||||
[JsonPropertyName("scan_dirs")]
|
||||
public Dictionary<string, SaveLocation> ScanDirs { get; }
|
||||
|
||||
@@ -939,9 +979,6 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
[JsonPropertyName("ssrf_mitigation")]
|
||||
public bool SsrfMitigation { get; }
|
||||
|
||||
[JsonPropertyName("start_paused_enabled")]
|
||||
public bool StartPausedEnabled { get; }
|
||||
|
||||
[JsonPropertyName("stop_tracker_timeout")]
|
||||
public int StopTrackerTimeout { get; }
|
||||
|
||||
@@ -957,6 +994,9 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
[JsonPropertyName("torrent_content_layout")]
|
||||
public string TorrentContentLayout { get; }
|
||||
|
||||
[JsonPropertyName("torrent_content_remove_option")]
|
||||
public string TorrentContentRemoveOption { get; }
|
||||
|
||||
[JsonPropertyName("torrent_file_size_limit")]
|
||||
public int TorrentFileSizeLimit { get; }
|
||||
|
||||
@@ -984,6 +1024,9 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
[JsonPropertyName("use_https")]
|
||||
public bool UseHttps { get; }
|
||||
|
||||
[JsonPropertyName("ignore_ssl_errors")]
|
||||
public bool IgnoreSslErrors { get; }
|
||||
|
||||
[JsonPropertyName("use_subcategories")]
|
||||
public bool UseSubcategories { get; }
|
||||
|
||||
@@ -996,6 +1039,9 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
[JsonPropertyName("web_ui_address")]
|
||||
public string WebUiAddress { get; }
|
||||
|
||||
[JsonPropertyName("web_ui_api_key")]
|
||||
public string WebUiApiKey { get; }
|
||||
|
||||
[JsonPropertyName("web_ui_ban_duration")]
|
||||
public int WebUiBanDuration { get; }
|
||||
|
||||
@@ -1049,5 +1095,14 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
|
||||
[JsonPropertyName("web_ui_password")]
|
||||
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 (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
|
||||
{
|
||||
IsWatchedFolder = true
|
||||
};
|
||||
}
|
||||
else if (intValue == 1)
|
||||
else if (value == 1)
|
||||
{
|
||||
return new SaveLocation
|
||||
{
|
||||
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
|
||||
{
|
||||
IsWatchedFolder = true
|
||||
};
|
||||
}
|
||||
else if (stringValue == "1")
|
||||
else if (value == "1")
|
||||
{
|
||||
return new SaveLocation
|
||||
{
|
||||
@@ -47,14 +67,11 @@
|
||||
{
|
||||
return new SaveLocation
|
||||
{
|
||||
SavePath = stringValue
|
||||
SavePath = value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
}
|
||||
|
||||
public object ToValue()
|
||||
{
|
||||
if (IsWatchedFolder)
|
||||
|
||||
@@ -30,7 +30,9 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
long? uploadRateLimit,
|
||||
bool? useAltSpeedLimits,
|
||||
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;
|
||||
AllTimeUploaded = allTimeUploaded;
|
||||
@@ -49,6 +51,8 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
UseAltSpeedLimits = useAltSpeedLimits;
|
||||
UseSubcategories = useSubcategories;
|
||||
WriteCacheOverload = writeCacheOverload;
|
||||
LastExternalAddressV4 = lastExternalAddressV4;
|
||||
LastExternalAddressV6 = lastExternalAddressV6;
|
||||
}
|
||||
|
||||
[JsonPropertyName("alltime_dl")]
|
||||
@@ -101,5 +105,11 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
|
||||
[JsonPropertyName("write_cache_overload")]
|
||||
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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Lantean.QBitTorrentClient.Models
|
||||
{
|
||||
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")]
|
||||
public string Hash { get; }
|
||||
public string Hash { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("infohash_v1")]
|
||||
public string? InfoHashV1 { get; }
|
||||
public string? InfoHashV1 { get; init; }
|
||||
|
||||
[JsonPropertyName("infohash_v2")]
|
||||
public string? InfoHashV2 { get; }
|
||||
|
||||
[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; }
|
||||
public string? InfoHashV2 { get; init; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; }
|
||||
public string? Name { get; init; }
|
||||
|
||||
[JsonPropertyName("num_complete")]
|
||||
public int? NumberComplete { get; }
|
||||
|
||||
[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("magnet_uri")]
|
||||
public string? MagnetUri { get; init; }
|
||||
|
||||
[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")]
|
||||
public string? State { get; }
|
||||
public string? State { get; init; }
|
||||
|
||||
[JsonPropertyName("super_seeding")]
|
||||
public bool? SuperSeeding { get; }
|
||||
[JsonPropertyName("seq_dl")]
|
||||
public bool? SequentialDownload { get; init; }
|
||||
|
||||
[JsonPropertyName("f_l_piece_prio")]
|
||||
public bool? FirstLastPiecePriority { get; init; }
|
||||
|
||||
[JsonPropertyName("category")]
|
||||
public string? Category { get; init; }
|
||||
|
||||
[JsonPropertyName("tags")]
|
||||
[JsonConverter(typeof(CommaSeparatedJsonConverter))]
|
||||
public IReadOnlyList<string>? Tags { get; }
|
||||
public IReadOnlyList<string> Tags { get; init; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("time_active")]
|
||||
public int? TimeActive { get; }
|
||||
[JsonPropertyName("super_seeding")]
|
||||
public bool? SuperSeeding { get; init; }
|
||||
|
||||
[JsonPropertyName("total_size")]
|
||||
public long? TotalSize { get; }
|
||||
[JsonPropertyName("force_start")]
|
||||
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")]
|
||||
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")]
|
||||
public long? UploadLimit { get; }
|
||||
public long? UploadLimit { get; init; }
|
||||
|
||||
[JsonPropertyName("downloaded")]
|
||||
public long? Downloaded { get; init; }
|
||||
|
||||
[JsonPropertyName("uploaded")]
|
||||
public long? Uploaded { get; }
|
||||
public long? Uploaded { get; init; }
|
||||
|
||||
[JsonPropertyName("downloaded_session")]
|
||||
public long? DownloadedSession { get; init; }
|
||||
|
||||
[JsonPropertyName("uploaded_session")]
|
||||
public long? UploadedSession { get; }
|
||||
public long? UploadedSession { get; init; }
|
||||
|
||||
[JsonPropertyName("upspeed")]
|
||||
public long? UploadSpeed { get; }
|
||||
[JsonPropertyName("amount_left")]
|
||||
public long? AmountLeft { get; init; }
|
||||
|
||||
[JsonPropertyName("reannounce")]
|
||||
public long? Reannounce { get; }
|
||||
[JsonPropertyName("completed")]
|
||||
public long? Completed { get; init; }
|
||||
|
||||
[JsonPropertyName("inactive_seeding_time_limit")]
|
||||
public float? InactiveSeedingTimeLimit { get; }
|
||||
[JsonPropertyName("connections_count")]
|
||||
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")]
|
||||
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
|
||||
{
|
||||
@@ -13,7 +15,10 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
int seeds,
|
||||
int leeches,
|
||||
int downloads,
|
||||
string message)
|
||||
string message,
|
||||
long? nextAnnounce,
|
||||
long? minAnnounce,
|
||||
IReadOnlyList<TrackerEndpoint>? endpoints)
|
||||
{
|
||||
Url = url;
|
||||
Status = status;
|
||||
@@ -23,6 +28,9 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
Leeches = leeches;
|
||||
Downloads = downloads;
|
||||
Message = message;
|
||||
NextAnnounce = nextAnnounce;
|
||||
MinAnnounce = minAnnounce;
|
||||
Endpoints = endpoints ?? Array.Empty<TrackerEndpoint>();
|
||||
}
|
||||
|
||||
[JsonPropertyName("url")]
|
||||
@@ -48,5 +56,27 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
|
||||
[JsonPropertyName("msg")]
|
||||
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,
|
||||
Updating = 3,
|
||||
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