Compare commits

...

15 Commits

Author SHA1 Message Date
Wesselinator f6a5d78faa
Merge b9e026aa2b into c81ae65461 2024-04-28 01:09:17 +00:00
Bogdan c81ae65461 Fixed: Limit titles in task name to 10 series 2024-04-27 18:09:08 -07:00
Bogdan efb3fa93e4
Fixed: Retrying download for pushed releases
ignore-downstream
#6752
2024-04-27 21:08:40 -04:00
Stevie Robinson 04bd535cfc
New: Don't initially select 0 byte files in Interactive Import
Closes #6686
2024-04-27 21:07:41 -04:00
Bogdan 9738101042 Treat CorruptDatabaseException as a startup failure
ignore-downstream
2024-04-27 18:06:49 -07:00
Bogdan 1df7cdc65e New: Add KRaLiMaRKo and BluDragon to release group parsing exceptions
ignore-downstream
2024-04-27 18:06:38 -07:00
Jared d051dac12c
New: Optionally use Environment Variables for settings in config.xml
Closes #6744
2024-04-27 21:06:26 -04:00
Bogdan 5d01ecd30e Bump frontend dependencies
ignore-downstream
2024-04-27 18:05:16 -07:00
Mark McDowall 316b5cbf75
New: Validate that folders in paths don't start or end with a space
Closes #6709
2024-04-27 21:04:50 -04:00
Bogdan 2440672179 Bump SixLabors.ImageSharp to 3.1.4
ignore-downstream
2024-04-27 18:04:35 -07:00
Mark McDowall a97fbcc40a Fixed: Improve paths longer than 256 on Windows failing to hardlink 2024-04-27 18:04:26 -07:00
Christopher d738035fed
New: Remove qBitorrent torrents that reach inactive seeding time 2024-04-27 21:04:16 -04:00
Mark McDowall dc3e932102 macOS tests now run on arm64 2024-04-27 18:03:12 -07:00
Weblate aded9d95f7 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ano10 <arnaudthommeray+github@ik.me>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Mailme Dashite <mailmedashite@protonmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: aghus <aghus.m@outlook.com>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: maodun96 <435795439@qq.com>
Co-authored-by: toeiazarothis <patrickdealmeida89000@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-04-27 18:03:03 -07:00
Wesselinator b9e026aa2b
Porla Download Client 2024-04-13 17:48:04 +02:00
58 changed files with 5211 additions and 1768 deletions

10
.containerignore Normal file
View File

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

View File

@ -76,11 +76,11 @@ jobs:
framework: ${{ env.FRAMEWORK }}
runtime: linux-x64
- name: Publish osx-x64 Test Artifact
- name: Publish osx-arm64 Test Artifact
uses: ./.github/actions/publish-test-artifact
with:
framework: ${{ env.FRAMEWORK }}
runtime: osx-x64
runtime: osx-arm64
# Build Artifacts (grouped by OS)
@ -143,7 +143,7 @@ jobs:
artifact: tests-linux-x64
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
- os: macos-latest
artifact: tests-osx-x64
artifact: tests-osx-arm64
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
- os: windows-latest
artifact: tests-win-x64
@ -190,10 +190,10 @@ jobs:
binary_artifact: build_linux
binary_path: linux-x64/${{ needs.backend.outputs.framework }}/Sonarr
- os: macos-latest
artifact: tests-osx-x64
artifact: tests-osx-arm64
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory=IntegrationTest
binary_artifact: build_macos
binary_path: osx-x64/${{ needs.backend.outputs.framework }}/Sonarr
binary_path: osx-arm64/${{ needs.backend.outputs.framework }}/Sonarr
- os: windows-latest
artifact: tests-win-x64
filter: TestCategory!=ManualTest&TestCategory!=LINUX&TestCategory=IntegrationTest

8
Taskfile.yml Normal file
View File

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

19
docker/Taskfile.yml Normal file
View File

@ -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'

View File

@ -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"]

View File

@ -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"]

13
docker/test/Containerfile Normal file
View File

@ -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

View File

