Wesselinator 2 weeks ago committed by GitHub
commit f6a5d78faa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,10 @@
.git*
*/TestResults*
node_modules/
_output*
_artifacts
_rawPackage/
_dotTrace*
_tests*
_publish*
_temp*

@ -0,0 +1,8 @@
# https://taskfile.dev
version: '3'
includes:
containers:
taskfile: './docker/Taskfile.yml'
dir: './'

@ -0,0 +1,19 @@
# https://taskfile.dev
version: '3'
vars:
container_command: "podman"
tasks:
test:
cmds:
- '{{.container_command}} build -v $(realpath ./node_modules):/source/node_modules:U -v $(realpath ~/.nuget):/root/.nuget:U -v $(realpath .git):/source/.git:ro,U . -f docker/test/Containerfile -t sonarr:test'
build:alpine:
cmds:
- '{{.container_command}} build -v $(realpath ./node_modules):/source/node_modules:U -v $(realpath ~/.nuget):/root/.nuget:U -v $(realpath .git):/source/.git:ro,U --layers --squash-all . -f docker/build/alpine/Containerfile -t sonarr:local-alpine'
pack:alpine:
cmds:
- '{{.container_command}} build -v $(realpath ./node_modules):/source/node_modules:U -v $(realpath ~/.nuget):/root/.nuget:U -v $(realpath .git):/source/.git:ro,U --layers --squash-all . -f docker/pack/alpine/Containerfile -t sonarr:local-pack'

@ -0,0 +1,32 @@
FROM mcr.microsoft.com/dotnet/sdk:6.0 as BACKEND
COPY . /source
WORKDIR /source
RUN ./build.sh --backend --runtime linux-musl-x64
FROM docker.io/node:lts as FRONTEND_PACKAGE
COPY . /source
COPY --from=BACKEND /source/_output/ /source/_output/
WORKDIR /source
RUN ./build.sh --frontend --packages --runtime linux-musl-x64
FROM docker.io/alpine:latest as FINAL
RUN apk --no-cache add icu-libs sqlite-libs xmlstarlet
ENV XDG_DATA_HOME="/config"
ENV XDG_CONFIG_HOME="/config"
RUN addgroup -S sonarr && adduser sonarr -G sonarr -S -D -H
RUN mkdir -p /app
COPY --from=FRONTEND_PACKAGE /source/_artifacts/linux-musl-x64/net6.0/Sonarr/ /app/Sonarr/
RUN chown sonarr:sonarr -R /app
USER sonarr
VOLUME /blackhole
VOLUME /config
EXPOSE 8989
WORKDIR /config
ENTRYPOINT ["/app/Sonarr/Sonarr"]

@ -0,0 +1,20 @@
# This packs the latest release artifact directly into a container
FROM docker.io/alpine:latest
RUN apk --no-cache add icu-libs sqlite-libs xmlstarlet
ENV XDG_DATA_HOME="/config"
ENV XDG_CONFIG_HOME="/config"
RUN addgroup -S sonarr && adduser sonarr -G sonarr -S -D -H
RUN mkdir -p /app && \
download=$(wget -q https://api.github.com/repos/Sonarr/Sonarr/releases/latest -O - | grep -e 'linux-musl-x64' | grep 'browser_download_url' | cut -d \" -f 4) && \
wget "$download" -O - | tar xzv -C /app && \
chown sonarr:sonarr -R /app
USER sonarr
VOLUME /blackhole
VOLUME /config
EXPOSE 8989
WORKDIR /config
ENTRYPOINT ["/app/Sonarr/Sonarr"]

@ -0,0 +1,13 @@
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS TESTER
RUN apt update && apt install -y tofrodos tzdata sqlite3 mediainfo xmlstarlet && apt clean
COPY . /source
WORKDIR /source
RUN dotnet test src/Sonarr.sln --filter "Category!=IntegrationTest&Category!=AutomationTest&Category!=WINDOWS" --logger":html;LogFileName=results.html" || true
# TODO: figure this step to collect all the results.html
#FROM docker.io/alpine
#RUN ????
# For now, as you need them, add them
FROM scratch AS RESULT
COPY --from=TESTER /source/src/NzbDrone.Core.Test/TestResults/results.html /results/NzbDrone_Core_Test.html

@ -15,11 +15,14 @@ namespace NzbDrone.Common.Http
public string JsonMethod { get; private set; }
public List<object> JsonParameters { get; private set; }
public bool JsonParametersToObject { get; private set; }
public JsonRpcRequestBuilder(string baseUrl)
: base(baseUrl)
{
Method = HttpMethod.Post;
JsonParameters = new List<object>();
JsonParametersToObject = false;
}
public JsonRpcRequestBuilder(string baseUrl, string method, IEnumerable<object> parameters)
@ -28,6 +31,16 @@ namespace NzbDrone.Common.Http
Method = HttpMethod.Post;
JsonMethod = method;
JsonParameters = parameters.ToList();
JsonParametersToObject = false;
}
public JsonRpcRequestBuilder(string baseUrl, string method, bool paramToObj, IEnumerable<object> parameters)
: base(baseUrl)
{
Method = HttpMethod.Post;
JsonMethod = method;
JsonParameters = parameters.ToList();
JsonParametersToObject = paramToObj;
}
public override HttpRequestBuilder Clone()
@ -51,18 +64,29 @@ namespace NzbDrone.Common.Http
request.Headers.ContentType = JsonRpcContentType;
var parameterData = new object[JsonParameters.Count];
var parameterAsArray = new object[JsonParameters.Count];
var parameterSummary = new string[JsonParameters.Count];
for (var i = 0; i < JsonParameters.Count; i++)
{
ConvertParameter(JsonParameters[i], out parameterData[i], out parameterSummary[i]);
ConvertParameter(JsonParameters[i], out parameterAsArray[i], out parameterSummary[i]);
}
object paramFinal = parameterAsArray;
if (JsonParametersToObject)
{
var left = parameterAsArray.Skip(0).Where((v, i) => i % 2 == 0);
var right = parameterAsArray.Skip(1).Where((v, i) => i % 2 == 0);
var parameterAsDict = left.Zip(right, Tuple.Create).ToDictionary(x => x.Item1.ToString(), x => x.Item2);
paramFinal = parameterAsDict;
}
var message = new Dictionary<string, object>();
message["jsonrpc"] = "2.0";
message["method"] = JsonMethod;
message["params"] = parameterData;
message["params"] = paramFinal;
message["id"] = CreateNextId();
request.SetContent(message.ToJson());

