mirror of
https://github.com/lantean-code/qbtmud.git
synced 2025-10-24 08:33:36 +00:00
Compare commits
11 Commits
feature/ap
...
feature/v5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
075ea9f855 | ||
|
|
d01204a703 | ||
|
|
ab1c594b07 | ||
|
|
6a5d8b2610 | ||
|
|
b8412bb232 | ||
|
|
e64a13c7c9 | ||
|
|
e4ea79a8ed | ||
|
|
0976b72411 | ||
|
|
965fbcd010 | ||
|
|
3d0dbde9f4 | ||
|
|
5b4fbde7b2 |
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
<MudSelectItem Value="@("Original")">Original</MudSelectItem>
|
||||
<MudSelectItem Value="@("Subfolder")">Create subfolder</MudSelectItem>
|
||||
<MudSelectItem Value="@("NoSubfolder")">Don't create subfolder'</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudItem xs="12">
|
||||
<FieldSwitch Label="Download in sequentual order" @bind-Value="DownloadInSequentialOrder" />
|
||||
<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>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<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;
|
||||
|
||||
_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!;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
if (Value.SeedingTimeLimit >= 0)
|
||||
{
|
||||
TotalMinutesEnabled = true;
|
||||
TotalMinutes = (int)Value.SeedingTimeLimit;
|
||||
}
|
||||
if (Value.InactiveSeedingTimeLimit >= 0)
|
||||
{
|
||||
InactiveMinutesEnabled = true;
|
||||
InactiveMinutes = (int)Value.InactiveSeedingTimeLimit;
|
||||
}
|
||||
Ratio = 0;
|
||||
}
|
||||
|
||||
if (baseline.SeedingTimeLimit >= 0)
|
||||
{
|
||||
TotalMinutesEnabled = true;
|
||||
TotalMinutes = (int)baseline.SeedingTimeLimit;
|
||||
}
|
||||
else
|
||||
{
|
||||
TotalMinutes = 0;
|
||||
}
|
||||
|
||||
if (baseline.InactiveSeedingTimeLimit >= 0)
|
||||
{
|
||||
InactiveMinutesEnabled = true;
|
||||
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; } = [];
|
||||
|
||||
|
||||
@@ -371,7 +371,7 @@ namespace Lantean.QBTMud.Components
|
||||
{
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,7 +7,6 @@ using Lantean.QBTMud.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
using System.Linq;
|
||||
|
||||
namespace Lantean.QBTMud.Components
|
||||
{
|
||||
@@ -30,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!;
|
||||
@@ -105,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)),
|
||||
];
|
||||
@@ -162,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)
|
||||
{
|
||||
@@ -258,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()
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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,9 +1,9 @@
|
||||
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;
|
||||
using MudBlazor;
|
||||
using System.Linq;
|
||||
|
||||
namespace Lantean.QBTMud.Helpers
|
||||
{
|
||||
@@ -76,12 +76,18 @@ namespace Lantean.QBTMud.Helpers
|
||||
addTorrentParams.ContentLayout = Enum.Parse<QBitTorrentClient.Models.TorrentContentLayout>(options.ContentLayout);
|
||||
}
|
||||
addTorrentParams.DownloadLimit = options.DownloadLimit;
|
||||
addTorrentParams.DownloadPath = options.DownloadPath;
|
||||
if (!string.IsNullOrWhiteSpace(options.DownloadPath))
|
||||
{
|
||||
addTorrentParams.DownloadPath = options.DownloadPath;
|
||||
}
|
||||
addTorrentParams.FirstLastPiecePriority = options.DownloadFirstAndLastPiecesFirst;
|
||||
addTorrentParams.InactiveSeedingTimeLimit = options.InactiveSeedingTimeLimit;
|
||||
addTorrentParams.RatioLimit = options.RatioLimit;
|
||||
addTorrentParams.RenameTorrent = options.RenameTorrent;
|
||||
addTorrentParams.SavePath = options.SavePath;
|
||||
if (!options.TorrentManagementMode)
|
||||
{
|
||||
addTorrentParams.SavePath = options.SavePath;
|
||||
}
|
||||
addTorrentParams.SeedingTimeLimit = options.SeedingTimeLimit;
|
||||
addTorrentParams.SequentialDownload = options.DownloadInSequentialOrder;
|
||||
if (!string.IsNullOrEmpty(options.ShareLimitAction))
|
||||
@@ -96,7 +102,10 @@ namespace Lantean.QBTMud.Helpers
|
||||
addTorrentParams.Stopped = !options.StartTorrent;
|
||||
addTorrentParams.Tags = options.Tags;
|
||||
addTorrentParams.UploadLimit = options.UploadLimit;
|
||||
addTorrentParams.UseDownloadPath = options.UseDownloadPath;
|
||||
if (options.UseDownloadPath.HasValue)
|
||||
{
|
||||
addTorrentParams.UseDownloadPath = options.UseDownloadPath;
|
||||
}
|
||||
return addTorrentParams;
|
||||
}
|
||||
|
||||
@@ -122,7 +131,7 @@ namespace Lantean.QBTMud.Helpers
|
||||
_ = 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)
|
||||
{
|
||||
@@ -134,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)
|
||||
@@ -146,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();
|
||||
@@ -213,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);
|
||||
|
||||
@@ -239,7 +285,7 @@ namespace Lantean.QBTMud.Helpers
|
||||
|
||||
var shareRatio = (ShareRatio)dialogResult.Data;
|
||||
|
||||
await apiClient.SetTorrentShareLimit(shareRatio.RatioLimit, shareRatio.SeedingTimeLimit, shareRatio.InactiveSeedingTimeLimit, hashes: 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)
|
||||
@@ -433,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;
|
||||
@@ -415,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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,23 +4,27 @@
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<CompressionEnabled>false</CompressionEnabled>
|
||||
<LangVersion>12</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<CompressionEnabled>false</CompressionEnabled>
|
||||
<LangVersion>12</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
||||
<PackageReference Include="ByteSize" Version="2.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.10" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
|
||||
<PackageReference Include="MudBlazor" Version="8.13.0" />
|
||||
<PackageReference Include="MudBlazor.ThemeManager" Version="3.0.0" />
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
||||
<PackageReference Include="ByteSize" Version="2.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.10" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
|
||||
<PackageReference Include="MudBlazor" Version="8.13.0" />
|
||||
<PackageReference Include="MudBlazor.ThemeManager" Version="3.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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!;
|
||||
@@ -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)
|
||||
|
||||
@@ -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,7 +6,6 @@
|
||||
Downloading,
|
||||
Seeding,
|
||||
Completed,
|
||||
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,7 +2,7 @@
|
||||
|
||||
namespace Lantean.QBTMud.Services
|
||||
{
|
||||
public interface IDataManager
|
||||
public interface ITorrentDataManager
|
||||
{
|
||||
MainData CreateMainData(QBitTorrentClient.Models.MainData mainData);
|
||||
|
||||
@@ -10,16 +10,8 @@ namespace Lantean.QBTMud.Services
|
||||
|
||||
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,30 +1,13 @@
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var torrents = new Dictionary<string, Torrent>(mainData.Torrents?.Count ?? 0);
|
||||
@@ -145,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)
|
||||
@@ -183,7 +168,6 @@ namespace Lantean.QBTMud.Services
|
||||
{
|
||||
filterChanged = true;
|
||||
}
|
||||
torrentList.TagState.Remove(normalizedTag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,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;
|
||||
}
|
||||
@@ -352,9 +341,7 @@ namespace Lantean.QBTMud.Services
|
||||
return _statusArray;
|
||||
}
|
||||
|
||||
_statusArray = Enum.GetValues<Status>()
|
||||
.Where(s => s != Status.Paused)
|
||||
.ToArray();
|
||||
_statusArray = Enum.GetValues<Status>();
|
||||
|
||||
return _statusArray;
|
||||
}
|
||||
@@ -367,15 +354,8 @@ 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);
|
||||
|
||||
@@ -553,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?
|
||||
@@ -688,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))
|
||||
{
|
||||
@@ -704,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,
|
||||
@@ -714,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)
|
||||
{
|
||||
@@ -736,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)
|
||||
{
|
||||
@@ -756,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);
|
||||
|
||||
@@ -778,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);
|
||||
|
||||
@@ -796,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;
|
||||
|
||||
@@ -827,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))
|
||||
{
|
||||
@@ -844,7 +773,7 @@ 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())
|
||||
{
|
||||
@@ -873,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))
|
||||
{
|
||||
@@ -898,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))
|
||||
{
|
||||
@@ -912,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))
|
||||
{
|
||||
@@ -940,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))
|
||||
{
|
||||
@@ -951,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))
|
||||
{
|
||||
@@ -962,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))
|
||||
{
|
||||
@@ -973,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)
|
||||
{
|
||||
@@ -984,7 +913,7 @@ namespace Lantean.QBTMud.Services
|
||||
return false;
|
||||
}
|
||||
|
||||
private readonly struct TorrentUpdateResult
|
||||
internal readonly struct TorrentUpdateResult
|
||||
{
|
||||
public TorrentUpdateResult(bool dataChanged, bool filterChanged)
|
||||
{
|
||||
@@ -997,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;
|
||||
@@ -1314,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);
|
||||
}
|
||||
|
||||
@@ -1439,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;
|
||||
@@ -1450,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;
|
||||
@@ -1462,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;
|
||||
@@ -1560,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,
|
||||
AddStoppedEnabled = changed.AddStoppedEnabled,
|
||||
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.AddStoppedEnabled = changed.AddStoppedEnabled ?? original.AddStoppedEnabled;
|
||||
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)
|
||||
@@ -2000,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)
|
||||
{
|
||||
@@ -2094,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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,17 @@
|
||||
</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>
|
||||
|
||||
@@ -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,11 +0,0 @@
|
||||
namespace Lantean.QBitTorrentClient.Test
|
||||
{
|
||||
public class UnitTest1
|
||||
{
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
using Lantean.QBitTorrentClient.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
@@ -1230,10 +1226,10 @@ namespace Lantean.QBitTorrentClient
|
||||
|
||||
public async Task<string> AddTorrentCreationTask(TorrentCreationTaskRequest request)
|
||||
{
|
||||
if (request is null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
if (string.IsNullOrWhiteSpace(request.SourcePath))
|
||||
{
|
||||
throw new ArgumentException("SourcePath is required.", nameof(request));
|
||||
}
|
||||
|
||||
var builder = new FormUrlEncodedBuilder()
|
||||
.Add("sourcePath", request.SourcePath);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,11 @@
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Lantean.QBitTorrentClient.Test" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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,7 @@ 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)
|
||||
{
|
||||
|
||||
@@ -221,7 +221,10 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
bool webUiUpnp,
|
||||
bool webUiUseCustomHttpHeadersEnabled,
|
||||
string webUiUsername,
|
||||
string webUiPassword
|
||||
string webUiPassword,
|
||||
bool confirmTorrentDeletion,
|
||||
bool confirmTorrentRecheck,
|
||||
bool statusBarExternalIp
|
||||
)
|
||||
{
|
||||
AddToTopOfQueue = addToTopOfQueue;
|
||||
@@ -440,6 +443,9 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
WebUiUseCustomHttpHeadersEnabled = webUiUseCustomHttpHeadersEnabled;
|
||||
WebUiUsername = webUiUsername;
|
||||
WebUiPassword = webUiPassword;
|
||||
ConfirmTorrentDeletion = confirmTorrentDeletion;
|
||||
ConfirmTorrentRecheck = confirmTorrentRecheck;
|
||||
StatusBarExternalIp = statusBarExternalIp;
|
||||
}
|
||||
|
||||
[JsonPropertyName("add_to_top_of_queue")]
|
||||
@@ -1089,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,49 +12,66 @@
|
||||
{
|
||||
if (value is int intValue)
|
||||
{
|
||||
if (intValue == 0)
|
||||
{
|
||||
return new SaveLocation
|
||||
{
|
||||
IsWatchedFolder = true
|
||||
};
|
||||
}
|
||||
else if (intValue == 1)
|
||||
{
|
||||
return new SaveLocation
|
||||
{
|
||||
IsDefaultFolder = true
|
||||
};
|
||||
}
|
||||
return Create(intValue);
|
||||
}
|
||||
else if (value is string stringValue)
|
||||
{
|
||||
if (stringValue == "0")
|
||||
{
|
||||
return new SaveLocation
|
||||
{
|
||||
IsWatchedFolder = true
|
||||
};
|
||||
}
|
||||
else if (stringValue == "1")
|
||||
{
|
||||
return new SaveLocation
|
||||
{
|
||||
IsDefaultFolder = true
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new SaveLocation
|
||||
{
|
||||
SavePath = 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 (value == 1)
|
||||
{
|
||||
return new SaveLocation
|
||||
{
|
||||
IsDefaultFolder = true
|
||||
};
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
}
|
||||
|
||||
public static SaveLocation Create(string? value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
}
|
||||
|
||||
if (value == "0")
|
||||
{
|
||||
return new SaveLocation
|
||||
{
|
||||
IsWatchedFolder = true
|
||||
};
|
||||
}
|
||||
else if (value == "1")
|
||||
{
|
||||
return new SaveLocation
|
||||
{
|
||||
IsDefaultFolder = true
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new SaveLocation
|
||||
{
|
||||
SavePath = 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; }
|
||||
}
|
||||
}
|
||||
@@ -653,6 +653,15 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
[JsonPropertyName("web_ui_password")]
|
||||
public string? WebUiPassword { get; set; }
|
||||
|
||||
[JsonPropertyName("confirm_torrent_deletion")]
|
||||
public bool? ConfirmTorrentDeletion { get; set; }
|
||||
|
||||
[JsonPropertyName("confirm_torrent_recheck")]
|
||||
public bool? ConfirmTorrentRecheck { get; set; }
|
||||
|
||||
[JsonPropertyName("status_bar_external_ip")]
|
||||
public bool? StatusBarExternalIp { get; set; }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (MaxRatio.HasValue && MaxRatioEnabled.HasValue)
|
||||
|
||||
@@ -73,5 +73,10 @@ namespace Lantean.QBitTorrentClient
|
||||
{
|
||||
return ToQueryString();
|
||||
}
|
||||
|
||||
internal IList<KeyValuePair<string, string>> GetParameters()
|
||||
{
|
||||
return _parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Upgrade-To-v5-Planning.md
Normal file
25
Upgrade-To-v5-Planning.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Upgrade to qBittorrent WebUI v5 – UI Alignment Plan
|
||||
|
||||
## Torrent List Filtering
|
||||
- **Regex toggle & field selector**: Introduce the regex checkbox and the "Filter by" (Name/Save path) select found in v5. Update `FilterState`/`LoggedInLayout` to carry both values, wire them to `TorrentList`’s toolbar, and validate invalid patterns gracefully.
|
||||
- **Filter helper parity**: Rework `FilterHelper.ContainsAllTerms/FilterTerms` to mirror `window.qBittorrent.Misc.containsAllTerms` (evaluate every term, respect `+`/`-` prefixes). Ensure filtering applies to the selected field, not just the torrent name.
|
||||
- **New status buckets**: Add `Running` and `Moving` to `Status` enum, update `FilterHelper.FilterStatus`, `DisplayHelpers`, and `FiltersNav` so counts/icons match upstream.
|
||||
|
||||
## Tracker Filters
|
||||
- **Special buckets**: Extend `FilterHelper`/`DataManager` to create sets for "Announce error", "Error", "Warning", and "Trackerless" in addition to "All". Store the required flags on the UI `Torrent` model (`HasTrackerError`, `HasTrackerWarning`, `HasOtherAnnounceError`, `TrackersCount`, etc.).
|
||||
- **Tracker grouping & removal**: When grouping trackers by host in `FiltersNav`, retain original URL entries so removal can target the right string. Replace the placeholder "Remove tracker" action with a real implementation and disable it for synthetic buckets.
|
||||
|
||||
## ~~Torrent Data Model & Columns~~
|
||||
- ~~**Model sync**: Bring `Lantean.QBTMud.Models.Torrent` into parity with v5 (`Popularity`, `DownloadPath`, `RootPath`, `InfoHashV1/2`, `IsPrivate`, share-limit action fields, tracker flags, etc.) and map them in `DataManager.CreateTorrent`.~~
|
||||
- ~~**Column set alignment**: Match the v5 table defaults—add missing columns (Popularity, Reannounce in, Info hashes, Download path, Private, etc.), fix "Ratio Limit" to display `RatioLimit`, and ensure column ordering/enabled state mirrors `DynamicTable.TorrentsTable`.~~
|
||||
- ~~**Helper updates**: Extend `DisplayHelpers` to format the new fields (popularity, private flag, info hashes, error state icons).~~
|
||||
|
||||
## Actions & Dialogs
|
||||
- ~~**Copy submenu**: Add "Copy comment" and "Copy content path" to the copy submenu in `TorrentActions`, keeping clipboard behaviour identical to v5.~~
|
||||
- ~~**Share ratio dialog**: Update `ShareRatioDialog`, `ShareRatio/ShareRatioMax`, and `DialogHelper.InvokeShareRatioDialog` to surface `ShareLimitAction`, fix the `MaxInactiveSeedingTime` mapping, and call `SetTorrentShareLimit` with the action.~~
|
||||
|
||||
## ~~Add-Torrent Flow~~
|
||||
- ~~Mirror the v5 add-torrent pane: add controls for incomplete save path, tags, auto-start, queue position, share-limit action, etc., in `AddTorrentOptions.razor`, and wire the new fields into the submission object.~~
|
||||
|
||||
## ~~Preferences & Local Settings~~
|
||||
- ~~Introduce new v5 toggles such as "Display full tracker URL" in `AdvancedOptions`, persist them via the preferences service, and respect the setting in the tracker column rendering.~~
|
||||
Reference in New Issue
Block a user