@ -128,7 +128,8 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
seasonNumber != null &&
episodes.length &&
quality &&
languages
languages &&
size > 0
) {
onSelectedChange({
id,

View File

@ -6,6 +6,22 @@ import createMultiSeriesSelector from 'Store/Selectors/createMultiSeriesSelector
import translate from 'Utilities/String/translate';
import styles from './QueuedTaskRowNameCell.css';
function formatTitles(titles: string[]) {
if (!titles) {
return null;
}
if (titles.length > 11) {
return (
<span title={titles.join(', ')}>
{titles.slice(0, 10).join(', ')}, {titles.length - 10} more
</span>
);
}
return <span>{titles.join(', ')}</span>;
}
export interface QueuedTaskRowNameCellProps {
commandName: string;
body: CommandBody;
@ -32,7 +48,7 @@ export default function QueuedTaskRowNameCell(
<span className={styles.commandName}>
{commandName}
{sortedSeries.length ? (
<span> - {sortedSeries.map((s) => s.title).join(', ')}</span>
<span> - {formatTitles(sortedSeries.map((s) => s.title))}</span>
) : null}
{body.seasonNumber ? (
<span>

View File

@ -29,9 +29,9 @@
"@juggle/resize-observer": "3.4.0",
"@microsoft/signalr": "6.0.21",
"@sentry/browser": "7.100.0",
"@types/node": "18.16.8",
"@types/react": "18.2.6",
"@types/react-dom": "18.2.4",
"@types/node": "18.19.31",
"@types/react": "18.2.79",
"@types/react-dom": "18.2.25",
"classnames": "2.3.2",
"clipboard": "2.0.11",
"connected-react-router": "6.9.3",
@ -81,17 +81,16 @@
"redux-thunk": "2.4.2",
"reselect": "4.1.8",
"stacktrace-js": "2.0.2",
"typescript": "4.9.5"
"typescript": "5.1.6"
},
"devDependencies": {
"@babel/core": "7.22.11",
"@babel/eslint-parser": "7.22.11",
"@babel/plugin-proposal-export-default-from": "7.22.5",
"@babel/core": "7.24.4",
"@babel/eslint-parser": "7.24.1",
"@babel/plugin-proposal-export-default-from": "7.24.1",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.22.14",
"@babel/preset-react": "7.22.5",
"@babel/preset-typescript": "7.22.11",
"@types/classnames": "2.3.1",
"@babel/preset-env": "7.24.4",
"@babel/preset-react": "7.24.1",
"@babel/preset-typescript": "7.24.1",
"@types/lodash": "4.14.194",
"@types/react-lazyload": "3.2.0",
"@types/react-router-dom": "5.3.3",
@ -99,30 +98,30 @@
"@types/react-window": "1.8.5",
"@types/redux-actions": "2.6.2",
"@types/webpack-livereload-plugin": "^2.3.3",
"@typescript-eslint/eslint-plugin": "5.59.5",
"@typescript-eslint/parser": "5.59.5",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"autoprefixer": "10.4.14",
"babel-loader": "9.1.2",
"babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.32.1",
"core-js": "3.37.0",
"css-loader": "6.7.3",
"css-modules-typescript-loader": "4.0.1",
"eslint": "8.40.0",
"eslint-config-prettier": "8.8.0",
"eslint": "8.57.0",
"eslint-config-prettier": "8.10.0",
"eslint-plugin-filenames": "1.3.2",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react": "7.34.1",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-simple-import-sort": "10.0.0",
"eslint-plugin-simple-import-sort": "12.1.0",
"file-loader": "6.2.0",
"filemanager-webpack-plugin": "8.0.0",
"fork-ts-checker-webpack-plugin": "8.0.0",
"html-webpack-plugin": "5.5.1",
"loader-utils": "^3.2.1",
"mini-css-extract-plugin": "2.7.5",
"postcss": "8.4.23",
"postcss": "8.4.38",
"postcss-color-function": "4.1.0",
"postcss-loader": "7.3.0",
"postcss-mixins": "9.0.4",

View File

@ -1,10 +1,12 @@
using System.Collections.Generic;
using FluentAssertions;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Options;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration;
using NzbDrone.Test.Common;
@ -43,6 +45,26 @@ namespace NzbDrone.Common.Test
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.WriteAllText(configFile, It.IsAny<string>()))
.Callback<string, string>((p, t) => _configFileContents = t);
Mocker.GetMock<IOptions<AuthOptions>>()
.Setup(v => v.Value)
.Returns(new AuthOptions());
Mocker.GetMock<IOptions<AppOptions>>()
.Setup(v => v.Value)
.Returns(new AppOptions());
Mocker.GetMock<IOptions<ServerOptions>>()
.Setup(v => v.Value)
.Returns(new ServerOptions());
Mocker.GetMock<IOptions<LogOptions>>()
.Setup(v => v.Value)
.Returns(new LogOptions());
Mocker.GetMock<IOptions<UpdateOptions>>()
.Setup(v => v.Value)
.Returns(new UpdateOptions());
}
[Test]

View File

@ -3,6 +3,7 @@ using System.IO;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Test.Common;
@ -34,7 +35,7 @@ namespace NzbDrone.Common.Test
[TestCase(@"\\Testserver\\Test\", @"\\Testserver\Test")]
[TestCase(@"\\Testserver\Test\file.ext", @"\\Testserver\Test\file.ext")]
[TestCase(@"\\Testserver\Test\file.ext\\", @"\\Testserver\Test\file.ext")]
[TestCase(@"\\Testserver\Test\file.ext \\", @"\\Testserver\Test\file.ext")]
[TestCase(@"\\Testserver\Test\file.ext ", @"\\Testserver\Test\file.ext")]
[TestCase(@"//CAPITAL//lower// ", @"\\CAPITAL\lower")]
public void Clean_Path_Windows(string dirty, string clean)
{
@ -335,5 +336,30 @@ namespace NzbDrone.Common.Test
result[2].Should().Be(@"TV");
result[3].Should().Be(@"Series Title");
}
[TestCase(@"C:\Test\")]
[TestCase(@"C:\Test")]
[TestCase(@"C:\Test\TV\")]
[TestCase(@"C:\Test\TV")]
public void IsPathValid_should_be_true(string path)
{
path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeTrue();
}
[TestCase(@"C:\Test \")]
[TestCase(@"C:\Test ")]
[TestCase(@"C:\ Test\")]
[TestCase(@"C:\ Test")]
[TestCase(@"C:\Test \TV")]
[TestCase(@"C:\ Test\TV")]
[TestCase(@"C:\Test \TV\")]
[TestCase(@"C:\ Test\TV\")]
[TestCase(@" C:\Test\TV\")]
[TestCase(@" C:\Test\TV")]
public void IsPathValid_should_be_false(string path)
{
path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeFalse();
}
}
}

View File

@ -10,6 +10,7 @@ using NUnit.Framework;
using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.Options;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Lifecycle;
@ -33,6 +34,11 @@ namespace NzbDrone.Common.Test
container.RegisterInstance(new Mock<IHostLifetime>().Object);
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
container.RegisterInstance(new Mock<IOptions<AppOptions>>().Object);
container.RegisterInstance(new Mock<IOptions<AuthOptions>>().Object);
container.RegisterInstance(new Mock<IOptions<ServerOptions>>().Object);
container.RegisterInstance(new Mock<IOptions<LogOptions>>().Object);
container.RegisterInstance(new Mock<IOptions<UpdateOptions>>().Object);
var serviceProvider = container.GetServiceProvider();

View File

@ -29,6 +29,12 @@ namespace NzbDrone.Common.Extensions
public static string CleanFilePath(this string path)
{
if (path.IsNotNullOrWhiteSpace())
{
// Trim trailing spaces before checking if the path is valid so validation doesn't fail for something we can fix.
path = path.TrimEnd(' ');
}
Ensure.That(path, () => path).IsNotNullOrWhiteSpace();
Ensure.That(path, () => path).IsValidPath(PathValidationType.AnyOs);
@ -37,10 +43,10 @@ namespace NzbDrone.Common.Extensions
// UNC
if (!info.FullName.Contains('/') && info.FullName.StartsWith(@"\\"))
{
return info.FullName.TrimEnd('/', '\\', ' ');
return info.FullName.TrimEnd('/', '\\');
}
return info.FullName.TrimEnd('/').Trim('\\', ' ');
return info.FullName.TrimEnd('/').Trim('\\');
}
public static bool PathNotEquals(this string firstPath, string secondPath, StringComparison? comparison = null)
@ -154,6 +160,23 @@ namespace NzbDrone.Common.Extensions
return false;
}
if (path.Trim() != path)
{
return false;
}
var directoryInfo = new DirectoryInfo(path);
while (directoryInfo != null)
{
if (directoryInfo.Name.Trim() != directoryInfo.Name)
{
return false;
}
directoryInfo = directoryInfo.Parent;
}
if (validationType == PathValidationType.AnyOs)
{
return IsPathValidForWindows(path) || IsPathValidForNonWindows(path);
@ -291,6 +314,11 @@ namespace NzbDrone.Common.Extensions
return processName;
}
public static string CleanPath(this string path)
{
return Path.Join(path.Split(Path.DirectorySeparatorChar).Select(s => s.Trim()).ToArray());
}
public static string GetAppDataPath(this IAppFolderInfo appFolderInfo)
{
return appFolderInfo.AppDataFolder;

View File

@ -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());

View File

@ -0,0 +1,8 @@
namespace NzbDrone.Common.Options;
public class AppOptions
{
public string InstanceName { get; set; }
public string Theme { get; set; }
public bool? LaunchBrowser { get; set; }
}

View File

@ -0,0 +1,9 @@
namespace NzbDrone.Common.Options;
public class AuthOptions
{
public string ApiKey { get; set; }
public bool? Enabled { get; set; }
public string Method { get; set; }
public string Required { get; set; }
}

View File

@ -0,0 +1,14 @@
namespace NzbDrone.Common.Options;
public class LogOptions
{
public string Level { get; set; }
public bool? FilterSentryEvents { get; set; }
public int? Rotate { get; set; }
public bool? Sql { get; set; }
public string ConsoleLevel { get; set; }
public bool? AnalyticsEnabled { get; set; }
public string SyslogServer { get; set; }
public int? SyslogPort { get; set; }
public string SyslogLevel { get; set; }
}

View File

@ -0,0 +1,12 @@
namespace NzbDrone.Common.Options;
public class ServerOptions
{
public string UrlBase { get; set; }
public string BindAddress { get; set; }
public int? Port { get; set; }
public bool? EnableSsl { get; set; }
public int? SslPort { get; set; }
public string SslCertPath { get; set; }
public string SslCertPassword { get; set; }
}

View File

@ -0,0 +1,9 @@
namespace NzbDrone.Common.Options;
public class UpdateOptions
{
public string Mechanism { get; set; }
public bool? Automatically { get; set; }
public string ScriptPath { get; set; }
public string Branch { get; set; }
}

View File

@ -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);
// }
}
}

View File

@ -108,7 +108,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Subject.Definition.Settings.As<QBittorrentSettings>().RecentTvPriority = (int)QBittorrentPriority.First;
}
protected void GivenGlobalSeedLimits(float maxRatio, int maxSeedingTime = -1, QBittorrentMaxRatioAction maxRatioAction = QBittorrentMaxRatioAction.Pause)
protected void GivenGlobalSeedLimits(float maxRatio, int maxSeedingTime = -1, int maxInactiveSeedingTime = -1, QBittorrentMaxRatioAction maxRatioAction = QBittorrentMaxRatioAction.Pause)
{
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
@ -118,7 +118,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
MaxRatio = maxRatio,
MaxRatioEnabled = maxRatio >= 0,
MaxSeedingTime = maxSeedingTime,
MaxSeedingTimeEnabled = maxSeedingTime >= 0
MaxSeedingTimeEnabled = maxSeedingTime >= 0,
MaxInactiveSeedingTime = maxInactiveSeedingTime,
MaxInactiveSeedingTimeEnabled = maxInactiveSeedingTime >= 0
});
}
@ -610,7 +612,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
float ratio = 0.1f,
float ratioLimit = -2,
int seedingTime = 1,
int seedingTimeLimit = -2)
int seedingTimeLimit = -2,
int inactiveSeedingTimeLimit = -2,
long lastActivity = -1)
{
var torrent = new QBittorrentTorrent
{
@ -624,7 +628,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
SavePath = "",
Ratio = ratio,
RatioLimit = ratioLimit,
SeedingTimeLimit = seedingTimeLimit
SeedingTimeLimit = seedingTimeLimit,
InactiveSeedingTimeLimit = inactiveSeedingTimeLimit,
LastActivity = lastActivity == -1 ? DateTimeOffset.UtcNow.ToUnixTimeSeconds() : lastActivity
};
GivenTorrents(new List<QBittorrentTorrent>() { torrent });
@ -739,6 +745,50 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_not_be_removable_and_should_not_allow_move_files_if_max_inactive_seedingtime_reached_and_not_paused()
{
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
GivenCompletedTorrent("uploading", ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused()
{
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused()
{
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 40);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, inactiveSeedingTimeLimit: 10, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(15)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused()
{
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, inactiveSeedingTimeLimit: 40, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(30)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused()
{
@ -750,6 +800,17 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused()
{
GivenGlobalSeedLimits(2.0f, maxInactiveSeedingTime: 20);
GivenCompletedTorrent("pausedUP", ratio: 1.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_not_fetch_details_twice()
{

View File

@ -85,6 +85,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series Title - S01E01 - Girls Gone Wild Exposed (720p x265 EDGE2020).mkv", "EDGE2020")]
[TestCase("Series.Title.S01E02.1080p.BluRay.Remux.AVC.FLAC.2.0-E.N.D", "E.N.D")]
[TestCase("Show Name (2016) Season 1 S01 (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 5 1 RZeroX) QxR", "RZeroX")]
[TestCase("Series Title S01 1080p Blu-ray Remux AVC FLAC 2.0 - KRaLiMaRKo", "KRaLiMaRKo")]
[TestCase("Series Title S01 1080p Blu-ray Remux AVC DTS-HD MA 2.0 - BluDragon", "BluDragon")]
public void should_parse_exception_release_group(string title, string expected)
{
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);

View File

@ -10,6 +10,7 @@ using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Options;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Datastore;
@ -70,6 +71,11 @@ namespace NzbDrone.Core.Configuration
private readonly IDiskProvider _diskProvider;
private readonly ICached<string> _cache;
private readonly PostgresOptions _postgresOptions;
private readonly AuthOptions _authOptions;
private readonly AppOptions _appOptions;
private readonly ServerOptions _serverOptions;
private readonly UpdateOptions _updateOptions;
private readonly LogOptions _logOptions;
private readonly string _configFile;
private static readonly Regex HiddenCharacterRegex = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
@ -80,13 +86,23 @@ namespace NzbDrone.Core.Configuration
ICacheManager cacheManager,
IEventAggregator eventAggregator,
IDiskProvider diskProvider,
IOptions<PostgresOptions> postgresOptions)
IOptions<PostgresOptions> postgresOptions,
IOptions<AuthOptions> authOptions,
IOptions<AppOptions> appOptions,
IOptions<ServerOptions> serverOptions,
IOptions<UpdateOptions> updateOptions,
IOptions<LogOptions> logOptions)
{
_cache = cacheManager.GetCache<string>(GetType());
_eventAggregator = eventAggregator;
_diskProvider = diskProvider;
_configFile = appFolderInfo.GetConfigPath();
_postgresOptions = postgresOptions.Value;
_authOptions = authOptions.Value;
_appOptions = appOptions.Value;
_serverOptions = serverOptions.Value;
_updateOptions = updateOptions.Value;
_logOptions = logOptions.Value;
}
public Dictionary<string, object> GetConfigDictionary()
@ -142,7 +158,7 @@ namespace NzbDrone.Core.Configuration
{
const string defaultValue = "*";
var bindAddress = GetValue("BindAddress", defaultValue);
var bindAddress = _serverOptions.BindAddress ?? GetValue("BindAddress", defaultValue);
if (string.IsNullOrWhiteSpace(bindAddress))
{
return defaultValue;
@ -152,19 +168,19 @@ namespace NzbDrone.Core.Configuration
}
}
public int Port => GetValueInt("Port", 8989);
public int Port => _serverOptions.Port ?? GetValueInt("Port", 8989);
public int SslPort => GetValueInt("SslPort", 9898);
public int SslPort => _serverOptions.SslPort ?? GetValueInt("SslPort", 9898);
public bool EnableSsl => GetValueBoolean("EnableSsl", false);
public bool EnableSsl => _serverOptions.EnableSsl ?? GetValueBoolean("EnableSsl", false);
public bool LaunchBrowser => GetValueBoolean("LaunchBrowser", true);
public bool LaunchBrowser => _appOptions.LaunchBrowser ?? GetValueBoolean("LaunchBrowser", true);
public string ApiKey
{
get
{
var apiKey = GetValue("ApiKey", GenerateApiKey());
var apiKey = _authOptions.ApiKey ?? GetValue("ApiKey", GenerateApiKey());
if (apiKey.IsNullOrWhiteSpace())
{
@ -180,7 +196,7 @@ namespace NzbDrone.Core.Configuration
{
get
{
var enabled = GetValueBoolean("AuthenticationEnabled", false, false);
var enabled = _authOptions.Enabled ?? GetValueBoolean("AuthenticationEnabled", false, false);
if (enabled)
{
@ -188,20 +204,25 @@ namespace NzbDrone.Core.Configuration
return AuthenticationType.Basic;
}
return GetValueEnum("AuthenticationMethod", AuthenticationType.None);
return Enum.TryParse<AuthenticationType>(_authOptions.Method, out var enumValue)
? enumValue
: GetValueEnum("AuthenticationMethod", AuthenticationType.None);
}
}
public AuthenticationRequiredType AuthenticationRequired => GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
public AuthenticationRequiredType AuthenticationRequired =>
Enum.TryParse<AuthenticationRequiredType>(_authOptions.Required, out var enumValue)
? enumValue
: GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false);
public bool AnalyticsEnabled => _logOptions.AnalyticsEnabled ?? GetValueBoolean("AnalyticsEnabled", true, persist: false);
public string Branch => GetValue("Branch", "main").ToLowerInvariant();
public string Branch => _updateOptions.Branch ?? GetValue("Branch", "main").ToLowerInvariant();
public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant();
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "info").ToLowerInvariant();
public string ConsoleLogLevel => _logOptions.ConsoleLevel ?? GetValue("ConsoleLogLevel", string.Empty, persist: false);
public string Theme => GetValue("Theme", "auto", persist: false);
public string Theme => _appOptions.Theme ?? GetValue("Theme", "auto", persist: false);
public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false);
public string PostgresUser => _postgresOptions?.User ?? GetValue("PostgresUser", string.Empty, persist: false);
@ -210,17 +231,17 @@ namespace NzbDrone.Core.Configuration
public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "sonarr-log", persist: false);
public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false);
public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);
public string SslCertPath => GetValue("SslCertPath", "");
public string SslCertPassword => GetValue("SslCertPassword", "");
public bool LogSql => _logOptions.Sql ?? GetValueBoolean("LogSql", false, persist: false);
public int LogRotate => _logOptions.Rotate ?? GetValueInt("LogRotate", 50, persist: false);
public bool FilterSentryEvents => _logOptions.FilterSentryEvents ?? GetValueBoolean("FilterSentryEvents", true, persist: false);
public string SslCertPath => _serverOptions.SslCertPath ?? GetValue("SslCertPath", "");
public string SslCertPassword => _serverOptions.SslCertPassword ?? GetValue("SslCertPassword", "");
public string UrlBase
{
get
{
var urlBase = GetValue("UrlBase", "").Trim('/');
var urlBase = _serverOptions.UrlBase ?? GetValue("UrlBase", "").Trim('/');
if (urlBase.IsNullOrWhiteSpace())
{
@ -237,7 +258,7 @@ namespace NzbDrone.Core.Configuration
{
get
{
var instanceName = GetValue("InstanceName", BuildInfo.AppName);
var instanceName = _appOptions.InstanceName ?? GetValue("InstanceName", BuildInfo.AppName);
if (instanceName.StartsWith(BuildInfo.AppName) || instanceName.EndsWith(BuildInfo.AppName))
{
@ -248,17 +269,20 @@ namespace NzbDrone.Core.Configuration
}
}
public bool UpdateAutomatically => GetValueBoolean("UpdateAutomatically", false, false);
public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", false, false);
public UpdateMechanism UpdateMechanism => GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false);
public UpdateMechanism UpdateMechanism =>
Enum.TryParse<UpdateMechanism>(_updateOptions.Mechanism, out var enumValue)
? enumValue
: GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false);
public string UpdateScriptPath => GetValue("UpdateScriptPath", "", false);
public string UpdateScriptPath => _updateOptions.ScriptPath ?? GetValue("UpdateScriptPath", "", false);
public string SyslogServer => GetValue("SyslogServer", "", persist: false);
public string SyslogServer => _logOptions.SyslogServer ?? GetValue("SyslogServer", "", persist: false);
public int SyslogPort => GetValueInt("SyslogPort", 514, persist: false);
public int SyslogPort => _logOptions.SyslogPort ?? GetValueInt("SyslogPort", 514, persist: false);
public string SyslogLevel => GetValue("SyslogLevel", LogLevel, persist: false).ToLowerInvariant();
public string SyslogLevel => _logOptions.SyslogLevel ?? GetValue("SyslogLevel", LogLevel, persist: false).ToLowerInvariant();
public int GetValueInt(string key, int defaultValue, bool persist = true)
{

View File

@ -3,7 +3,7 @@ using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Datastore
{
public class CorruptDatabaseException : NzbDroneException
public class CorruptDatabaseException : SonarrStartupException
{
public CorruptDatabaseException(string message, params object[] args)
: base(message, args)
@ -16,12 +16,12 @@ namespace NzbDrone.Core.Datastore
}
public CorruptDatabaseException(string message, Exception innerException, params object[] args)
: base(message, innerException, args)
: base(innerException, message, args)
{
}
public CorruptDatabaseException(string message, Exception innerException)
: base(message, innerException)
: base(innerException, message)
{
}
}

View File

@ -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;
}
}

View File

@ -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; }
}
}

View File

@ -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
}
}

View File

@ -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..._

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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
{
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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));
}
}
}

View File

@ -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
}
}
```

View File

@ -630,7 +630,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
}
if (HasReachedSeedingTimeLimit(torrent, config))
if (HasReachedSeedingTimeLimit(torrent, config) || HasReachedInactiveSeedingTimeLimit(torrent, config))
{
return true;
}
@ -702,6 +702,26 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return false;
}
protected bool HasReachedInactiveSeedingTimeLimit(QBittorrentTorrent torrent, QBittorrentPreferences config)
{
long inactiveSeedingTimeLimit;
if (torrent.InactiveSeedingTimeLimit >= 0)
{
inactiveSeedingTimeLimit = torrent.InactiveSeedingTimeLimit * 60;
}
else if (torrent.InactiveSeedingTimeLimit == -2 && config.MaxInactiveSeedingTimeEnabled)
{
inactiveSeedingTimeLimit = config.MaxInactiveSeedingTime * 60;
}
else
{
return false;
}
return DateTimeOffset.UtcNow.ToUnixTimeSeconds() - torrent.LastActivity > inactiveSeedingTimeLimit;
}
protected void FetchTorrentDetails(QBittorrentTorrent torrent)
{
var torrentProperties = Proxy.GetTorrentProperties(torrent.Hash, Settings);

View File

@ -28,6 +28,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[JsonProperty(PropertyName = "max_seeding_time")]
public long MaxSeedingTime { get; set; } // Get the global share time limit in minutes
[JsonProperty(PropertyName = "max_inactive_seeding_time_enabled")]
public bool MaxInactiveSeedingTimeEnabled { get; set; } // True if share inactive time limit is enabled
[JsonProperty(PropertyName = "max_inactive_seeding_time")]
public long MaxInactiveSeedingTime { get; set; } // Get the global share inactive time limit in minutes
[JsonProperty(PropertyName = "max_ratio_act")]
public QBittorrentMaxRatioAction MaxRatioAction { get; set; } // Action performed when a torrent reaches the maximum share ratio.

View File

@ -37,6 +37,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[JsonProperty(PropertyName = "seeding_time_limit")] // Per torrent seeding time limit (-2 = use global, -1 = unlimited)
public long SeedingTimeLimit { get; set; } = -2;
[JsonProperty(PropertyName = "inactive_seeding_time_limit")] // Per torrent inactive seeding time limit (-2 = use global, -1 = unlimited)
public long InactiveSeedingTimeLimit { get; set; } = -2;
[JsonProperty(PropertyName = "last_activity")] // Timestamp in unix seconds when a chunk was last downloaded/uploaded
public long LastActivity { get; set; }
}
public class QBittorrentTorrentProperties

View File

@ -34,6 +34,7 @@ namespace NzbDrone.Core.Download
{
{ Result.HasHttpServerError: true } => PredicateResult.True(),
{ Result.StatusCode: HttpStatusCode.RequestTimeout } => PredicateResult.True(),
{ Exception: HttpException { Response.HasHttpServerError: true } } => PredicateResult.True(),
_ => PredicateResult.False()
},
Delay = TimeSpan.FromSeconds(3),

View File

@ -23,7 +23,7 @@
"CloneCondition": "Bedingung klonen",
"DeleteCondition": "Bedingung löschen",
"DeleteConditionMessageText": "Bist du sicher, dass du die Bedingung '{name}' löschen willst?",
"DeleteCustomFormatMessageText": "Bist du sicher, dass du das eigene Format '{name}' löschen willst?",
"DeleteCustomFormatMessageText": "Bist du sicher, dass du das benutzerdefinierte Format '{name}' wirklich löschen willst?",
"RemoveSelectedItemQueueMessageText": "Bist du sicher, dass du ein Eintrag aus der Warteschlange entfernen willst?",
"RemoveSelectedItemsQueueMessageText": "Bist du sicher, dass du {selectedCount} Einträge aus der Warteschlange entfernen willst?",
"DeleteSelectedDownloadClients": "Lösche Download Client(s)",
@ -146,7 +146,7 @@
"AuthenticationRequiredHelpText": "Ändern, welche anfragen Authentifizierung benötigen. Ändere nichts wenn du dir nicht des Risikos bewusst bist.",
"AnalyseVideoFilesHelpText": "Videoinformationen wie Auflösung, Laufzeit und Codec-Informationen aus Dateien extrahieren. Dies erfordert, dass {appName} Teile der Datei liest, was bei Scans zu hoher Festplatten- oder Netzwerkaktivität führen kann.",
"AnalyticsEnabledHelpText": "Senden Sie anonyme Nutzungs- und Fehlerinformationen an die Server von {appName}. Dazu gehören Informationen zu Ihrem Browser, welche {appName}-WebUI-Seiten Sie verwenden, Fehlerberichte sowie Betriebssystem- und Laufzeitversion. Wir werden diese Informationen verwenden, um Funktionen und Fehlerbehebungen zu priorisieren.",
"AutoTaggingNegateHelpText": "Falls aktiviert wird das eigene Format nicht angewendet solange diese {0} Bedingung zutrifft.",
"AutoTaggingNegateHelpText": "Falls aktiviert wird die Auto Tagging Regel nicht angewendet, solange diese Bedingung {implementationName} zutrifft.",
"CopyUsingHardlinksSeriesHelpText": "Mithilfe von Hardlinks kann {appName} Seeding-Torrents in den Serienordner importieren, ohne zusätzlichen Speicherplatz zu beanspruchen oder den gesamten Inhalt der Datei zu kopieren. Hardlinks funktionieren nur, wenn sich Quelle und Ziel auf demselben Volume befinden",
"DailyEpisodeTypeFormat": "Datum ({format})",
"DefaultDelayProfileSeries": "Dies ist das Standardprofil. Es gilt für alle Serien, die kein explizites Profil haben.",
@ -172,7 +172,7 @@
"BuiltIn": "Eingebaut",
"ChangeFileDate": "Ändern Sie das Dateidatum",
"CustomFormatsLoadError": "Eigene Formate konnten nicht geladen werden",
"DeleteQualityProfileMessageText": "Sind Sie sicher, dass Sie das Qualitätsprofil „{name}“ löschen möchten?",
"DeleteQualityProfileMessageText": "Bist du sicher, dass du das Qualitätsprofil '{name}' wirklich löschen willst?",
"DeletedReasonUpgrade": "Die Datei wurde gelöscht, um ein Upgrade zu importieren",
"DeleteEpisodesFiles": "{episodeFileCount} Episodendateien löschen",
"Seeders": "Seeders",
@ -185,7 +185,7 @@
"DeleteSelectedIndexersMessageText": "Sind Sie sicher, dass Sie {count} ausgewählte(n) Indexer löschen möchten?",
"DeleteSelectedSeries": "Ausgewählte Serie löschen",
"DeleteSpecification": "Spezifikation löschen",
"DeleteTagMessageText": "Sind Sie sicher, dass Sie das Tag „{label}“ löschen möchten?",
"DeleteTagMessageText": "Bist du sicher, dass du den Tag '{label}' wirklich löschen willst?",
"DeletedSeriesDescription": "Die Serie wurde aus TheTVDB gelöscht",
"DetailedProgressBar": "Detaillierter Fortschrittsbalken",
"DetailedProgressBarHelpText": "Text auf Fortschrittsbalken anzeigen",
@ -238,9 +238,9 @@
"AddingTag": "Tag hinzufügen",
"Apply": "Anwenden",
"Disabled": "Deaktiviert",
"ApplyTagsHelpTextHowToApplyImportLists": "So wenden Sie Tags auf die ausgewählten Importlisten an",
"ApplyTagsHelpTextHowToApplyDownloadClients": "So wenden Sie Tags auf die ausgewählten Download-Clients an",
"ApplyTagsHelpTextHowToApplyIndexers": "So wenden Sie Tags auf die ausgewählten Indexer an",
"ApplyTagsHelpTextHowToApplyImportLists": "Wie Tags den selektierten Importlisten hinzugefügt werden können",
"ApplyTagsHelpTextHowToApplyDownloadClients": "Wie Tags zu den selektierten Downloadclients hinzugefügt werden können",
"ApplyTagsHelpTextHowToApplyIndexers": "Wie Tags zu den selektierten Indexern hinzugefügt werden können",
"RestrictionsLoadError": "Einschränkungen können nicht geladen werden",
"SslCertPath": "SSL-Zertifikatpfad",
"TheTvdb": "TheTVDB",
@ -269,7 +269,7 @@
"Connection": "Verbindung",
"ConnectionLost": "Verbindung unterbrochen",
"Connections": "Verbindungen",
"ContinuingOnly": "Nur Fortsetzung",
"ContinuingOnly": "Nur fortlaufend",
"ContinuingSeriesDescription": "Weitere Episoden/eine weitere Staffel werden erwartet",
"CopyToClipboard": "In die Zwischenablage kopieren",
"CouldNotFindResults": "Es konnten keine Ergebnisse für „{term}“ gefunden werden.",
@ -300,18 +300,18 @@
"DeleteAutoTag": "Auto-Tag löschen",
"DeleteAutoTagHelpText": "Sind Sie sicher, dass Sie das automatische Tag „{name}“ löschen möchten?",
"DeleteBackup": "Sicherung löschen",
"DeleteBackupMessageText": "Sind Sie sicher, dass Sie die Sicherung „{name}“ löschen möchten?",
"DeleteBackupMessageText": "Soll das Backup '{name}' wirklich gelöscht werden?",
"DeleteCustomFormat": "Benutzerdefiniertes Format löschen",
"DeleteDelayProfileMessageText": "Sind Sie sicher, dass Sie dieses Verzögerungsprofil löschen möchten?",
"DeleteDownloadClient": "Download-Client löschen",
"DeleteDownloadClientMessageText": "Sind Sie sicher, dass Sie den Download-Client „{name}“ löschen möchten?",
"DeleteDownloadClientMessageText": "Bist du sicher, dass du den Download Client '{name}' wirklich löschen willst?",
"DeleteEmptyFolders": "Leere Ordner löschen",
"DeleteEpisodeFile": "Episodendatei löschen",
"DeleteEpisodeFileMessage": "Sind Sie sicher, dass Sie „{path}“ löschen möchten?",
"DeleteEpisodeFromDisk": "Episode von der Festplatte löschen",
"DeleteEpisodesFilesHelpText": "Löschen Sie die Episodendateien und den Serienordner",
"DeleteImportList": "Importliste löschen",
"DeleteIndexerMessageText": "Sind Sie sicher, dass Sie den Indexer „{name}“ löschen möchten?",
"DeleteIndexerMessageText": "Bist du sicher, dass du den Indexer '{name}' wirklich löschen willst?",
"DeleteQualityProfile": "Qualitätsprofil löschen",
"DeleteReleaseProfile": "Release-Profil löschen",
"DeleteReleaseProfileMessageText": "Sind Sie sicher, dass Sie dieses Release-Profil „{name}“ löschen möchten?",
@ -416,12 +416,12 @@
"DownloadClientSabnzbdValidationEnableDisableDateSorting": "Deaktivieren Sie die Datumssortierung",
"DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Sie müssen die Datumssortierung für die von {appName} verwendete Kategorie deaktivieren, um Importprobleme zu vermeiden. Gehen Sie zu Sabnzbd, um das Problem zu beheben.",
"DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Deaktivieren Sie die Filmsortierung",
"AllResultsAreHiddenByTheAppliedFilter": "Alle Ergebnisse werden durch den angewendeten Filter ausgeblendet",
"AllResultsAreHiddenByTheAppliedFilter": "Alle Resultate werden wegen des angewandten Filters nicht angezeigt",
"RegularExpressionsCanBeTested": "Reguläre Ausdrücke können [hier] getestet werden ({url}).",
"ReleaseSceneIndicatorUnknownSeries": "Unbekannte Folge oder Serie.",
"RemoveFilter": "Filter entfernen",
"RemoveFailedDownloadsHelpText": "Entfernen Sie fehlgeschlagene Downloads aus dem Download-Client-Verlauf",
"RemoveFromDownloadClient": "Vom Download-Client entfernen",
"RemoveFromDownloadClient": "Aus dem Download Client entfernen",
"RemoveFromBlocklist": "Aus der Sperrliste entfernen",
"Age": "Alter",
"All": "Alle",
@ -535,11 +535,11 @@
"WeekColumnHeader": "Spaltenüberschrift „Woche“.",
"Week": "Woche",
"XmlRpcPath": "XML-RPC-Pfad",
"WouldYouLikeToRestoreBackup": "Möchten Sie die Sicherung „{name}“ wiederherstellen?",
"WouldYouLikeToRestoreBackup": "Willst du das Backup '{name}' wiederherstellen?",
"WithFiles": "Mit Dateien",
"ApplyTagsHelpTextAdd": "Hinzufügen: Fügen Sie die Tags der vorhandenen Tag-Liste hinzu",
"ApplyTagsHelpTextRemove": "Entfernen: Die eingegebenen Tags entfernen",
"ApplyTagsHelpTextReplace": "Ersetzen: Ersetzen Sie die Tags durch die eingegebenen Tags (geben Sie keine Tags ein, um alle Tags zu löschen).",
"ApplyTagsHelpTextAdd": "Hinzufügen: Füge Tags zu den bestehenden Tags hinzu",
"ApplyTagsHelpTextRemove": "Entfernen: Entferne die hinterlegten Tags",
"ApplyTagsHelpTextReplace": "Ersetzen: Ersetze die Tags mit den eingegebenen Tags (keine Tags eingeben um alle Tags zu löschen)",
"Wanted": "Gesucht",
"ConnectionLostToBackend": "{appName} hat die Verbindung zum Backend verloren und muss neu geladen werden, um die Funktionalität wiederherzustellen.",
"Continuing": "Fortsetzung",
@ -585,7 +585,7 @@
"CollapseMultipleEpisodesHelpText": "Reduzieren Sie mehrere Episoden, die am selben Tag ausgestrahlt werden",
"Connect": "Verbinden",
"CreateEmptySeriesFolders": "Erstellen Sie leere Serienordner",
"DeleteNotificationMessageText": "Sind Sie sicher, dass Sie die Benachrichtigung „{name}“ löschen möchten?",
"DeleteNotificationMessageText": "Bist du sicher, dass du die Benachrichtigung '{name}' wirklich löschen willst?",
"Started": "Gestartet",
"AuthenticationMethod": "Authentifizierungsmethode",
"RemoveTagsAutomatically": "Tags automatisch entfernen",
@ -597,7 +597,7 @@
"CustomFormatJson": "Benutzerdefiniertes JSON-Format",
"DeleteDelayProfile": "Verzögerungsprofil löschen",
"DeleteIndexer": "Indexer löschen",
"DeleteImportListMessageText": "Sind Sie sicher, dass Sie die Liste „{name}“ löschen möchten?",
"DeleteImportListMessageText": "Bist du sicher, dass du die Liste '{name}' wirklich löschen willst?",
"Deleted": "Gelöscht",
"System": "System",
"RemoveFailed": "Entferne fehlgeschlagene",
@ -642,8 +642,8 @@
"CollapseAll": "Alles reduzieren",
"DeleteSeriesFolder": "Serienordner löschen",
"DeleteSeriesFolderConfirmation": "Der Serienordner „{path}“ und sein gesamter Inhalt werden gelöscht.",
"AddToDownloadQueue": "Zur Download-Warteschlange hinzufügen",
"AddedToDownloadQueue": "Zur Download-Warteschlange hinzugefügt",
"AddToDownloadQueue": "Zur Download Warteschlange hinzufügen",
"AddedToDownloadQueue": "Zur Download Warteschlange hinzugefügt",
"AirsDateAtTimeOn": "{date} um {time} auf {networkLabel}",
"AirsTbaOn": "TBA auf {networkLabel}",
"AirsTimeOn": "{time} auf {networkLabel}",
@ -787,5 +787,10 @@
"BlocklistOnly": "Nur der Sperrliste hinzufügen",
"BlocklistOnlyHint": "Der Sperrliste hinzufügen, ohne nach Alternative zu suchen",
"BlocklistReleaseHelpText": "Dieses Release für erneuten Download durch {appName} via RSS oder automatische Suche sperren",
"ChangeCategory": "Kategorie wechseln"
"ChangeCategory": "Kategorie wechseln",
"ReplaceIllegalCharactersHelpText": "Ersetze illegale Zeichen. Wenn nicht ausgewählt, werden sie stattdessen von {appName} entfernt",
"MediaManagement": "Medienverwaltung",
"StartupDirectory": "Start-Verzeichnis",
"OnRename": "Bei Umbenennung",
"MaintenanceRelease": "Maintenance Release: Fehlerbehebungen und andere Verbesserungen. Siehe Github Commit Verlauf für weitere Details"
}

View File

@ -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",

View File

@ -29,7 +29,7 @@
"AddImportList": "Añadir Lista de Importación",
"AddImportListExclusion": "Añadir Exclusión de Lista de Importación",
"AddImportListExclusionError": "No se pudo añadir una nueva exclusión de lista de importación, inténtelo de nuevo.",
"AddIndexerError": "No se pudo añadir un nuevo indexador, inténtelo de nuevo.",
"AddIndexerError": "No se pudo añadir un nuevo indexador, por favor inténtalo de nuevo.",
"AddList": "Añadir Lista",
"AddListExclusionError": "No se pudo añadir una nueva exclusión de lista, inténtelo de nuevo.",
"AddNotificationError": "No se pudo añadir una nueva notificación, inténtelo de nuevo.",
@ -53,7 +53,7 @@
"BindAddressHelpText": "Dirección IP4 válida, localhost o '*' para todas las interfaces",
"BindAddress": "Dirección de Ligado",
"Branch": "Rama",
"BuiltIn": "Incorporado",
"BuiltIn": "Integrado",
"Condition": "Condición",
"Component": "Componente",
"Custom": "Personalizado",
@ -76,9 +76,9 @@
"Trace": "Rastro",
"TvdbId": "TVDB ID",
"Torrents": "Torrents",
"Ui": "UI",
"Ui": "Interfaz",
"Underscore": "Guion bajo",
"UpdateMechanismHelpText": "Usar el actualizador incorporado de {appName} o un script",
"UpdateMechanismHelpText": "Usar el actualizador integrado de {appName} o un script",
"Warn": "Advertencia",
"AutoTagging": "Etiquetado Automático",
"AddAutoTag": "Añadir Etiqueta Automática",
@ -173,7 +173,7 @@
"Season": "Temporada",
"Clone": "Clonar",
"Connections": "Conexiones",
"Dash": "Guión",
"Dash": "Guion",
"AnalyticsEnabledHelpText": "Envíe información anónima de uso y error a los servidores de {appName}. Esto incluye información sobre su navegador, qué páginas de {appName} WebUI utiliza, informes de errores, así como el sistema operativo y la versión en tiempo de ejecución. Usaremos esta información para priorizar funciones y correcciones de errores.",
"BackupIntervalHelpText": "Intervalo entre copias de seguridad automáticas",
"BackupRetentionHelpText": "Las copias de seguridad automáticas anteriores al período de retención serán borradas automáticamente",
@ -185,7 +185,7 @@
"AddNewSeriesSearchForMissingEpisodes": "Empezar búsqueda de episodios faltantes",
"AddQualityProfile": "Añadir Perfil de Calidad",
"AddQualityProfileError": "No se pudo añadir un nuevo perfil de calidad, inténtelo de nuevo.",
"AddReleaseProfile": "Añadir Perfil de Lanzamiento",
"AddReleaseProfile": "Añadir perfil de lanzamiento",
"AddSeriesWithTitle": "Añadir {title}",
"AfterManualRefresh": "Tras Refrescar Manualmente",
"AllSeriesInRootFolderHaveBeenImported": "Todas las series en {path} han sido importadas",
@ -216,7 +216,7 @@
"ImportScriptPath": "Importar Ruta de Script",
"Absolute": "Absoluto",
"AddANewPath": "Añadir una nueva ruta",
"AddConditionImplementation": "Añadir Condición - {implementationName}",
"AddConditionImplementation": "Añadir condición - {implementationName}",
"AppUpdated": "{appName} Actualizado",
"AutomaticUpdatesDisabledDocker": "Las actualizaciones automáticas no están soportadas directamente cuando se utiliza el mecanismo de actualización de Docker. Tendrá que actualizar la imagen del contenedor fuera de {appName} o utilizar un script",
"AuthenticationRequiredHelpText": "Cambiar para que las solicitudes requieran autenticación. No lo cambie a menos que entienda los riesgos.",
@ -233,8 +233,8 @@
"CountIndexersSelected": "{count} indexador(es) seleccionado(s)",
"CouldNotFindResults": "No se pudieron encontrar resultados para '{term}'",
"CountImportListsSelected": "{count} lista(s) de importación seleccionada(s)",
"DelayingDownloadUntil": "Retrasar la descarga hasta {date} a {time}",
"DeleteIndexerMessageText": "Seguro que quieres eliminar el indexer '{name}'?",
"DelayingDownloadUntil": "Retrasar la descarga hasta el {date} a las {time}",
"DeleteIndexerMessageText": "¿Estás seguro que quieres eliminar el indexador '{name}'?",
"BlocklistLoadError": "No se ha podido cargar la lista de bloqueos",
"BypassDelayIfAboveCustomFormatScore": "Omitir si está por encima de la puntuación del formato personalizado",
"BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Puntuación mínima de formato personalizado necesaria para evitar el retraso del protocolo preferido",
@ -245,8 +245,8 @@
"DeleteConditionMessageText": "Seguro que quieres eliminar la etiqueta '{name}'?",
"DeleteQualityProfileMessageText": "¿Seguro que quieres eliminar el perfil de calidad {name}?",
"DeleteRootFolderMessageText": "¿Está seguro de querer eliminar la carpeta raíz '{path}'?",
"DeleteSelectedDownloadClientsMessageText": "¿Está seguro de querer eliminar {count} cliente(s) de descarga seleccionado(s)?",
"ConnectionLostToBackend": "{appName} ha perdido su conexión con el backend y tendrá que ser recargado para recuperar su funcionalidad.",
"DeleteSelectedDownloadClientsMessageText": "¿Estás seguro que quieres eliminar {count} cliente(s) de descarga seleccionado(s)?",
"ConnectionLostToBackend": "{appName} ha perdido su conexión con el backend y tendrá que ser recargado para restaurar su funcionalidad.",
"CalendarOptions": "Opciones de Calendario",
"BypassDelayIfAboveCustomFormatScoreHelpText": "Habilitar ignorar cuando la versión tenga una puntuación superior a la puntuación mínima configurada para el formato personalizado",
"Default": "Por defecto",
@ -255,15 +255,15 @@
"AddImportListImplementation": "Añadir lista de importación - {implementationName}",
"AddIndexerImplementation": "Agregar Indexador - {implementationName}",
"AutoRedownloadFailed": "Descarga fallida",
"ConnectionLostReconnect": "{appName} intentará conectarse automáticamente, o puede hacer clic en recargar abajo.",
"ConnectionLostReconnect": "{appName} intentará conectarse automáticamente, o puedes pulsar en recargar abajo.",
"CustomFormatJson": "Formato JSON personalizado",
"CountDownloadClientsSelected": "{count} cliente(s) de descarga seleccionado(s)",
"DeleteImportList": "Eliminar Lista(s) de Importación",
"DeleteImportList": "Eliminar lista de importación",
"DeleteImportListMessageText": "Seguro que quieres eliminar la lista '{name}'?",
"AutoRedownloadFailedFromInteractiveSearchHelpText": "Búsqueda automática e intento de descarga de una versión diferente cuando se obtiene una versión fallida de la búsqueda interactiva",
"AutoRedownloadFailedFromInteractiveSearch": "Fallo al volver a descargar desde la búsqueda interactiva",
"DeleteSelectedIndexersMessageText": "¿Está seguro de querer eliminar {count} indexador(es) seleccionado(s)?",
"DeleteSelectedImportListsMessageText": "Seguro que quieres eliminar {count} lista(s) de importación seleccionada(s)?",
"DeleteSelectedIndexersMessageText": "¿Estás seguro que quieres eliminar {count} indexador(es) seleccionado(s)?",
"DeleteSelectedImportListsMessageText": "¿Estás seguro que quieres eliminar {count} lista(s) de importación seleccionada(s)?",
"DeletedReasonUpgrade": "Se ha borrado el archivo para importar una versión mejorada",
"DeleteTagMessageText": "¿Está seguro de querer eliminar la etiqueta '{label}'?",
"DisabledForLocalAddresses": "Deshabilitado para Direcciones Locales",
@ -311,7 +311,7 @@
"CheckDownloadClientForDetails": "Revisar el cliente de descarga para más detalles",
"ChooseAnotherFolder": "Elige otra Carpeta",
"ClientPriority": "Prioridad del Cliente",
"CloneIndexer": "Clonar Indexer",
"CloneIndexer": "Clonar indexador",
"BranchUpdateMechanism": "La rama se uso por un mecanisco de actualizacion externo",
"BrowserReloadRequired": "Se requiere recargar el explorador",
"CalendarLoadError": "Incapaz de cargar el calendario",
@ -332,7 +332,7 @@
"WeekColumnHeaderHelpText": "Mostrado sobre cada columna cuando la vista activa es semana",
"WhyCantIFindMyShow": "Por que no puedo encontrar mi serie?",
"WouldYouLikeToRestoreBackup": "Te gustaria restaurar la copia de seguridad '{name}'?",
"ClickToChangeReleaseGroup": "Clic para cambiar el grupo de lanzamiento",
"ClickToChangeReleaseGroup": "Pulsa para cambiar el grupo de lanzamiento",
"ClickToChangeSeries": "Click para cambiar la serie",
"ColonReplacement": "Reemplazar dos puntos",
"CollapseMultipleEpisodes": "Colapsar episodios multiples",
@ -344,10 +344,10 @@
"CopyUsingHardlinksHelpTextWarning": "Ocasionalmente, los archivos bloqueados impiden renombrar los archivos que están siendo sembrados. Puedes desactivar temporalmente la siembra y usar la función de renombrado de {appName} como alternativa.",
"CurrentlyInstalled": "Actualmente instalado",
"CustomFilters": "Filtros Personalizados",
"CustomFormat": "Formatos Personalizados",
"CustomFormat": "Formato personalizado",
"CustomFormatHelpText": "{appName} puntúa cada lanzamiento utilizando la suma de puntuaciones para hacer coincidir formatos personalizados. Si una nueva versión mejorara la puntuación, con la misma o mejor calidad, {appName} la tomará.",
"CustomFormatUnknownConditionOption": "Opción Desconocida '{key}' para condición '{implementation}'",
"CustomFormatScore": "Puntuación de Formato personalizado",
"CustomFormatScore": "Puntuación de formato personalizado",
"Connection": "Conexiones",
"CancelPendingTask": "Estas seguro de que deseas cancelar esta tarea pendiente?",
"Clear": "Borrar",
@ -356,15 +356,15 @@
"Cancel": "Cancelar",
"ChangeFileDate": "Cambiar fecha de archivo",
"CertificateValidationHelpText": "Cambiar como es la validacion de la certificacion estricta de HTTPS. No cambiar a menos que entiendas las consecuencias.",
"AddListExclusion": "Agregar Lista de Exclusión",
"AddListExclusion": "Añadir lista de exclusión",
"AddedDate": "Agregado: {date}",
"AllSeriesAreHiddenByTheAppliedFilter": "Todos los resultados estan ocultos por el filtro aplicado",
"AlternateTitles": "Titulos alternativos",
"ChmodFolderHelpText": "Octal, aplicado durante la importación / cambio de nombre a carpetas y archivos multimedia (sin bits de ejecución)",
"AddedToDownloadQueue": "Agregado a la cola de descarga",
"AddedToDownloadQueue": "Añadido a la cola de descarga",
"AirsTimeOn": "{time} en{networkLabel}",
"AirsDateAtTimeOn": "{date} en {time} en{networkLabel}",
"AddToDownloadQueue": "Agregar a la cola de descarga",
"AddToDownloadQueue": "Añadir a la cola de descarga",
"Airs": "Emision",
"AirsTbaOn": "A anunciar en {networkLabel}",
"AllFiles": "Todos los archivos",
@ -380,17 +380,17 @@
"CalendarLegendEpisodeUnmonitoredTooltip": "El episodio esta sin monitorizar",
"AnimeEpisodeTypeFormat": "Numero de episodio absoluto ({format})",
"BypassDelayIfHighestQualityHelpText": "Evitar el retardo cuando el lanzamiento tiene habilitada la máxima calidad en el perfil de calidad con el protocolo preferido",
"CalendarFeed": "Feed de calendario de {appName}",
"CalendarFeed": "Canal de calendario de {appName}",
"ChooseImportMode": "Elegir Modo de Importación",
"ClickToChangeLanguage": "Clic para cambiar el idioma",
"ClickToChangeQuality": "Clic para cambiar la calidad",
"ClickToChangeSeason": "Click para cambiar la temporada",
"ChmodFolderHelpTextWarning": "Esto solo funciona si el usuario que ejecuta {appName} es el propietario del archivo. Es mejor asegurarse de que el cliente de descarga establezca los permisos correctamente.",
"BlackholeWatchFolderHelpText": "Carpeta desde donde {appName} debera importar las descargas completas",
"BlackholeWatchFolderHelpText": "Carpeta desde la que {appName} debería importar las descargas completadas",
"BlackholeFolderHelpText": "La carpeta en donde {appName} se almacenaran los {extension} file",
"CancelProcessing": "Procesando cancelacion",
"Category": "Categoria",
"WhatsNew": "Que es lo nuevo?",
"Category": "Categoría",
"WhatsNew": "¿Qué hay nuevo?",
"BlocklistReleases": "Lista de bloqueos de lanzamientos",
"BypassDelayIfHighestQuality": "Pasar sí es la calidad más alta",
"ChownGroupHelpTextWarning": "Esto solo funciona si el usuario que ejecuta {appName} es el propietario del archivo. Es mejor asegurarse de que el cliente de descarga use el mismo grupo que {appName}.",
@ -427,7 +427,7 @@
"Daily": "Diario",
"CollapseMultipleEpisodesHelpText": "Contraer varios episodios que se emiten el mismo día",
"ContinuingOnly": "Solo continuando",
"ConditionUsingRegularExpressions": "Esta condición coincide con el uso de expresiones regulares. Tenga en cuenta que los caracteres `\\^$.|?*+()[{` tienen significados especiales y deben escaparse con un `\\`",
"ConditionUsingRegularExpressions": "Esta condición coincide usando expresiones regulares. Ten en cuenta que los caracteres `\\^$.|?*+()[{` tienen significados especiales y necesitan ser escapados con un `\\`",
"CountSelectedFiles": "{selectedCount} archivos seleccionados",
"CreateEmptySeriesFolders": "Crear carpetas de series vacías",
"CountSelectedFile": "{selectedCount} archivo seleccionado",
@ -439,7 +439,7 @@
"CutoffUnmet": "Umbrales no alcanzados",
"DailyEpisodeFormat": "Formato de episodio diario",
"Database": "Base de datos",
"DelayMinutes": "{delay} Minutos",
"DelayMinutes": "{delay} minutos",
"Continuing": "Continua",
"CustomFormats": "Formatos personalizados",
"AddRootFolderError": "No se pudoagregar la carpeta raíz",
@ -465,7 +465,7 @@
"InteractiveImportNoQuality": "La calidad debe elegirse para cada archivo seleccionado",
"InteractiveSearchModalHeader": "Búsqueda Interactiva",
"InvalidUILanguage": "Su interfaz de usuario está configurada en un idioma no válido, corríjalo y guarde la configuración",
"ChownGroup": "Cambiar grupo propietario",
"ChownGroup": "chown grupo",
"DelayProfileProtocol": "Protocolo: {preferredProtocol}",
"DelayProfilesLoadError": "Incapaz de cargar Perfiles de Retardo",
"ContinuingSeriesDescription": "Se esperan más episodios u otra temporada",
@ -476,16 +476,16 @@
"DeleteDelayProfile": "Eliminar Perfil de Retardo",
"DownloadClientQbittorrentSettingsContentLayoutHelpText": "Si usar el diseño de contenido configurado de qBittorrent, el diseño original del torrent o siempre crear una subcarpeta (qBittorrent 4.3.2+)",
"DelayProfiles": "Perfiles de retardo",
"DeleteCustomFormatMessageText": "¿Estás seguro de que quieres eliminar el formato personalizado '{name}'?",
"DeleteCustomFormatMessageText": "¿Estás seguro que quieres eliminar el formato personalizado '{name}'?",
"DeleteBackup": "Eliminar copia de seguridad",
"CopyUsingHardlinksSeriesHelpText": "Los hardlinks permiten a {appName} a importar los torrents que se estén compartiendo a la carpeta de la serie sin usar espacio adicional en el disco o sin copiar el contenido completo del archivo. Los hardlinks solo funcionarán si el origen y el destino están en el mismo volumen",
"DefaultDelayProfileSeries": "Este es el perfil por defecto. Aplica a todas las series que no tienen un perfil explícito.",
"DelayProfileSeriesTagsHelpText": "Aplica a series con al menos una etiqueta coincidente",
"DeleteCustomFormat": "Eliminar Formato Personalizado",
"BlackholeWatchFolder": "Monitorizar Carpeta",
"DeleteCustomFormat": "Eliminar formato personalizado",
"BlackholeWatchFolder": "Monitorizar carpeta",
"DeleteEmptyFolders": "Eliminar directorios vacíos",
"DeleteNotification": "Borrar Notificacion",
"DeleteReleaseProfile": "Borrar perfil de estreno",
"DeleteReleaseProfile": "Eliminar perfil de lanzamiento",
"Details": "Detalles",
"DeleteDownloadClient": "Borrar cliente de descarga",
"DeleteSelectedSeries": "Eliminar serie seleccionada",
@ -496,7 +496,7 @@
"DeleteEpisodesFiles": "Eliminar {episodeFileCount} archivos de episodios",
"DeleteImportListExclusionMessageText": "¿Está seguro de que desea eliminar esta exclusión de la lista de importación?",
"DeleteQualityProfile": "Borrar perfil de calidad",
"DeleteReleaseProfileMessageText": "Esta seguro que quiere borrar este perfil de estreno? '{name}'?",
"DeleteReleaseProfileMessageText": "¿Estás seguro que quieres eliminar este perfil de lanzamiento '{name}'?",
"DeleteRemotePathMapping": "Borrar mapeo de ruta remota",
"DeleteSelectedEpisodeFiles": "Borrar los archivos de episodios seleccionados",
"DeleteSelectedEpisodeFilesHelpText": "Esta seguro que desea borrar los archivos de episodios seleccionados?",
@ -506,15 +506,15 @@
"DeleteSeriesFolderHelpText": "Eliminar el directorio de series y sus contenidos",
"DeleteSeriesFoldersHelpText": "Eliminar los directorios de series y sus contenidos",
"DeleteSeriesModalHeader": "Borrar - {title}",
"DeleteSpecification": "Borrar especificacion",
"DeleteSpecification": "Eliminar especificación",
"Directory": "Directorio",
"Disabled": "Deshabilitado",
"Discord": "Discord",
"DiskSpace": "Espacio en Disco",
"DeleteSpecificationHelpText": "Esta seguro que desea borrar la especificacion '{name}'?",
"DeleteSelectedIndexers": "Borrar indexer(s)",
"DeleteIndexer": "Borrar Indexer",
"DeleteSelectedDownloadClients": "Borrar Cliente de Descarga(s)",
"DeleteSpecificationHelpText": "¿Estás seguro que quieres eliminar la especificación '{name}'?",
"DeleteSelectedIndexers": "Borrar indexador(es)",
"DeleteIndexer": "Borrar indexador",
"DeleteSelectedDownloadClients": "Borrar cliente(s) de descarga",
"DeleteSeriesFolderCountConfirmation": "Esta seguro que desea eliminar '{count}' series seleccionadas?",
"DeleteSeriesFolders": "Eliminar directorios de series",
"DeletedSeriesDescription": "Serie fue eliminada de TheTVDB",
@ -533,14 +533,14 @@
"IndexerSettingsSeedRatioHelpText": "El ratio que un torrent debería alcanzar antes de detenerse, vacío usa el predeterminado del cliente de descarga. El ratio debería ser al menos 1.0 y seguir las reglas de los indexadores",
"Download": "Descargar",
"Donate": "Donar",
"DownloadClientDelugeValidationLabelPluginFailure": "Falló la configuración de la etiqueta",
"DownloadClientDelugeTorrentStateError": "Deluge está informando de un error",
"DownloadClientDownloadStationValidationFolderMissing": "No existe la carpeta",
"DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Debes iniciar sesión en tu Diskstation como {username} y configurarlo manualmente en los ajustes de DownloadStation en BT/HTTP/FTP/NZB -> Ubicación.",
"DownloadClientDelugeValidationLabelPluginFailure": "La configuración de etiqueta falló",
"DownloadClientDelugeTorrentStateError": "Deluge está reportando un error",
"DownloadClientDownloadStationValidationFolderMissing": "La carpeta no existe",
"DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Debes iniciar sesión en tu Diskstation como {username} y configurarlo manualmente en las opciones de DownloadStation en BT/HTTP/FTP/NZB -> Ubicación.",
"DownloadClientFreeboxSettingsAppIdHelpText": "ID de la app dada cuando se crea acceso a la API de Freebox (esto es 'app_id')",
"DownloadClientFreeboxSettingsAppToken": "Token de la app",
"DownloadClientFreeboxUnableToReachFreebox": "No es posible acceder a la API de Freebox. Verifica las opciones 'Host', 'Puerto' o 'Usar SSL'. (Error: {exceptionMessage})",
"DownloadClientNzbVortexMultipleFilesMessage": "La descarga contiene varios archivos y no está en una carpeta de trabajo: {outputPath}",
"DownloadClientFreeboxUnableToReachFreebox": "No se pudo alcanzar la API de Freebox. Verifica las opciones 'Host', 'Puerto' o 'Usar SSL'. (Error: {exceptionMessage})",
"DownloadClientNzbVortexMultipleFilesMessage": "La descarga contiene múltiples archivos y no está en una carpeta de trabajo: {outputPath}",
"DownloadClientNzbgetSettingsAddPausedHelpText": "Esta opción requiere al menos NzbGet versión 16.0",
"DownloadClientOptionsLoadError": "No es posible cargar las opciones del cliente de descarga",
"DownloadClientPneumaticSettingsNzbFolderHelpText": "Esta carpeta necesitará ser alcanzable desde XBMC",
@ -571,33 +571,33 @@
"DownloadClientCheckNoneAvailableHealthCheckMessage": "Ningún cliente de descarga disponible",
"DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "No es posible comunicarse con {downloadClientName}. {errorMessage}",
"DownloadClientDelugeSettingsUrlBaseHelpText": "Añade un prefijo al url del json de deluge, vea {url}",
"DownloadClientDownloadStationValidationApiVersion": "Versión de la API de la Estación de Descarga no soportada, debería ser al menos {requiredVersion}. Soporte desde {minVersion} hasta {maxVersion}",
"DownloadClientDownloadStationValidationFolderMissingDetail": "No existe la carpeta '{downloadDir}', debe ser creada manualmente dentro de la Carpeta Compartida '{sharedFolder}'.",
"DownloadClientDownloadStationValidationApiVersion": "Versión API de estación de descarga no soportada, debería ser al menos {requiredVersion}. Soporta desde {minVersion} hasta {maxVersion}",
"DownloadClientDownloadStationValidationFolderMissingDetail": "La carpeta '{downloadDir}' no existe, debe ser creada manualmente dentro de la carpeta compartida '{sharedFolder}'.",
"DownloadClientDownloadStationValidationNoDefaultDestination": "Sin destino predeterminado",
"DownloadClientFreeboxNotLoggedIn": "No ha iniciado sesión",
"DownloadClientFreeboxNotLoggedIn": "Sin sesión iniciada",
"DownloadClientFreeboxSettingsApiUrl": "URL de API",
"DownloadClientFreeboxSettingsApiUrlHelpText": "Define la URL base de la API Freebox con la versión de la API, p. ej. '{url}', por defecto a '{defaultApiUrl}'",
"DownloadClientFreeboxSettingsAppId": "ID de la app",
"DownloadClientFreeboxSettingsAppTokenHelpText": "Token de la app recuperado cuando se crea acceso a la API de Freebox (esto es 'app_token')",
"DownloadClientFreeboxSettingsPortHelpText": "Puerto usado para acceder a la interfaz de Freebox, predeterminado a '{port}'",
"DownloadClientFreeboxSettingsHostHelpText": "Nombre de host o dirección IP de host del Freebox, predeterminado a '{url}' (solo funcionará en la misma red)",
"DownloadClientFreeboxUnableToReachFreeboxApi": "No es posible acceder a la API de Freebox. Verifica la configuración 'URL de la API' para la URL base y la versión.",
"DownloadClientNzbgetValidationKeepHistoryOverMax": "La opción KeepHistory de NZBGet debería ser menor de 25000",
"DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "La opción KeepHistory de NzbGet está establecida demasiado alta.",
"DownloadClientFreeboxUnableToReachFreeboxApi": "No se pudo alcanzar la API de Freebox. Verifica la opción 'URL de la API' para la URL base y la versión.",
"DownloadClientNzbgetValidationKeepHistoryOverMax": "La configuración KeepHistory de NzbGet debería ser menor de 25000",
"DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "La configuración KeepHistory de NzbGet está establecida demasiado alta.",
"UpgradeUntilEpisodeHelpText": "Una vez alcanzada esta calidad {appName} no se descargarán más episodios",
"DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} no pudo añadir la etiqueta a {clientName}.",
"DownloadClientDownloadStationProviderMessage": "{appName} no pudo conectarse a la Estación de Descarga si la Autenticación de 2 factores está habilitada en su cuenta de DSM",
"DownloadClientDownloadStationSettingsDirectoryHelpText": "Carpeta compartida opcional en la que poner las descargas, dejar en blanco para usar la ubicación de la Estación de Descarga predeterminada",
"DownloadClientPriorityHelpText": "Prioridad del cliente de descarga desde 1 (la más alta) hasta 50 (la más baja). Por defecto: 1. Se usa Round-Robin para clientes con la misma prioridad.",
"DownloadClientDelugeValidationLabelPluginInactive": "Extensión de etiqueta no activada",
"DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent está configurado para eliminar torrents cuando alcanzan su Límite de Ratio de Compartición",
"DownloadClientDelugeValidationLabelPluginInactive": "Plugin de etiqueta no activado",
"DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent está configurado para eliminar torrents cuando alcanzan su Límite de ratio de compartición",
"UpgradeUntilCustomFormatScoreEpisodeHelpText": "Una vez alcanzada esta puntuación de formato personalizada {appName} no capturará más lanzamientos de episodios",
"IndexerValidationRequestLimitReached": "Límite de petición alcanzado: {exceptionMessage}",
"DownloadClientDelugeValidationLabelPluginInactiveDetail": "Debes tener la Extensión de etiqueta habilitada en {clientName} para usar categorías.",
"DownloadClientDelugeValidationLabelPluginInactiveDetail": "Debes tener el plugir de etiqueta habilitado en {clientName} para usar categorías.",
"DownloadClientAriaSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación de Aria2 predeterminada",
"DownloadClientNzbgetValidationKeepHistoryZero": "La opción KeepHistory de NzbGet debería ser mayor que 0",
"DownloadClientNzbgetValidationKeepHistoryZero": "La configuración KeepHistory de NzbGet debería ser mayor de 0",
"DownloadClientNzbgetValidationKeepHistoryZeroDetail": "La configuración KeepHistory de NzbGet está establecida a 0. Esto evita que {appName} vea las descargas completadas.",
"DownloadClientDownloadStationValidationSharedFolderMissing": "No existe la carpeta compartida",
"DownloadClientDownloadStationValidationSharedFolderMissing": "La carpeta compartida no existe",
"DownloadPropersAndRepacksHelpText": "Decidir si automáticamente actualizar a Propers/Repacks",
"EditListExclusion": "Editar exclusión de lista",
"EnableAutomaticAdd": "Habilitar añadido automático",
@ -612,28 +612,28 @@
"EnableAutomaticAddSeriesHelpText": "Añade series de esta lista a {appName} cuando las sincronizaciones se llevan a cabo vía interfaz de usuario o por {appName}",
"EditReleaseProfile": "Editar perfil de lanzamiento",
"DownloadClientPneumaticSettingsStrmFolder": "Carpeta de Strm",
"DownloadClientQbittorrentValidationCategoryAddFailure": "Falló la configuración de categoría",
"DownloadClientQbittorrentValidationCategoryAddFailure": "La configuración de categoría falló",
"DownloadClientRTorrentSettingsUrlPath": "Ruta de url",
"DownloadClientSabnzbdValidationDevelopVersion": "Versión de desarrollo de Sabnzbd, asumiendo versión 3.0.0 o superior.",
"DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Usar 'Verificar antes de descargar' afecta a la habilidad de {appName} de rastrear nuevas descargas. Sabnzbd también recomienda 'Abortar trabajos que no pueden ser completados' en su lugar ya que resulta más efectivo.",
"DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Usar 'Verificar antes de descargar' afecta a la habilidad de {appName} para rastrear nuevas descargas. Sabnzbd recomienda 'Abortar trabajos que no pueden ser completados' en su lugar ya que es más efectivo.",
"DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName} puede no ser capaz de soportar nuevas características añadidas a SABnzbd cuando se ejecutan versiones de desarrollo.",
"DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Debe deshabilitar la ordenación por fechas para la categoría que {appName} usa para evitar problemas al importar. Vaya a Sabnzbd para arreglarlo.",
"DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Debes deshabilitar el ordenamiento de fecha para la categoría que {appName} usa para evitar problemas al importar. Ve a Sabnzbd para corregirlo.",
"DownloadClientSabnzbdValidationEnableJobFolders": "Habilitar carpetas de trabajo",
"DownloadClientSettingsUrlBaseHelpText": "Añade un prefijo a la url {clientName}, como {url}",
"DownloadClientStatusAllClientHealthCheckMessage": "Ningún cliente de descarga está disponible debido a fallos",
"DownloadClientValidationGroupMissing": "El grupo no existe",
"DownloadClientValidationSslConnectFailure": "No es posible conectarse a través de SSL",
"DownloadClientValidationSslConnectFailure": "No se pudo conectar a través de SSL",
"DownloadClientsSettingsSummary": "Clientes de descarga, manejo de descarga y mapeo de rutas remotas",
"EditSelectedSeries": "Editar series seleccionadas",
"EnableAutomaticSearchHelpTextWarning": "Será usado cuando se use la búsqueda interactiva",
"EnableColorImpairedMode": "Habilitar Modo de dificultad con los colores",
"EnableMetadataHelpText": "Habilitar la creación de un fichero de metadatos para este tipo de metadato",
"DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Las categorías no están soportadas hasta qBittorrent versión 3.3.0. Por favor, actualice o inténtelo de nuevo con una categoría vacía.",
"DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Las categorías no están soportadas por debajo de la versión 3.3.0 de qBittorrent. Por favor actualiza o inténtalo de nuevo con una categoría vacía.",
"DownloadClientValidationCategoryMissing": "La categoría no existe",
"DownloadClientValidationGroupMissingDetail": "El grupo que introdujo no existe en {clientName}. Créelo primero en {clientName}",
"DownloadClientValidationGroupMissingDetail": "El grupo que introdujiste no existe en {clientName}. Créalo en {clientName} primero.",
"DownloadClientValidationTestNzbs": "Fallo al obtener la lista de NZBs: {exceptionMessage}",
"DownloadClientValidationUnableToConnectDetail": "Por favor, verifique el nombre de host y el puerto.",
"DownloadClientValidationUnableToConnect": "No es posible conectarse a {clientName}",
"DownloadClientValidationUnableToConnectDetail": "Por favor verifica el nombre de host y el puerto.",
"DownloadClientValidationUnableToConnect": "No se pudo conectar a {clientName}",
"DownloadPropersAndRepacksHelpTextWarning": "Usar formatos personalizados para actualizaciones automáticas a propers/repacks",
"DownloadStationStatusExtracting": "Extrayendo: {progress}%",
"EditConnectionImplementation": "Editar Conexión - {implementationName}",
@ -642,49 +642,49 @@
"DoneEditingGroups": "Terminado de editar grupos",
"DownloadClientFloodSettingsAdditionalTags": "Etiquetas adicionales",
"DownloadClientFloodSettingsAdditionalTagsHelpText": "Añade propiedades de medios como etiquetas. Los consejos son ejemplos.",
"DownloadClientFloodSettingsPostImportTagsHelpText": "Añade etiquetas después de que se importe una descarga.",
"DownloadClientFloodSettingsRemovalInfo": "{appName} manejará la eliminación automática de torrents basada en el criterio de sembrado actual en Ajustes -> Indexadores",
"DownloadClientFloodSettingsPostImportTags": "Etiquetas tras importación",
"DownloadClientFloodSettingsPostImportTagsHelpText": "Añadir etiquetas una vez una descarga sea importada.",
"DownloadClientFloodSettingsRemovalInfo": "{appName} manejará la eliminación automática de torrents basados en los criterios de sembrado actuales en Opciones -> Indexadores",
"DownloadClientFloodSettingsPostImportTags": "Etiquetas post-importación",
"DownloadClientFloodSettingsTagsHelpText": "Etiquetas iniciales de una descarga. Para ser reconocida, una descarga debe tener todas las etiquetas iniciales. Esto evita conflictos con descargas no relacionadas.",
"DownloadClientFloodSettingsStartOnAdd": "Inicial al añadir",
"DownloadClientFloodSettingsStartOnAdd": "Iniciar al añadir",
"DownloadClientFreeboxApiError": "La API de Freebox devolvió el error: {errorDescription}",
"DownloadClientFreeboxAuthenticationError": "La autenticación a la API de Freebox falló. El motivo: {errorDescription}",
"DownloadClientFreeboxAuthenticationError": "La autenticación a la API de Freebox falló. Motivo: {errorDescription}",
"DownloadClientPneumaticSettingsStrmFolderHelpText": "Los archivos .strm en esta carpeta será importados por drone",
"DownloadClientQbittorrentTorrentStateError": "qBittorrent está informando de un error",
"DownloadClientQbittorrentTorrentStateError": "qBittorrent está reportando un error",
"DownloadClientQbittorrentSettingsSequentialOrder": "Orden secuencial",
"DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent no puede resolver enlaces magnet con DHT deshabilitado",
"DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent no puede resolver el enlace magnet con DHT deshabilitado",
"DownloadClientQbittorrentTorrentStateStalled": "La descarga está parada sin conexiones",
"DownloadClientQbittorrentValidationCategoryRecommended": "Se recomienda una categoría",
"DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} no intentará importar descargas completadas sin una categoría.",
"DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Poner en cola el torrent no está habilitado en los ajustes de su qBittorrent. Habilítelo en qBittorrent o seleccione 'Último' como prioridad.",
"DownloadClientQbittorrentValidationCategoryRecommended": "Una categoría es recomendada",
"DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} no intentará importar las descargas completadas sin una categoría.",
"DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "El encolado de torrent no está habilitado en las opciones de tu qBittorrent. Habilítalo en qBittorrent o selecciona 'Último' como prioridad.",
"DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} no podrá efectuar el Manejo de Descargas Completadas según lo configurado. Puede arreglar esto en qBittorrent ('Herramientas -> Opciones...' en el menú) cambiando 'Opciones -> BitTorrent -> Límite de ratio ' de 'Eliminarlas' a 'Pausarlas'",
"DownloadClientRTorrentSettingsAddStopped": "Añadir detenido",
"DownloadClientRTorrentSettingsAddStoppedHelpText": "Permite añadir torrents y magnets a rTorrent en estado detenido. Esto puede romper los archivos magnet.",
"DownloadClientRTorrentSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación predeterminada de rTorrent",
"DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "El cliente de descarga {downloadClientName} se establece para eliminar las descargas completadas. Esto puede resultar en descargas siendo eliminadas de tu cliente antes de que {appName} pueda importarlas.",
"DownloadClientRootFolderHealthCheckMessage": "El cliente de descarga {downloadClientName} ubica las descargas en la carpeta raíz {rootFolderPath}. No debería descargar a una carpeta raíz.",
"DownloadClientSabnzbdValidationEnableDisableDateSorting": "Deshabilitar la ordenación por fechas",
"DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Debe deshabilitar la ordenación de películas para la categoría que {appName} usa para evitar problemas al importar. Vaya a Sabnzbd para arreglarlo.",
"DownloadClientSettingsCategorySubFolderHelpText": "Añadir una categoría específica a {appName} evita conflictos con descargas no-{appName} no relacionadas. Usar una categoría es opcional, pero bastante recomendado. Crea un subdirectorio [categoría] en el directorio de salida.",
"DownloadClientSabnzbdValidationEnableDisableDateSorting": "Deshabilitar ordenamiento de fecha",
"DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Debes deshabilitar el ordenamiento de película para la categoría que {appName} usa para evitar problemas al importar. Ve a Sabnzbd para corregirlo.",
"DownloadClientSettingsCategorySubFolderHelpText": "Añade una categoría específica para que {appName} evite conflictos con descargas no relacionadas con {appName}. Usar una categoría es opcional, pero altamente recomendado. Crea un subdirectorio [categoría] en el directorio de salida.",
"DownloadClientSettingsDestinationHelpText": "Especifica manualmente el destino de descarga, dejar en blanco para usar el predeterminado",
"DownloadClientSettingsInitialState": "Estado inicial",
"DownloadClientSettingsInitialStateHelpText": "Estado inicial para torrents añadidos a {clientName}",
"DownloadClientSettingsOlderPriorityEpisodeHelpText": "Prioridad a usar cuando se tomen episodios que llevan en emisión hace más de 14 días",
"DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent está descargando metadatos",
"DownloadClientSettingsPostImportCategoryHelpText": "Categoría para {appName} que se establece después de que se haya importado la descarga. {appName} no eliminará los torrents en esa categoría incluso si finalizó la siembra. Déjelo en blanco para mantener la misma categoría.",
"DownloadClientSettingsPostImportCategoryHelpText": "Categoría para que {appName} establezca una vez se haya importado la descarga. {appName} no eliminará torrents en esa categoría incluso si finalizó el sembrado. Dejar en blanco para mantener la misma categoría.",
"DownloadClientSettingsRecentPriorityEpisodeHelpText": "Prioridad a usar cuando se tomen episodios que llevan en emisión dentro de los últimos 14 días",
"DownloadClientSettingsUseSslHelpText": "Usa una conexión segura cuando haya una conexión a {clientName}",
"DownloadClientSortingHealthCheckMessage": "El cliente de descarga {downloadClientName} tiene habilitada la ordenación {sortingMode} para la categoría de {appName}. Debería deshabilitar la ordenación en su cliente de descarga para evitar problemas al importar.",
"DownloadClientStatusSingleClientHealthCheckMessage": "Clientes de descarga no disponibles debido a fallos: {downloadClientNames}",
"DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación predeterminada de Transmission",
"DownloadClientTransmissionSettingsUrlBaseHelpText": "Añade un prefijo a la url rpc de {clientName}, p. ej. {url}, predeterminado a '{defaultUrl}'",
"DownloadClientUTorrentTorrentStateError": "uTorrent está informando de un error",
"DownloadClientUTorrentTorrentStateError": "uTorrent está reportando un error",
"DownloadClientValidationApiKeyIncorrect": "Clave API incorrecta",
"DownloadClientValidationApiKeyRequired": "Clave API requerida",
"DownloadClientValidationAuthenticationFailure": "Fallo de autenticación",
"DownloadClientValidationCategoryMissingDetail": "La categoría que ha introducido no existe en {clientName}. Créela primero en {clientName}",
"DownloadClientValidationCategoryMissingDetail": "La categoría que has introducido no existe en {clientName}. Créala en {clientName} primero.",
"DownloadClientValidationUnknownException": "Excepción desconocida: {exception}",
"DownloadClientVuzeValidationErrorVersion": "Versión de protocolo no soportada, use Vuze 5.0.0.0 o superior con la extensión Vuze Web remote.",
"DownloadClientVuzeValidationErrorVersion": "Versión de protocolo no soportada, usa Vuze 5.0.0.0 o superior con el plugin de web remota de Vuze.",
"Downloaded": "Descargado",
"Downloading": "Descargando",
"Duration": "Duración",
@ -696,7 +696,7 @@
"EditSeriesModalHeader": "Editar - {title}",
"DownloadClientQbittorrentTorrentStateUnknown": "Estado de descarga desconocido: {state}",
"DownloadClientSettingsOlderPriority": "Priorizar más antiguos",
"DownloadClientSettingsRecentPriority": "Priorizar más recientes",
"DownloadClientSettingsRecentPriority": "Priorizar recientes",
"EditRemotePathMapping": "Editar mapeo de ruta remota",
"EditRestriction": "Editar restricción",
"EnableAutomaticSearch": "Habilitar Búsqueda Automática",
@ -709,34 +709,34 @@
"DownloadClientQbittorrentSettingsUseSslHelpText": "Usa una conexión segura. Ver en Opciones -> Interfaz web -> 'Usar HTTPS en lugar de HTTP' en qbittorrent.",
"DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} no pudo añadir la etiqueta a qBittorrent.",
"DownloadClientQbittorrentValidationCategoryUnsupported": "La categoría no está soportada",
"DownloadClientQbittorrentValidationQueueingNotEnabled": "Poner en cola no está habilitado",
"DownloadClientQbittorrentValidationQueueingNotEnabled": "Encolado no habilitado",
"DownloadClientRTorrentSettingsUrlPathHelpText": "Ruta al endpoint de XMLRPC, ver {url}. Esto es usualmente RPC2 o [ruta a ruTorrent]{url2} cuando se usa ruTorrent.",
"DownloadClientQbittorrentTorrentStatePathError": "No es posible importar. La ruta coincide con el directorio de descarga base del cliente, ¿es posible que 'Mantener carpeta de nivel superior' esté deshabilitado para este torrent o que 'Diseño de contenido de torrent' NO se haya establecido en 'Original' o 'Crear subcarpeta'?",
"DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Deshabilitar la ordenación de películas",
"DownloadClientSabnzbdValidationEnableDisableTvSorting": "Deshabilitar ordenación de TV",
"DownloadClientQbittorrentTorrentStatePathError": "No se pudo importar. La ruta coincide con el directorio de descarga base del cliente. ¿Es posible que 'Mantener carpeta de nivel superior' esté deshabilitado para este torrent o que 'Distribución de contenido de torrent' NO esté configurado a 'Original' o 'Crear subcarpeta'?",
"DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Deshabilitar ordenamiento de película",
"DownloadClientSabnzbdValidationEnableDisableTvSorting": "Deshabilitar ordenamiento de TV",
"DownloadClientSabnzbdValidationCheckBeforeDownload": "Deshabilita la opción 'Verificar antes de descargar' en Sabnbzd",
"DownloadClientValidationTestTorrents": "Fallo al obtener la lista de torrents: {exceptionMessage}",
"DownloadClientValidationVerifySsl": "Verificar las opciones SSL",
"DownloadClientValidationVerifySslDetail": "Por favor, verifique su configuración SSL en {clientName} y {appName}",
"DownloadClientValidationVerifySsl": "Verificar opciones de SSL",
"DownloadClientValidationVerifySslDetail": "Por favor verifica tu configuración SSL tanto en {clientName} como en {appName}",
"DownloadClients": "Clientes de descarga",
"DownloadFailed": "La descarga falló",
"DownloadPropersAndRepacks": "Propers y repacks",
"DownloadClientValidationErrorVersion": "La versión de {clientName} debería ser al menos {requiredVersion}. La versión devuelta es {reportedVersion}",
"DownloadClientValidationErrorVersion": "La versión de {clientName} debería ser al menos {requiredVersion}. La versión reportada es {reportedVersion}",
"EnableAutomaticSearchHelpText": "Será usado cuando las búsquedas automáticas sean realizadas por la interfaz de usuario o por {appName}",
"EnableColorImpairedModeHelpText": "Estilo modificado para permitir que usuarios con problemas de color distingan mejor la información codificada por colores",
"NotificationsDiscordSettingsUsernameHelpText": "El nombre de usuario para publicar, por defecto es el webhook predeterminado de Discord",
"DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Debe deshabilitar la ordenación de TV para la categoría que {appName} usa para evitar problemas al importar. Vaya a Sabnzbd para arreglarlo.",
"DownloadClientSettingsCategoryHelpText": "Añadir una categoría específica a {appName} evita conflictos con descargas no-{appName} no relacionadas. Usar una categoría es opcional, pero bastante recomendado.",
"DownloadClientRTorrentProviderMessage": "rTorrent no pausará los torrents cuando satisfagan los criterios de siembra. {appName} manejará la eliminación automática de torrents basada en el actual criterio de siembra en Opciones ->Indexadores solo cuando Eliminar completados esté habilitado. Después de importarla también establecerá {importedView} como una vista de rTorrent, la cuál puede ser usada en los scripts de rTorrent para personalizar el comportamiento.",
"DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} prefiere que cada descarga tenga una carpeta separada. Con * añadida a la carpeta/ruta, Sabnzbd no creará esas carpetas de trabajo. Vaya a Sabnzbd para arreglarlo.",
"DownloadClientValidationAuthenticationFailureDetail": "Por favor, verifique su nombre de usuario y contraseña. También verifique si no está bloqueado el acceso del host en ejecución {appName} a {clientName} por limitaciones en la lista blanca en la configuración de {clientName}.",
"DownloadClientValidationSslConnectFailureDetail": "{appName} no se puede conectar a {clientName} usando SSL. Este problema puede estar relacionado con el ordenador. Por favor, intente configurar tanto {appName} como {clientName} para no usar SSL.",
"DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Debes deshabilitar el ordenamiento de TV para la categoría que {appName} usa para evitar problemas al importar. Ve a Sabnzbd para corregirlo.",
"DownloadClientSettingsCategoryHelpText": "Añade una categoría específica para que {appName} evite conflictos con descargas no relacionadas con {appName}. Usar una categoría es opcional, pero altamente recomendado.",
"DownloadClientRTorrentProviderMessage": "rTorrent no pausará torrents cuando cumplan el criterio de sembrado. {appName} manejará la eliminación automática de torrents basados en el criterio de sembrado actual en Opciones -> Indexadores solo cuando Eliminar completados esté habilitado. Después de importarlo también se establecerá {importedView} como una vista de rTorrent, lo que puede ser usado en los scripts de rTorrent para personalizar su comportamiento.",
"DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} prefiere que cada descarga tenga una carpeta separada. Con * añadido a la carpeta/ruta, Sabnzbd no creará esas carpetas de trabajo. Ve a Sabnzbd para corregirlo.",
"DownloadClientValidationAuthenticationFailureDetail": "Por favor verifica tu usuario y contraseña. Verifica también si al host que ejecuta {appName} no se le ha bloqueado el acceso a {clientName} por limitaciones en la lista blanca en la configuración de {clientName}.",
"DownloadClientValidationSslConnectFailureDetail": "{appName} no se pudo conectar a {clientName} usando SSL. Este problema puede estar relacionado con el ordenador. Por favor intenta configurar tanto {appName} como {clientName} para no usar SSL.",
"DownloadFailedEpisodeTooltip": "La descarga del episodio falló",
"DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Descarga primero las primeras y últimas piezas (qBittorrent 4.1.0+)",
"DownloadClientQbittorrentSettingsFirstAndLastFirst": "Primeras y últimas primero",
"DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para los torrents añadidos a qBittorrent. Ten en cuenta que Forzar torrents no cumple las restricciones de semilla",
"DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Descarga en orden secuencial (qBittorrent 4.1.0+)",
"DownloadClientDownloadStationValidationSharedFolderMissingDetail": "El Diskstation no tiene una Carpeta Compartida con el nombre '{sharedFolder}', ¿estás seguro que lo has especificado correctamente?",
"DownloadClientDownloadStationValidationSharedFolderMissingDetail": "El Diskstation no tiene una carpeta compartida con el nombre '{sharedFolder}'. ¿Estás seguro que lo has especificado correctamente?",
"EnableCompletedDownloadHandlingHelpText": "Importar automáticamente las descargas completas del gestor de descargas",
"EnableInteractiveSearchHelpTextWarning": "Buscar no está soportado por este indexador",
"EnableRss": "Habilitar RSS",
@ -883,21 +883,21 @@
"Host": "Host",
"HideEpisodes": "Ocultar episodios",
"Hostname": "Nombre de host",
"ICalSeasonPremieresOnlyHelpText": "Solo el primer episodio de una temporada estará en el feed",
"ICalSeasonPremieresOnlyHelpText": "Solo el primer episodio de una temporada estará en el canal",
"ImportExistingSeries": "Importar series existentes",
"ImportErrors": "Importar errores",
"ImportList": "Importar lista",
"ImportListSettings": "Importar ajustes de lista",
"ImportListsSettingsSummary": "Importar desde otra instancia de {appName} o desde listas de Trakt y gestionar listas de exclusiones",
"ImportListsSettingsSummary": "Importar desde otra instancia de {appName} o listas de Trakt y gestionar exclusiones de lista",
"IncludeCustomFormatWhenRenamingHelpText": "Incluir en formato de renombrado {Custom Formats}",
"QualityCutoffNotMet": "Calidad del umbral que no ha sido alcanzado",
"SearchForCutoffUnmetEpisodesConfirmationCount": "¿Estás seguro que quieres buscar los {totalRecords} episodios en Umbrales no alcanzados?",
"IndexerOptionsLoadError": "No se pudo cargar las opciones del indexador",
"IndexerIPTorrentsSettingsFeedUrl": "URL de feed",
"ICalFeed": "Feed de iCal",
"IndexerOptionsLoadError": "No se pudieron cargar las opciones del indexador",
"IndexerIPTorrentsSettingsFeedUrl": "URL del canal",
"ICalFeed": "Canal de iCal",
"Import": "Importar",
"ImportFailed": "La importación falló: {sourceTitle}",
"HiddenClickToShow": "Oculto, click para mostrar",
"HiddenClickToShow": "Oculto, pulsa para mostrar",
"HttpHttps": "HTTP(S)",
"ICalLink": "Enlace de iCal",
"IconForFinalesHelpText": "Muestra un icono para finales de series/temporadas basado en la información de episodio disponible",
@ -992,16 +992,16 @@
"ImportUsingScriptHelpText": "Copiar archivos para importar usando un script (p. ej. para transcodificación)",
"Importing": "Importando",
"IncludeUnmonitored": "Incluir sin monitorizar",
"IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Ningún indexador disponible debido a fallos durante más de 6 horas",
"IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Ningún indexador está disponible debido a errores durante más de 6 horas",
"IRC": "IRC",
"ICalShowAsAllDayEvents": "Mostrar como eventos para todo el día",
"IndexerHDBitsSettingsCategories": "Categorías",
"IndexerHDBitsSettingsCategoriesHelpText": "Si no se especifica, se usan todas las opciones.",
"HomePage": "Página principal",
"ImportSeries": "Importar series",
"IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexadores no disponibles debido a fallos durante más de 6 horas: {indexerNames}",
"IndexerIPTorrentsSettingsFeedUrlHelpText": "La URL completa de feed RSS generada por IPTorrents, usa solo las categorías que seleccionaste (HD, SD, x264, etc...)",
"ICalIncludeUnmonitoredEpisodesHelpText": "Incluye episodios sin monitorizar en el feed de iCal",
"IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexadores no disponibles debido a errores durante más de 6 horas: {indexerNames}",
"IndexerIPTorrentsSettingsFeedUrlHelpText": "La URL completa del canal RSS generada por IPTorrents, usa solo las categorías que seleccionaste (HD, SD, x264, etc...)",
"ICalIncludeUnmonitoredEpisodesHelpText": "Incluye episodios sin monitorizar en el canal de iCal",
"Forecast": "Previsión",
"IndexerDownloadClientHelpText": "Especifica qué cliente de descarga es usado para capturas desde este indexador",
"IndexerHDBitsSettingsCodecs": "Códecs",
@ -1051,18 +1051,18 @@
"ImportListsAniListSettingsImportWatchingHelpText": "Lista: Viendo actualmente",
"ImportListsAniListSettingsImportNotYetReleased": "Importar Aún sin lanzar",
"ImportListsSonarrSettingsFullUrlHelpText": "URL, incluyendo puerto, de la instancia de {appName} de la que importar",
"IndexerPriorityHelpText": "Prioridad del Indexador de 1 (la más alta) a 50 (la más baja). Por defecto: 25. Usada para desempatar lanzamientos iguales cuando se capturan, {appName} seguirá usando todos los indexadores habilitados para Sincronización de RSS y Búsqueda",
"IndexerPriorityHelpText": "Prioridad del indexador desde 1 (la más alta) a 50 (la más baja). Predeterminado: 25. Usado para desempatar lanzamientos capturados que, de otra forma, serían iguales. {appName} aún empleará todos los indexadores habilitados para la sincronización RSS y la búsqueda",
"IncludeHealthWarnings": "Incluir avisos de salud",
"IndexerJackettAllHealthCheckMessage": "Indexadores usan el endpoint de Jackett no soportado 'todo': {indexerNames}",
"IndexerJackettAllHealthCheckMessage": "Indexadores que usan el endpoint no soportado de Jackett 'all': {indexerNames}",
"HourShorthand": "h",
"ICalFeedHelpText": "Copia esta URL a tu(s) cliente(s) o haz click para suscribirte si tu navegador soportar WebCal",
"ICalTagsSeriesHelpText": "El feed solo contendrá series con al menos una etiqueta coincidente",
"ICalFeedHelpText": "Copia esta URL a tu(s) cliente(s) o pulsa para suscribirse si tu navegador soporta Webcal",
"ICalTagsSeriesHelpText": "El canal solo contendrá series con al menos una etiqueta coincidente",
"IconForCutoffUnmet": "Icono para Umbrales no alcanzados",
"IconForCutoffUnmetHelpText": "Mostrar icono para archivos cuando el umbral no haya sido alcanzado",
"EpisodeCount": "Recuento de episodios",
"IndexerSettings": "Ajustes de Indexador",
"AddDelayProfileError": "No se pudo añadir un nuevo perfil de retraso, inténtelo de nuevo.",
"IndexerRssNoIndexersAvailableHealthCheckMessage": "Todos los indexers capaces de RSS están temporalmente desactivados debido a errores recientes con el indexer",
"IndexerSettings": "Opciones del indexador",
"AddDelayProfileError": "No se pudo añadir un nuevo perfil de retraso, por favor inténtalo de nuevo.",
"IndexerRssNoIndexersAvailableHealthCheckMessage": "Todos los indexadores con capacidad de RSS no están disponibles temporalmente debido a errores recientes con el indexador",
"IndexerRssNoIndexersEnabledHealthCheckMessage": "No hay indexadores disponibles con la sincronización RSS activada, {appName} no capturará nuevos estrenos automáticamente",
"IndexerSearchNoAutomaticHealthCheckMessage": "No hay indexadores disponibles con Búsqueda Automática activada, {appName} no proporcionará ningún resultado de búsquedas automáticas",
"IndexerSearchNoAvailableIndexersHealthCheckMessage": "Todos los indexadores con capacidad de búsqueda no están disponibles temporalmente debido a errores recientes del indexadores",
@ -1241,7 +1241,7 @@
"Name": "Nombre",
"No": "No",
"NotificationTriggers": "Disparadores de notificación",
"ClickToChangeIndexerFlags": "Clic para cambiar las banderas del indexador",
"ClickToChangeIndexerFlags": "Pulsa para cambiar los indicadores del indexador",
"MinutesSixty": "60 minutos: {sixty}",
"MonitorMissingEpisodesDescription": "Monitoriza episodios que no tienen archivos o que no se han emitido aún",
"MoveFiles": "Mover archivos",
@ -1252,16 +1252,16 @@
"NoMatchFound": "¡Ninguna coincidencia encontrada!",
"NoMinimumForAnyRuntime": "No hay mínimo para ningún tiempo de ejecución",
"NoMonitoredEpisodes": "No hay episodios monitorizados en esta serie",
"NotificationStatusSingleClientHealthCheckMessage": "Notificaciones no disponible debido a fallos: {notificationNames}",
"CustomFormatsSpecificationFlag": "Bandera",
"NotificationStatusSingleClientHealthCheckMessage": "Notificaciones no disponibles debido a errores: {notificationNames}",
"CustomFormatsSpecificationFlag": "Indicador",
"Never": "Nunca",
"MinimumAge": "Edad mínima",
"Mixed": "Mezclado",
"MultiLanguages": "Multi-idiomas",
"NoEpisodesFoundForSelectedSeason": "No se encontró ningún episodio para la temporada seleccionada",
"NoEventsFound": "Ningún evento encontrado",
"IndexerFlags": "Banderas del indexador",
"CustomFilter": "Filtros personalizados",
"IndexerFlags": "Indicadores del indexador",
"CustomFilter": "Filtro personalizado",
"Filters": "Filtros",
"Label": "Etiqueta",
"MonitorExistingEpisodes": "Episodios existentes",
@ -1285,7 +1285,7 @@
"NoChanges": "Sin cambios",
"NoEpisodeOverview": "No hay sinopsis de episodio",
"MultiEpisodeStyle": "Estilo de multi-episodio",
"NotificationStatusAllClientHealthCheckMessage": "Las notificaciones no están disponibles debido a fallos",
"NotificationStatusAllClientHealthCheckMessage": "Las notificaciones no están disponibles debido a errores",
"NotificationsAppriseSettingsConfigurationKeyHelpText": "Clave de configuración para la Solución de almacenamiento persistente. Dejar vacío si se usan URLs sin estado.",
"NotificationsAppriseSettingsPasswordHelpText": "Autenticación básica HTTP de contraseña",
"Monitoring": "Monitorizando",
@ -1648,11 +1648,11 @@
"RemotePathMappingLocalPathHelpText": "Ruta que {appName} debería usar para acceder a la ruta remota localmente",
"Remove": "Eliminar",
"RetentionHelpText": "Solo usenet: Establece a cero para establecer una retención ilimitada",
"SelectIndexerFlags": "Seleccionar banderas del indexador",
"SelectIndexerFlags": "Seleccionar indicadores del indexador",
"SelectSeasonModalTitle": "{modalTitle} - Seleccionar temporada",
"SeriesFinale": "Final de serie",
"SeriesAndEpisodeInformationIsProvidedByTheTVDB": "La información de serie y episodio es proporcionada por TheTVDB.com. [Por favor considera apoyarlos]({url}).",
"SetIndexerFlags": "Establecer banderas del indexador",
"SetIndexerFlags": "Establecer indicadores del indexador",
"SkipRedownload": "Saltar redescarga",
"ShowMonitored": "Mostrar monitorizado",
"Space": "Espacio",
@ -1795,7 +1795,7 @@
"Reason": "Razón",
"RegularExpression": "Expresión regular",
"ReleaseHash": "Hash de lanzamiento",
"Rejections": "Rechazos",
"Rejections": "Rechazados",
"RecyclingBinCleanupHelpTextWarning": "Los archivos en la papelera de reciclaje anteriores al número de días seleccionado serán limpiados automáticamente",
"ReleaseProfiles": "Perfiles de lanzamiento",
"ReleaseRejected": "Lanzamiento rechazado",
@ -1879,7 +1879,7 @@
"SelectFolderModalTitle": "{modalTitle} - Seleccionar carpeta",
"SeriesDetailsCountEpisodeFiles": "{episodeFileCount} archivos de episodio",
"SeriesIndexFooterContinuing": "Continuando (Todos los episodios descargados)",
"SetIndexerFlagsModalTitle": "{modalTitle} - Establecer banderas del indexador",
"SetIndexerFlagsModalTitle": "{modalTitle} - Establecer indicadores del indexador",
"ShortDateFormat": "Formato de fecha breve",
"ShowUnknownSeriesItemsHelpText": "Muestra elementos sin una serie en la cola, esto incluiría series eliminadas, películas o cualquier cosa más en la categoría de {appName}",
"ShownClickToHide": "Mostrado, haz clic para ocultar",
@ -1931,7 +1931,7 @@
"ProfilesSettingsSummary": "Perfiles de calidad, de retraso de idioma y de lanzamiento",
"QualitiesHelpText": "Calidades superiores en la lista son más preferibles. Calidades dentro del mismo grupo son iguales. Comprobar solo calidades que se busquen",
"RssIsNotSupportedWithThisIndexer": "RSS no está soportado con este indexador",
"Repack": "Reempaquetar",
"Repack": "Repack",
"NotificationsGotifySettingsPriorityHelpText": "Prioridad de la notificación",
"NotificationsGotifySettingsServer": "Servidor Gotify",
"NotificationsPlexSettingsAuthToken": "Token de autenticación",
@ -2044,7 +2044,7 @@
"SeriesType": "Tipo de serie",
"TagCannotBeDeletedWhileInUse": "La etiqueta no puede ser borrada mientras esté en uso",
"UnmonitorSpecialEpisodes": "Dejar de monitorizar especiales",
"UpdateStartupTranslocationHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de inicio '{startupFolder}' está en una carpeta de translocalización de la aplicación.",
"UpdateStartupTranslocationHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de arranque '{startupFolder}' está en una carpeta de translocación de aplicaciones.",
"Yesterday": "Ayer",
"RemoveQueueItemRemovalMethodHelpTextWarning": "'Eliminar del cliente de descarga' eliminará la descarga y el archivo(s) del cliente de descarga.",
"RemotePathMappingLocalFolderMissingHealthCheckMessage": "El cliente de descarga remoto {downloadClientName} ubica las descargas en {path} pero este directorio no parece existir. Probablemente mapeo de ruta remota perdido o incorrecto.",
@ -2057,7 +2057,7 @@
"NotificationsJoinSettingsApiKeyHelpText": "La clave API de tus ajustes de Añadir cuenta (haz clic en el botón Añadir API).",
"NotificationsPushBulletSettingsDeviceIdsHelpText": "Lista de IDs de dispositivo (deja en blanco para enviar a todos los dispositivos)",
"NotificationsSettingsUpdateMapPathsToHelpText": "Ruta de {appName}, usado para modificar rutas de series cuando {serviceName} ve la ubicación de ruta de biblioteca de forma distinta a {appName} (Requiere 'Actualizar biblioteca')",
"ReleaseProfileIndexerHelpTextWarning": "Establecer un indexador específico en un perfil de lanzamiento provocará que este perfil solo se aplique a lanzamientos desde ese indexador.",
"ReleaseProfileIndexerHelpTextWarning": "Establecer un indexador específico en un perfil de lanzamiento causará que este perfil solo se aplique a lanzamientos desde ese indexador.",
"ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Autenticar con MyAnimeList",
"ImportListsMyAnimeListSettingsListStatus": "Estado de lista",
"ImportListsMyAnimeListSettingsListStatusHelpText": "Tipo de lista desde la que quieres importar, establecer a 'Todo' para todas las listas",
@ -2069,5 +2069,7 @@
"EpisodeTitleFootNote": "Opcionalmente controla el truncamiento hasta un número máximo de bytes, incluyendo elipsis (`...`). Está soportado truncar tanto desde el final (p. ej. `{Título de episodio:30}`) como desde el principio (p. ej. `{Título de episodio:-30}`). Los títulos de episodio serán truncados automáticamente acorde a las limitaciones del sistema de archivos si es necesario.",
"AutoTaggingSpecificationTag": "Etiqueta",
"NotificationsTelegramSettingsIncludeAppName": "Incluir {appName} en el título",
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Prefija opcionalmente el título de mensaje con {appName} para diferenciar notificaciones de aplicaciones diferentes"
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Opcionalmente prefija el título del mensaje con {appName} para diferenciar las notificaciones de las diferentes aplicaciones",
"IndexerSettingsMultiLanguageRelease": "Múltiples idiomas",
"IndexerSettingsMultiLanguageReleaseHelpText": "¿Qué idiomas están normalmente en un lanzamiento múltiple en este indexador?"
}

View File

@ -359,7 +359,7 @@
"CountSeasons": "{count} kautta",
"CountIndexersSelected": "{count} tietolähde(ttä) on valittu",
"SetTags": "Tunnisteiden määritys",
"Monitored": "Valvotut",
"Monitored": "Valvonta",
"ApplyTagsHelpTextHowToApplyDownloadClients": "Tunnisteiden käyttö valituissa lataustyökaluissa",
"ApplyTagsHelpTextHowToApplyImportLists": "Tunnisteiden käyttö valituissa tuontilistoissa",
"ApplyTagsHelpTextHowToApplySeries": "Tunnisteiden käyttö valituissa sarjoissa",
@ -590,7 +590,7 @@
"DownloadClientFloodSettingsAdditionalTags": "Lisätunnisteet",
"DownloadClientFloodSettingsPostImportTagsHelpText": "Sisällyttää tunnisteet kun lataus on tuotu.",
"DownloadClientFloodSettingsStartOnAdd": "Käynnistä lisättäessä",
"ClickToChangeQuality": "Vaihda laatua klikkaamalla",
"ClickToChangeQuality": "Vaihda laatua painamalla tästä",
"EpisodeDownloaded": "Jakso on ladattu",
"InteractiveImportNoQuality": "Jokaisen valitun tiedoston laatu on määritettävä.",
"DownloadClientNzbgetSettingsAddPausedHelpText": "Tämä vaatii vähintään NzbGet-version 16.0.",
@ -1335,7 +1335,7 @@
"NotificationsTelegramSettingsSendSilently": "Lähetä äänettömästi",
"NotificationsTelegramSettingsTopicId": "Ketjun ID",
"ProcessingFolders": "Käsittelykansiot",
"Preferred": "Haluttu",
"Preferred": "Tavoite",
"SslCertPasswordHelpText": "Pfx-tiedoston salasana",
"TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Magnet-linkeille käytettävä tiedostopääte. Oletus on \".magnet\".",
"TorrentBlackholeSaveMagnetFilesReadOnly": "Vain luku",
@ -1768,7 +1768,7 @@
"ResetDefinitions": "Palauta määritykset",
"DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent on määritetty poistamaan torrentit niiden saavuttaessa niitä koskevan jakosuhderajoituksen.",
"SecretToken": "Salainen tunniste",
"ShownClickToHide": "Näkyvissä, piilota painamalla",
"ShownClickToHide": "Näytetään, piilota painamalla tästä",
"DownloadClientValidationAuthenticationFailure": "Tunnistautuminen epäonnistui",
"DownloadClientValidationApiKeyRequired": "Rajapinnan avain on pakollinen",
"DownloadClientValidationVerifySsl": "Vahvista SSL-asetukset",
@ -1794,7 +1794,7 @@
"DownloadClientSabnzbdValidationEnableDisableDateSorting": "Älä järjestele päiväyksellä",
"DownloadClientTransmissionSettingsUrlBaseHelpText": "Lisää etuliite lataustyökalun {clientName} RPC-URL-osoitteeseen. Esimerkiksi {url}. Oletus on \"{defaultUrl}\".",
"DownloadClientValidationApiKeyIncorrect": "Rajapinnan avain ei kelpaa",
"HiddenClickToShow": "Piilotettu, näytä painalla",
"HiddenClickToShow": "Piilotettu, näytä painamalla tästä",
"ImportListsCustomListValidationAuthenticationFailure": "Tunnistautuminen epäonnistui",
"ImportListsSonarrSettingsApiKeyHelpText": "{appName}-instanssin, josta tuodaan, rajapinan (API) avain.",
"IndexerSettingsApiUrlHelpText": "Älä muuta tätä, jos et tiedä mitä teet, koska rajapinta-avaimesi lähetetään kyseiselle palvelimelle.",
@ -1802,5 +1802,13 @@
"Required": "Pakollinen",
"TaskUserAgentTooltip": "User-Agent-tiedon ilmoitti rajapinnan kanssa viestinyt sovellus.",
"TorrentBlackhole": "Torrent Blackhole",
"WantMoreControlAddACustomFormat": "Haluatko hallita tarkemmin mitä latauksia suositaan? Lisää [Mukautettu muoto](/settings/customformats)."
"WantMoreControlAddACustomFormat": "Haluatko hallita tarkemmin mitä latauksia suositaan? Lisää [Mukautettu muoto](/settings/customformats).",
"LabelIsRequired": "Nimi on pakollinen",
"SetIndexerFlags": "Aseta tietolähteen liput",
"ClickToChangeIndexerFlags": "Vaihda tietolähteen lippuja painamalla tästä",
"CustomFormatsSpecificationFlag": "Lippu",
"SelectIndexerFlags": "Valitse tietolähteen liput",
"SetIndexerFlagsModalTitle": "{modalTitle} - Aseta tietolähteen liput",
"CustomFilter": "Oma suodatin",
"Label": "Nimi"
}

View File

@ -299,12 +299,12 @@
"SkipFreeSpaceCheck": "Ignorer la vérification de l'espace libre",
"Sunday": "Dimanche",
"TorrentDelay": "Retard du torrent",
"DownloadClients": "Clients de télécharg.",
"DownloadClients": "Clients de téléchargement",
"CustomFormats": "Formats perso.",
"NoIndexersFound": "Aucun indexeur n'a été trouvé",
"Profiles": "Profils",
"Dash": "Tiret",
"DelayProfileProtocol": "Protocole: {preferredProtocol}",
"DelayProfileProtocol": "Protocole : {preferredProtocol}",
"DeleteBackupMessageText": "Voulez-vous supprimer la sauvegarde « {name} » ?",
"DeleteConditionMessageText": "Voulez-vous vraiment supprimer la condition « {name} » ?",
"DeleteCondition": "Supprimer la condition",
@ -335,7 +335,7 @@
"EditGroups": "Modifier les groupes",
"False": "Faux",
"Example": "Exemple",
"FileNameTokens": "Tokens des noms de fichier",
"FileNameTokens": "Jetons de nom de fichier",
"FileNames": "Noms de fichier",
"Extend": "Étendu",
"FileManagement": "Gestion de fichiers",
@ -343,7 +343,7 @@
"FailedToLoadQualityProfilesFromApi": "Échec du chargement des profils de qualité depuis l'API",
"Filename": "Nom de fichier",
"FailedToLoadTagsFromApi": "Échec du chargement des étiquettes depuis l'API",
"FormatTimeSpanDays": "{days}j {time}",
"FormatTimeSpanDays": "{days} j {time}",
"FormatShortTimeSpanSeconds": "{seconds} seconde(s)",
"FilterEqual": "égale",
"Implementation": "Mise en œuvre",
@ -362,7 +362,7 @@
"Lowercase": "Minuscule",
"MaximumSizeHelpText": "Taille maximale d'une version à récupérer en Mo. Régler sur zéro pour définir sur illimité",
"MissingNoItems": "Aucun élément manquant",
"MoveAutomatically": "Se déplacer automatiquement",
"MoveAutomatically": "Importation automatique",
"MoreInfo": "Plus d'informations",
"NoHistory": "Aucun historique",
"MonitoredStatus": "Surveillé/Statut",
@ -528,7 +528,7 @@
"IndexerDownloadClientHelpText": "Spécifiez quel client de téléchargement est utilisé pour les récupérations à partir de cet indexeur",
"IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Tous les indexeurs sont indisponibles en raison de pannes pendant plus de 6 heures",
"IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexeurs indisponibles en raison d'échecs pendant plus de six heures : {indexerNames}",
"IndexerPriorityHelpText": "Priorité de l'indexeur de 1 (la plus élevée) à 50 (la plus basse). Valeur par défaut : 25. Utilisé lors de la récupération des versions comme départage pour des versions par ailleurs égales, {appName} utilisera toujours tous les indexeurs activés pour la synchronisation RSS et la recherche",
"IndexerPriorityHelpText": "Priorité de l'indexeur de 1 (la plus élevée) à 50 (la plus basse). Par défaut : 25. Utilisé lors de la récupération de versions pour départager des versions égales, {appName} utilisera toujours tous les indexeurs activés pour la synchronisation et la recherche RSS",
"IndexerRssNoIndexersAvailableHealthCheckMessage": "Tous les indexeurs compatibles RSS sont temporairement indisponibles en raison d'erreurs récentes de l'indexeur",
"IndexerSearchNoAutomaticHealthCheckMessage": "Aucun indexeur disponible avec la recherche automatique activée, {appName} ne fournira aucun résultat de recherche automatique",
"IndexerSearchNoInteractiveHealthCheckMessage": "Aucun indexeur n'est disponible avec la recherche interactive activée. {appName} ne fournira aucun résultat de recherche interactif",
@ -930,7 +930,7 @@
"NoLeaveIt": "Non, laisse tomber",
"NotificationsLoadError": "Impossible de charger les notifications",
"OnEpisodeFileDeleteForUpgrade": "Lors de la suppression du fichier de l'épisode pour la mise à niveau",
"OnGrab": "À saisir",
"OnGrab": "Lors de la saisie",
"OnlyForBulkSeasonReleases": "Uniquement pour les versions de saison en masse",
"RegularExpressionsCanBeTested": "Les expressions régulières peuvent être testées [ici]({url}).",
"ReleaseProfileIndexerHelpText": "Spécifier l'indexeur auquel le profil s'applique",
@ -968,7 +968,7 @@
"Ungroup": "Dissocier",
"Folder": "Dossier",
"FullColorEvents": "Événements en couleur",
"GeneralSettingsSummary": "Port, SSL/TLS, nom d'utilisateur/mot de passe, proxy, analyses et mises à jour",
"GeneralSettingsSummary": "Port, SSL, nom d'utilisateur/mot de passe, proxy, analyses et mises à jour",
"HistoryModalHeaderSeason": "Historique {season}",
"HistorySeason": "Afficher l'historique de cette saison",
"Images": "Images",
@ -1249,7 +1249,7 @@
"Debug": "Déboguer",
"DelayProfileSeriesTagsHelpText": "S'applique aux séries avec au moins une balise correspondante",
"DelayingDownloadUntil": "Retarder le téléchargement jusqu'au {date} à {time}",
"DeletedReasonManual": "Le fichier a été supprimé à l'aide de {appName}, soit manuellement, soit par un autre outil via l'API.",
"DeletedReasonManual": "Le fichier a été supprimé à l'aide de {appName}, soit manuellement, soit par un autre outil via l'API",
"DeleteRemotePathMapping": "Supprimer la correspondance de chemin distant",
"DestinationPath": "Chemin de destination",
"DestinationRelativePath": "Chemin relatif de destination",
@ -1743,7 +1743,7 @@
"NotificationsMailgunSettingsUseEuEndpointHelpText": "Activer pour utiliser le point de terminaison MailGun européen",
"NotificationsMailgunSettingsSenderDomain": "Domaine de l'expéditeur",
"NotificationsMailgunSettingsApiKeyHelpText": "La clé API générée depuis MailGun",
"NotificationsKodiSettingsUpdateLibraryHelpText": "Mettre à jour la bibliothèque dans Importer & Renommer ?",
"NotificationsKodiSettingsUpdateLibraryHelpText": "Mettre à jour la bibliothèque lors de l'importation et du renommage ?",
"NotificationsKodiSettingsGuiNotification": "Notification GUI",
"NotificationsKodiSettingsDisplayTime": "Temps d'affichage",
"NotificationsKodiSettingsCleanLibraryHelpText": "Nettoyer la bibliothèque après une mise à jour",
@ -1918,7 +1918,7 @@
"IgnoreDownload": "Ignorer le téléchargement",
"IgnoreDownloads": "Ignorer les téléchargements",
"IgnoreDownloadsHint": "Empêche {appName} de poursuivre le traitement de ces téléchargements",
"IndexerSettingsRejectBlocklistedTorrentHashes": "Rejeter les hachages de torrents bloqués lors de la saisie",
"IndexerSettingsRejectBlocklistedTorrentHashes": "Rejeter les hachages de torrent sur liste noir lors de la saisie",
"IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Si un torrent est bloqué par le hachage, il peut ne pas être correctement rejeté pendant le RSS/recherche pour certains indexeurs. L'activation de cette fonction permet de le rejeter après que le torrent a été saisi, mais avant qu'il ne soit envoyé au client.",
"DownloadClientAriaSettingsDirectoryHelpText": "Emplacement facultatif pour les téléchargements, laisser vide pour utiliser l'emplacement par défaut Aria2",
"AddDelayProfileError": "Impossible d'ajouter un nouveau profil de délai, veuillez réessayer.",
@ -2069,5 +2069,7 @@
"ReleaseGroupFootNote": "Contrôlez éventuellement la troncature à un nombre maximum d'octets, y compris les points de suspension (`...`). La troncature de la fin (par exemple `{Release Group:30}`) ou du début (par exemple `{Release Group:-30}`) sont toutes deux prises en charge.`).",
"AutoTaggingSpecificationTag": "Étiquette",
"NotificationsTelegramSettingsIncludeAppName": "Inclure {appName} dans le Titre",
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Préfixer éventuellement le titre du message par {appName} pour différencier les notifications des différentes applications"
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Préfixer éventuellement le titre du message par {appName} pour différencier les notifications des différentes applications",
"IndexerSettingsMultiLanguageRelease": "Multilingue",
"IndexerSettingsMultiLanguageReleaseHelpText": "Quelles langues sont normalement présentes dans une version multiple de l'indexeur ?"
}

View File

@ -2068,5 +2068,8 @@
"ClickToChangeReleaseType": "Clique para alterar o tipo de lançamento",
"NotificationsTelegramSettingsIncludeAppName": "Incluir {appName} no Título",
"SelectReleaseType": "Selecionar o Tipo de Lançamento",
"SeriesFootNote": "Opcionalmente, controle o truncamento para um número máximo de bytes, incluindo reticências (`...`). Truncar do final (por exemplo, `{Series Title:30}`) ou do início (por exemplo, `{Series Title:-30}`) é suportado."
"SeriesFootNote": "Opcionalmente, controle o truncamento para um número máximo de bytes, incluindo reticências (`...`). Truncar do final (por exemplo, `{Series Title:30}`) ou do início (por exemplo, `{Series Title:-30}`) é suportado.",
"IndexerSettingsMultiLanguageReleaseHelpText": "Quais idiomas normalmente estão em um lançamento multi neste indexador?",
"AutoTaggingSpecificationTag": "Etiqueta",
"IndexerSettingsMultiLanguageRelease": "Multi Idiomas"
}

View File

@ -11,7 +11,7 @@
"EditIndexerImplementation": "Koşul Ekle - {implementationName}",
"AddToDownloadQueue": "İndirme kuyruğuna ekleyin",
"AddedToDownloadQueue": "İndirme sırasına eklendi",
"AllTitles": "Tüm Filmler",
"AllTitles": "Tüm Başlıklar",
"AbsoluteEpisodeNumbers": "Mutlak Bölüm Numaraları",
"Actions": "Eylemler",
"AbsoluteEpisodeNumber": "Mutlak Bölüm Numarası",
@ -24,8 +24,8 @@
"AirDate": "Yayınlanma Tarihi",
"Add": "Ekle",
"AddingTag": "Etiket ekleniyor",
"Age": "Y",
"AgeWhenGrabbed": "Y (yakalandığında)",
"Age": "Yıl",
"AgeWhenGrabbed": "Yıl (yakalandığında)",
"AddDelayProfileError": "Yeni bir gecikme profili eklenemiyor, lütfen tekrar deneyin.",
"AddImportList": "İçe Aktarım Listesi Ekle",
"AddImportListExclusion": "İçe Aktarma Listesi Hariç Tutma Ekle",
@ -219,7 +219,292 @@
"DownloadClientDownloadStationSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı paylaşımlı klasör, varsayılan Download Station konumunu kullanmak için boş bırakın",
"ApiKey": "API Anahtarı",
"Analytics": "Analiz",
"All": "Herşey",
"All": "Hepsi",
"AppDataLocationHealthCheckMessage": "Güncellemede AppData'nın silinmesini önlemek için güncelleme mümkün olmayacak",
"AnalyticsEnabledHelpText": "Anonim kullanım ve hata bilgilerini {appName} sunucularına gönderin. Bu, tarayıcınızla ilgili bilgileri, kullandığınız {appName} Web arayüz sayfalarını, hata raporlamasının yanı sıra işletim sistemi ve çalışma zamanı sürümünü içerir. Bu bilgileri, özellikleri ve hata düzeltmelerini önceliklendirmek için kullanacağız."
"AnalyticsEnabledHelpText": "Anonim kullanım ve hata bilgilerini {appName} sunucularına gönderin. Buna, tarayıcınız, hangi {appName} WebUI sayfalarını kullandığınız, hata raporlamanın yanı sıra işletim sistemi ve çalışma zamanı sürümü hakkındaki bilgiler de dahildir. Bu bilgiyi özelliklere ve hata düzeltmelerine öncelik vermek için kullanacağız.",
"Backup": "Yedek",
"BindAddress": "Bind Adresi",
"DownloadClientFreeboxSettingsApiUrl": "API URL'si",
"DownloadClientFreeboxSettingsAppId": "Uygulama kimliği",
"DownloadClientFreeboxNotLoggedIn": "Giriş yapmadınız",
"DownloadClientFreeboxSettingsAppToken": "Uygulama Token'ı",
"DownloadClientFreeboxSettingsAppTokenHelpText": "Freebox API'sine erişim oluşturulurken alınan uygulama jetonu (ör. 'app_token')",
"Apply": "Uygula",
"DownloadClientFreeboxAuthenticationError": "Freebox API'sinde kimlik doğrulama başarısız oldu. Sebep: {errorDescription}",
"DownloadClientFreeboxSettingsAppIdHelpText": "Freebox API'sine erişim oluşturulurken verilen uygulama kimliği (ör. 'app_id')",
"DownloadClientFreeboxSettingsHostHelpText": "Freebox'un ana bilgisayar adı veya ana bilgisayar IP adresi, varsayılan olarak '{url}' şeklindedir (yalnızca aynı ağdaysa çalışır)",
"DownloadClientFreeboxSettingsApiUrlHelpText": "Freebox API temel URL'sini API sürümüyle tanımlayın, örneğin '{url}', varsayılan olarak '{defaultApiUrl}' olur",
"BlocklistReleases": "Kara Liste Sürümü",
"DownloadClientNzbgetValidationKeepHistoryZeroDetail": "NzbGet ayarı KeepHistory 0 olarak ayarlandı. Bu, {appName}'in tamamlanan indirmeleri görmesini engelliyor.",
"DownloadClientQbittorrentSettingsUseSslHelpText": "Güvenli bir bağlantı kullanın. qBittorrent'te Seçenekler -> Web Kullanıcı Arayüzü -> 'HTTP yerine HTTPS kullan' bölümüne bakın.",
"DownloadClientValidationCategoryMissingDetail": "Girdiğiniz kategori {clientName} içinde mevcut değil. Önce bunu {clientName} içinde oluşturun.",
"EditDownloadClientImplementation": "İndirme İstemcisini Düzenle - {implementationName}",
"DownloadClientQbittorrentValidationCategoryUnsupported": "Kategori desteklenmiyor",
"DownloadClientValidationCategoryMissing": "Kategori mevcut değil",
"DownloadClientNzbgetSettingsAddPausedHelpText": "Bu seçenek en az NzbGet sürüm 16.0'ı gerektirir",
"DownloadClientFreeboxUnableToReachFreeboxApi": "Freebox API'sine ulaşılamıyor. Temel URL ve sürüm için 'API URL'si' ayarını doğrulayın.",
"DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "NzbGet KeepHistory ayarı çok yüksek ayarlanmış.",
"DownloadClientPneumaticSettingsNzbFolder": "Nzb Klasörü",
"DownloadClientPneumaticSettingsStrmFolderHelpText": "Bu klasördeki .strm dosyaları drone ile içe aktarılacak",
"DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Önce ilk ve son parçaları indirin (qBittorrent 4.1.0+)",
"DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent, DHT devre dışıyken magnet bağlantısını çözemiyor",
"DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent meta verileri indiriyor",
"DownloadClientQbittorrentTorrentStateError": "qBittorrent bir hata bildiriyor",
"DownloadClientQbittorrentTorrentStateStalled": "Bağlantı kesildi indirme işlemi durduruldu",
"DownloadClientQbittorrentValidationCategoryAddFailure": "Kategori yapılandırması başarısız oldu",
"DownloadClientQbittorrentTorrentStateUnknown": "Bilinmeyen indirme durumu: {state}",
"DownloadClientQbittorrentValidationCategoryRecommended": "Kategori önerilir",
"DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName}, tamamlanan indirmeleri kategorisi olmadan içe aktarmaya çalışmaz.",
"DownloadClientRTorrentSettingsAddStopped": "Ekleme Durduruldu",
"DownloadClientSabnzbdValidationEnableDisableDateSorting": "Tarih Sıralamayı Devre Dışı Bırak",
"DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "İçe aktarma sorunlarını önlemek için {appName}'in kullandığı kategori için Film sıralamayı devre dışı bırakmalısınız. Düzeltmek için Sabnzbd'a gidin.",
"DownloadClientSabnzbdValidationUnknownVersion": "Bilinmeyen Sürüm: {rawVersion}",
"DownloadClientSabnzbdValidationEnableJobFolders": "İş klasörlerini etkinleştir",
"DownloadClientSettingsAddPaused": "Ekleme Durduruldu",
"DownloadClientSettingsDestinationHelpText": "İndirme hedefini manuel olarak belirtir, varsayılanı kullanmak için boş bırakın",
"DownloadClientSettingsInitialState": "Başlangıç Durumu",
"DownloadClientSettingsPostImportCategoryHelpText": "{appName}'in indirmeyi içe aktardıktan sonra ayarlayacağı kategori. {appName}, tohumlama tamamlansa bile bu kategorideki torrentleri kaldırmaz. Aynı kategoriyi korumak için boş bırakın.",
"DownloadClientSettingsUseSslHelpText": "{clientName} ile bağlantı kurulurken güvenli bağlantıyı kullan",
"DownloadClientSettingsUrlBaseHelpText": "{clientName} URL'sine {url} gibi bir önek ekler",
"DownloadClientValidationApiKeyRequired": "API Anahtarı Gerekli",
"DownloadClientValidationAuthenticationFailure": "Kimlik doğrulama hatası",
"AlreadyInYourLibrary": "Kütüphanenizde mevcut",
"EditMetadata": "{metadataType} Meta Verisini Düzenle",
"DownloadClientPneumaticSettingsStrmFolder": "Strm Klasörü",
"DownloadClientPneumaticSettingsNzbFolderHelpText": "Bu klasöre XBMC'den erişilmesi gerekecek",
"DownloadClientPriorityHelpText": "İstemci Önceliğini 1'den (En Yüksek) 50'ye (En Düşük) indirin. Varsayılan: 1. Aynı önceliğe sahip istemciler için Round-Robin kullanılır.",
"DownloadClientQbittorrentSettingsFirstAndLastFirst": "İlk ve Son İlk",
"DownloadClientQbittorrentSettingsContentLayoutHelpText": "qBittorrent'in yapılandırılmış içerik düzenini mi, torrentteki orijinal düzeni mi kullanacağınızı yoksa her zaman bir alt klasör oluşturup oluşturmayacağınızı (qBittorrent 4.3.2+)",
"DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName}, etiketi qBittorrent'e ekleyemedi.",
"DownloadClientQbittorrentValidationQueueingNotEnabled": "Sıraya Alma Etkin Değil",
"DownloadClientRTorrentSettingsUrlPathHelpText": "XMLRPC uç noktasının yolu, bkz. {url}. RuTorrent kullanılırken bu genellikle RPC2 veya [ruTorrent yolu]{url2} olur.",
"DownloadClientSabnzbdValidationDevelopVersion": "Sabnzbd, sürüm 3.0.0 veya üzerini varsayarak sürüm geliştirir.",
"DownloadClientValidationUnableToConnect": "{clientName} ile bağlantı kurulamıyor",
"DownloadClientValidationUnknownException": "Bilinmeyen istisna: {exception}",
"DownloadClientValidationVerifySsl": "SSL Doğrulama ayarı",
"DownloadClientVuzeValidationErrorVersion": "Protokol sürümü desteklenmiyor; Vuze Web Remote eklentisi ile Vuze 5.0.0.0 veya üstünü kullanın.",
"EditAutoTag": "Otomatik Etiket Düzenle",
"EditConditionImplementation": "Koşulu Düzenle - {implementationName}",
"DownloadClientNzbgetValidationKeepHistoryZero": "NzbGet ayarı KeepHistory 0'dan büyük olmalıdır",
"DownloadClientNzbgetValidationKeepHistoryOverMax": "NzbGet ayarı KeepHistory 25000'den az olmalıdır",
"DownloadClientQbittorrentSettingsSequentialOrder": "Sıralı Sıra",
"DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Sıralı olarak indirin (qBittorrent 4.1.0+)",
"DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Kategoriler qBittorrent 3.3.0 sürümüne kadar desteklenmemektedir. Lütfen yükseltme yapın veya boş bir Kategoriyle tekrar deneyin.",
"DownloadClientRTorrentSettingsUrlPath": "URL Yolu",
"DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Torrent Kuyruğa Alma, qBittorrent ayarlarınızda etkin değil. qBittorrent'te etkinleştirin veya öncelik olarak 'Son'u seçin.",
"DownloadClientSabnzbdValidationCheckBeforeDownload": "Sabnbzd'de 'İndirmeden önce kontrol et' seçeneğini devre dışı bırakın",
"DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Film Sıralamayı Devre Dışı Bırak",
"DownloadClientSabnzbdValidationEnableDisableTvSorting": "TV Sıralamasını Devre Dışı Bırak",
"DownloadClientValidationVerifySslDetail": "Lütfen hem {clientName} hem de {appName} üzerinde SSL yapılandırmanızı doğrulayın",
"DownloadClientValidationUnableToConnectDetail": "Lütfen ana bilgisayar adını ve bağlantı noktasını doğrulayın.",
"EditImportListImplementation": "İçe Aktarma Listesini Düzenle - {implementationName}",
"DownloadClientQbittorrentSettingsContentLayout": "İçerik Düzeni",
"DownloadClientSettingsRecentPriority": "Yeni Öncelik",
"DownloadClientUTorrentTorrentStateError": "uTorrent bir hata bildirdi",
"DownloadClientValidationApiKeyIncorrect": "API Anahtarı Yanlış",
"DownloadClientValidationGroupMissingDetail": "Girdiğiniz grup {clientName} içinde mevcut değil. Önce bunu {clientName} içinde oluşturun.",
"Duration": "Süre",
"DownloadIgnored": "Yoksayılanları İndir",
"DownloadClientsLoadError": "İndirme istemcileri yüklenemiyor",
"DownloadClientQbittorrentSettingsInitialStateHelpText": "Torrentlerin başlangıç durumu qBittorrent'e eklendi. Zorunlu Torrentlerin seed kısıtlamalarına uymadığını unutmayın",
"DownloadClientRTorrentSettingsAddStoppedHelpText": "Etkinleştirme, durdurulmuş durumdaki rTorrent'e torrentler ve magnet ekleyecektir. Bu magnet dosyalarını bozabilir.",
"DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "'İndirmeden önce kontrol et' seçeneğinin kullanılması, {appName} uygulamasının yeni indirilenleri takip etme yeteneğini etkiler. Ayrıca Sabnzbd, daha etkili olduğu için bunun yerine 'Tamamlanamayan işleri iptal et' seçeneğini öneriyor.",
"DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName}, geliştirme sürümlerini çalıştırırken SABnzbd'ye eklenen yeni özellikleri desteklemeyebilir.",
"DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "İçe aktarma sorunlarını önlemek için {appName}'in kullandığı kategori için Tarih sıralamayı devre dışı bırakmalısınız. Düzeltmek için Sabnzbd'a gidin.",
"DownloadClientValidationGroupMissing": "Grup mevcut değil",
"DownloadClientValidationTestTorrents": "Torrentlerin listesi alınamadı: {exceptionMessage}",
"DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName}, Tamamlanan İndirme İşlemini yapılandırıldığı şekilde gerçekleştiremeyecek. Bunu qBittorrent'te (menüde 'Araçlar -> Seçenekler...') 'Seçenekler -> BitTorrent -> Paylaşım Oranı Sınırlaması'nı 'Kaldır' yerine 'Duraklat' olarak değiştirerek düzeltebilirsiniz.",
"DownloadClientSettingsCategoryHelpText": "{appName}'e özel bir kategori eklemek, {appName} dışındaki ilgisiz indirmelerle çakışmaları önler. Kategori kullanmak isteğe bağlıdır ancak önemle tavsiye edilir.",
"DownloadClientSettingsOlderPriority": "Eski Önceliği",
"DownloadClientValidationTestNzbs": "NZB'lerin listesi alınamadı: {exceptionMessage}",
"DownloadClientValidationSslConnectFailure": "SSL aracılığıyla bağlanılamıyor",
"DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "{downloadClientName} indirme istemcisi, tamamlanan indirmeleri kaldıracak şekilde ayarlandı. Bu, indirilenlerin {appName} içe aktarılmadan önce istemcinizden kaldırılmasına neden olabilir.",
"DownloadClientQbittorrentTorrentStatePathError": "İçe Aktarılamıyor. Yol, istemci tabanlı indirme dizini ile eşleşiyor, bu torrent için 'Üst düzey klasörü tut' seçeneği devre dışı bırakılmış olabilir veya 'Torrent İçerik Düzeni' 'Orijinal' veya 'Alt Klasör Oluştur' olarak ayarlanmamış olabilir mi?",
"DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent, Torrentleri Paylaşım Oranı Sınırına ulaştıklarında kaldıracak şekilde yapılandırılmıştır",
"DownloadClientRTorrentProviderMessage": "rTorrent, başlangıç kriterlerini karşılayan torrentleri duraklatmaz. {appName}, torrentlerin otomatik olarak kaldırılmasını Ayarlar->Dizinleyiciler'deki geçerli tohum kriterlerine göre yalnızca Tamamlandı Kaldırma etkinleştirildiğinde gerçekleştirecektir. İçe aktardıktan sonra, davranışı özelleştirmek için rTorrent komut dosyalarında kullanılabilen {importedView}'ı bir rTorrent görünümü olarak ayarlayacaktır.",
"DownloadClientValidationAuthenticationFailureDetail": "Kullanıcı adınızı ve şifrenizi kontrol edin. Ayrıca, {appName} çalıştıran ana bilgisayarın, {clientName} yapılandırmasındaki WhiteList sınırlamaları nedeniyle {clientName} erişiminin engellenip engellenmediğini de doğrulayın.",
"DownloadStationStatusExtracting": ıkarılıyor: %{progress}",
"DownloadClientNzbVortexMultipleFilesMessage": "İndirme birden fazla dosya içeriyor ve bir iş klasöründe değil: {outputPath}",
"DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} her indirme işleminin ayrı bir klasöre sahip olmasını tercih ediyor. Klasör/Yol'a * eklendiğinde Sabnzbd bu iş klasörlerini oluşturmayacaktır. Düzeltmek için Sabnzbd'a gidin.",
"DownloadClientRTorrentSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum, varsayılan rTorrent konumunu kullanmak için boş bırakın",
"DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "İçe aktarma sorunlarını önlemek için {appName}'in kullandığı kategori için TV sıralamasını devre dışı bırakmalısınız. Düzeltmek için Sabnzbd'a gidin.",
"DownloadClientSettingsCategorySubFolderHelpText": "{appName}'e özel bir kategori eklemek, {appName} dışındaki ilgisiz indirmelerle çakışmaları önler. Kategori kullanmak isteğe bağlıdır ancak önemle tavsiye edilir. Çıkış dizininde bir [kategori] alt dizini oluşturur.",
"DownloadClientSettingsInitialStateHelpText": "{clientName} dosyasına eklenen torrentler için başlangıç durumu",
"DownloadClientTransmissionSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum, varsayılan İletim konumunu kullanmak için boş bırakın",
"DownloadClientTransmissionSettingsUrlBaseHelpText": "{clientName} rpc URL'sine bir önek ekler, örneğin {url}, varsayılan olarak '{defaultUrl}' olur",
"DownloadClientValidationErrorVersion": "{clientName} sürümü en az {requiredVersion} olmalıdır. Bildirilen sürüm: {reportedVersion}",
"DownloadClientValidationSslConnectFailureDetail": "{appName}, SSL kullanarak {clientName} uygulamasına bağlanamıyor. Bu sorun bilgisayarla ilgili olabilir. Lütfen hem {appName} hem de {clientName} uygulamasını SSL kullanmayacak şekilde yapılandırmayı deneyin.",
"Imported": "İçe aktarıldı",
"NotificationsAppriseSettingsTagsHelpText": "İsteğe bağlı olarak yalnızca uygun şekilde etiketlenenleri bilgilendirin.",
"NotificationsDiscordSettingsAvatar": "Avatar",
"NotificationsGotifySettingsAppTokenHelpText": "Gotify tarafından oluşturulan Uygulama Tokenı",
"ImportScriptPath": "Komut Dosyası Yolunu İçe Aktar",
"History": "Geçmiş",
"EditSelectedImportLists": "Seçilen İçe Aktarma Listelerini Düzenle",
"FormatShortTimeSpanSeconds": "{seconds} saniye",
"LabelIsRequired": "Etiket gerekli",
"NoHistoryFound": "Geçmiş bulunamadı",
"NotificationsCustomScriptSettingsArguments": "Argümanlar",
"NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "'Manuel Etkileşimlerde' bildirimi için iletilen alanları değiştirin",
"FormatShortTimeSpanHours": "{hours} saat",
"FormatRuntimeMinutes": "{minutes}dk",
"FullColorEventsHelpText": "Etkinliğin tamamını yalnızca sol kenar yerine durum rengiyle renklendirecek şekilde stil değiştirildi. Gündem için geçerli değildir",
"GrabId": "ID Yakala",
"ImportUsingScriptHelpText": "Bir komut dosyası kullanarak içe aktarmak için dosyaları kopyalayın (ör. kod dönüştürme için)",
"InstanceNameHelpText": "Sekmedeki örnek adı ve Syslog uygulaması adı için",
"ManageDownloadClients": "İndirme İstemcilerini Yönet",
"ManageImportLists": "İçe Aktarma Listelerini Yönet",
"NotificationTriggersHelpText": "Bu bildirimi hangi olayların tetikleyeceğini seçin",
"NotificationsAppriseSettingsTags": "Apprise Etiketler",
"NotificationsCustomScriptSettingsArgumentsHelpText": "Komut dosyasına aktarılacak argümanlar",
"NotificationsCustomScriptValidationFileDoesNotExist": "Dosya bulunmuyor",
"NotificationsDiscordSettingsOnGrabFields": "Yakalamalarda",
"NotificationsDiscordSettingsAvatarHelpText": "Bu entegrasyondaki mesajlar için kullanılan avatarı değiştirin",
"NotificationsDiscordSettingsOnImportFieldsHelpText": "'İçe aktarmalarda' bildirimi için iletilen alanları değiştirin",
"NotificationsDiscordSettingsOnImportFields": "İçe Aktarmalarda",
"NotificationsEmailSettingsBccAddress": "BCC Adres(ler)i",
"NotificationsEmailSettingsName": "E-posta",
"NotificationsEmailSettingsFromAddress": "Adresten",
"NotificationsEmailSettingsServerHelpText": "E-posta sunucusunun ana bilgisayar adı veya IP'si",
"NotificationsGotifySettingsServer": "Gotify Sunucusu",
"NotificationsGotifySettingsPriorityHelpText": "Bildirimin önceliği",
"NotificationsJoinSettingsDeviceNames": "Cihaz Adları",
"NotificationsKodiSettingsDisplayTime": "Gösterim Süresi",
"NotificationsKodiSettingsDisplayTimeHelpText": "Bildirimin ne kadar süreyle görüntüleneceği (Saniye cinsinden)",
"NotificationsMailgunSettingsUseEuEndpoint": "AB Uç Noktasını Kullan",
"NotificationsMailgunSettingsSenderDomain": "Gönderen Alanı",
"NotificationsNtfySettingsAccessToken": "Erişim Token'ı",
"NotificationsNtfySettingsAccessTokenHelpText": "İsteğe bağlı belirteç tabanlı yetkilendirme. Kullanıcı adı/şifreye göre önceliklidir",
"NotificationsNtfySettingsPasswordHelpText": "İsteğe bağlı şifre",
"NotificationsNtfySettingsTagsEmojisHelpText": "Kullanılacak etiketlerin veya emojilerin isteğe bağlı listesi",
"NotificationsNtfySettingsTopics": "Konular",
"NotificationsNtfySettingsUsernameHelpText": "İsteğe bağlı kullanıcı adı",
"NotificationsCustomScriptSettingsName": "Özel Komut Dosyası",
"NotificationsKodiSettingAlwaysUpdate": "Daima Güncelle",
"NotificationsKodiSettingsCleanLibrary": "Kütüphaneyi Temizle",
"NotificationsKodiSettingsCleanLibraryHelpText": "Güncellemeden sonra kitaplığı temizle",
"Unmonitored": "İzlenmeyen",
"FormatAgeHour": "saat",
"FormatAgeHours": "saat",
"NoHistory": "Geçmiş yok",
"FailedToFetchUpdates": "Güncellemeler getirilemedi",
"InstanceName": "Örnek isim",
"MoveAutomatically": "Otomatik Olarak Taşı",
"MustContainHelpText": "İzin bu şartlardan en az birini içermelidir (büyük/küçük harfe duyarlı değildir)",
"NotificationStatusAllClientHealthCheckMessage": "Arızalar nedeniyle tüm bildirimler kullanılamıyor",
"EditSelectedIndexers": "Seçili Dizin Oluşturucuları Düzenle",
"EnableProfileHelpText": "Sürüm profilini etkinleştirmek için işaretleyin",
"EnableRssHelpText": "{appName}, RSS Senkronizasyonu aracılığıyla düzenli aralıklarla sürüm değişikliği aradığında kullanacak",
"FormatTimeSpanDays": "{days}g {time}",
"FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}",
"NotificationsNtfySettingsTagsEmojis": "Ntfy Etiketler ve Emojiler",
"NotificationsJoinSettingsDeviceNamesHelpText": "Bildirim göndermek istediğiniz tam veya kısmi cihaz adlarının virgülle ayrılmış listesi. Ayarlanmadığı takdirde tüm cihazlar bildirim alacaktır.",
"NotificationsKodiSettingAlwaysUpdateHelpText": "Bir video oynatılırken bile kitaplık güncellensin mi?",
"EnableProfile": "Profili Etkinleştir",
"Example": "Örnek",
"FormatAgeDay": "gün",
"FormatDateTime": "{formattedDate} {formattedTime}",
"FormatRuntimeHours": "{hours}s",
"LanguagesLoadError": "Diller yüklenemiyor",
"ListWillRefreshEveryInterval": "Liste her {refreshInterval} yenilenecektir",
"ManageIndexers": "Dizin Oluşturucuları Yönet",
"ManualGrab": "Manuel Yakalama",
"DownloadClientsSettingsSummary": "İndirme İstemcileri, indirme işlemleri ve uzaktan yol eşlemeleri",
"DownloadClients": "İndirme İstemcileri",
"InteractiveImportNoFilesFound": "Seçilen klasörde video dosyası bulunamadı",
"ListQualityProfileHelpText": "Kalite Profili listesi öğeleri şu şekilde eklenecektir:",
"MustNotContainHelpText": "Bir veya daha fazla terimi içeriyorsa yayın reddedilecektir (büyük/küçük harfe duyarlı değildir)",
"NoDownloadClientsFound": "İndirme istemcisi bulunamadı",
"NotificationStatusSingleClientHealthCheckMessage": "Arızalar nedeniyle bildirimler kullanılamıyor: {notificationNames}",
"NotificationsAppriseSettingsStatelessUrls": "Apprise Durum bilgisi olmayan URL'ler",
"NotificationsCustomScriptSettingsProviderMessage": "Test, betiği EventType {eventTypeTest} olarak ayarlıyken yürütür; betiğinizin bunu doğru şekilde işlediğinden emin olun",
"NotificationsDiscordSettingsAuthorHelpText": "Bu bildirim için gösterilen yerleştirme yazarını geçersiz kılın. Boş örnek adıdır",
"NotificationsDiscordSettingsOnGrabFieldsHelpText": "Bu 'yakalandı' bildirimi için iletilen alanları değiştirin",
"NotificationsDiscordSettingsUsernameHelpText": "Gönderimin yapılacağı kullanıcı adı varsayılan olarak Discord webhook varsayılanıdır",
"NotificationsKodiSettingsGuiNotification": "GUI Bildirimi",
"NotificationsJoinValidationInvalidDeviceId": "Cihaz kimlikleri geçersiz görünüyor.",
"NotificationsNtfySettingsClickUrl": "URL'ye tıklayın",
"NotificationsNotifiarrSettingsApiKeyHelpText": "Profilinizdeki API anahtarınız",
"EditReleaseProfile": "Sürüm Profilini Düzenle",
"EditSelectedDownloadClients": "Seçilen İndirme İstemcilerini Düzenle",
"FormatShortTimeSpanMinutes": "{minutes} dakika",
"FullColorEvents": "Tam Renkli Etkinlikler",
"ListRootFolderHelpText": "Kök Klasör listesi öğeleri eklenecek",
"HourShorthand": "s",
"LogFilesLocation": "Günlük dosyaları şu konumda bulunur: {location}",
"ImportUsingScript": "Komut Dosyası Kullanarak İçe Aktar",
"IncludeHealthWarnings": "Sağlık Uyarılarını Dahil Et",
"IndexerSettingsMultiLanguageRelease": "Çok dil",
"IndexerSettingsMultiLanguageReleaseHelpText": "Bu indeksleyicideki çoklu sürümde normalde hangi diller bulunur?",
"InteractiveImportNoImportMode": "Bir içe aktarma modu seçilmelidir",
"InteractiveImportNoQuality": "Seçilen her dosya için kalite seçilmelidir",
"NotificationsEmailSettingsServer": "Sunucu",
"InvalidUILanguage": "Kullanıcı arayüzünüz geçersiz bir dile ayarlanmış, düzeltin ve ayarlarınızı kaydedin",
"NotificationsAppriseSettingsServerUrl": "Apprise Sunucu URL'si",
"ManageClients": "İstemcileri Yönet",
"ManageLists": "Listeleri Yönet",
"MediaInfoFootNote": "Full/AudioLanguages/SubtitleLanguages, dosya adında yer alan dilleri filtrelemenize olanak tanıyan bir `:EN+DE` son ekini destekler. Belirli dilleri hariç tutmak için '-DE'yi kullanın. `+` (örneğin `:EN+`) eklenmesi, hariç tutulan dillere bağlı olarak `[EN]`/`[EN+--]`/`[--]` sonucunu verecektir. Örneğin `{MediaInfo Full:EN+DE}`.",
"Never": "Asla",
"NoHistoryBlocklist": "Geçmiş engellenenler listesi yok",
"NoIndexersFound": "Dizin oluşturucu bulunamadı",
"NotificationsAppriseSettingsConfigurationKeyHelpText": "Kalıcı Depolama Çözümü için Yapılandırma Anahtarı. Durum Bilgisi Olmayan URL'ler kullanılıyorsa boş bırakın.",
"NotificationsAppriseSettingsPasswordHelpText": "HTTP Temel Kimlik Doğrulama Parolası",
"NotificationsAppriseSettingsUsernameHelpText": "HTTP Temel Kimlik Doğrulama Kullanıcı Adı",
"NotificationsAppriseSettingsStatelessUrlsHelpText": "Bildirimin nereye gönderilmesi gerektiğini belirten, virgülle ayrılmış bir veya daha fazla URL. Kalıcı Depolama kullanılıyorsa boş bırakın.",
"NotificationsDiscordSettingsOnManualInteractionFields": "Manuel Etkileşimlerde",
"NotificationsEmbySettingsSendNotifications": "Bildirim Gönder",
"NotificationsEmbySettingsSendNotificationsHelpText": "MediaBrowser'ın yapılandırılmış sağlayıcılara bildirim göndermesini sağlayın",
"NotificationsJoinSettingsDeviceIdsHelpText": "Kullanımdan kaldırıldı, bunun yerine Cihaz Adlarını kullanın. Bildirim göndermek istediğiniz Cihaz Kimliklerinin virgülle ayrılmış listesi. Ayarlanmadığı takdirde tüm cihazlar bildirim alacaktır.",
"NotificationsMailgunSettingsApiKeyHelpText": "MailGun'dan oluşturulan API anahtarı",
"NotificationsMailgunSettingsUseEuEndpointHelpText": "AB MailGun uç noktasını kullanmayı etkinleştirin",
"NotificationsNtfySettingsClickUrlHelpText": "Kullanıcı bildirime tıkladığında isteğe bağlı bağlantı",
"NotificationsNtfySettingsServerUrl": "Sunucu URL'si",
"NotificationsNtfySettingsTopicsHelpText": "Bildirim gönderilecek konuların listesi",
"Save": "Kaydet",
"Connect": "Bildirimler",
"InfoUrl": "Bilgi URL'si",
"InteractiveSearchModalHeader": "İnteraktif Arama",
"No": "Hayır",
"NotificationsDiscordSettingsWebhookUrlHelpText": "Discord kanalı webhook URL'si",
"NotificationsEmailSettingsBccAddressHelpText": "E-posta bcc alıcılarının virgülle ayrılmış listesi",
"FailedToUpdateSettings": "Ayarlar güncellenemedi",
"False": "Pasif",
"HistoryLoadError": "Geçmiş yüklenemiyor",
"NotificationsEmailSettingsRecipientAddressHelpText": "E-posta alıcılarının virgülle ayrılmış listesi",
"FormatAgeDays": "gün",
"IgnoreDownload": "İndirmeyi Yoksay",
"IgnoreDownloadHint": "{appName}'in bu indirmeyi daha fazla işlemesini durdurur",
"IgnoreDownloads": "İndirilenleri Yoksay",
"IgnoreDownloadsHint": "{appName}'ın bu indirmeleri daha fazla işlemesi durdurulur",
"Implementation": "Uygula",
"Import": "İçe aktar",
"NotificationsEmailSettingsCcAddressHelpText": "E-posta CC alıcılarının virgülle ayrılmış listesi",
"NotificationsEmailSettingsCcAddress": "CC Adres(ler)i",
"NotificationsEmailSettingsRecipientAddress": "Alıcı Adres(ler)i",
"NotificationsEmbySettingsUpdateLibraryHelpText": "İçe Aktarma, Yeniden Adlandırma veya Silme sırasında Kitaplık Güncellensin mi?",
"NotificationsGotifySettingsAppToken": "Uygulama Token'ı",
"NotificationsJoinSettingsDeviceIds": "Cihaz Kimlikleri",
"NotificationsJoinSettingsNotificationPriority": "Bildirim Önceliği",
"Test": "Sına",
"HealthMessagesInfoBox": "Satırın sonundaki wiki bağlantısını (kitap simgesi) tıklayarak veya [günlüklerinizi]({link}) kontrol ederek bu durum kontrolü mesajlarının nedeni hakkında daha fazla bilgi bulabilirsiniz. Bu mesajları yorumlamakta zorluk yaşıyorsanız aşağıdaki bağlantılardan destek ekibimize ulaşabilirsiniz.",
"IndexerSettingsRejectBlocklistedTorrentHashes": "Yakalarken Engellenen Torrent Karmalarını Reddet",
"IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Bir torrent karma tarafından engellendiyse, RSS/Bazı dizin oluşturucuları arama sırasında düzgün şekilde reddedilmeyebilir; bunun etkinleştirilmesi, torrent yakalandıktan sonra ancak istemciye gönderilmeden önce reddedilmesine olanak tanır.",
"NotificationsAppriseSettingsConfigurationKey": "Apprise Yapılandırma Anahtarı",
"NotificationsAppriseSettingsNotificationType": "Apprise Bildirim Türü",
"NotificationsGotifySettingsServerHelpText": "Gerekiyorsa http(s):// ve bağlantı noktası dahil olmak üzere Gotify sunucu URL'si",
"NotificationsJoinSettingsApiKeyHelpText": "Katıl hesap ayarlarınızdaki API Anahtarı (API'ye Katıl düğmesine tıklayın).",
"ImportScriptPathHelpText": "İçe aktarma için kullanılacak komut dosyasının yolu",
"Label": "Etiket",
"NoDelay": "Gecikme yok",
"NoImportListsFound": "İçe aktarma listesi bulunamadı",
"FormatAgeMinute": "dakika",
"FormatAgeMinutes": "dakika",
"NotificationsDiscordSettingsAuthor": "Yazar",
"NotificationsEmailSettingsUseEncryption": "Şifreleme Kullan",
"NotificationsAppriseSettingsServerUrlHelpText": "Gerekiyorsa http(s):// ve bağlantı noktasını içeren Apprise sunucu URL'si",
"NotificationsEmailSettingsUseEncryptionHelpText": "Sunucuda yapılandırılmışsa şifrelemeyi kullanmayı mı tercih edeceğiniz, şifrelemeyi her zaman SSL (yalnızca Bağlantı Noktası 465) veya StartTLS (başka herhangi bir bağlantı noktası) aracılığıyla mı kullanacağınız veya hiçbir zaman şifrelemeyi kullanmama tercihlerini belirler",
"NotificationsKodiSettingsUpdateLibraryHelpText": "İçe Aktarma ve Yeniden Adlandırmada kitaplık güncellensin mi?",
"NotificationsNtfySettingsServerUrlHelpText": "Genel sunucuyu ({url}) kullanmak için boş bırakın",
"InteractiveImportLoadError": "Manuel içe aktarma öğeleri yüklenemiyor",
"IndexerDownloadClientHelpText": "Bu dizin oluşturucudan yakalamak için hangi indirme istemcisinin kullanılacağını belirtin"
}

View File

@ -1808,5 +1808,24 @@
"IgnoreDownloadHint": "阻止 {appName} 进一步处理此下载",
"IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "如果 torrent 的哈希被屏蔽了某些索引器在使用RSS或者搜索期间可能无法正确拒绝它启用此功能将允许在抓取 torrent 之后但在将其发送到客户端之前拒绝它。",
"RemoveQueueItemRemovalMethodHelpTextWarning": "“从下载客户端移除”将从下载客户端移除下载内容和文件。",
"AutoTaggingSpecificationOriginalLanguage": "语言"
"AutoTaggingSpecificationOriginalLanguage": "语言",
"CustomFormatsSpecificationFlag": "标记",
"CustomFormatsSpecificationLanguage": "语言",
"AddDelayProfileError": "无法添加新的延迟配置,请重试。",
"CustomFilter": "自定义过滤器",
"CustomFormatsSpecificationMinimumSizeHelpText": "必须大于该尺寸才会发布",
"AddAutoTagError": "无法添加新的自动标签,请重试。",
"AutoTaggingSpecificationTag": "标签",
"BlocklistReleaseHelpText": "禁止 {appName}通过 RSS 或自动搜索重新下载此版本",
"CleanLibraryLevel": "清除库等级",
"AutoTaggingSpecificationMinimumYear": "最小年份",
"AutoTaggingSpecificationRootFolder": "根文件夹",
"AutoTaggingSpecificationMaximumYear": "最大年份",
"AutoTaggingSpecificationQualityProfile": "质量概况",
"CustomFormatsSpecificationMaximumSize": "最大尺寸",
"CustomFormatsSpecificationMinimumSize": "最小尺寸",
"CustomFormatsSpecificationMaximumSizeHelpText": "必须小于或等于该尺寸时才会发布",
"AutoTaggingSpecificationGenre": "类型",
"AutoTaggingSpecificationSeriesType": "系列类型",
"AutoTaggingSpecificationStatus": "状态"
}

View File

@ -538,7 +538,7 @@ namespace NzbDrone.Core.Parser
// Handle Exception Release Groups that don't follow -RlsGrp; Manual List
// name only...be very careful with this last; high chance of false positives
private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?<releasegroup>(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?<releasegroup>(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D|KRaLiMaRKo|BluDragon)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
// groups whose releases end with RlsGroup) or RlsGroup]
private static readonly Regex ExceptionReleaseGroupRegex = new Regex(@"(?<=[._ \[])(?<releasegroup>(Silence|afm72|Panda|Ghost|MONOLITH|Tigole|Joy|ImE|UTR|t3nzin|Anime Time|Project Angel|Hakata Ramen|HONE|Vyndros|SEV|Garshasp|Kappa|Natty|RCVR|SAMPA|YOGI|r00t|EDGE2020|RZeroX)(?=\]|\)))", RegexOptions.IgnoreCase | RegexOptions.Compiled);

View File

@ -18,7 +18,7 @@
<PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
<PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
<PackageReference Include="FluentValidation" Version="9.5.4" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.3" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="4.7.14" />
<PackageReference Include="MonoTorrent" Version="2.0.7" />

View File

@ -12,6 +12,7 @@ using NzbDrone.Common;
using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.Options;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Download;
@ -46,6 +47,11 @@ namespace NzbDrone.App.Test
container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object);
container.RegisterInstance<IBroadcastSignalRMessage>(new Mock<IBroadcastSignalRMessage>().Object);
container.RegisterInstance<IOptions<PostgresOptions>>(new Mock<IOptions<PostgresOptions>>().Object);
container.RegisterInstance<IOptions<AuthOptions>>(new Mock<IOptions<AuthOptions>>().Object);
container.RegisterInstance<IOptions<AppOptions>>(new Mock<IOptions<AppOptions>>().Object);
container.RegisterInstance<IOptions<ServerOptions>>(new Mock<IOptions<ServerOptions>>().Object);
container.RegisterInstance<IOptions<UpdateOptions>>(new Mock<IOptions<UpdateOptions>>().Object);
container.RegisterInstance<IOptions<LogOptions>>(new Mock<IOptions<LogOptions>>().Object);
_container = container.GetServiceProvider();
}

View File

@ -20,6 +20,7 @@ using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.Options;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore.Extensions;
using Sonarr.Http.ClientSchema;
@ -98,6 +99,11 @@ namespace NzbDrone.Host
.ConfigureServices(services =>
{
services.Configure<PostgresOptions>(config.GetSection("Sonarr:Postgres"));
services.Configure<AppOptions>(config.GetSection("Sonarr:App"));
services.Configure<AuthOptions>(config.GetSection("Sonarr:Auth"));
services.Configure<ServerOptions>(config.GetSection("Sonarr:Server"));
services.Configure<LogOptions>(config.GetSection("Sonarr:Log"));
services.Configure<UpdateOptions>(config.GetSection("Sonarr:Update"));
}).Build();
break;
@ -119,12 +125,12 @@ namespace NzbDrone.Host
{
var config = GetConfiguration(context);
var bindAddress = config.GetValue(nameof(ConfigFileProvider.BindAddress), "*");
var port = config.GetValue(nameof(ConfigFileProvider.Port), 8989);
var sslPort = config.GetValue(nameof(ConfigFileProvider.SslPort), 9898);
var enableSsl = config.GetValue(nameof(ConfigFileProvider.EnableSsl), false);
var sslCertPath = config.GetValue<string>(nameof(ConfigFileProvider.SslCertPath));
var sslCertPassword = config.GetValue<string>(nameof(ConfigFileProvider.SslCertPassword));
var bindAddress = config.GetValue<string>($"Sonarr:Server:{nameof(ServerOptions.BindAddress)}") ?? config.GetValue(nameof(ConfigFileProvider.BindAddress), "*");
var port = config.GetValue<int?>($"Sonarr:Server:{nameof(ServerOptions.Port)}") ?? config.GetValue(nameof(ConfigFileProvider.Port), 8989);
var sslPort = config.GetValue<int?>($"Sonarr:Server:{nameof(ServerOptions.SslPort)}") ?? config.GetValue(nameof(ConfigFileProvider.SslPort), 9898);
var enableSsl = config.GetValue<bool?>($"Sonarr:Server:{nameof(ServerOptions.EnableSsl)}") ?? config.GetValue(nameof(ConfigFileProvider.EnableSsl), false);
var sslCertPath = config.GetValue<string>($"Sonarr:Server:{nameof(ServerOptions.SslCertPath)}") ?? config.GetValue<string>(nameof(ConfigFileProvider.SslCertPath));
var sslCertPassword = config.GetValue<string>($"Sonarr:Server:{nameof(ServerOptions.SslCertPassword)}") ?? config.GetValue<string>(nameof(ConfigFileProvider.SslCertPassword));
var urls = new List<string> { BuildUrl("http", bindAddress, port) };
@ -152,6 +158,11 @@ namespace NzbDrone.Host
.ConfigureServices(services =>
{
services.Configure<PostgresOptions>(config.GetSection("Sonarr:Postgres"));
services.Configure<AppOptions>(config.GetSection("Sonarr:App"));
services.Configure<AuthOptions>(config.GetSection("Sonarr:Auth"));
services.Configure<ServerOptions>(config.GetSection("Sonarr:Server"));
services.Configure<LogOptions>(config.GetSection("Sonarr:Log"));
services.Configure<UpdateOptions>(config.GetSection("Sonarr:Update"));
})
.ConfigureWebHost(builder =>
{

View File

@ -170,6 +170,11 @@ namespace NzbDrone.Windows.Disk
{
try
{
if (source.Length > 256 && !source.StartsWith(@"\\?\"))
{
source = @"\\?\" + source;
}
return CreateHardLink(destination, source, IntPtr.Zero);
}
catch (Exception ex)

View File

@ -92,8 +92,6 @@ namespace Sonarr.Api.V3.Series
.When(s => s.Path.IsNullOrWhiteSpace());
PostValidator.RuleFor(s => s.Title).NotEmpty();
PostValidator.RuleFor(s => s.TvdbId).GreaterThan(0).SetValidator(seriesExistsValidator);
PutValidator.RuleFor(s => s.Path).IsValidPath();
}
[HttpGet]

3319
yarn.lock

File diff suppressed because it is too large Load Diff