@ -0,0 +1,595 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.LibTorrent.Models;
using NzbDrone.Core.Download.Clients.Porla;
using NzbDrone.Core.Download.Clients.Porla.Models;
using NzbDrone.Core.MediaFiles.TorrentInfo;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.PorlaTests
{
[TestFixture]
[Description("Tests the Porla Download Client")]
public class PorlaFixture : DownloadClientFixtureBase<Porla>
{
private const string _somehash = "dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c";
protected PorlaTorrentDetail _queued;
protected PorlaTorrentDetail _downloading;
protected PorlaTorrentDetail _paused;
// protected PorlaTorrentDetail _failed;
// protected PorlaTorrentDetail _completed;
protected PorlaTorrentDetail _seeding;
private static readonly IList<string> SomeTags = new System.Collections.Generic.List<string> { "sometag", "someothertag" };
private static readonly IList<string> DefaultTags = new System.Collections.Generic.List<string> { "apply_ip_filter", "auto_managed" };
private static readonly IList<string> DefaultPausedTags = DefaultTags.Append("paused").ToList();
[SetUp]
public void Setup()
{
Subject.Definition = new DownloadClientDefinition();
Subject.Definition.Settings = new PorlaSettings();
_queued = new PorlaTorrentDetail
{
ActiveDuration = 1L,
AllTimeDownload = 0L,
AllTimeUpload = 0L,
Category = "sonarr-tv",
DownloadRate = 0,
Error = null,
ETA = -1L,
FinishedDuration = 0L,
Flags = new (DefaultTags),
InfoHash = new (_somehash, null),
LastDownload = -1L,
LastUpload = -1L,
ListPeers = 1,
ListSeeds = 0,
Metadata = null, // not exactly correct, should be json `{}`
MovingStorage = false,
Name = _title,
NumPeers = 5, // I am unsure here. I think the queued items hold on to it's list of peers
NumSeeds = 0,
Progress = 0.0f,
QueuePosition = 1,
Ratio = 0.0d,
SavePath = "/tmp",
SeedingDuration = 0L,
Session = "default",
Size = 100000000L,
State = LibTorrentStatus.downloading_metadata,
Tags = new (SomeTags), // do we have a remote episode aviable? can I use CreateRemoteEpisode
Total = 100000000L,
TotalDone = 0L,
UploadRate = 0
};
_downloading = new PorlaTorrentDetail
{
ActiveDuration = 10L,
AllTimeDownload = 100000000L,
AllTimeUpload = 0L,
Category = "sonarr-tv",
DownloadRate = 2000000,
Error = null,
ETA = 200L,
FinishedDuration = 0L,
Flags = new (DefaultTags),
InfoHash = new (_somehash, null),
LastDownload = 1L,
LastUpload = -1L,
ListPeers = 90,
ListSeeds = 6,
Metadata = null, // not exactly correct, should be json `{}`
MovingStorage = false,
Name = _title,
NumPeers = 10,
NumSeeds = 8,
Progress = 0.5f,
QueuePosition = 0,
Ratio = 0.0d,
SavePath = "/tmp",
SeedingDuration = 0L,
Session = "default",
Size = 100000000L,
State = LibTorrentStatus.downloading,
Tags = new (SomeTags),
Total = 100000000L,
TotalDone = 150000000L, // normally bigger than AllTimeDownload. compression? but doesn't encryption add overhead?
UploadRate = 100000
};
_paused = new PorlaTorrentDetail
{
ActiveDuration = 10L,
AllTimeDownload = 100000000L,
AllTimeUpload = 0L,
Category = "sonarr-tv",
DownloadRate = 0,
Error = null,
ETA = -1L,
FinishedDuration = 0L,
Flags = new (DefaultPausedTags), // "paused" should now exist in the flags.
InfoHash = new (_somehash, null),
LastDownload = 1L,
LastUpload = -1L,
ListPeers = 90,
ListSeeds = 6,
Metadata = null, // not exactly correct, should be json `{}`
MovingStorage = false,
Name = _title,
NumPeers = 0, // paused so this should be 0
NumSeeds = 0, // paused so this should be 0
Progress = 0.5f,
QueuePosition = 0, // seems to still retain it's queue possition
Ratio = 0.0d,
SavePath = "/tmp",
SeedingDuration = 0L,
Session = "default",
Size = 100000000L,
State = LibTorrentStatus.downloading, // LibTorrent does not set a state for paused
Tags = new (SomeTags),
Total = 100000000L,
TotalDone = 150000000L,
UploadRate = 0
};
// _failed = new PorlaTorrentDetail
// {
// ActiveDuration = 10L,
// AllTimeDownload = 100000000L,
// AllTimeUpload = 0L,
// Category = "sonarr-tv",
// DownloadRate = 2000000,
// Error = null, // pain: need an example
// ETA = 200L,
// FinishedDuration = 0L,
// Flags = new (DefaultTags),
// InfoHash = new ("HASH", null),
// LastDownload = 1L,
// LastUpload = -1L,
// ListPeers = 90,
// ListSeeds = 6,
// Metadata = new (null),
// MovingStorage = false,
// Name = _title,
// NumPeers = 10,
// NumSeeds = 8,
// Progress = 0.5f,
// QueuePosition = 0,
// Ratio = 0.0d,
// SavePath = "/tmp",
// SeedingDuration = 0L,
// Session = "default",
// Size = 100000000L,
// State = LibTorrentStatus.downloading, // ?
// Tags = new (SomeTags),
// Total = 100000000L,
// TotalDone = 150000000L,
// UploadRate = 100000
// };
_seeding = new PorlaTorrentDetail
{
ActiveDuration = 120L,
AllTimeDownload = 200000000L,
AllTimeUpload = 100000000L,
Category = "sonarr-tv",
DownloadRate = 100, // this seems to always be doing something / might be frozen at last value
Error = null,
ETA = -1L, // we are done so eta is infinate
FinishedDuration = 100L,
Flags = new (DefaultTags),
InfoHash = new (_somehash, null),
LastDownload = 100L,
LastUpload = 10L,
ListPeers = 128,
ListSeeds = 16,
Metadata = null,
MovingStorage = false,
Name = _title,
NumPeers = 1,
NumSeeds = 2,
Progress = 1.0f,
QueuePosition = -1,
Ratio = 1.0d,
SavePath = "/tmp",
SeedingDuration = 666L,
Session = "default",
Size = 190000000L, // usually a little smaller than downloaded total
State = LibTorrentStatus.seeding, // double check this one
Tags = new (SomeTags), // do we have a remote episode aviable? can I use CreateRemoteEpisode
Total = 0L,
TotalDone = 190000000L, // after we are finished this should be the same as `size`
UploadRate = 100
};
Mocker.GetMock<ITorrentFileInfoReader>()
.Setup(s => s.GetHashFromTorrentFile(It.IsAny<byte[]>()))
.Returns(_somehash);
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), Array.Empty<byte>()));
}
/// <summary> Setups the `Add*` to fail, by throwing Exceptions </summary>
protected void SetupFailedDownload()
{
Mocker.GetMock<IPorlaProxy>()
.Setup(s => s.AddMagnetTorrent(It.IsAny<PorlaSettings>(), It.IsAny<string>(), It.IsAny<IList<string>>()))
.Throws<InvalidOperationException>();
Mocker.GetMock<IPorlaProxy>()
.Setup(s => s.AddTorrentFile(It.IsAny<PorlaSettings>(), It.IsAny<byte[]>(), It.IsAny<IList<string>>()))
.Throws<InvalidOperationException>();
#nullable enable
Mocker.GetMock<IPorlaProxy>()
.Setup(s => s.AddMagnetTorrent(It.IsAny<PorlaSettings>(), It.IsAny<string>(), It.IsAny<IList<string>?>()))
.Throws<InvalidOperationException>();
Mocker.GetMock<IPorlaProxy>()
.Setup(s => s.AddTorrentFile(It.IsAny<PorlaSettings>(), It.IsAny<byte[]>(), It.IsAny<IList<string>?>()))
.Throws<InvalidOperationException>();
#nullable disable
}
/// <summary> Setups the `Add*` to succeed, by mocking successfull returns </summary>
protected void SetupSuccessfullDownload()
{
// Succesful Download should return Queued Items
// TODO: This should probs be Downloads
PrepareClientToReturnQueuedItem();
PorlaTorrent returnTorrent = new (_queued.InfoHash);
Mocker.GetMock<IPorlaProxy>()
.Setup(s => s.AddMagnetTorrent(It.IsAny<PorlaSettings>(), It.IsAny<string>(), It.IsAny<IList<string>>()))
.Returns(returnTorrent);
Mocker.GetMock<IPorlaProxy>()
.Setup(s => s.AddTorrentFile(It.IsAny<PorlaSettings>(), It.IsAny<byte[]>(), It.IsAny<IList<string>>()))
.Returns(returnTorrent);
#nullable enable
Mocker.GetMock<IPorlaProxy>()
.Setup(s => s.AddMagnetTorrent(It.IsAny<PorlaSettings>(), It.IsAny<string>(), It.IsAny<IList<string>?>()))
.Returns(returnTorrent);
Mocker.GetMock<IPorlaProxy>()
.Setup(s => s.AddTorrentFile(It.IsAny<PorlaSettings>(), It.IsAny<byte[]>(), It.IsAny<IList<string>?>()))
.Returns(returnTorrent);
#nullable disable
}
/// <summary> Helper to Mock `ListTorrents` to retun a spesific torrent detail </summary>
protected virtual void GivenTorrents(ReadOnlyCollection<PorlaTorrentDetail> torrents)
{
Mocker.GetMock<IPorlaProxy>()
.Setup(s => s.ListTorrents(It.IsAny<PorlaSettings>(), It.IsAny<int>(), It.IsAny<int>()))
.Returns(torrents);
}
protected void PrepareClientToReturnQueuedItem()
{
GivenTorrents(new ReadOnlyCollection<PorlaTorrentDetail>(
new List<PorlaTorrentDetail> { _queued }));
}
protected void PrepareClientToReturnDownloadingItem()
{
GivenTorrents(new ReadOnlyCollection<PorlaTorrentDetail>(
new List<PorlaTorrentDetail> { _downloading }));
}
// protected void PrepareClientToReturnFailedItem()
// {
// GivenTorrents(new ReadOnlyCollection<PorlaTorrentDetail>(
// new List<PorlaTorrentDetail> { _failed }));
// }
// protected void PrepareClientToReturnCompletedItem()
// {
// GivenTorrents(new ReadOnlyCollection<PorlaTorrentDetail>(
// new List<PorlaTorrentDetail> { _completed }));
// }
protected void PrepareClientToReturnSeedingItem()
{
GivenTorrents(new ReadOnlyCollection<PorlaTorrentDetail>(
new List<PorlaTorrentDetail> { _seeding }));
}
protected void PrepareClientToReturnPausedItem()
{
GivenTorrents(new ReadOnlyCollection<PorlaTorrentDetail>(
new List<PorlaTorrentDetail> { _paused }));
}
// TODO: We don't know what a Queued one looks like yet...
// [Test]
// public void queued_item_should_have_required_properties()
// {
// PrepareClientToReturnQueuedItem();
// var item = Subject.GetItems().Single();
// VerifyQueued(item);
// }
[Test]
public void downloading_item_should_have_required_properties()
{
PrepareClientToReturnDownloadingItem();
var item = Subject.GetItems().Single();
VerifyDownloading(item);
}
// TODO: We don't have an example yet
// [Test]
// public void failed_item_should_have_required_properties()
// {
// PrepareClientToReturnFailedItem();
// var item = Subject.GetItems().Single();
// VerifyWarning(item);
// }
// NOTE: Looks like parent class requires Zero (0) for time left? Porla (LibTorrent) send -1 to indicate infinity (done)
// We are considering a "completed" torrent as one in seeding progress
[Test]
public void completed_seeding_download_should_have_required_properties()
{
PrepareClientToReturnSeedingItem();
var item = Subject.GetItems().Single();
VerifyCompleted(item);
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void paused_download_should_have_required_properties()
{
PrepareClientToReturnPausedItem();
var item = Subject.GetItems().Single();
VerifyPaused(item);
}
[Test]
public async Task download_should_return_unique_id()
{
SetupSuccessfullDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = await Subject.Download(remoteEpisode, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}
[Test]
public async Task download_should_return_unique_id_with_no_seriestags()
{
Subject.Definition.Settings.As<PorlaSettings>().SeriesTag = false;
SetupSuccessfullDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = await Subject.Download(remoteEpisode, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}
// presets test
private static readonly Dictionary<string, PorlaPreset> _emptyPresetsDict = new ();
protected void GivenSetupWithSettingsPreset(PorlaSettings ourSettings, Dictionary<string, PorlaPreset> theirPorlaPresets)
{
var roPresetsDict = new ReadOnlyDictionary<string, PorlaPreset>(theirPorlaPresets);
// mock settings to return ours instead of the default
Subject.Definition.Settings = ourSettings;
// mock proxy to return an empty presets
Mocker.GetMock<IPorlaProxy>()
.Setup(v => v.ListPresets(ourSettings)) // strict, our settings
.Returns(roPresetsDict);
}
[TestCase("localhost")]
[TestCase("127.0.0.1")]
public void should_have_correct_isLocalhost_is_true(string host)
{
// What we have setup
var ourSettings = new PorlaSettings
{
Host = host
};
GivenSetupWithSettingsPreset(ourSettings, _emptyPresetsDict);
var result = Subject.GetStatus();
result.IsLocalhost.Should().BeTrue();
}
[TestCase("not.localhost.com")]
[TestCase("1.4.20.68")]
public void should_have_correct_isLocalhost_is_false(string host)
{
// What we have setup
var ourSettings = new PorlaSettings
{
Host = host
};
GivenSetupWithSettingsPreset(ourSettings, _emptyPresetsDict);
var result = Subject.GetStatus();
result.IsLocalhost.Should().BeFalse();
}
[Test]
public void should_return_status_with_outputdirs_when_no_preset()
{
var someDir = "/tmp/other";
// What we have setup
var ourSettings = new PorlaSettings
{
TvDirectory = someDir,
};
GivenSetupWithSettingsPreset(ourSettings, _emptyPresetsDict);
var result = Subject.GetStatus();
result.OutputRootFolders.Should().NotBeNull();
result.OutputRootFolders.First().Should().Be(someDir);
}
[Test]
public void should_return_preset_outputdirs_via_default_preset_when_set_empty_tvdirectory()
{
var someDir = "/tmp/downloads";
// What porla returns
var presetWithSavePath = new PorlaPreset()
{
SavePath = someDir
};
var defaultPresetsDict = new Dictionary<string, PorlaPreset>
{
{ "default", presetWithSavePath }
};
// What we have setup
var ourSettings = new PorlaSettings
{
TvDirectory = "" // set blank to use the preset's values
};
GivenSetupWithSettingsPreset(ourSettings, defaultPresetsDict);
var result = Subject.GetStatus();
result.OutputRootFolders.Should().NotBeNull();
result.OutputRootFolders.First().Should().Be(someDir);
}
[Test]
public void should_return_status_with_alt_outputdirs_when_alt_preset_set()
{
var someDir = "/home/user/downloads";
var someOtherDir = "/data/downloads";
var presetWithSomeSavePath = new PorlaPreset()
{
SavePath = someDir
};
var presetWithSomeOtherSavePath = new PorlaPreset()
{
SavePath = someOtherDir
};
var comboPresetsDict = new Dictionary<string, PorlaPreset>
{
{ "default", presetWithSomeSavePath },
{ "alternative", presetWithSomeOtherSavePath }
};
// What we have setup
var ourSettings = new PorlaSettings
{
Preset = "alternative"
};
GivenSetupWithSettingsPreset(ourSettings, comboPresetsDict);
var result = Subject.GetStatus();
result.OutputRootFolders.Should().NotBeNull();
result.OutputRootFolders.First().Should().Be(someOtherDir);
}
[Test]
public void GetItems_should_ignore_torrents_with_a_different_category_even_if_porla_sends_us_some()
{
// TODO: should probs deep copy _downloading
var someDownloadingTorrent = new PorlaTorrentDetail
{
ActiveDuration = 10L,
AllTimeDownload = 100000000L,
AllTimeUpload = 0L,
Category = "some-other-category",
DownloadRate = 2000000,
Error = null,
ETA = 200L,
FinishedDuration = 0L,
Flags = new (DefaultTags),
InfoHash = new ("HASH", null),
LastDownload = 1L,
LastUpload = -1L,
ListPeers = 90,
ListSeeds = 6,
Metadata = null,
MovingStorage = false,
Name = _title,
NumPeers = 10,
NumSeeds = 8,
Progress = 0.5f,
QueuePosition = -0,
Ratio = 0.0d,
SavePath = "/tmp",
SeedingDuration = 0L,
Session = "default",
Size = 100000000L,
State = LibTorrentStatus.downloading,
Tags = new (SomeTags),
Total = 100000000L,
TotalDone = 150000000L,
UploadRate = 100000
};
var torrents = new PorlaTorrentDetail[] { someDownloadingTorrent };
var roTorrents = new ReadOnlyCollection<PorlaTorrentDetail>(torrents);
// should return nothing (we are making the request to porla that should filter for us) but let's say they do.
Mocker.GetMock<IPorlaProxy>()
.Setup(v => v.ListTorrents(It.IsAny<PorlaSettings>(), 0, int.MaxValue))
.Returns(roTorrents);
Subject.GetItems().Should().BeEmpty();
}
// We are not incompatible yet, when we are, you can uncomment this
// [Test]
// public void Test_should_return_validation_failure_for_old_Porla()
// {
// var systemInfo = new PorlaSysVersions()
// {
// Porla = new PorlaSysVersionsPorla()
// {
// Version = "0.37.0"
// }
// };
//
// Mocker.GetMock<IPorlaProxy>()
// .Setup(v => v.GetSysVersion(It.IsAny<PorlaSettings>()))
// .Returns(systemInfo);
//
// var result = Subject.Test();
//
// result.Errors.Count.Should().Be(1);
// }
}
}

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.LibTorrent.Models
{
// TODO: Figure out correct json serilization of `null` values, currently setting them to ""
public class LibTorrentInfoHash
{
#nullable enable
public string? Hash { get; set; }
public string? Status { get; set; }
public LibTorrentInfoHash(string? hash, string? status)
{
Hash = hash;
Status = status;
}
public LibTorrentInfoHash(IList<string> list)
{
if (list == null)
{
throw new ArgumentNullException(nameof(list));
}
Hash = list[0];
Status = list[1];
}
public ICollection<string> ToList()
{
var hash = string.IsNullOrEmpty(Hash) ? "" : Hash;
var status = string.IsNullOrEmpty(Status) ? "" : Status;
string[] ret = { hash, status };
return ret;
}
#nullable disable
}
public class LibTorrentInfoHashConverter : JsonConverter<LibTorrentInfoHash>
{
public override LibTorrentInfoHash ReadJson(
JsonReader reader,
Type objectType,
LibTorrentInfoHash existingValue,
bool hasExistingValue,
JsonSerializer serializer)
{
if (hasExistingValue)
{
return existingValue;
}
return new (serializer.Deserialize<List<string>>(reader));
}
public override void WriteJson(
JsonWriter writer,
LibTorrentInfoHash value,
JsonSerializer serializer)
{
serializer.Serialize(writer, value?.ToList() ?? null);
}
public override bool CanRead => true;
}
}

@ -0,0 +1,638 @@
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.LibTorrent.Models
{
/// <summary> Re-Implements LibTorrent <a href="https://libtorrent.org/reference-Settings.html#settings_pack">settings_pack</a></summary>
public class LibTorrentSettingsPack
{
[JsonProperty("active_checking", NullValueHandling = NullValueHandling.Ignore)]
public int active_checking { get; set; }
[JsonProperty(nameof(active_dht_limit), NullValueHandling = NullValueHandling.Ignore)]
public int active_dht_limit { get; set; }
[JsonProperty(nameof(active_downloads), NullValueHandling = NullValueHandling.Ignore)]
public int active_downloads { get; set; }
[JsonProperty(nameof(active_limit), NullValueHandling = NullValueHandling.Ignore)]
public int active_limit { get; set; }
[JsonProperty(nameof(active_lsd_limit), NullValueHandling = NullValueHandling.Ignore)]
public int active_lsd_limit { get; set; }
[JsonProperty(nameof(active_seeds), NullValueHandling = NullValueHandling.Ignore)]
public int active_seeds { get; set; }
[JsonProperty(nameof(active_tracker_limit), NullValueHandling = NullValueHandling.Ignore)]
public int active_tracker_limit { get; set; }
[JsonProperty(nameof(aio_threads), NullValueHandling = NullValueHandling.Ignore)]
public int aio_threads { get; set; }
[JsonProperty(nameof(alert_mask), NullValueHandling = NullValueHandling.Ignore)]
public int alert_mask { get; set; }
[JsonProperty(nameof(alert_queue_size), NullValueHandling = NullValueHandling.Ignore)]
public int alert_queue_size { get; set; }
[JsonProperty(nameof(allow_i2p_mixed), NullValueHandling = NullValueHandling.Ignore)]
public bool allow_i2p_mixed { get; set; }
[JsonProperty(nameof(allow_idna), NullValueHandling = NullValueHandling.Ignore)]
public bool allow_idna { get; set; }
[JsonProperty(nameof(allow_multiple_connections_per_ip), NullValueHandling = NullValueHandling.Ignore)]
public bool allow_multiple_connections_per_ip { get; set; }
[JsonProperty(nameof(allowed_enc_level), NullValueHandling = NullValueHandling.Ignore)]
public int allowed_enc_level { get; set; }
[JsonProperty(nameof(allowed_fast_set_size), NullValueHandling = NullValueHandling.Ignore)]
public int allowed_fast_set_size { get; set; }
[JsonProperty(nameof(always_send_user_agent), NullValueHandling = NullValueHandling.Ignore)]
public bool always_send_user_agent { get; set; }
[JsonProperty(nameof(announce_crypto_support), NullValueHandling = NullValueHandling.Ignore)]
public bool announce_crypto_support { get; set; }
[JsonProperty(nameof(announce_ip), NullValueHandling = NullValueHandling.Ignore)]
public string announce_ip { get; set; }
[JsonProperty(nameof(announce_to_all_tiers), NullValueHandling = NullValueHandling.Ignore)]
public bool announce_to_all_tiers { get; set; }
[JsonProperty(nameof(announce_to_all_trackers), NullValueHandling = NullValueHandling.Ignore)]
public bool announce_to_all_trackers { get; set; }
[JsonProperty(nameof(anonymous_mode), NullValueHandling = NullValueHandling.Ignore)]
public bool anonymous_mode { get; set; }
[JsonProperty(nameof(apply_ip_filter_to_trackers), NullValueHandling = NullValueHandling.Ignore)]
public bool apply_ip_filter_to_trackers { get; set; }
[JsonProperty(nameof(auto_manage_interval), NullValueHandling = NullValueHandling.Ignore)]
public int auto_manage_interval { get; set; }
[JsonProperty(nameof(auto_manage_prefer_seeds), NullValueHandling = NullValueHandling.Ignore)]
public bool auto_manage_prefer_seeds { get; set; }
[JsonProperty(nameof(auto_manage_startup), NullValueHandling = NullValueHandling.Ignore)]
public int auto_manage_startup { get; set; }
[JsonProperty(nameof(auto_scrape_interval), NullValueHandling = NullValueHandling.Ignore)]
public int auto_scrape_interval { get; set; }
[JsonProperty(nameof(auto_scrape_min_interval), NullValueHandling = NullValueHandling.Ignore)]
public int auto_scrape_min_interval { get; set; }
[JsonProperty(nameof(auto_sequential), NullValueHandling = NullValueHandling.Ignore)]
public bool auto_sequential { get; set; }
[JsonProperty(nameof(ban_web_seeds), NullValueHandling = NullValueHandling.Ignore)]
public bool ban_web_seeds { get; set; }
[JsonProperty(nameof(checking_mem_usage), NullValueHandling = NullValueHandling.Ignore)]
public int checking_mem_usage { get; set; }
[JsonProperty(nameof(choking_algorithm), NullValueHandling = NullValueHandling.Ignore)]
public int choking_algorithm { get; set; }
[JsonProperty(nameof(close_file_interval), NullValueHandling = NullValueHandling.Ignore)]
public int close_file_interval { get; set; }
[JsonProperty(nameof(close_redundant_connections), NullValueHandling = NullValueHandling.Ignore)]
public bool close_redundant_connections { get; set; }
[JsonProperty(nameof(connect_seed_every_n_download), NullValueHandling = NullValueHandling.Ignore)]
public int connect_seed_every_n_download { get; set; }
[JsonProperty(nameof(connection_speed), NullValueHandling = NullValueHandling.Ignore)]
public int connection_speed { get; set; }
[JsonProperty(nameof(connections_limit), NullValueHandling = NullValueHandling.Ignore)]
public int connections_limit { get; set; }
[JsonProperty(nameof(connections_slack), NullValueHandling = NullValueHandling.Ignore)]
public int connections_slack { get; set; }
[JsonProperty(nameof(dht_aggressive_lookups), NullValueHandling = NullValueHandling.Ignore)]
public bool dht_aggressive_lookups { get; set; }
[JsonProperty(nameof(dht_announce_interval), NullValueHandling = NullValueHandling.Ignore)]
public int dht_announce_interval { get; set; }
[JsonProperty(nameof(dht_block_ratelimit), NullValueHandling = NullValueHandling.Ignore)]
public int dht_block_ratelimit { get; set; }
[JsonProperty(nameof(dht_block_timeout), NullValueHandling = NullValueHandling.Ignore)]
public int dht_block_timeout { get; set; }
[JsonProperty(nameof(dht_bootstrap_nodes), NullValueHandling = NullValueHandling.Ignore)]
public string dht_bootstrap_nodes { get; set; }
[JsonProperty(nameof(dht_enforce_node_id), NullValueHandling = NullValueHandling.Ignore)]
public bool dht_enforce_node_id { get; set; }
[JsonProperty(nameof(dht_extended_routing_table), NullValueHandling = NullValueHandling.Ignore)]
public bool dht_extended_routing_table { get; set; }
[JsonProperty(nameof(dht_ignore_dark_internet), NullValueHandling = NullValueHandling.Ignore)]
public bool dht_ignore_dark_internet { get; set; }
[JsonProperty(nameof(dht_item_lifetime), NullValueHandling = NullValueHandling.Ignore)]
public int dht_item_lifetime { get; set; }
[JsonProperty(nameof(dht_max_dht_items), NullValueHandling = NullValueHandling.Ignore)]
public int dht_max_dht_items { get; set; }
[JsonProperty(nameof(dht_max_fail_count), NullValueHandling = NullValueHandling.Ignore)]
public int dht_max_fail_count { get; set; }
[JsonProperty(nameof(dht_max_infohashes_sample_count), NullValueHandling = NullValueHandling.Ignore)]
public int dht_max_infohashes_sample_count { get; set; }
[JsonProperty(nameof(dht_max_peers), NullValueHandling = NullValueHandling.Ignore)]
public int dht_max_peers { get; set; }
[JsonProperty(nameof(dht_max_peers_reply), NullValueHandling = NullValueHandling.Ignore)]
public int dht_max_peers_reply { get; set; }
[JsonProperty(nameof(dht_max_torrent_search_reply), NullValueHandling = NullValueHandling.Ignore)]
public int dht_max_torrent_search_reply { get; set; }
[JsonProperty(nameof(dht_max_torrents), NullValueHandling = NullValueHandling.Ignore)]
public int dht_max_torrents { get; set; }
[JsonProperty(nameof(dht_prefer_verified_node_ids), NullValueHandling = NullValueHandling.Ignore)]
public bool dht_prefer_verified_node_ids { get; set; }
[JsonProperty(nameof(dht_privacy_lookups), NullValueHandling = NullValueHandling.Ignore)]
public bool dht_privacy_lookups { get; set; }
[JsonProperty(nameof(dht_read_only), NullValueHandling = NullValueHandling.Ignore)]
public bool dht_read_only { get; set; }
[JsonProperty(nameof(dht_restrict_routing_ips), NullValueHandling = NullValueHandling.Ignore)]
public bool dht_restrict_routing_ips { get; set; }
[JsonProperty(nameof(dht_restrict_search_ips), NullValueHandling = NullValueHandling.Ignore)]
public bool dht_restrict_search_ips { get; set; }
[JsonProperty(nameof(dht_sample_infohashes_interval), NullValueHandling = NullValueHandling.Ignore)]
public int dht_sample_infohashes_interval { get; set; }
[JsonProperty(nameof(dht_search_branching), NullValueHandling = NullValueHandling.Ignore)]
public int dht_search_branching { get; set; }
[JsonProperty(nameof(dht_upload_rate_limit), NullValueHandling = NullValueHandling.Ignore)]
public int dht_upload_rate_limit { get; set; }
[JsonProperty(nameof(disable_hash_checks), NullValueHandling = NullValueHandling.Ignore)]
public bool disable_hash_checks { get; set; }
[JsonProperty(nameof(disk_io_read_mode), NullValueHandling = NullValueHandling.Ignore)]
public int disk_io_read_mode { get; set; }
[JsonProperty(nameof(disk_io_write_mode), NullValueHandling = NullValueHandling.Ignore)]
public int disk_io_write_mode { get; set; }
[JsonProperty(nameof(disk_write_mode), NullValueHandling = NullValueHandling.Ignore)]
public int disk_write_mode { get; set; }
[JsonProperty(nameof(dont_count_slow_torrents), NullValueHandling = NullValueHandling.Ignore)]
public bool dont_count_slow_torrents { get; set; }
[JsonProperty(nameof(download_rate_limit), NullValueHandling = NullValueHandling.Ignore)]
public int download_rate_limit { get; set; }
[JsonProperty(nameof(enable_dht), NullValueHandling = NullValueHandling.Ignore)]
public bool enable_dht { get; set; }
[JsonProperty(nameof(enable_incoming_tcp), NullValueHandling = NullValueHandling.Ignore)]
public bool enable_incoming_tcp { get; set; }
[JsonProperty(nameof(enable_incoming_utp), NullValueHandling = NullValueHandling.Ignore)]
public bool enable_incoming_utp { get; set; }
[JsonProperty(nameof(enable_ip_notifier), NullValueHandling = NullValueHandling.Ignore)]
public bool enable_ip_notifier { get; set; }
[JsonProperty(nameof(enable_lsd), NullValueHandling = NullValueHandling.Ignore)]
public bool enable_lsd { get; set; }
[JsonProperty(nameof(enable_natpmp), NullValueHandling = NullValueHandling.Ignore)]
public bool enable_natpmp { get; set; }
[JsonProperty(nameof(enable_outgoing_tcp), NullValueHandling = NullValueHandling.Ignore)]
public bool enable_outgoing_tcp { get; set; }
[JsonProperty(nameof(enable_outgoing_utp), NullValueHandling = NullValueHandling.Ignore)]
public bool enable_outgoing_utp { get; set; }
[JsonProperty(nameof(enable_set_file_valid_data), NullValueHandling = NullValueHandling.Ignore)]
public bool enable_set_file_valid_data { get; set; }
[JsonProperty(nameof(enable_upnp), NullValueHandling = NullValueHandling.Ignore)]
public bool enable_upnp { get; set; }
[JsonProperty(nameof(file_pool_size), NullValueHandling = NullValueHandling.Ignore)]
public int file_pool_size { get; set; }
[JsonProperty(nameof(handshake_client_version), NullValueHandling = NullValueHandling.Ignore)]
public string handshake_client_version { get; set; }
[JsonProperty(nameof(handshake_timeout), NullValueHandling = NullValueHandling.Ignore)]
public int handshake_timeout { get; set; }
[JsonProperty(nameof(hashing_threads), NullValueHandling = NullValueHandling.Ignore)]
public int hashing_threads { get; set; }
[JsonProperty(nameof(i2p_hostname), NullValueHandling = NullValueHandling.Ignore)]
public string i2p_hostname { get; set; }
[JsonProperty(nameof(i2p_inbound_length), NullValueHandling = NullValueHandling.Ignore)]
public int i2p_inbound_length { get; set; }
[JsonProperty(nameof(i2p_inbound_quantity), NullValueHandling = NullValueHandling.Ignore)]
public int i2p_inbound_quantity { get; set; }
[JsonProperty(nameof(i2p_outbound_length), NullValueHandling = NullValueHandling.Ignore)]
public int i2p_outbound_length { get; set; }
[JsonProperty(nameof(i2p_outbound_quantity), NullValueHandling = NullValueHandling.Ignore)]
public int i2p_outbound_quantity { get; set; }
[JsonProperty(nameof(i2p_port), NullValueHandling = NullValueHandling.Ignore)]
public int i2p_port { get; set; }
[JsonProperty(nameof(in_enc_policy), NullValueHandling = NullValueHandling.Ignore)]
public int in_enc_policy { get; set; }
[JsonProperty(nameof(inactive_down_rate), NullValueHandling = NullValueHandling.Ignore)]
public int inactive_down_rate { get; set; }
[JsonProperty(nameof(inactive_up_rate), NullValueHandling = NullValueHandling.Ignore)]
public int inactive_up_rate { get; set; }
[JsonProperty(nameof(inactivity_timeout), NullValueHandling = NullValueHandling.Ignore)]
public int inactivity_timeout { get; set; }
[JsonProperty(nameof(incoming_starts_queued_torrents), NullValueHandling = NullValueHandling.Ignore)]
public bool incoming_starts_queued_torrents { get; set; }
[JsonProperty(nameof(initial_picker_threshold), NullValueHandling = NullValueHandling.Ignore)]
public int initial_picker_threshold { get; set; }
[JsonProperty(nameof(listen_interfaces), NullValueHandling = NullValueHandling.Ignore)]
public string listen_interfaces { get; set; }
[JsonProperty(nameof(listen_queue_size), NullValueHandling = NullValueHandling.Ignore)]
public int listen_queue_size { get; set; }
[JsonProperty(nameof(listen_system_port_fallback), NullValueHandling = NullValueHandling.Ignore)]
public bool listen_system_port_fallback { get; set; }
[JsonProperty(nameof(local_service_announce_interval), NullValueHandling = NullValueHandling.Ignore)]
public int local_service_announce_interval { get; set; }
[JsonProperty(nameof(max_allowed_in_request_queue), NullValueHandling = NullValueHandling.Ignore)]
public int max_allowed_in_request_queue { get; set; }
[JsonProperty(nameof(max_concurrent_http_announces), NullValueHandling = NullValueHandling.Ignore)]
public int max_concurrent_http_announces { get; set; }
[JsonProperty(nameof(max_failcount), NullValueHandling = NullValueHandling.Ignore)]
public int max_failcount { get; set; }
[JsonProperty(nameof(max_http_recv_buffer_size), NullValueHandling = NullValueHandling.Ignore)]
public int max_http_recv_buffer_size { get; set; }
[JsonProperty(nameof(max_metadata_size), NullValueHandling = NullValueHandling.Ignore)]
public int max_metadata_size { get; set; }
[JsonProperty(nameof(max_out_request_queue), NullValueHandling = NullValueHandling.Ignore)]
public int max_out_request_queue { get; set; }
[JsonProperty(nameof(max_paused_peerlist_size), NullValueHandling = NullValueHandling.Ignore)]
public int max_paused_peerlist_size { get; set; }
[JsonProperty(nameof(max_peer_recv_buffer_size), NullValueHandling = NullValueHandling.Ignore)]
public int max_peer_recv_buffer_size { get; set; }
[JsonProperty(nameof(max_peerlist_size), NullValueHandling = NullValueHandling.Ignore)]
public int max_peerlist_size { get; set; }
[JsonProperty(nameof(max_pex_peers), NullValueHandling = NullValueHandling.Ignore)]
public int max_pex_peers { get; set; }
[JsonProperty(nameof(max_piece_count), NullValueHandling = NullValueHandling.Ignore)]
public int max_piece_count { get; set; }
[JsonProperty(nameof(max_queued_disk_bytes), NullValueHandling = NullValueHandling.Ignore)]
public int max_queued_disk_bytes { get; set; }
[JsonProperty(nameof(max_rejects), NullValueHandling = NullValueHandling.Ignore)]
public int max_rejects { get; set; }
[JsonProperty(nameof(max_retry_port_bind), NullValueHandling = NullValueHandling.Ignore)]
public int max_retry_port_bind { get; set; }
[JsonProperty(nameof(max_suggest_pieces), NullValueHandling = NullValueHandling.Ignore)]
public int max_suggest_pieces { get; set; }
[JsonProperty(nameof(max_web_seed_connections), NullValueHandling = NullValueHandling.Ignore)]
public int max_web_seed_connections { get; set; }
[JsonProperty(nameof(metadata_token_limit), NullValueHandling = NullValueHandling.Ignore)]
public int metadata_token_limit { get; set; }
[JsonProperty(nameof(min_announce_interval), NullValueHandling = NullValueHandling.Ignore)]
public int min_announce_interval { get; set; }
[JsonProperty(nameof(min_reconnect_time), NullValueHandling = NullValueHandling.Ignore)]
public int min_reconnect_time { get; set; }
[JsonProperty(nameof(mixed_mode_algorithm), NullValueHandling = NullValueHandling.Ignore)]
public int mixed_mode_algorithm { get; set; }
[JsonProperty(nameof(mmap_file_size_cutoff), NullValueHandling = NullValueHandling.Ignore)]
public int mmap_file_size_cutoff { get; set; }
[JsonProperty(nameof(no_atime_storage), NullValueHandling = NullValueHandling.Ignore)]
public bool no_atime_storage { get; set; }
[JsonProperty(nameof(no_connect_privileged_ports), NullValueHandling = NullValueHandling.Ignore)]
public bool no_connect_privileged_ports { get; set; }
[JsonProperty(nameof(no_recheck_incomplete_resume), NullValueHandling = NullValueHandling.Ignore)]
public bool no_recheck_incomplete_resume { get; set; }
[JsonProperty(nameof(num_optimistic_unchoke_slots), NullValueHandling = NullValueHandling.Ignore)]
public int num_optimistic_unchoke_slots { get; set; }
[JsonProperty(nameof(num_outgoing_ports), NullValueHandling = NullValueHandling.Ignore)]
public int num_outgoing_ports { get; set; }
[JsonProperty(nameof(num_want), NullValueHandling = NullValueHandling.Ignore)]
public int num_want { get; set; }
[JsonProperty(nameof(optimistic_disk_retry), NullValueHandling = NullValueHandling.Ignore)]
public int optimistic_disk_retry { get; set; }
[JsonProperty(nameof(optimistic_unchoke_interval), NullValueHandling = NullValueHandling.Ignore)]
public int optimistic_unchoke_interval { get; set; }
[JsonProperty(nameof(out_enc_policy), NullValueHandling = NullValueHandling.Ignore)]
public int out_enc_policy { get; set; }
[JsonProperty(nameof(outgoing_interfaces), NullValueHandling = NullValueHandling.Ignore)]
public string outgoing_interfaces { get; set; }
[JsonProperty(nameof(outgoing_port), NullValueHandling = NullValueHandling.Ignore)]
public int outgoing_port { get; set; }
[JsonProperty(nameof(peer_connect_timeout), NullValueHandling = NullValueHandling.Ignore)]
public int peer_connect_timeout { get; set; }
[JsonProperty(nameof(peer_dscp), NullValueHandling = NullValueHandling.Ignore)]
public int peer_dscp { get; set; }
[JsonProperty(nameof(peer_fingerprint), NullValueHandling = NullValueHandling.Ignore)]
public string peer_fingerprint { get; set; }
[JsonProperty(nameof(peer_timeout), NullValueHandling = NullValueHandling.Ignore)]
public int peer_timeout { get; set; }
[JsonProperty(nameof(peer_turnover), NullValueHandling = NullValueHandling.Ignore)]
public int peer_turnover { get; set; }
[JsonProperty(nameof(peer_turnover_cutoff), NullValueHandling = NullValueHandling.Ignore)]
public int peer_turnover_cutoff { get; set; }
[JsonProperty(nameof(peer_turnover_interval), NullValueHandling = NullValueHandling.Ignore)]
public int peer_turnover_interval { get; set; }
[JsonProperty(nameof(piece_extent_affinity), NullValueHandling = NullValueHandling.Ignore)]
public bool piece_extent_affinity { get; set; }
[JsonProperty(nameof(piece_timeout), NullValueHandling = NullValueHandling.Ignore)]
public int piece_timeout { get; set; }
[JsonProperty(nameof(predictive_piece_announce), NullValueHandling = NullValueHandling.Ignore)]
public int predictive_piece_announce { get; set; }
[JsonProperty(nameof(prefer_rc4), NullValueHandling = NullValueHandling.Ignore)]
public bool prefer_rc4 { get; set; }
[JsonProperty(nameof(prefer_udp_trackers), NullValueHandling = NullValueHandling.Ignore)]
public bool prefer_udp_trackers { get; set; }
[JsonProperty(nameof(prioritize_partial_pieces), NullValueHandling = NullValueHandling.Ignore)]
public bool prioritize_partial_pieces { get; set; }
[JsonProperty(nameof(proxy_hostname), NullValueHandling = NullValueHandling.Ignore)]
public string proxy_hostname { get; set; }
[JsonProperty(nameof(proxy_hostnames), NullValueHandling = NullValueHandling.Ignore)]
public bool proxy_hostnames { get; set; }
[JsonProperty(nameof(proxy_password), NullValueHandling = NullValueHandling.Ignore)]
public string proxy_password { get; set; }
[JsonProperty(nameof(proxy_peer_connections), NullValueHandling = NullValueHandling.Ignore)]
public bool proxy_peer_connections { get; set; }
[JsonProperty(nameof(proxy_port), NullValueHandling = NullValueHandling.Ignore)]
public int proxy_port { get; set; }
[JsonProperty(nameof(proxy_tracker_connections), NullValueHandling = NullValueHandling.Ignore)]
public bool proxy_tracker_connections { get; set; }
[JsonProperty(nameof(proxy_type), NullValueHandling = NullValueHandling.Ignore)]
public int proxy_type { get; set; }
[JsonProperty(nameof(proxy_username), NullValueHandling = NullValueHandling.Ignore)]
public string proxy_username { get; set; }
[JsonProperty(nameof(rate_choker_initial_threshold), NullValueHandling = NullValueHandling.Ignore)]
public int rate_choker_initial_threshold { get; set; }
[JsonProperty(nameof(rate_limit_ip_overhead), NullValueHandling = NullValueHandling.Ignore)]
public bool rate_limit_ip_overhead { get; set; }
[JsonProperty(nameof(recv_socket_buffer_size), NullValueHandling = NullValueHandling.Ignore)]
public int recv_socket_buffer_size { get; set; }
[JsonProperty(nameof(report_redundant_bytes), NullValueHandling = NullValueHandling.Ignore)]
public bool report_redundant_bytes { get; set; }
[JsonProperty(nameof(report_true_downloaded), NullValueHandling = NullValueHandling.Ignore)]
public bool report_true_downloaded { get; set; }
[JsonProperty(nameof(report_web_seed_downloads), NullValueHandling = NullValueHandling.Ignore)]
public bool report_web_seed_downloads { get; set; }
[JsonProperty(nameof(request_queue_time), NullValueHandling = NullValueHandling.Ignore)]
public int request_queue_time { get; set; }
[JsonProperty(nameof(request_timeout), NullValueHandling = NullValueHandling.Ignore)]
public int request_timeout { get; set; }
[JsonProperty(nameof(resolver_cache_timeout), NullValueHandling = NullValueHandling.Ignore)]
public int resolver_cache_timeout { get; set; }
[JsonProperty(nameof(seed_choking_algorithm), NullValueHandling = NullValueHandling.Ignore)]
public int seed_choking_algorithm { get; set; }
[JsonProperty(nameof(seed_time_limit), NullValueHandling = NullValueHandling.Ignore)]
public int seed_time_limit { get; set; }
[JsonProperty(nameof(seed_time_ratio_limit), NullValueHandling = NullValueHandling.Ignore)]
public int seed_time_ratio_limit { get; set; }
[JsonProperty(nameof(seeding_outgoing_connections), NullValueHandling = NullValueHandling.Ignore)]
public bool seeding_outgoing_connections { get; set; }
[JsonProperty(nameof(seeding_piece_quota), NullValueHandling = NullValueHandling.Ignore)]
public int seeding_piece_quota { get; set; }
[JsonProperty(nameof(send_buffer_low_watermark), NullValueHandling = NullValueHandling.Ignore)]
public int send_buffer_low_watermark { get; set; }
[JsonProperty(nameof(send_buffer_watermark), NullValueHandling = NullValueHandling.Ignore)]
public int send_buffer_watermark { get; set; }
[JsonProperty(nameof(send_buffer_watermark_factor), NullValueHandling = NullValueHandling.Ignore)]
public int send_buffer_watermark_factor { get; set; }
[JsonProperty(nameof(send_not_sent_low_watermark), NullValueHandling = NullValueHandling.Ignore)]
public int send_not_sent_low_watermark { get; set; }
[JsonProperty(nameof(send_redundant_have), NullValueHandling = NullValueHandling.Ignore)]
public bool send_redundant_have { get; set; }
[JsonProperty(nameof(send_socket_buffer_size), NullValueHandling = NullValueHandling.Ignore)]
public int send_socket_buffer_size { get; set; }
[JsonProperty(nameof(share_mode_target), NullValueHandling = NullValueHandling.Ignore)]
public int share_mode_target { get; set; }
[JsonProperty(nameof(share_ratio_limit), NullValueHandling = NullValueHandling.Ignore)]
public int share_ratio_limit { get; set; }
[JsonProperty(nameof(smooth_connects), NullValueHandling = NullValueHandling.Ignore)]
public bool smooth_connects { get; set; }
[JsonProperty(nameof(socks5_udp_send_local_ep), NullValueHandling = NullValueHandling.Ignore)]
public bool socks5_udp_send_local_ep { get; set; }
[JsonProperty(nameof(ssrf_mitigation), NullValueHandling = NullValueHandling.Ignore)]
public bool ssrf_mitigation { get; set; }
[JsonProperty(nameof(stop_tracker_timeout), NullValueHandling = NullValueHandling.Ignore)]
public int stop_tracker_timeout { get; set; }
[JsonProperty(nameof(strict_end_game_mode), NullValueHandling = NullValueHandling.Ignore)]
public bool strict_end_game_mode { get; set; }
[JsonProperty(nameof(suggest_mode), NullValueHandling = NullValueHandling.Ignore)]
public int suggest_mode { get; set; }
[JsonProperty(nameof(support_share_mode), NullValueHandling = NullValueHandling.Ignore)]
public bool support_share_mode { get; set; }
[JsonProperty(nameof(tick_interval), NullValueHandling = NullValueHandling.Ignore)]
public int tick_interval { get; set; }
[JsonProperty(nameof(torrent_connect_boost), NullValueHandling = NullValueHandling.Ignore)]
public int torrent_connect_boost { get; set; }
[JsonProperty(nameof(tracker_backoff), NullValueHandling = NullValueHandling.Ignore)]
public int tracker_backoff { get; set; }
[JsonProperty(nameof(tracker_completion_timeout), NullValueHandling = NullValueHandling.Ignore)]
public int tracker_completion_timeout { get; set; }
[JsonProperty(nameof(tracker_maximum_response_length), NullValueHandling = NullValueHandling.Ignore)]
public int tracker_maximum_response_length { get; set; }
[JsonProperty(nameof(tracker_receive_timeout), NullValueHandling = NullValueHandling.Ignore)]
public int tracker_receive_timeout { get; set; }
[JsonProperty(nameof(udp_tracker_token_expiry), NullValueHandling = NullValueHandling.Ignore)]
public int udp_tracker_token_expiry { get; set; }
[JsonProperty(nameof(unchoke_interval), NullValueHandling = NullValueHandling.Ignore)]
public int unchoke_interval { get; set; }
[JsonProperty(nameof(unchoke_slots_limit), NullValueHandling = NullValueHandling.Ignore)]
public int unchoke_slots_limit { get; set; }
[JsonProperty(nameof(upload_rate_limit), NullValueHandling = NullValueHandling.Ignore)]
public int upload_rate_limit { get; set; }
[JsonProperty(nameof(upnp_ignore_nonrouters), NullValueHandling = NullValueHandling.Ignore)]
public bool upnp_ignore_nonrouters { get; set; }
[JsonProperty(nameof(upnp_lease_duration), NullValueHandling = NullValueHandling.Ignore)]
public int upnp_lease_duration { get; set; }
[JsonProperty(nameof(urlseed_max_request_bytes), NullValueHandling = NullValueHandling.Ignore)]
public int urlseed_max_request_bytes { get; set; }
[JsonProperty(nameof(urlseed_pipeline_size), NullValueHandling = NullValueHandling.Ignore)]
public int urlseed_pipeline_size { get; set; }
[JsonProperty(nameof(urlseed_timeout), NullValueHandling = NullValueHandling.Ignore)]
public int urlseed_timeout { get; set; }
[JsonProperty(nameof(urlseed_wait_retry), NullValueHandling = NullValueHandling.Ignore)]
public int urlseed_wait_retry { get; set; }
[JsonProperty(nameof(use_dht_as_fallback), NullValueHandling = NullValueHandling.Ignore)]
public bool use_dht_as_fallback { get; set; }
[JsonProperty(nameof(use_parole_mode), NullValueHandling = NullValueHandling.Ignore)]
public bool use_parole_mode { get; set; }
[JsonProperty(nameof(user_agent), NullValueHandling = NullValueHandling.Ignore)]
public string user_agent { get; set; }
[JsonProperty(nameof(utp_connect_timeout), NullValueHandling = NullValueHandling.Ignore)]
public int utp_connect_timeout { get; set; }
[JsonProperty(nameof(utp_cwnd_reduce_timer), NullValueHandling = NullValueHandling.Ignore)]
public int utp_cwnd_reduce_timer { get; set; }
[JsonProperty(nameof(utp_fin_resends), NullValueHandling = NullValueHandling.Ignore)]
public int utp_fin_resends { get; set; }
[JsonProperty(nameof(utp_gain_factor), NullValueHandling = NullValueHandling.Ignore)]
public int utp_gain_factor { get; set; }
[JsonProperty(nameof(utp_loss_multiplier), NullValueHandling = NullValueHandling.Ignore)]
public int utp_loss_multiplier { get; set; }
[JsonProperty(nameof(utp_min_timeout), NullValueHandling = NullValueHandling.Ignore)]
public int utp_min_timeout { get; set; }
[JsonProperty(nameof(utp_num_resends), NullValueHandling = NullValueHandling.Ignore)]
public int utp_num_resends { get; set; }
[JsonProperty(nameof(utp_syn_resends), NullValueHandling = NullValueHandling.Ignore)]
public int utp_syn_resends { get; set; }
[JsonProperty(nameof(utp_target_delay), NullValueHandling = NullValueHandling.Ignore)]
public int utp_target_delay { get; set; }
[JsonProperty(nameof(validate_https_trackers), NullValueHandling = NullValueHandling.Ignore)]
public bool validate_https_trackers { get; set; }
[JsonProperty(nameof(web_seed_name_lookup_retry), NullValueHandling = NullValueHandling.Ignore)]
public int web_seed_name_lookup_retry { get; set; }
[JsonProperty(nameof(whole_pieces_threshold), NullValueHandling = NullValueHandling.Ignore)]
public int whole_pieces_threshold { get; set; }
}
}

@ -0,0 +1,27 @@
namespace NzbDrone.Core.Download.Clients.LibTorrent.Models
{
/// <summary> Re-Implements LibTorrent <a href="https://libtorrent.org/reference-Torrent_Status.html#state_t">state_t</a> </summary>
public enum LibTorrentStatus
{
/// <summary> The torrent has not started its download yet, and is currently checking existing files. </summary>
checking_files = 1,
/// <summary> The torrent is trying to download metadata from peers. This implies the ut_metadata extension is in use. </summary>
downloading_metadata = 2,
/// <summary> The torrent is being downloaded. This is the state most torrents will be in most of the time. The progress meter will tell how much of the files that has been downloaded. </summary>
downloading = 3,
/// <summary> In this state the torrent has finished downloading but still doesn't have the entire torrent. i.e. some pieces are filtered and won't get downloaded. </summary>
finished = 4,
/// <summary> In this state the torrent has finished downloading and is a pure seeder. </summary>
seeding = 5,
/// <summary> If the torrent was started in full allocation mode, this indicates that the (disk) storage for the torrent is allocated. </summary>
unused_enum_for_backwards_compatibility_allocating = 6,
/// <summary> The torrent is currently checking the fast resume data and comparing it to the files on disk. This is typically completed in a fraction of a second, but if you add a large number of torrents at once, they will queue up. </summary>
checking_resume_data = 7
}
}

@ -0,0 +1,17 @@
# LibTorrent
The Be-All-End-All. The referance Implementation for the BitTorrent Protocol.
# Models
Some Other clients (notably Porla) pass through the signatures for LibTorrent types, usefull to consolidate them in one place.
## Parameter Naming Style
Try to keep the names as close to the base implementation as posible.
> i.e. `checking_files` **-/>** `CheckingFiles`
> Use: `checking_files` instead
# Implementation
_maybe one day..._

@ -0,0 +1,86 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.Porla.Models
{
/// <summary> Porla Session Extention Methods </summary>
public static class PorlaPresetsExtentions
{
/// <summary> Gets the spesified preset values merged with the values from the default preset </summary>
public static PorlaPreset GetEffective(this ReadOnlyDictionary<string, PorlaPreset> presets, string preset)
{
if (presets == null)
{
// presets is null
return new PorlaPreset();
}
var defaultExist = presets.ContainsKey("default");
var presetExist = presets.ContainsKey(preset ?? "");
var defaultPreset = presets.GetValueOrDefault("default");
var activePreset = presets.GetValueOrDefault(preset ?? "");
if (defaultExist && presetExist)
{
// TODO: There has to be a better way to merge these
return new PorlaPreset()
{
Category = activePreset.Category ?? defaultPreset.Category,
MaxConnections = activePreset.MaxConnections ?? defaultPreset.MaxConnections,
MaxUploads = activePreset.MaxUploads ?? defaultPreset.MaxUploads,
SavePath = activePreset.SavePath ?? defaultPreset.SavePath,
Session = activePreset.Session ?? defaultPreset.Session,
Tags = activePreset.Tags ?? defaultPreset.Tags,
UploadLimit = activePreset.UploadLimit ?? defaultPreset.UploadLimit
};
}
// default doesn't exist
if (presetExist)
{
return activePreset;
}
// active doesn't exist
if (defaultExist)
{
return defaultPreset;
}
// neither exists.
return new PorlaPreset();
}
}
/// <summary> Implementation of the list presets response data type from <a href="https://github.com/porla/porla/blob/v0.37.0/src/lua/packages/presets.cpp">presets.cpp</a></summary>
public sealed class ResponsePorlaPresetsList
{
[JsonProperty("presets", NullValueHandling = NullValueHandling.Ignore)]
public ReadOnlyDictionary<string, PorlaPreset> Presets { get; set; }
}
/// <summary> Implementation of the <em>preset</em> data type in the response data from <a href="https://github.com/porla/porla/blob/v0.37.0/src/lua/packages/presets.cpp">presets.cpp</a></summary>
public sealed class PorlaPreset : object
{
[JsonProperty("category", NullValueHandling = NullValueHandling.Ignore)]
public string Category { get; set; }
[JsonProperty("max_connections", NullValueHandling = NullValueHandling.Ignore)]
public int? MaxConnections { get; set; }
[JsonProperty("max_uploads", NullValueHandling = NullValueHandling.Ignore)]
public int? MaxUploads { get; set; }
[JsonProperty("save_path", NullValueHandling = NullValueHandling.Ignore)]
public string SavePath { get; set; }
[JsonProperty("session", NullValueHandling = NullValueHandling.Ignore)]
public string Session { get; set; }
[JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)]
public ReadOnlyCollection<string> Tags { get; set; }
[JsonProperty("upload_limit", NullValueHandling = NullValueHandling.Ignore)]
public int? UploadLimit { get; set; }
}
}

@ -0,0 +1,31 @@
using System.Collections.ObjectModel;
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.Porla.Models
{
/// <summary> Implementation of the <em>session</em> field in the response from <a href="https://github.com/porla/porla/blob/v0.37.0/src/methods/sessions/sessionslist.cpp">sessionslist.cpp</a> data type </summary>
public sealed class ResponsePorlaSessionList
{
[JsonProperty("sessions", NullValueHandling = NullValueHandling.Ignore)]
public ReadOnlyCollection<PorlaSession> Sessions { get; set; }
}
/// <summary> Implementation of the session data type from the response <a href="https://github.com/porla/porla/blob/v0.37.0/src/methods/sessions/sessionslist.cpp">sessionslist.cpp</a></summary>
public class PorlaSession
{
[JsonProperty("is_dht_running", NullValueHandling = NullValueHandling.Ignore)]
public bool IsDHTRunning { get; set; }
[JsonProperty("is_listening", NullValueHandling = NullValueHandling.Ignore)]
public bool IsListening { get; set; }
[JsonProperty("is_paused", NullValueHandling = NullValueHandling.Ignore)]
public bool IsPaused { get; set; }
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { get; set; }
[JsonProperty("torrents_total", NullValueHandling = NullValueHandling.Ignore)]
public int TorrentsTotal { get; set; }
}
}

@ -0,0 +1,18 @@
using Newtonsoft.Json;
using NzbDrone.Core.Download.Clients.LibTorrent.Models;
namespace NzbDrone.Core.Download.Clients.Porla.Models
{
/// <summary> Implements the ListSessionsSettings response from <a href="https://github.com/porla/porla/blob/v0.37.0/src/methods/sessions/sessionssettingslist_reqres.hpp">sessionssettingslist_reqres.hpp</a> </summary>
public sealed class ResponsePorlaSessionSettingsList
{
[JsonProperty("settings", NullValueHandling = NullValueHandling.Ignore)]
public PorlaSessionSettings Settings { get; set; }
}
/// <summary> Wraps the LibTorrentSettingsPack type </summary>
/// <see cref="LibTorrentSettingsPack"/>
public sealed class PorlaSessionSettings : LibTorrentSettingsPack
{
}
}

@ -0,0 +1,43 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.Porla.Models
{
/// <summary> The data type for the <em>porla</em> field in the <em>sys.versions</em> response </summary>
public class PorlaSysVersionsPorla
{
[JsonProperty("branch", NullValueHandling = NullValueHandling.Ignore)]
public string Branch { get; set; }
[JsonProperty("commitish", NullValueHandling = NullValueHandling.Ignore)]
public string Commitish { get; set; }
[JsonProperty("version", NullValueHandling = NullValueHandling.Ignore)]
public string Version { get; set; }
}
/// <summary> The response for the <em>sys.versions</em> call to porla </summary>
public class PorlaSysVersions
{
[JsonProperty("boost", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, string> Boost { get; set; }
[JsonProperty("libtorrent", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, string> LibTorrent { get; set; }
[JsonProperty("nlohmann_json", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, string> NlohmannJson { get; set; }
[JsonProperty("openssl", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, string> OpenSSL { get; set; }
[JsonProperty("porla", NullValueHandling = NullValueHandling.Ignore)]
public PorlaSysVersionsPorla Porla { get; set; }
[JsonProperty("sqlite", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, string> Sqlite { get; set; }
[JsonProperty("tomlplusplus", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, string> TOMLPlusPlus { get; set; }
}
}

@ -0,0 +1,36 @@
using Newtonsoft.Json;
using NzbDrone.Core.Download.Clients.LibTorrent.Models;
namespace NzbDrone.Core.Download.Clients.Porla.Models
{
/// <summary> Wraps the LibTorrent Infohash type into a Porla Type for easier handling </summary>
public sealed class PorlaTorrent
{
[JsonProperty("info_hash", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(LibTorrentInfoHashConverter))]
public LibTorrentInfoHash InfoHash { get; set; }
[JsonConstructor]
public PorlaTorrent(string hash, string status)
{
InfoHash = new LibTorrentInfoHash(hash, status);
}
public PorlaTorrent(LibTorrentInfoHash ltif)
{
InfoHash = ltif;
}
public object[] AsParam()
{
string[] ret = { InfoHash.Hash, null };
return ret;
}
public object[] AsParams()
{
object[] ret = { "info_hash", AsParam() };
return ret;
}
}
}

@ -0,0 +1,156 @@
using System.Collections.ObjectModel;
using Newtonsoft.Json;
using NzbDrone.Core.Download.Clients.LibTorrent.Models;
namespace NzbDrone.Core.Download.Clients.Porla.Models
{
/// <summary> Implementation of the <em>Torrents</em> field type in the response data from <a href="https://github.com/porla/porla/blob/v0.37.0/src/methods/torrentslist_reqres.hpp">torrentslist_reqres.hpp</a></summary>
public sealed class PorlaTorrentDetail
{
/// <summary> cumulative counter in the active state means not paused and added to session </summary>
[JsonProperty("active_duration", NullValueHandling = NullValueHandling.Ignore)]
public long ActiveDuration { get; set; }
/// <summary> are accumulated download payload byte counters. They are saved in and restored from resume data to keep totals across sessions. </summary>
[JsonProperty("all_time_download", NullValueHandling = NullValueHandling.Ignore)]
public long AllTimeDownload { get; set; }
/// <summary> are accumulated upload payload byte counters. They are saved in and restored from resume data to keep totals across sessions. </summary>
[JsonProperty("all_time_upload", NullValueHandling = NullValueHandling.Ignore)]
public long AllTimeUpload { get; set; }
[JsonProperty("category", NullValueHandling = NullValueHandling.Ignore)]
public string Category { get; set; }
/// <summary> the total rates for all peers for this torrent. These will usually have better precision than summing the rates from all peers. The rates are given as the number of bytes per second. </summary>
[JsonProperty("download_rate", NullValueHandling = NullValueHandling.Ignore)]
public int DownloadRate { get; set; }
[JsonProperty("error", NullValueHandling = NullValueHandling.Ignore)]
public string Error { get; set; }
/// <summary> Estimated Time of Arrivial. The estimated amount of seconds until the torrent finishes downloading. -1 indicates forever </summary>
[JsonProperty("eta", NullValueHandling = NullValueHandling.Ignore)]
public long ETA { get; set; }
/// <summary> cumulative counter in the fisished means all selected files/pieces were downloaded and available to other peers (this is always a subset of active time) </summary>
[JsonProperty("finished_duration", NullValueHandling = NullValueHandling.Ignore)]
public long FinishedDuration { get; set; }
/// <summary> reflects several of the torrent's flags </summary>
[JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)]
public ReadOnlyCollection<string> Flags { get; set; }
[JsonProperty("info_hash", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(LibTorrentInfoHashConverter))]
public LibTorrentInfoHash InfoHash { get; set; }
/// <summary> the timestamps of the last time this downloaded payload from any peer. (might be relative) </summary>
[JsonProperty("last_download", NullValueHandling = NullValueHandling.Ignore)]
public long LastDownload { get; set; }
/// <summary> the timestamps of the last time this uploaded payload to any peer. (might be relative) </summary>
[JsonProperty("last_upload", NullValueHandling = NullValueHandling.Ignore)]
public long LastUpload { get; set; }
/// <summary> the total number of peers (including seeds). We are not necessarily connected to all the peers in our peer list. This is the number of peers we know of in total, including banned peers and peers that we have failed to connect to. </summary>
[JsonProperty("list_peers", NullValueHandling = NullValueHandling.Ignore)]
public int ListPeers { get; set; }
/// <summary> the number of seeds in our peer list </summary>
[JsonProperty("list_seeds", NullValueHandling = NullValueHandling.Ignore)]
public int ListSeeds { get; set; }
// technically any valid json should be able to fit here. including `[]`
[JsonProperty("metadata", NullValueHandling = NullValueHandling.Ignore)]
public ReadOnlyDictionary<string, string> Metadata { get; set; }
/// <summary> this is true if this torrent's storage is currently being moved from one location to another. This may potentially be a long operation if a large file ends up being copied from one drive to another. </summary>
[JsonProperty("moving_storage", NullValueHandling = NullValueHandling.Ignore)]
public bool MovingStorage { get; set; }
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { get; set; }
/// <summary> the number of peers this torrent currently is connected to. Peer connections that are in the half-open state (is attempting to connect) or are queued for later connection attempt do not count. </summary>
[JsonProperty("num_peers", NullValueHandling = NullValueHandling.Ignore)]
public int NumPeers { get; set; }
/// <summary> the number of peers that are seeding that this client is currently connected to. </summary>
[JsonProperty("num_seeds", NullValueHandling = NullValueHandling.Ignore)]
public int NumSeeds { get; set; }
/// <summary> ratio of upload / downloaded </summary>
[JsonProperty("progress", NullValueHandling = NullValueHandling.Ignore)]
public float Progress { get; set; }
/// <summary> the position this torrent has in the download queue. If the torrent is a seed or finished, this is -1. </summary>
[JsonProperty("queue_position", NullValueHandling = NullValueHandling.Ignore)]
public int QueuePosition { get; set; }
/// <summary> ratio of upload / downloaded </summary>
[JsonProperty("ratio", NullValueHandling = NullValueHandling.Ignore)]
public double Ratio { get; set; }
/// <summary> the path to which the torrent is downloaded to </summary>
[JsonProperty("save_path", NullValueHandling = NullValueHandling.Ignore)]
public string SavePath { get; set; }
/// <summary> cumulative counter in the seeding means all files/pieces were downloaded and available to peers </summary>
[JsonProperty("seeding_duration", NullValueHandling = NullValueHandling.Ignore)]
public long SeedingDuration { get; set; }
/// <summary> name of the session this torrent is a part of </summary>
[JsonProperty("session", NullValueHandling = NullValueHandling.Ignore)]
public string Session { get; set; }
/// <summary> the total number of bytes the torrent-file represents. Note that this is the number of pieces times the piece size (modulo the last piece possibly being smaller). With pad files, the total size will be larger than the sum of all (regular) file sizes. </summary>
[JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)]
public long Size { get; set; }
/// <summary> the main state the torrent is in </summary>
/// <see cref="LibTorrentStatus"/>
[JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)]
public LibTorrentStatus State { get; set; }
[JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)]
public ReadOnlyCollection<string> Tags { get; set; }
/// <summary> the total number of bytes to download for this torrent. This may be less than the size of the torrent in case there are pad files. This number only counts bytes that will actually be requested from peers. </summary>
[JsonProperty("total", NullValueHandling = NullValueHandling.Ignore)]
public long Total { get; set; }
/// <summary> the total number of bytes of the file(s) that we have. All this does not necessarily has to be downloaded during this session </summary>
[JsonProperty("total_done", NullValueHandling = NullValueHandling.Ignore)]
public long TotalDone { get; set; }
/// <summary> the total rates for all peers for this torrent. These will usually have better precision than summing the rates from all peers. The rates are given as the number of bytes per second. </summary>
[JsonProperty("upload_rate", NullValueHandling = NullValueHandling.Ignore)]
public int UploadRate { get; set; }
}
/// <summary> Implementation of the torrent response data type from <a href="https://github.com/porla/porla/blob/v0.37.0/src/methods/torrentslist_reqres.hpp">torrentslist_reqres.hpp</a></summary>
public sealed class ResponsePorlaTorrentList
{
[JsonProperty("order_by", NullValueHandling = NullValueHandling.Ignore)]
public string OrderBy { get; set; }
[JsonProperty("order_by_dir", NullValueHandling = NullValueHandling.Ignore)]
public string OrderByDir { get; set; }
[JsonProperty("page", NullValueHandling = NullValueHandling.Ignore)]
public int Page { get; set; }
[JsonProperty("page_size", NullValueHandling = NullValueHandling.Ignore)]
public int PageSize { get; set; }
[JsonProperty("torrents", NullValueHandling = NullValueHandling.Ignore)]
public ReadOnlyCollection<PorlaTorrentDetail> Torrents { get; set; }
[JsonProperty("torrents_total", NullValueHandling = NullValueHandling.Ignore)]
public int TorrentsTotal { get; set; }
[JsonProperty("torrents_total_unfiltered", NullValueHandling = NullValueHandling.Ignore)]
public int TorrentsTotalUnfiltered { get; set; }
}
}

@ -0,0 +1,281 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.Porla.Models;
using NzbDrone.Core.Localization;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Porla
{
public class Porla : TorrentClientBase<PorlaSettings>
{
private readonly IPorlaProxy _proxy;
public Porla(IPorlaProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
ILocalizationService localizationService,
IBlocklistService blocklistService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger)
{
_proxy = proxy;
}
public override string Name => "Porla";
public override IEnumerable<DownloadClientItem> GetItems()
{
var plist = _proxy.ListTorrents(Settings);
var items = new List<DownloadClientItem>();
// should probs paginate instead of cheating
foreach (var torrent in plist)
{
// we don't need to check the category, the filter did that for us, but we are checking anyway becuase why not :)
if (torrent.Category != Settings.Category)
{
// TODO: Figure out how to make the test work with warnings
_logger.Info($"Porla Should not have sent us a torrrent in the catagory {torrent.Category}! We expected {Settings.Category}");
continue;
}
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath));
var item = new DownloadClientItem
{
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
DownloadId = torrent.InfoHash.Hash,
OutputPath = outputPath + torrent.Name,
RemainingSize = torrent.Total,
RemainingTime = torrent.ETA < 1 ? (TimeSpan?)null : TimeSpan.FromSeconds((double)torrent.ETA), // LibTorent uses `-1` to denote "infinite" time i.e. I am stuck, or I am done. FromSeconds will convert `-1` to 1, so we need to do this.
Title = torrent.Name,
TotalSize = torrent.Size,
SeedRatio = torrent.Ratio
};
// deal with moving_storage=true ?
if (!string.IsNullOrEmpty(torrent.Error))
{
item.Status = DownloadItemStatus.Warning;
item.Message = torrent.Error;
} // TODO: check paused or finished first?
else if (torrent.FinishedDuration > 0)
{
item.Status = DownloadItemStatus.Completed;
if (torrent.ETA < 1)
{
// Sonarr wants to see a TimeSpan.Zero when it is done ( -1 -> 0 )
item.RemainingTime = TimeSpan.Zero;
}
}
else if (torrent.Flags.Contains("paused"))
{
item.Status = DownloadItemStatus.Paused;
} /* I don't know what the torent looks like if it is "Queued"
else if (???)
{
item.Status = DownloadItemStatus.Queued;
} */
else
{
item.Status = DownloadItemStatus.Downloading;
}
item.CanMoveFiles = item.CanBeRemoved = true; // usure of what restricts this on porla. Currently these is always true
items.Add(item);
}
if (items.Count < 1)
{
_logger.Debug("No Items Returned");
}
return items;
}
public override void RemoveItem(DownloadClientItem item, bool deleteData)
{
// Kinda sucks we don't have a `RemoveItems`, porla has a batch interface for removals
PorlaTorrent[] singleItem = { new PorlaTorrent(item.DownloadId, "") };
_proxy.RemoveTorrent(Settings, deleteData, singleItem);
// when do we set item.Removed ?
}
public override DownloadClientInfo GetStatus()
{
var presetEffectiveSettings = _proxy.ListPresets(Settings).GetEffective(Settings.Preset.IsNullOrWhiteSpace() ? "default" : Settings.Preset);
// var sessionSettings = _proxy.GetSessionSettings(Settings);
var status = new DownloadClientInfo
{
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost",
RemovesCompletedDownloads = false, // TODO: I don't think porla has config for this, it feels like it should
};
var savePath = ((presetEffectiveSettings?.SavePath.IsNullOrWhiteSpace() ?? true) ? Settings.TvDirectory : presetEffectiveSettings?.SavePath) ?? "";
var destDir = new OsPath(savePath);
if (!destDir.IsEmpty)
{
status.OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) };
}
return status;
}
/// <summary> Converts a RemoteEpisode into a list of <em>starr</em> tags </summary>
/// <see cref="RemoteEpisode"/>
private static IList<string> ConvertRemoteEpisodeToTags(RemoteEpisode remoteEpisode)
{
var tags = new List<string>
{
$"starr.series={remoteEpisode.Series.CleanTitle}",
$"starr.season={remoteEpisode.MappedSeasonNumber}",
$"starr.tvdbid={remoteEpisode.Series.TvdbId}",
$"starr.imdbid={remoteEpisode.Series.ImdbId}",
$"starr.tvmazeid={remoteEpisode.Series.TvMazeId}",
$"starr.year={remoteEpisode.Series.Year}"
};
return tags;
}
// NOTE: If the torrent already exists in the client, it'll fail with error code -3
// {
// "code": -3,
// "data": null,
// "message": "Torrent already in session 'default'"
// }
// IDK If I should deal with that...
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
{
PorlaTorrent torrent;
if (Settings.SeriesTag)
{
var tags = ConvertRemoteEpisodeToTags(remoteEpisode);
torrent = _proxy.AddMagnetTorrent(Settings, magnetLink, tags);
}
else
{
torrent = _proxy.AddMagnetTorrent(Settings, magnetLink);
}
return torrent.InfoHash.Hash ?? "";
}
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent)
{
PorlaTorrent torrent;
if (Settings.SeriesTag)
{
var tags = ConvertRemoteEpisodeToTags(remoteEpisode);
torrent = _proxy.AddTorrentFile(Settings, fileContent, tags);
}
else
{
torrent = _proxy.AddTorrentFile(Settings, fileContent);
}
return torrent.InfoHash.Hash ?? "";
}
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestVersion());
if (failures.HasErrors())
{
return;
}
failures.AddIfNotNull(TestGetTorrents());
}
/// <summary> Test the connection by calling the `sys.version` </summary>
private ValidationFailure TestVersion()
{
try
{
// Version Compatability check.
var sysVers = _proxy.GetSysVersion(Settings);
var badVersions = new List<string> { }; // List of Broken Versions
var goodVersion = new Version("0.37.0"); // The main version we want to see
var firstGoodVersion = new Version("0.37.0"); // The first (cronological) version that we are sure works (usually the goodVersion)
var lastGoodVersion = new Version("0.37.0"); // The last (cronological) version that we are sure works (usually the goodVersion)
var actualVersion = new Version(sysVers.Porla.Version);
if (badVersions.Any(s => new Version(s) == actualVersion))
{
_logger.Error($"Your Porla version isn't compatible with Sonarr!: {actualVersion}");
return new ValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationErrorVersion",
new Dictionary<string, object> { { "clientName", Name }, { "requiredVersion", goodVersion.ToString() }, { "reportedVersion", actualVersion } }));
}
if (actualVersion < firstGoodVersion)
{
_logger.Warn($"Your version might not be forwards compatible: {actualVersion}");
}
if (actualVersion > lastGoodVersion)
{
_logger.Warn($"Your version might not be backwards compatible: {actualVersion}");
}
}
catch (DownloadClientAuthenticationException ex)
{
_logger.Error(ex, ex.Message);
return new NzbDroneValidationFailure("Password", _localizationService.GetLocalizedString("DownloadClientValidationAuthenticationFailure"));
}
catch (DownloadClientUnavailableException ex)
{
_logger.Error(ex, ex.Message);
return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary<string, object> { { "clientName", Name } }))
{
DetailedDescription = ex.Message
};
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to test");
return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationUnknownException", new Dictionary<string, object> { { "exception", ex.Message } }));
}
return null;
}
private ValidationFailure TestGetTorrents()
{
try
{
_ = _proxy.ListTorrents(Settings, 0, 1);
}
catch (Exception ex)
{
_logger.Error(ex, ex.Message);
return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationTestTorrents", new Dictionary<string, object> { { "exceptionMessage", ex.Message } }));
}
return null;
}
}
}

@ -0,0 +1,234 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net;
using System.Net.Http;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Download.Clients.Porla.Models;
namespace NzbDrone.Core.Download.Clients.Porla
{
#nullable enable
public interface IPorlaProxy
{
// sys
PorlaSysVersions GetSysVersion(PorlaSettings settings); // sys.versions
// fs
// fs.space
// sessions
ReadOnlyCollection<PorlaSession> ListSessions(PorlaSettings settings); // sessions.list
void PauseSessions(PorlaSettings settings); // sessions.pause
void ResumeSessions(PorlaSettings settings); // sessions.resume
PorlaSessionSettings GetSessionSettings(PorlaSettings settings); // sessions.settings.list
// presets
ReadOnlyDictionary<string, PorlaPreset> ListPresets(PorlaSettings settings); // presets.list
// torrents
PorlaTorrent AddMagnetTorrent(PorlaSettings settings, string uri, IList<string>? tags = null); // torrents.add
PorlaTorrent AddTorrentFile(PorlaSettings settings, byte[] fileContent, IList<string>? tags = null); // torrents.add
void RemoveTorrent(PorlaSettings settings, bool removeData, PorlaTorrent[] pts); // torrents.remove
// torrents.move
void PauseTorrent(PorlaSettings settings, PorlaTorrent pt); // torrents.pause
void ResumeTorrent(PorlaSettings settings, PorlaTorrent pt); // torrents.resume
ReadOnlyCollection<PorlaTorrentDetail> ListTorrents(PorlaSettings settings, int page = 0, int size = int.MaxValue); // torrents.list
// torrents.recheck
// torrents.files.list
// torrents.metadata.list
// torrents.trackers.list
// torrents.peers
// torrents.peer.add
// torrents.peer.list
// torrents.properties
// torrents.properties.get
// torrents.properties.set
}
public class PorlaProxy : IPorlaProxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public PorlaProxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
private T ProcessRequest<T>(PorlaSettings settings, string method, params object?[] parameters)
{
var baseUrl = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase);
var jwt = settings.InfinteJWT ??= string.Empty;
var apiurl = settings.ApiUrl ??= string.Empty;
// this block will run a lot, don't want to be too noisy in the logs
if (string.IsNullOrEmpty(jwt))
{
// _logger.Warn("Porla: We don't implemenet alternate JWT methods (yet)")
// add logic here
}
else
{
// _logger.Notice("Porla: Setting Infinte JWT")
}
var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, true, parameters)
.Resource(apiurl)
.SetHeader("Authorization", $"Bearer {jwt}");
requestBuilder.LogResponseContent = true;
var httpRequest = requestBuilder.Build();
_logger.Debug(httpRequest.ToString());
HttpResponse response;
// TODO: catch and throw auth exceptions like in Qbit
try
{
response = _httpClient.Execute(httpRequest);
}
catch (HttpRequestException ex)
{
throw new DownloadClientException("Unable to connect to Porla, please check your settings", ex);
}
catch (HttpException ex)
{
throw new DownloadClientException("Unable to connect to Porla, please check your settings", ex);
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.TrustFailure)
{
throw new DownloadClientUnavailableException("Unable to connect to Porla, certificate validation failed.", ex);
}
throw new DownloadClientUnavailableException("Unable to connect to Porla, please check your settings", ex);
}
catch (Exception ex)
{
_logger.Error("Unkown Connection Error");
throw new DownloadClientException("Unable to connect to Porla, Unkown error", ex);
}
var result = Json.Deserialize<JsonRpcResponse<T>>(response.Content);
if (result.Error != null)
{
throw new DownloadClientException("Error response received from Porla: {0}", result.Error.ToString());
}
return result.Result;
}
private void LogSupposedToBeNothing(string method, string something)
{
if (!string.IsNullOrEmpty(something))
{
_logger.Warn($"method: {method} was not expected to return: {something}");
}
}
// sys
public PorlaSysVersions GetSysVersion(PorlaSettings settings)
{
return ProcessRequest<PorlaSysVersions>(settings, "sys.versions");
}
// fs
// session
public ReadOnlyCollection<PorlaSession> ListSessions(PorlaSettings settings)
{
var sessions = ProcessRequest<ResponsePorlaSessionList>(settings, "sessions.list");
return sessions.Sessions;
}
public void PauseSessions(PorlaSettings settings)
{
var empty = ProcessRequest<string>(settings, "sessions.pause");
LogSupposedToBeNothing("PauseSessions", empty);
}
public void ResumeSessions(PorlaSettings settings)
{
var empty = ProcessRequest<string>(settings, "sessions.resume");
LogSupposedToBeNothing("ResumeSessions", empty);
}
public PorlaSessionSettings GetSessionSettings(PorlaSettings settings)
{
var resp = ProcessRequest<ResponsePorlaSessionSettingsList>(settings, "sessions.settings.list");
return resp.Settings;
}
// presets
public ReadOnlyDictionary<string, PorlaPreset> ListPresets(PorlaSettings settings)
{
var presets = ProcessRequest<ResponsePorlaPresetsList>(settings, "presets.list");
return presets.Presets;
}
// torrents
public PorlaTorrent AddMagnetTorrent(PorlaSettings settings, string uri, IList<string>? tags = null)
{
var dir = string.IsNullOrWhiteSpace(settings.TvDirectory) ? null : settings.TvDirectory;
var category = string.IsNullOrWhiteSpace(settings.Category) ? "" : settings.Category;
var preset = string.IsNullOrWhiteSpace(settings.Preset) ? "" : settings.Preset;
var torrent = ProcessRequest<PorlaTorrent>(settings, "torrents.add", "preset", preset, "tags", tags, "category", category, "magnet_uri", uri, "save_path", dir);
return torrent;
}
public PorlaTorrent AddTorrentFile(PorlaSettings settings, byte[] fileContent, IList<string>? tags = null)
{
var dir = string.IsNullOrWhiteSpace(settings.TvDirectory) ? null : settings.TvDirectory;
var category = string.IsNullOrWhiteSpace(settings.Category) ? "" : settings.Category;
var preset = string.IsNullOrWhiteSpace(settings.Preset) ? "" : settings.Preset;
var torrent = ProcessRequest<PorlaTorrent>(settings, "torrents.add", "preset", preset, "tags", tags, "category", category, "ti", fileContent.ToBase64(), "save_path", dir);
return torrent;
}
public void RemoveTorrent(PorlaSettings settings, bool removeData, PorlaTorrent[] pts)
{
var empty = ProcessRequest<string>(settings, "torrents.remove", "info_hashes", pts.SelectMany(pt => pt.AsParam()).ToArray(), "remove_data", removeData);
LogSupposedToBeNothing("RemoveTorrent", empty);
}
public void PauseTorrent(PorlaSettings settings, PorlaTorrent pt)
{
var empty = ProcessRequest<string>(settings, "torrents.pause", pt.AsParams());
LogSupposedToBeNothing("PauseTorrent", empty);
}
public void ResumeTorrent(PorlaSettings settings, PorlaTorrent pt)
{
var empty = ProcessRequest<string>(settings, "torrents.resume", pt.AsParams());
LogSupposedToBeNothing("ResumeTorrent", empty);
}
public ReadOnlyCollection<PorlaTorrentDetail> ListTorrents(PorlaSettings settings, int page = 0, int size = int.MaxValue)
{
// cheating with the int.MaxValue. Should do proper Pagination :P
var resp = ProcessRequest<ResponsePorlaTorrentList>(settings, "torrents.list", "filters", new { category = settings.Category ?? "" }, "page", page, "size", size);
return resp.Torrents;
}
}
}

@ -0,0 +1,80 @@
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Porla
{
// I hate C# constants :(
// private static readonly string defaultPorlaApiUrl = "/api/v1/jsonrpc";
public class PorlaSettingsValidator : AbstractValidator<PorlaSettings>
{
public PorlaSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.ApiUrl).ValidUrlBase("/api/v1/jsonrpc").When(c => c.UrlBase.IsNotNullOrWhiteSpace());
RuleFor(c => c.UrlBase).ValidUrlBase().When(c => c.UrlBase.IsNotNullOrWhiteSpace());
RuleFor(c => c.InfinteJWT).NotEmpty().WithMessage("'JWT Token' must not be empty!");
}
}
public class PorlaSettings : IProviderConfig
{
private static readonly PorlaSettingsValidator Validator = new PorlaSettingsValidator();
public PorlaSettings()
{
Host = "localhost";
Port = 1337;
ApiUrl = "/api/v1/jsonrpc";
Category = "sonarr-tv";
Preset = "default";
TvDirectory = "/tmp";
SeriesTag = true;
}
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)]
public string Host { get; set; }
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; }
[FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")]
[FieldToken(TokenField.HelpText, "UseSsl", "clientName", "Porla")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsUrlBaseHelpText")]
[FieldToken(TokenField.HelpText, "UrlBase", "clientName", "Porla")]
[FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/[apiUrl]")]
public string UrlBase { get; set; }
[FieldDefinition(4, Label = "ApiUrl", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsApiUrlHelpText")]
[FieldToken(TokenField.HelpText, "ApiUrl", "clientName", "Porla")]
[FieldToken(TokenField.HelpText, "ApiUrl", "defaultUrl", "/api/v1/jsonrpc")]
public string ApiUrl { get; set; }
[FieldDefinition(5, Label = "InfinteJWT", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string InfinteJWT { get; set; }
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategoryHelpText")]
public string Category { get; set; }
[FieldDefinition(7, Label = "Preset", Type = FieldType.Textbox, HelpText = "DownloadClientPorlaSettingsPreset")]
public string Preset { get; set; }
[FieldDefinition(8, Label = "Directory", Type = FieldType.Textbox, HelpText = "DownloadClientPorlaSettingsDirectoryHelpText")]
public string TvDirectory { get; set; }
[FieldDefinition(9, Label = "DownloadClientPorlaSeriesTag", Type = FieldType.Checkbox, Advanced = true, HelpText = "DownloadClientPorlaSeriesTagHelpText")]
public bool SeriesTag { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

@ -0,0 +1,98 @@
# Porla Client
[Porla](https://porla.org/)! The (Unofficial) Web frontend for [LibTorrent](https://libtorrent.org/)!
## Details
* Uses JSONRPC
* Written in C++
* Has LUA Plugin Capability
* Session Capable
* Has Proxy Support (Looking at you Transmission!)
* Presets!
> Currently (2024-02) still in bleeding-edge active development.
## Deving
I coppied large parts of this code from the Hadouken Client, as it was the other JSONRPC client in the collection.
Docs are iffy currently, check out the methods at the [source (v0.37.0)](https://github.com/porla/porla/tree/v0.37.0/src/methods). They are not too hard to figure out in conjunction with the [method docs](https://porla.org/api/methods/)
### `GetStatus()`
Unsure what this needs to do exactly.
I _think_ it's supposed to introspect the configuration.
_IF_ that is the case. TODO: Figure out extracting the `save_path` from the prefix
## Feature Implementation
### Sessions
I **DID NOT** do anything with sessions.
They could be VERY powerfull to work with Private and Public trackers, but the front end currently doesn't have strong support for it.
Could also be a way to have multiple Sonarrs pointed at the same Porla, or a diffrent way to deal with the other Starr ecosystem.
### Presets
Preset ussage are being set on the torrent adds, through a client setting.
So I needed to do something with the `save_path` on the `torrent.add` request.
Theis field is optional if you set a defualt path on porla BUT required if you didn't.
I wanted this to work "by default", so if you have NO presets set, you **NEED** to set the `save_path` _BUT_ if you want to use the presets values you should NOT set the `save_path` in the RPC request. That is why you see that intersting branch on the `AddTorrent` functions.
Interesting handling, the chosen preset is **overlayed** over the values from the _default_ preset if it exists.
We need to deal with that, to determine the "effective" settings loaded inside porla
### Tags
Would be great to use to filter BIG lists of torrents even more! But Sonarr expects everything. Something to look at in the future if Sonarr ever does granular torrent requests.
The idea is to use the tags on torrents to make listing more efficient, in our case tagging them with series/show/season so we can find what we are looking for.
What I did set tags with the series attributes. I want to be able to come around later and filter torrents outside the context of Sonarr.
You can set a client setting to disable this behavior.
### Torrent Metadata
I **DID NOT USE** this, maybe add something?
It's basically a any-field that we can fill with anything we want.
### LibTorrent [Torrent Flags](https://libtorrent.org/single-page-ref.html#torrent_flags_t)
#### share_mode
Should be very useful for private trackers. Sadly unclear how to implement it without doing some spagetti.
# Data / Response Examples
Config Definition can be found at [`config.hpp`](https://github.com/porla/porla/blob/v0.37.0/src/config.hpp)
## Presets.list
Return should be defined in [`presets.hpp`](https://github.com/porla/porla/blob/v0.37.0/src/lua/packages/presets.cpp)
### Config.toml
```toml
[presets.default]
save_path = "/tmp/"
[presets.other]
tags = ["other"]
```
### Response
```json
{
"default":{
"category":null,"download_limit":null,"max_connections":null,"max_uploads":null,"save_path":"/tmp/","session":null,"tags":null,"upload_limit":null
},
"other":{
"category":null,"download_limit":null,"max_connections":null,"max_uploads":null,"save_path":null,"session":null,"tags":["other"],"upload_limit":null
}
}
```

@ -88,6 +88,7 @@
"Any": "Any",
"ApiKey": "API Key",
"ApiKeyValidationHealthCheckMessage": "Please update your API key to be at least {length} characters long. You can do this via settings or the config file",
"ApiUrl": "API URL",
"AppDataDirectory": "AppData Directory",
"AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update",
"AppUpdated": "{appName} Updated",
@ -462,6 +463,10 @@
"DownloadClientPneumaticSettingsStrmFolder": "Strm Folder",
"DownloadClientPneumaticSettingsStrmFolderHelpText": ".strm files in this folder will be import by drone",
"DownloadClientPriorityHelpText": "Download Client Priority from 1 (Highest) to 50 (Lowest). Default: 1. Round-Robin is used for clients with the same priority.",
"DownloadClientPorlaSeriesTag": "Series Tagging",
"DownloadClientPorlaSeriesTagHelpText": "Enables tagging of torrents inside Porla with series data (name, season, tvdbid)",
"DownloadClientPorlaSettingsDirectoryHelpText": "Where to put downloads. Set blank to use the preset's value.",
"DownloadClientPorlaSettingsPreset": "Sets the Preset for newly added Torrents. Remember to have it configured in Porla!",
"DownloadClientQbittorrentSettingsContentLayout": "Content Layout",
"DownloadClientQbittorrentSettingsContentLayoutHelpText": "Whether to use qBittorrent's configured content layout, the original layout from the torrent or always create a subfolder (qBittorrent 4.3.2+)",
"DownloadClientQbittorrentSettingsFirstAndLastFirst": "First and Last First",
@ -510,6 +515,7 @@
"DownloadClientSeriesTagHelpText": "Only use this download client for series with at least one matching tag. Leave blank to use with all series.",
"DownloadClientSettings": "Download Client Settings",
"DownloadClientSettingsAddPaused": "Add Paused",
"DownloadClientSettingsApiUrlHelpText": "Changes the url resource for {clientName}, default is {defaultUrl}",
"DownloadClientSettingsCategoryHelpText": "Adding a category specific to {appName} avoids conflicts with unrelated non-{appName} downloads. Using a category is optional, but strongly recommended.",
"DownloadClientSettingsCategorySubFolderHelpText": "Adding a category specific to {appName} avoids conflicts with unrelated non-{appName} downloads. Using a category is optional, but strongly recommended. Creates a [category] subdirectory in the output directory.",
"DownloadClientSettingsDestinationHelpText": "Manually specifies download destination, leave blank to use the default",

Loading…
Cancel
Save