Compare commits

...

20 Commits

Author SHA1 Message Date
Erik Persson
b371f2d913 New: Added setting to not include animebytes synonyms 2022-12-08 17:54:57 -06:00
Qstick
3ff3452e2d Handling for Obsolete API Endpoints 2022-12-08 17:50:18 -06:00
Qstick
df13537e29 Fixed: Use route Id for PUT requests if not passed in body 2022-12-08 17:48:33 -06:00
Qstick
5d2fefde8f Fixed: Correct Attribute compare for Id validation 2022-12-08 17:46:49 -06:00
Qstick
ffb3f83324 Simplify X-Forwarded-For handling
This happens in asp.net middleware now

Co-Authored-By: ta264 <ta264@users.noreply.github.com>
2022-12-08 17:45:14 -06:00
Qstick
1c125733b2 New: Improve IPAddress.IsLocal method
Co-Authored-By: ta264 <ta264@users.noreply.github.com>
2022-12-08 17:45:04 -06:00
Mark McDowall
2af7fac15e New: IPv6 support for connections/indexers/download clients 2022-12-08 17:44:24 -06:00
Mark McDowall
f172d17ecc Fixed: Improve Bind Address validation and help text 2022-12-08 17:42:55 -06:00
Zak Saunders
c69843931e New: Auto theme option to match OS theme
Co-authored-by: Qstick <qstick@gmail.com>
2022-12-08 17:40:59 -06:00
bakerboy448
cd3e99ad87 Fixed: Indexer Error handling improvements (#1172)
* Fixed: Indexer Error handling improvements

* fixup! Fixed: Indexer Error handling improvements
2022-12-01 21:30:27 -06:00
Qstick
1cce39b404 Fix Orpheus Tests 2022-11-29 20:15:23 -06:00
Qstick
9b46ab73e4 Fixed: (Orpheus) Parse date from epoch or date time string 2022-11-29 19:42:35 -06:00
Mark McDowall
a352c053ab Fixed: Publish ApplicationStartingEvent during startup
(cherry picked from commit 5400bce1295bdc4198d2cfe0b9258bbb7ccf0852)

Fixes #1199
2022-11-26 10:17:05 -06:00
Weblate
b33e45d266 Translated using Weblate (Slovak)
Currently translated at 23.0% (107 of 464 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 23.9% (111 of 464 strings)

Translated using Weblate (Catalan)

Currently translated at 75.6% (351 of 464 strings)

Translated using Weblate (Arabic)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Vietnamese)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Turkish)

Currently translated at 72.8% (338 of 464 strings)

Translated using Weblate (Thai)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Swedish)

Currently translated at 88.1% (409 of 464 strings)

Translated using Weblate (Russian)

Currently translated at 77.5% (360 of 464 strings)

Translated using Weblate (Romanian)

Currently translated at 73.2% (340 of 464 strings)

Translated using Weblate (Portuguese)

Currently translated at 80.8% (375 of 464 strings)

Translated using Weblate (Polish)

Currently translated at 75.6% (351 of 464 strings)

Translated using Weblate (Dutch)

Currently translated at 88.7% (412 of 464 strings)

Translated using Weblate (Korean)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Japanese)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Italian)

Currently translated at 98.0% (455 of 464 strings)

Translated using Weblate (Icelandic)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Hindi)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Hebrew)

Currently translated at 74.5% (346 of 464 strings)

Translated using Weblate (French)

Currently translated at 96.7% (449 of 464 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Spanish)

Currently translated at 79.3% (368 of 464 strings)

Translated using Weblate (Greek)

Currently translated at 72.8% (338 of 464 strings)

Translated using Weblate (German)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (German)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Danish)

Currently translated at 72.8% (338 of 464 strings)

Translated using Weblate (Czech)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Czech)

Currently translated at 73.0% (339 of 464 strings)

Translated using Weblate (Bulgarian)

Currently translated at 68.3% (317 of 464 strings)

Translated using Weblate (German)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (German)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (German)

Currently translated at 99.3% (461 of 464 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Don-Chris <Chr_Sch@t-online.de>
Co-authored-by: Tordai, Ralph <ralph_t@posteo.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Zalhera <tobias.bechen@gmail.com>
Co-authored-by: marapavelka <mara.pavelka@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translation: Servarr/Prowlarr
2022-11-22 19:58:33 -06:00
bakerboy448
817d61de91 Fixed: (SpeedApp) Migrate Legacy URL without slash 2022-11-22 19:57:18 -06:00
Rumplin
c7e5cc6462 Removed suspicious URL in the default definitions (#1208)
* Removed suspicious URL in the default definitions

Revert "Removed suspicious URL in the default definitions"

This reverts commit e26853f9aa919cd413b0f8b914ac426f220b9475.

* Update Torznab.cs

Removed suspicious URL from the code. Looks like the site that was originally there doesn't exist anymore and it's hosting malware (HD4Free.xyz).

Co-authored-by: admin <stanislav.ivanov@performit.ie>
2022-11-22 19:56:48 -06:00
Qstick
25596fc2e8 Fixed: Orpheus migration fails on Postgres 2022-11-19 13:59:31 -05:00
Qstick
9ff0b90626 Convert Notifiarr Payload to JSON, Standardize with Webhook (#1194)
* Convert Notifiarr Payload to JSON, Standardize with Webhook

* fixup!
2022-11-13 14:18:44 -05:00
ta264
4f4c011436 Swap Orpheus to API key (#946)
* New: Orpheus uses API key instead of user/pass

* fixup! New: Orpheus uses API key instead of user/pass

Co-authored-by: Qstick <qstick@gmail.com>
2022-11-13 11:07:55 -06:00
Qstick
bd0115931f Bump version to 0.4.10 2022-11-12 22:02:13 -06:00
60 changed files with 3674 additions and 275 deletions

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.4.9'
majorVersion: '0.4.10'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'

View File

@@ -1,7 +1,11 @@
import * as dark from './dark';
import * as light from './light';
const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const auto = defaultDark ? { ...dark } : { ...light };
export default {
auto,
light,
dark
};

View File

@@ -0,0 +1,25 @@
using System.Globalization;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Test.ExtensionTests.StringExtensionTests
{
[TestFixture]
public class IsValidIPAddressFixture
{
[TestCase("192.168.0.1")]
[TestCase("::1")]
[TestCase("2001:db8:4006:812::200e")]
public void should_validate_ip_address(string input)
{
input.IsValidIpAddress().Should().BeTrue();
}
[TestCase("sonarr.tv")]
public void should_not_parse_non_ip_address(string input)
{
input.IsValidIpAddress().Should().BeFalse();
}
}
}

View File

@@ -1,4 +1,4 @@
using FluentAssertions;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Test.Common;
@@ -10,6 +10,7 @@ namespace NzbDrone.Common.Test.Http
[TestCase("abc://my_host.com:8080/root/api/")]
[TestCase("abc://my_host.com:8080//root/api/")]
[TestCase("abc://my_host.com:8080/root//api/")]
[TestCase("abc://[::1]:8080/root//api/")]
public void should_parse(string uri)
{
var newUri = new HttpUri(uri);

View File

@@ -7,34 +7,50 @@ namespace NzbDrone.Common.Extensions
{
public static bool IsLocalAddress(this IPAddress ipAddress)
{
if (ipAddress.IsIPv6LinkLocal)
// Map back to IPv4 if mapped to IPv6, for example "::ffff:1.2.3.4" to "1.2.3.4".
if (ipAddress.IsIPv4MappedToIPv6)
{
return true;
ipAddress = ipAddress.MapToIPv4();
}
// Checks loopback ranges for both IPv4 and IPv6.
if (IPAddress.IsLoopback(ipAddress))
{
return true;
}
// IPv4
if (ipAddress.AddressFamily == AddressFamily.InterNetwork)
{
byte[] bytes = ipAddress.GetAddressBytes();
switch (bytes[0])
{
case 10:
case 127:
return true;
case 172:
return bytes[1] < 32 && bytes[1] >= 16;
case 192:
return bytes[1] == 168;
default:
return false;
}
return IsLocalIPv4(ipAddress.GetAddressBytes());
}
// IPv6
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
{
return ipAddress.IsIPv6LinkLocal ||
ipAddress.IsIPv6UniqueLocal ||
ipAddress.IsIPv6SiteLocal;
}
return false;
}
private static bool IsLocalIPv4(byte[] ipv4Bytes)
{
// Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16)
bool IsLinkLocal() => ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
// Class A private range: 10.0.0.0 10.255.255.255 (10.0.0.0/8)
bool IsClassA() => ipv4Bytes[0] == 10;
// Class B private range: 172.16.0.0 172.31.255.255 (172.16.0.0/12)
bool IsClassB() => ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
// Class C private range: 192.168.0.0 192.168.255.255 (192.168.0.0/16)
bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB();
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
@@ -231,5 +232,30 @@ namespace NzbDrone.Common.Extensions
.Replace("'", "%27")
.Replace("%7E", "~");
}
public static bool IsValidIpAddress(this string value)
{
if (!IPAddress.TryParse(value, out var parsedAddress))
{
return false;
}
if (parsedAddress.Equals(IPAddress.Parse("255.255.255.255")))
{
return false;
}
if (parsedAddress.IsIPv6Multicast)
{
return false;
}
return parsedAddress.AddressFamily == AddressFamily.InterNetwork || parsedAddress.AddressFamily == AddressFamily.InterNetworkV6;
}
public static string ToUrlHost(this string input)
{
return input.Contains(":") ? $"[{input}]" : input;
}
}
}

View File

@@ -8,7 +8,7 @@ namespace NzbDrone.Common.Http
{
public class HttpUri : IEquatable<HttpUri>
{
private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+)(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+|\[[[A-F0-9:]+\])(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private readonly string _uri;
public string FullUri => _uri;
@@ -70,6 +70,8 @@ namespace NzbDrone.Common.Http
private void Parse()
{
var parseSuccess = Uri.TryCreate(_uri, UriKind.RelativeOrAbsolute, out var uri);
var match = RegexUri.Match(_uri);
var scheme = match.Groups["scheme"];
@@ -79,7 +81,7 @@ namespace NzbDrone.Common.Http
var query = match.Groups["query"];
var fragment = match.Groups["fragment"];
if (!match.Success || (scheme.Success && !host.Success && path.Success))
if (!parseSuccess || (scheme.Success && !host.Success && path.Success))
{
throw new ArgumentException("Uri didn't match expected pattern: " + _uri);
}

View File

@@ -0,0 +1,58 @@
using System;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class orpheus_apiFixture : MigrationTest<orpheus_api>
{
[Test]
public void should_convert_and_disable_orpheus_instance()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Indexers").Row(new
{
Enable = true,
Name = "Orpheus",
Priority = 25,
Added = DateTime.UtcNow,
Implementation = "Orpheus",
Settings = new GazelleIndexerSettings021
{
Username = "some name",
Password = "some pass"
}.ToJson(),
ConfigContract = "GazelleSettings"
});
});
var items = db.Query<IndexerDefinition022>("SELECT \"Id\", \"Enable\", \"ConfigContract\", \"Settings\" FROM \"Indexers\"");
items.Should().HaveCount(1);
items.First().ConfigContract.Should().Be("OrpheusSettings");
items.First().Enable.Should().Be(false);
items.First().Settings.Should().NotContain("username");
items.First().Settings.Should().NotContain("password");
}
}
public class IndexerDefinition022
{
public int Id { get; set; }
public bool Enable { get; set; }
public string ConfigContract { get; set; }
public string Settings { get; set; }
}
public class GazelleIndexerSettings021
{
public string Username { get; set; }
public string Password { get; set; }
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Definitions;
using NzbDrone.Core.Indexers.Gazelle;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
{
[TestFixture]
public class OrpheusFixture : CoreTest<Orpheus>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "Orpheus",
Settings = new OrpheusSettings() { Apikey = "somekey" }
};
}
[Test]
public async Task should_parse_recent_feed_from_GazelleGames()
{
var recentFeed = ReadAllText(@"Files/Indexers/Orpheus/recentfeed.json");
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 2000 } })).Releases;
releases.Should().HaveCount(65);
releases.First().Should().BeOfType<GazelleInfo>();
var torrentInfo = releases.First() as GazelleInfo;
torrentInfo.Title.Should().Be("The Beatles - Abbey Road (1969) [MP3 V2 (VBR)] [BD]");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://orpheus.network/ajax.php?action=download&id=1902448");
torrentInfo.InfoUrl.Should().Be("https://orpheus.network/torrents.php?id=466&torrentid=1902448");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-08-08 2:07:39"));
torrentInfo.Size.Should().Be(68296866);
torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null);
torrentInfo.Peers.Should().Be(0);
torrentInfo.Seeders.Should().Be(0);
torrentInfo.ImdbId.Should().Be(0);
torrentInfo.TmdbId.Should().Be(0);
torrentInfo.TvdbId.Should().Be(0);
torrentInfo.Languages.Should().HaveCount(0);
torrentInfo.Subs.Should().HaveCount(0);
torrentInfo.DownloadVolumeFactor.Should().Be(1);
torrentInfo.UploadVolumeFactor.Should().Be(1);
}
}
}

View File

@@ -21,7 +21,4 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Datastore\Migration\" />
</ItemGroup>
</Project>

View File

@@ -206,7 +206,7 @@ namespace NzbDrone.Core.Configuration
public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "prowlarr-main", persist: false);
public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "prowlarr-log", persist: false);
public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false);
public string Theme => GetValue("Theme", "light", persist: false);
public string Theme => GetValue("Theme", "auto", 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);

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Data;
using FluentMigrator;
using Newtonsoft.Json.Linq;
@@ -21,6 +22,8 @@ namespace NzbDrone.Core.Datastore.Migration
cmd.Transaction = tran;
cmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Indexers\" WHERE \"Implementation\" = 'Redacted'";
var updatedIndexers = new List<Indexer008>();
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
@@ -45,19 +48,26 @@ namespace NzbDrone.Core.Datastore.Migration
// write new json back to db, switch to new ConfigContract, and disable the indexer
settings = jsonObject.ToJson();
using (var updateCmd = conn.CreateCommand())
updatedIndexers.Add(new Indexer008
{
updateCmd.Transaction = tran;
updateCmd.CommandText = "UPDATE \"Indexers\" SET \"Settings\" = ?, \"ConfigContract\" = ?, \"Enable\" = 0 WHERE \"Id\" = ?";
updateCmd.AddParameter(settings);
updateCmd.AddParameter("RedactedSettings");
updateCmd.AddParameter(id);
updateCmd.ExecuteNonQuery();
}
Id = id,
Settings = settings,
ConfigContract = "RedactedSettings",
Enable = false
});
}
}
}
}
}
public class Indexer008
{
public int Id { get; set; }
public string Settings { get; set; }
public string ConfigContract { get; set; }
public bool Enable { get; set; }
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Data;
using Dapper;
using FluentMigrator;
using Newtonsoft.Json.Linq;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework;
using static NzbDrone.Core.Datastore.Migration.redacted_api;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(22)]
public class orpheus_api : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.WithConnection(MigrateToRedactedApi);
}
private void MigrateToRedactedApi(IDbConnection conn, IDbTransaction tran)
{
using (var cmd = conn.CreateCommand())
{
cmd.Transaction = tran;
cmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Indexers\" WHERE \"Implementation\" = 'Orpheus'";
var updatedIndexers = new List<Indexer008>();
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var id = reader.GetInt32(0);
var settings = reader.GetString(1);
if (!string.IsNullOrWhiteSpace(settings))
{
var jsonObject = Json.Deserialize<JObject>(settings);
// Remove username
if (jsonObject.ContainsKey("username"))
{
jsonObject.Remove("username");
}
// Remove password
if (jsonObject.ContainsKey("password"))
{
jsonObject.Remove("password");
}
// write new json back to db, switch to new ConfigContract, and disable the indexer
settings = jsonObject.ToJson();
updatedIndexers.Add(new Indexer008
{
Id = id,
Settings = settings,
ConfigContract = "OrpheusSettings",
Enable = false
});
}
}
}
var updateSql = "UPDATE \"Indexers\" SET \"Settings\" = @Settings, \"ConfigContract\" = @ConfigContract, \"Enable\" = @Enable WHERE \"Id\" = @Id";
conn.Execute(updateSql, updatedIndexers, transaction: tran);
}
}
}
}

View File

@@ -228,34 +228,34 @@ namespace NzbDrone.Core.Indexers.Definitions
if (syn.StringArray != null)
{
if (syn.StringArray.Count >= 1)
if (_settings.AddJapaneseTitle && syn.StringArray.Count >= 1)
{
synonyms.Add(syn.StringArray[0]);
}
if (syn.StringArray.Count >= 2)
if (_settings.AddRomajiTitle && syn.StringArray.Count >= 2)
{
synonyms.Add(syn.StringArray[1]);
}
if (syn.StringArray.Count == 3)
if (_settings.AddAlternativeTitle && syn.StringArray.Count == 3)
{
synonyms.AddRange(syn.StringArray[2].Split(',').Select(t => t.Trim()));
}
}
else
{
if (syn.StringMap.ContainsKey("0"))
if (_settings.AddJapaneseTitle && syn.StringMap.ContainsKey("0"))
{
synonyms.Add(syn.StringMap["0"]);
}
if (syn.StringMap.ContainsKey("1"))
if (_settings.AddRomajiTitle && syn.StringMap.ContainsKey("1"))
{
synonyms.Add(syn.StringMap["1"]);
}
if (syn.StringMap.ContainsKey("2"))
if (_settings.AddAlternativeTitle && syn.StringMap.ContainsKey("2"))
{
synonyms.AddRange(syn.StringMap["2"].Split(',').Select(t => t.Trim()));
}
@@ -543,6 +543,9 @@ namespace NzbDrone.Core.Indexers.Definitions
Username = "";
EnableSonarrCompatibility = true;
UseFilenameForSingleEpisodes = false;
AddJapaneseTitle = true;
AddRomajiTitle = true;
AddAlternativeTitle = true;
}
[FieldDefinition(2, Label = "Passkey", HelpText = "Site Passkey", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
@@ -557,6 +560,15 @@ namespace NzbDrone.Core.Indexers.Definitions
[FieldDefinition(5, Label = "Use Filenames for Single Episodes", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr replace AnimeBytes release names with the actual filename, this currently only works for single episode releases")]
public bool UseFilenameForSingleEpisodes { get; set; }
[FieldDefinition(6, Label = "Add Japanese title as a synonym", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr add Japanese titles as synonyms, i.e kanji/hiragana/katakana.")]
public bool AddJapaneseTitle { get; set; }
[FieldDefinition(7, Label = "Add Romaji title as a synonym", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr add Romaji title as a synonym, i.e \"Shingeki no Kyojin\" with Attack on Titan")]
public bool AddRomajiTitle { get; set; }
[FieldDefinition(8, Label = "Add alternative title as a synonym", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr add alternative title as a synonym, i.e \"AoT\" with Attack on Titan, but also \"Attack on Titan Season 4\" Instead of \"Attack on Titan: The Final Season\"")]
public bool AddAlternativeTitle { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
public string AuthKey;
public string PassKey;
[FieldDefinition(4, Type = FieldType.Checkbox, Label = "Use Freeleech Token", HelpText = "Use Freeleech Token")]
[FieldDefinition(4, Type = FieldType.Checkbox, Label = "Use Freeleech Token", HelpText = "Use freeleech tokens when available")]
public bool UseFreeleechToken { get; set; }
}
}

View File

@@ -1,25 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using FluentValidation;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Gazelle;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{
public class Orpheus : Gazelle.Gazelle
public class Orpheus : TorrentIndexerBase<OrpheusSettings>
{
public override string Name => "Orpheus";
public override string[] IndexerUrls => new string[] { "https://orpheus.network/" };
public override string Description => "Orpheus (APOLLO) is a Private Torrent Tracker for MUSIC";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public override bool SupportsRedirect => true;
public Orpheus(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
protected override IndexerCapabilities SetCapabilities()
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new OrpheusRequestGenerator() { Settings = Settings, Capabilities = Capabilities, HttpClient = _httpClient };
}
public override IParseIndexerResponse GetParser()
{
return new OrpheusParser(Settings, Capabilities.Categories);
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
@@ -44,23 +70,252 @@ namespace NzbDrone.Core.Indexers.Definitions
return caps;
}
public override IParseIndexerResponse GetParser()
public override async Task<byte[]> Download(Uri link)
{
return new OrpheusParser(Settings, Capabilities);
var request = new HttpRequestBuilder(link.AbsoluteUri)
.SetHeader("Authorization", $"token {Settings.Apikey}")
.Build();
var downloadBytes = Array.Empty<byte>();
try
{
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
downloadBytes = response.ResponseData;
if (downloadBytes.Length >= 1
&& downloadBytes[0] != 'd' // simple test for torrent vs HTML content
&& link.Query.Contains("usetoken=1"))
{
var html = Encoding.GetString(downloadBytes);
if (html.Contains("You do not have any freeleech tokens left.")
|| html.Contains("You do not have enough freeleech tokens")
|| html.Contains("This torrent is too large.")
|| html.Contains("You cannot use tokens here"))
{
// download again without usetoken
request.Url = new HttpUri(link.ToString().Replace("&usetoken=1", ""));
response = await _httpClient.ExecuteProxiedAsync(request, Definition);
downloadBytes = response.ResponseData;
}
}
}
catch (Exception)
{
_indexerStatusService.RecordFailure(Definition.Id);
_logger.Error("Download failed");
}
return downloadBytes;
}
}
public class OrpheusParser : GazelleParser
public class OrpheusRequestGenerator : IIndexerRequestGenerator
{
public OrpheusParser(GazelleSettings settings, IndexerCapabilities capabilities)
: base(settings, capabilities)
public OrpheusSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
public IIndexerHttpClient HttpClient { get; set; }
public OrpheusRequestGenerator()
{
}
protected override string GetDownloadUrl(int torrentId)
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetRequest(string.Format("&artistname={0}&groupname={1}", searchCriteria.Artist, searchCriteria.Album)));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetRequest(string searchParameters)
{
var req = RequestBuilder()
.Resource($"ajax.php?action=browse&searchstr={searchParameters}")
.Build();
yield return new IndexerRequest(req);
}
private HttpRequestBuilder RequestBuilder()
{
return new HttpRequestBuilder($"{Settings.BaseUrl.Trim().TrimEnd('/')}")
.Accept(HttpAccept.Json)
.SetHeader("Authorization", $"token {Settings.Apikey}");
}
}
public class OrpheusParser : IParseIndexerResponse
{
private readonly OrpheusSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
public OrpheusParser(OrpheusSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var torrentInfos = new List<ReleaseInfo>();
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
{
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
}
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
{
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
}
var jsonResponse = new HttpResponse<GazelleResponse>(indexerResponse.HttpResponse);
if (jsonResponse.Resource.Status != "success" ||
string.IsNullOrWhiteSpace(jsonResponse.Resource.Status) ||
jsonResponse.Resource.Response == null)
{
return torrentInfos;
}
foreach (var result in jsonResponse.Resource.Response.Results)
{
if (result.Torrents != null)
{
foreach (var torrent in result.Torrents)
{
var id = torrent.TorrentId;
var artist = WebUtility.HtmlDecode(result.Artist);
var album = WebUtility.HtmlDecode(result.GroupName);
var title = $"{result.Artist} - {result.GroupName} ({result.GroupYear}) [{torrent.Format} {torrent.Encoding}] [{torrent.Media}]";
if (torrent.HasCue)
{
title += " [Cue]";
}
var infoUrl = GetInfoUrl(result.GroupId, id);
GazelleInfo release = new GazelleInfo()
{
Guid = infoUrl,
// Splice Title from info to avoid calling API again for every torrent.
Title = WebUtility.HtmlDecode(title),
Container = torrent.Encoding,
Codec = torrent.Format,
Size = long.Parse(torrent.Size),
DownloadUrl = GetDownloadUrl(id, torrent.CanUseToken),
InfoUrl = infoUrl,
Seeders = int.Parse(torrent.Seeders),
Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders),
PublishDate = torrent.Time.ToUniversalTime(),
Scene = torrent.Scene,
Freeleech = torrent.IsFreeLeech || torrent.IsPersonalFreeLeech,
Files = torrent.FileCount,
Grabs = torrent.Snatches,
DownloadVolumeFactor = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsPersonalFreeLeech ? 0 : 1,
UploadVolumeFactor = torrent.IsNeutralLeech ? 0 : 1
};
var category = torrent.Category;
if (category == null || category.Contains("Select Category"))
{
release.Categories = _categories.MapTrackerCatToNewznab("1");
}
else
{
release.Categories = _categories.MapTrackerCatDescToNewznab(category);
}
torrentInfos.Add(release);
}
}
// Non-Audio files are formatted a little differently (1:1 for group and torrents)
else
{
var id = result.TorrentId;
var infoUrl = GetInfoUrl(result.GroupId, id);
GazelleInfo release = new GazelleInfo()
{
Guid = infoUrl,
Title = WebUtility.HtmlDecode(result.GroupName),
Size = long.Parse(result.Size),
DownloadUrl = GetDownloadUrl(id, result.CanUseToken),
InfoUrl = infoUrl,
Seeders = int.Parse(result.Seeders),
Peers = int.Parse(result.Leechers) + int.Parse(result.Seeders),
PublishDate = long.TryParse(result.GroupTime, out var num) ? DateTimeOffset.FromUnixTimeSeconds(num).UtcDateTime : DateTimeUtil.FromFuzzyTime(result.GroupTime),
Freeleech = result.IsFreeLeech || result.IsPersonalFreeLeech,
Files = result.FileCount,
Grabs = result.Snatches,
DownloadVolumeFactor = result.IsFreeLeech || result.IsNeutralLeech || result.IsPersonalFreeLeech ? 0 : 1,
UploadVolumeFactor = result.IsNeutralLeech ? 0 : 1
};
var category = result.Category;
if (category == null || category.Contains("Select Category"))
{
release.Categories = _categories.MapTrackerCatToNewznab("1");
}
else
{
release.Categories = _categories.MapTrackerCatDescToNewznab(category);
}
torrentInfos.Add(release);
}
}
// order by date
return
torrentInfos
.OrderByDescending(o => o.PublishDate)
.ToArray();
}
private string GetDownloadUrl(int torrentId, bool canUseToken)
{
// AuthKey is required but not checked, just pass in a dummy variable
// to avoid having to track authkey, which is randomly cycled
var url = new HttpUri(_settings.BaseUrl)
.CombinePath("/torrents.php")
.CombinePath("/ajax.php")
.AddQueryParam("action", "download")
.AddQueryParam("id", torrentId);
@@ -72,5 +327,45 @@ namespace NzbDrone.Core.Indexers.Definitions
return url.FullUri;
}
private string GetInfoUrl(string groupId, int torrentId)
{
var url = new HttpUri(_settings.BaseUrl)
.CombinePath("/torrents.php")
.AddQueryParam("id", groupId)
.AddQueryParam("torrentid", torrentId);
return url.FullUri;
}
}
public class OrpheusSettingsValidator : AbstractValidator<OrpheusSettings>
{
public OrpheusSettingsValidator()
{
RuleFor(c => c.Apikey).NotEmpty();
}
}
public class OrpheusSettings : NoAuthTorrentBaseSettings
{
private static readonly OrpheusSettingsValidator Validator = new OrpheusSettingsValidator();
public OrpheusSettings()
{
Apikey = "";
UseFreeleechToken = false;
}
[FieldDefinition(2, Label = "API Key", HelpText = "API Key from the Site (Found in Settings => Access Settings)", Privacy = PrivacyLevel.ApiKey)]
public string Apikey { get; set; }
[FieldDefinition(3, Label = "Use Freeleech Tokens", HelpText = "Use freeleech tokens when available", Type = FieldType.Checkbox)]
public bool UseFreeleechToken { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -27,8 +27,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Name => "Redacted";
public override string[] IndexerUrls => new string[] { "https://redacted.ch/" };
public override string Description => "REDActed (Aka.PassTheHeadPhones) is one of the most well-known music trackers.";
public override string Language => "en-US";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
@@ -82,10 +80,26 @@ namespace NzbDrone.Core.Indexers.Definitions
return caps;
}
protected override async Task Test(List<ValidationFailure> failures)
public override async Task<byte[]> Download(Uri link)
{
((RedactedRequestGenerator)GetRequestGenerator()).FetchPasskey();
await base.Test(failures);
var request = new HttpRequestBuilder(link.AbsoluteUri)
.SetHeader("Authorization", Settings.Apikey)
.Build();
var downloadBytes = Array.Empty<byte>();
try
{
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
downloadBytes = response.ResponseData;
}
catch (Exception)
{
_indexerStatusService.RecordFailure(Definition.Id);
_logger.Error("Download failed");
}
return downloadBytes;
}
}
@@ -138,24 +152,6 @@ namespace NzbDrone.Core.Indexers.Definitions
return pageableRequests;
}
public void FetchPasskey()
{
// GET on index for the passkey
var request = RequestBuilder().Resource("ajax.php?action=index").Build();
var indexResponse = HttpClient.Execute(request);
var index = Json.Deserialize<GazelleAuthResponse>(indexResponse.Content);
if (index == null ||
string.IsNullOrWhiteSpace(index.Status) ||
index.Status != "success" ||
string.IsNullOrWhiteSpace(index.Response.Passkey))
{
throw new Exception("Failed to authenticate with Redacted.");
}
// Set passkey on settings so it can be used to generate the download URL
Settings.Passkey = index.Response.Passkey;
}
private IEnumerable<IndexerRequest> GetRequest(string searchParameters)
{
var req = RequestBuilder()
@@ -311,11 +307,9 @@ namespace NzbDrone.Core.Indexers.Definitions
// AuthKey is required but not checked, just pass in a dummy variable
// to avoid having to track authkey, which is randomly cycled
var url = new HttpUri(_settings.BaseUrl)
.CombinePath("/torrents.php")
.CombinePath("/ajax.php")
.AddQueryParam("action", "download")
.AddQueryParam("id", torrentId)
.AddQueryParam("authkey", "prowlarr")
.AddQueryParam("torrent_pass", _settings.Passkey)
.AddQueryParam("usetoken", (_settings.UseFreeleechToken && canUseToken) ? 1 : 0);
return url.FullUri;
@@ -347,7 +341,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public RedactedSettings()
{
Apikey = "";
Passkey = "";
UseFreeleechToken = false;
}
@@ -357,8 +350,6 @@ namespace NzbDrone.Core.Indexers.Definitions
[FieldDefinition(3, Label = "Use Freeleech Tokens", HelpText = "Use freeleech tokens when available", Type = FieldType.Checkbox)]
public bool UseFreeleechToken { get; set; }
public string Passkey { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -10,6 +10,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Name => "SpeedApp.io";
public override string[] IndexerUrls => new string[] { "https://speedapp.io/" };
public override string[] LegacyUrls => new string[] { "https://speedapp.io" };
public override string Description => "SpeedApp is a ROMANIAN Private Torrent Tracker for MOVIES / TV / GENERAL";

View File

@@ -89,7 +89,6 @@ namespace NzbDrone.Core.Indexers.Torznab
get
{
yield return GetDefinition("AnimeTosho", GetSettings("https://feed.animetosho.org"));
yield return GetDefinition("HD4Free.xyz", GetSettings("http://hd4free.xyz"));
yield return GetDefinition("Generic Torznab", GetSettings(""));
}
}

View File

@@ -210,7 +210,7 @@ namespace NzbDrone.Core.Indexers
}
if (webException.Message.Contains("502") || webException.Message.Contains("503") ||
webException.Message.Contains("timed out"))
webException.Message.Contains("504") || webException.Message.Contains("timed out"))
{
_logger.Warn("{0} server is currently unavailable. {1} {2}", this, url, webException.Message);
}
@@ -223,16 +223,10 @@ namespace NzbDrone.Core.Indexers
{
result.Queries.Add(new IndexerQueryResult { Response = ex.Response });
if (ex.RetryAfter != TimeSpan.Zero)
{
_indexerStatusService.RecordFailure(Definition.Id, ex.RetryAfter);
}
else
{
_indexerStatusService.RecordFailure(Definition.Id, TimeSpan.FromHours(1));
}
var retryTime = ex.RetryAfter != TimeSpan.Zero ? ex.RetryAfter : TimeSpan.FromHours(1);
_logger.Warn("Request Limit reached for {0}", this);
_indexerStatusService.RecordFailure(Definition.Id, retryTime);
_logger.Warn("Request Limit reached for {0}. Disabled for {1}", this, retryTime);
}
catch (HttpException ex)
{

View File

@@ -331,5 +331,12 @@
"UnableToLoadIndexers": "تعذر تحميل المفهرسات",
"Yes": "نعم",
"GrabReleases": "انتزاع الإصدار",
"No": "لا"
"No": "لا",
"Ended": "انتهى",
"LastDuration": "المدة الماضية",
"ApplicationLongTermStatusCheckAllClientMessage": "جميع المفهرسات غير متوفرة بسبب الفشل لأكثر من 6 ساعات",
"ApplicationLongTermStatusCheckSingleClientMessage": "المفهرسات غير متاحة بسبب الإخفاقات لأكثر من 6 ساعات: {0}",
"LastExecution": "آخر تنفيذ",
"NextExecution": "التنفيذ القادم",
"Queued": "في قائمة الانتظار"
}

View File

@@ -331,5 +331,12 @@
"UnableToLoadIndexers": "Индексаторите не могат да се заредят",
"Yes": "Да",
"ConnectionLostMessage": "Whisparr е загубил връзката си с бекенда и ще трябва да се презареди, за да възстанови функционалността.",
"MappedDrivesRunningAsService": "Картографираните мрежови устройства не са налични, когато се изпълняват като услуга на Windows. Моля, вижте често задаваните въпроси за повече информация"
"MappedDrivesRunningAsService": "Картографираните мрежови устройства не са налични, когато се изпълняват като услуга на Windows. Моля, вижте често задаваните въпроси за повече информация",
"ApplicationLongTermStatusCheckSingleClientMessage": "Индексатори не са налични поради неуспехи за повече от 6 часа: {0}",
"Ended": "Приключи",
"LastExecution": "Последно изпълнение",
"ApplicationLongTermStatusCheckAllClientMessage": "Всички индексатори са недостъпни поради грешки за повече от 6 часа",
"LastDuration": "lastDuration",
"NextExecution": "Следващо изпълнение",
"Queued": "На опашка"
}

View File

@@ -305,5 +305,49 @@
"URLBase": "Base URL",
"Usenet": "Usenet",
"View": "Visualitza",
"Yesterday": "Ahir"
"Yesterday": "Ahir",
"ApplicationStatusCheckSingleClientMessage": "Llistes no disponibles a causa d'errors: {0}",
"AnalyticsEnabledHelpText": "Envieu informació anònima d'ús i errors als servidors de Radarr. Això inclou informació sobre el vostre navegador, quines pàgines Radarr WebUI feu servir, informes d'errors, així com el sistema operatiu i la versió del temps d'execució. Utilitzarem aquesta informació per prioritzar les funcions i les correccions d'errors.",
"ApplyTagsHelpTexts1": "Com aplicar etiquetes a les pel·lícules seleccionades",
"ApplyTagsHelpTexts2": "Afegeix: afegeix les etiquetes a la llista d'etiquetes existent",
"ConnectionLostAutomaticMessage": "Radarr intentarà connectar-se automàticament, o podeu fer clic a recarregar.",
"ConnectionLostMessage": "Radarr ha perdut la connexió amb el backend i s'haurà de tornar a carregar per restaurar la funcionalitat.",
"HistoryCleanupDaysHelpTextWarning": "Els fitxers de la paperera de reciclatge més antics que el nombre de dies seleccionat es netejaran automàticament",
"UnableToAddANewAppProfilePleaseTryAgain": "No es pot afegir un perfil de qualitat nou, torneu-ho a provar.",
"BackupFolderHelpText": "Els camins relatius estaran sota el directori AppData del Lidarr",
"AllIndexersHiddenDueToFilter": "Totes les pel·lícules estan ocultes a causa del filtre aplicat.",
"EnableRss": "Activa RSS",
"Grabs": "Captura",
"EnableAutomaticSearchHelpText": "S'utilitzarà quan es realitzin cerques automàtiques mitjançant la interfície d'usuari o per Radarr",
"UnableToAddANewApplicationPleaseTryAgain": "No es pot afegir una notificació nova, torneu-ho a provar.",
"Application": "Aplicacions",
"Applications": "Aplicacions",
"ApplicationStatusCheckAllClientMessage": "Totes les llistes no estan disponibles a causa d'errors",
"AuthenticationMethodHelpText": "Requereix nom d'usuari i contrasenya per accedir al radar",
"ApplicationLongTermStatusCheckAllClientMessage": "Tots els indexadors no estan disponibles a causa d'errors durant més de 6 hores",
"ApplicationLongTermStatusCheckSingleClientMessage": "Els indexadors no estan disponibles a causa d'errors durant més de 6 hores: {0}",
"BindAddressHelpText": "Adreça IPv4 vàlida o '*' per a totes les interfícies",
"BranchUpdate": "Branca que s'utilitza per actualitzar Radarr",
"Connect": "Notificacions",
"DeleteApplicationMessageText": "Esteu segur que voleu suprimir la notificació '{0}'?",
"DeleteIndexerProxyMessageText": "Esteu segur que voleu suprimir la llista '{0}'?",
"Encoding": "Codificació",
"ForMoreInformationOnTheIndividualDownloadClients": "Per obtenir més informació sobre els clients de baixada individuals, feu clic als botons de més informació.",
"GeneralSettingsSummary": "Port, SSL, nom d'usuari/contrasenya, servidor intermediari, analítiques i actualitzacions",
"GrabReleases": "Captura novetat",
"HistoryCleanupDaysHelpText": "Establiu a 0 per desactivar la neteja automàtica",
"Notification": "Notificacions",
"Notifications": "Notificacions",
"PrioritySettings": "Prioritat",
"ReleaseBranchCheckOfficialBranchMessage": "La branca {0} no és una branca de llançament de Radarr vàlida, no rebreu actualitzacions",
"TagsHelpText": "S'aplica a pel·lícules amb almenys una etiqueta coincident",
"Torrent": "Torrent",
"UnableToAddANewIndexerProxyPleaseTryAgain": "No es pot afegir un indexador nou, torneu-ho a provar.",
"UpdateMechanismHelpText": "Utilitzeu l'actualitzador integrat de Prowlarr o un script",
"UserAgentProvidedByTheAppThatCalledTheAPI": "Agent d'usuari proporcionat per l'aplicació per fer peticions a l'API",
"IndexerProxyStatusCheckAllClientMessage": "Tots els indexadors no estan disponibles a causa d'errors",
"IndexerProxyStatusCheckSingleClientMessage": "Els indexadors no estan disponibles a causa d'errors: {0}",
"LaunchBrowserHelpText": " Obriu un navegador web i navegueu a la pàgina d'inici de Radarr a l'inici de l'aplicació.",
"Link": "Enllaços",
"UILanguageHelpText": "Idioma que utilitzarà Radarr per a la interfície d'usuari"
}

View File

@@ -17,7 +17,7 @@
"Settings": "Nastavení",
"StartTypingOrSelectAPathBelow": "Začněte psát nebo vyberte cestu níže",
"Usenet": "Usenet",
"AddDownloadClient": "Přidat staženého klienta",
"AddDownloadClient": "Přidat klienta pro stahování",
"Backups": "Zálohy",
"CancelPendingTask": "Opravdu chcete zrušit tento nevyřízený úkol?",
"MovieIndexScrollBottom": "Rejstřík filmů: Posun dolů",
@@ -116,7 +116,7 @@
"System": "Systém",
"Enabled": "Povoleno",
"IgnoredAddresses": "Ignorované adresy",
"AcceptConfirmationModal": "Přijměte potvrzení Modal",
"AcceptConfirmationModal": "Přijměte potvrzovací modální okno",
"Actions": "Akce",
"Added": "Přidané",
"AddIndexer": "Přidat indexátor",
@@ -331,5 +331,12 @@
"No": "Ne",
"UnableToLoadIndexers": "Nelze načíst indexery",
"Yes": "Ano",
"GrabReleases": "Uchopte uvolnění"
"GrabReleases": "Uchopte uvolnění",
"ApplicationLongTermStatusCheckSingleClientMessage": "Indexery nedostupné z důvodu selhání po dobu delší než 6 hodin: {0}",
"ApplicationLongTermStatusCheckAllClientMessage": "Všechny indexery nejsou k dispozici z důvodu selhání po dobu delší než 6 hodin",
"Ended": "Skončil",
"LastDuration": "lastDuration",
"LastExecution": "Poslední poprava",
"NextExecution": "Další spuštění",
"Queued": "Ve frontě"
}

View File

@@ -334,5 +334,12 @@
"No": "Ingen",
"NetCore": ".NET Core",
"UnableToLoadIndexers": "Kan ikke indlæse indeksatorer",
"Yes": "Ja"
"Yes": "Ja",
"ApplicationLongTermStatusCheckSingleClientMessage": "Indeksatorer er ikke tilgængelige på grund af fejl i mere end 6 timer: {0}",
"ApplicationLongTermStatusCheckAllClientMessage": "Alle indeksatorer er ikke tilgængelige på grund af fejl i mere end 6 timer",
"Ended": "Afsluttet",
"LastDuration": "lastDuration",
"LastExecution": "Sidste henrettelse",
"NextExecution": "Næste udførelse",
"Queued": "I kø"
}

View File

@@ -317,7 +317,7 @@
"IndexerRss": "Indexer RSS",
"IndexerQuery": "Indexer Anfrage",
"IndexerHealthCheckNoIndexers": "Keine Indexer aktiviert, Prowlarr wird keine Suchergebnisse zurückgeben",
"IndexerAuth": "Indexer Auth",
"IndexerAuth": "Indexer Authentifizierung",
"EnableIndexer": "Indexer aktivieren",
"IndexerObsoleteCheckMessage": "Indexer sind nicht mehr verfügbar oder wurden aktualiiert: {0}. Bitte enfernen und (oder) neu zu Prowlarr hinzufügen",
"DevelopmentSettings": "Entwicklungseinstellungen",
@@ -445,7 +445,7 @@
"LastDuration": "Letzte Dauer",
"LastExecution": "Letzte Ausführung",
"MinimumSeeders": "Mindest-Seeder",
"MinimumSeedersHelpText": "Mindest-Seeder sind benötigt von der App für den Indexer um zu holen",
"MinimumSeedersHelpText": "Minimale Anzahl an Seedern die von der Anwendung benötigt werden um den Indexer zu holen",
"NextExecution": "Nächste Ausführung",
"Parameters": "Parameter",
"Queued": "In der Warteschlange",
@@ -460,5 +460,7 @@
"AddSyncProfile": "Synchronisationsprofil hinzufügen",
"BookSearchTypes": "Buch-Suchtypen",
"IndexerDetails": "Indexer-Details",
"IndexerName": "Indexer-Name"
"IndexerName": "Indexer-Name",
"ApplicationLongTermStatusCheckAllClientMessage": "Alle Anwendungen sind nicht verfügbar, da es zu Störungen für mehr als 6 Stunden kam",
"ApplicationLongTermStatusCheckSingleClientMessage": "Anwendungen nicht verfügbar, da es zu Störungen für mehr als 6 Stunden kam: {0}"
}

View File

@@ -334,5 +334,12 @@
"MappedDrivesRunningAsService": "Οι αντιστοιχισμένες μονάδες δίσκου δικτύου δεν είναι διαθέσιμες κατά την εκτέλεση ως υπηρεσία Windows. Ανατρέξτε στις Συχνές Ερωτήσεις για περισσότερες πληροφορίες",
"No": "Οχι",
"UnableToLoadIndexers": "Δεν είναι δυνατή η φόρτωση του ευρετηρίου",
"Yes": "Ναί"
"Yes": "Ναί",
"Ended": "Έληξε",
"LastDuration": "τελευταία Διάρκεια",
"LastExecution": "Τελευταία εκτέλεση",
"ApplicationLongTermStatusCheckAllClientMessage": "Όλοι οι δείκτες δεν είναι διαθέσιμοι λόγω αστοχιών για περισσότερο από 6 ώρες",
"ApplicationLongTermStatusCheckSingleClientMessage": "Τα ευρετήρια δεν είναι διαθέσιμα λόγω αστοχιών για περισσότερο από 6 ώρες: {0}",
"NextExecution": "Επόμενη εκτέλεση",
"Queued": "Σε ουρά"
}

View File

@@ -54,7 +54,7 @@
"Backups": "Backups",
"BeforeUpdate": "Before update",
"BindAddress": "Bind Address",
"BindAddressHelpText": "Valid IP4 address or '*' for all interfaces",
"BindAddressHelpText": "Valid IP address, localhost or '*' for all interfaces",
"BookSearch": "Book Search",
"BookSearchTypes": "Book Search Types",
"Branch": "Branch",
@@ -404,7 +404,7 @@
"TestAllApps": "Test All Apps",
"TestAllClients": "Test All Clients",
"TestAllIndexers": "Test All Indexers",
"ThemeHelpText": "Change Prowlarr UI theme, inspired by {0}",
"ThemeHelpText": "Change Application UI Theme, 'Auto' Theme will use your OS Theme to set Light or Dark mode. Inspired by {0}",
"Time": "Time",
"Title": "Title",
"Today": "Today",

View File

@@ -364,5 +364,14 @@
"UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent proporcionado por la aplicación llamó a la API",
"InstanceName": "Nombre de Instancia",
"InstanceNameHelpText": "Nombre de instancia en pestaña y para nombre de aplicación en Syslog",
"Database": "Base de Datos"
"Database": "Base de Datos",
"Duration": "Duración",
"LastDuration": "Duración",
"LastExecution": "Última ejecución",
"Queued": "En Cola",
"ApplicationLongTermStatusCheckAllClientMessage": "Ningún indexer está disponible por errores durando más de 6 horas",
"ApplicationLongTermStatusCheckSingleClientMessage": "Indexers no disponible por errores durando más de 6 horas: {0}",
"Ended": "Terminó",
"NextExecution": "Siguiente ejecución",
"Started": "Iniciado"
}

View File

@@ -460,5 +460,7 @@
"NextExecution": "Seuraava suoritus",
"Parameters": "Parametrit",
"Queued": "Jonossa",
"Started": "Alkoi"
"Started": "Alkoi",
"ApplicationLongTermStatusCheckAllClientMessage": "Mikään tietolähde ei ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi.",
"ApplicationLongTermStatusCheckSingleClientMessage": "Tietolähteet eivät ole käytettävissä yli 6 tuntia kestäneiden virheiden vuoksi: {0}"
}

View File

@@ -444,5 +444,12 @@
"Duration": "Durée",
"LastDuration": "Dernière durée",
"InstanceName": "Nom de l'instance",
"InstanceNameHelpText": "Nom de l'instance dans l'onglet du navigateur et pour le nom d'application dans Syslog"
"InstanceNameHelpText": "Nom de l'instance dans l'onglet du navigateur et pour le nom d'application dans Syslog",
"ApplicationLongTermStatusCheckSingleClientMessage": "Indexeurs indisponibles en raison de pannes pendant plus de 6 heures : {0}",
"ApplicationLongTermStatusCheckAllClientMessage": "Tous les indexeurs sont indisponibles en raison d'échecs de plus de 6 heures",
"Ended": "Terminé",
"LastExecution": "Dernière exécution",
"NextExecution": "Prochaine exécution",
"Queued": "En file d'attente",
"Started": "Démarré"
}

View File

@@ -337,5 +337,13 @@
"NotificationTriggersHelpText": "בחר איזה אירועים יפעילו את ההתראה הזאת",
"OnApplicationUpdate": "כשהאפליקציה מעדכנת גרסא",
"OnApplicationUpdateHelpText": "כשהאפליקציה מעדכנת גרסא",
"Database": "מסד נתונים"
"Database": "מסד נתונים",
"ApplicationLongTermStatusCheckSingleClientMessage": "אינדקסים לא זמינים עקב כשלים במשך יותר משש שעות: {0}",
"Duration": "אורך",
"Queued": "בתור",
"ApplicationLongTermStatusCheckAllClientMessage": "כל האינדקסים אינם זמינים עקב כשלים במשך יותר מ -6 שעות",
"Ended": "הסתיים",
"LastDuration": "lastDuration",
"LastExecution": "ביצוע אחרון",
"NextExecution": "הביצוע הבא"
}

View File

@@ -331,5 +331,12 @@
"Link": "लिंक",
"MappedDrivesRunningAsService": "विंडोज सर्विस के रूप में चलने पर मैप्ड नेटवर्क ड्राइव उपलब्ध नहीं हैं। अधिक जानकारी के लिए कृपया FAQ देखें",
"No": "नहीं",
"UnableToLoadIndexers": "अनुक्रमणिका लोड करने में असमर्थ"
"UnableToLoadIndexers": "अनुक्रमणिका लोड करने में असमर्थ",
"ApplicationLongTermStatusCheckAllClientMessage": "6 घंटे से अधिक समय तक विफलताओं के कारण सभी सूचकांक अनुपलब्ध हैं",
"ApplicationLongTermStatusCheckSingleClientMessage": "6 घंटे से अधिक समय तक विफलताओं के कारण सूचकांक उपलब्ध नहीं: {0}",
"Ended": "समाप्त",
"NextExecution": "अगला निष्पादन",
"LastDuration": "lastDuration",
"LastExecution": "अंतिम निष्पादन",
"Queued": "कतारबद्ध"
}

View File

@@ -331,5 +331,12 @@
"No": "Nei",
"UnableToLoadIndexers": "Ekki er hægt að hlaða Indexers",
"Yes": "Já",
"ConnectionLostMessage": "Whisparr hefur misst tenginguna við bakendann og þarf að endurhlaða hann til að endurheimta virkni."
"ConnectionLostMessage": "Whisparr hefur misst tenginguna við bakendann og þarf að endurhlaða hann til að endurheimta virkni.",
"LastExecution": "Síðasta aftaka",
"ApplicationLongTermStatusCheckAllClientMessage": "Allir verðtryggingaraðilar eru ekki tiltækir vegna bilana í meira en 6 klukkustundir",
"ApplicationLongTermStatusCheckSingleClientMessage": "Vísitölufólk er ekki tiltækt vegna bilana í meira en 6 klukkustundir: {0}",
"LastDuration": "lastDuration",
"Queued": "Í biðröð",
"Ended": "Lauk",
"NextExecution": "Næsta framkvæmd"
}

View File

@@ -449,5 +449,13 @@
"MinimumSeeders": "Seeder Minimi",
"InstanceName": "Nome Istanza",
"InstanceNameHelpText": "Nome dell'istanza nella scheda e per il nome dell'applicazione Syslog",
"ThemeHelpText": "Cambia il tema dell'interfaccia di Prowlarr, ispirato da {0}"
"ThemeHelpText": "Cambia il tema dell'interfaccia di Prowlarr, ispirato da {0}",
"LastDuration": "Ultima Durata",
"LastExecution": "Ultima esecuzione",
"Queued": "Messo in coda",
"ApplicationLongTermStatusCheckAllClientMessage": "Nessun Indicizzatore è disponibile da più di 6 ore a causa di errori",
"ApplicationLongTermStatusCheckSingleClientMessage": "Alcuni Indicizzatori non sono disponibili da più di 6 ore a causa di errori: {0}",
"Duration": "Durata",
"Ended": "Finito",
"NextExecution": "Prossima esecuzione"
}

View File

@@ -331,5 +331,12 @@
"No": "番号",
"UnableToLoadIndexers": "インデクサーを読み込めません",
"Yes": "はい",
"ConnectionLostMessage": "Whisparrはバックエンドへの接続を失ったため、機能を復元するには再ロードする必要があります。"
"ConnectionLostMessage": "Whisparrはバックエンドへの接続を失ったため、機能を復元するには再ロードする必要があります。",
"LastDuration": "lastDuration",
"LastExecution": "最後の実行",
"ApplicationLongTermStatusCheckAllClientMessage": "6時間以上の障害のため、すべてのインデクサーが使用できなくなります",
"ApplicationLongTermStatusCheckSingleClientMessage": "6時間以上の障害のため、インデクサーを使用できません{0}",
"Ended": "終了しました",
"NextExecution": "次の実行",
"Queued": "キューに入れられました"
}

View File

@@ -331,5 +331,12 @@
"UnableToLoadIndexers": "인덱서를로드 할 수 없습니다.",
"UpdateCheckStartupNotWritableMessage": "'{1}'사용자가 '{0}'시작 폴더에 쓸 수 없기 때문에 업데이트를 설치할 수 없습니다.",
"Yes": "예",
"GrabReleases": "그랩 릴리스"
"GrabReleases": "그랩 릴리스",
"NextExecution": "다음 실행",
"ApplicationLongTermStatusCheckSingleClientMessage": "6 시간 이상 오류로 인해 인덱서를 사용할 수 없음 : {0}",
"ApplicationLongTermStatusCheckAllClientMessage": "6 시간 이상 오류로 인해 모든 인덱서를 사용할 수 없습니다.",
"Ended": "종료",
"LastDuration": "lastDuration",
"LastExecution": "마지막 실행",
"Queued": "대기 중"
}

View File

@@ -109,5 +109,6 @@
"Updates": "Oppdater",
"URLBase": "URL Base",
"Details": "detaljer",
"Info": "Info"
"Info": "Info",
"Queued": "Kø"
}

View File

@@ -409,5 +409,13 @@
"Yes": "Ja",
"OnApplicationUpdateHelpText": "Bij applicatie update",
"Database": "Databasis",
"OnApplicationUpdate": "Bij applicatie update"
"OnApplicationUpdate": "Bij applicatie update",
"Duration": "Duur",
"Ended": "Beëindigd",
"NextExecution": "Volgende uitvoering",
"ApplicationLongTermStatusCheckAllClientMessage": "Alle indexeerders zijn niet beschikbaar vanwege storingen gedurende meer dan 6 uur",
"ApplicationLongTermStatusCheckSingleClientMessage": "Indexeerders zijn niet beschikbaar vanwege storingen gedurende meer dan 6 uur: {0}",
"LastDuration": "Laatste Looptijd",
"LastExecution": "Laatste Uitvoering",
"Queued": "Afwachtend"
}

View File

@@ -338,5 +338,17 @@
"OnApplicationUpdate": "Przy aktualizacji aplikacji",
"OnApplicationUpdateHelpText": "Przy aktualizacji aplikacji",
"Database": "Baza danych",
"NotificationTriggersHelpText": "Wybierz zdarzenia, które mają uruchamiać to powiadomienie"
"NotificationTriggersHelpText": "Wybierz zdarzenia, które mają uruchamiać to powiadomienie",
"ApplicationLongTermStatusCheckSingleClientMessage": "Indeksatory niedostępne z powodu błędów przez ponad 6 godzin: {0}",
"Duration": "Czas trwania",
"Ended": "Zakończone",
"LastDuration": "Ostatni czas trwania",
"LastExecution": "Ostatnia egzekucja",
"NextExecution": "Następne wykonanie",
"Queued": "W kolejce",
"Started": "Rozpoczęto",
"Encoding": "Kodowanie",
"Application": "Aplikacje",
"Applications": "Aplikacje",
"ApplicationLongTermStatusCheckAllClientMessage": "Wszystkie indeksatory są niedostępne z powodu awarii przez ponad 6 godzin"
}

View File

@@ -404,5 +404,16 @@
"InstanceName": "Nome da Instancia",
"InstanceNameHelpText": "Nome da instância na aba e nome da aplicação para Syslog",
"UnableToLoadIndexerProxies": "Incapaz de ler o indexador de proxies",
"UnableToLoadApplicationList": "Não foi possível carregar a lista de aplicações"
"UnableToLoadApplicationList": "Não foi possível carregar a lista de aplicações",
"ApplicationLongTermStatusCheckAllClientMessage": "Todos os indexadores estão indisponíveis devido a erros á mais de 6 horas",
"ApplicationLongTermStatusCheckSingleClientMessage": "Indexadores indisponíveis devido a erros à mais de 6 horas: {0}",
"Duration": "Duração",
"Ended": "Terminado",
"LastDuration": "Última Duração",
"LastExecution": "Execução mais recente",
"Notification": "Notificações",
"Notifications": "Notificações",
"Started": "Começado",
"NextExecution": "Próxima execução",
"Queued": "Em fila"
}

View File

@@ -376,5 +376,13 @@
"FullSync": "Sincronizare completă",
"IndexerObsoleteCheckMessage": "Indexatorii sunt învechiți sau nu au fost actualizați: {0}. Vă rugăm să-i ștergeți și (sau) să-i adăugați din nou în Prowlarr",
"IndexerProxies": "Proxiuri indexatoare",
"IndexerVipCheckExpiringClientMessage": "Beneficiile VIP pentru indexator expiră în curând: {0}"
"IndexerVipCheckExpiringClientMessage": "Beneficiile VIP pentru indexator expiră în curând: {0}",
"ApplicationLongTermStatusCheckAllClientMessage": "Toți indexatorii nu sunt disponibili din cauza unor eșecuri de mai mult de 6 ore",
"ApplicationLongTermStatusCheckSingleClientMessage": "Indexatori indisponibili din cauza unor eșecuri de mai mult de 6 ore: {0}",
"LastDuration": "lastDuration",
"LastExecution": "Ultima executare",
"Queued": "În așteptare",
"Application": "Aplicații",
"Applications": "Aplicații",
"NextExecution": "Următoarea execuție"
}

View File

@@ -351,5 +351,13 @@
"InstanceName": "Имя экземпляра",
"InstanceNameHelpText": "Имя экземпляра на вкладке и для имени приложения системного журнала",
"Started": "Запущено",
"Database": "База данных"
"Database": "База данных",
"Duration": "Длительность",
"ApplicationLongTermStatusCheckSingleClientMessage": "Все индексаторы недоступны из-за ошибок за последние 6 часов: {0}",
"Ended": "Закончился",
"LastExecution": "Последнее выполнение",
"NextExecution": "Следующее выполнение",
"Queued": "В очереди",
"ApplicationLongTermStatusCheckAllClientMessage": "Все индексаторы недоступны из-за ошибок за последние 6 часов",
"LastDuration": "Последняя длительность"
}

View File

@@ -56,5 +56,54 @@
"CloseCurrentModal": "Zatvoriť aktuálne okno",
"Columns": "Stĺpce",
"Component": "Komponent",
"ConnectionLost": "Spojenie prerušené"
"ConnectionLost": "Spojenie prerušené",
"Files": "Súbor",
"Filter": "Filter",
"Filters": "Filtre",
"Connections": "Spojenia",
"Custom": "Vlastné",
"Delete": "Vymazať",
"Notification": "Notifikácie",
"Notifications": "Notifikácie",
"Refresh": "Obnoviť",
"Scheduled": "Naplánované",
"Settings": "Nastavenia",
"Torrent": "Torrent",
"Torrents": "Torrenty",
"Updates": "Aktualizovať",
"URLBase": "Základ URL",
"Usenet": "Usenet",
"Username": "Používateľské meno",
"Language": "jazyk",
"Enabled": "Povoliť",
"Encoding": "Kódovanie",
"Queue": "Fronta",
"Search": "Hľadať",
"Host": "Hostiteľ",
"Hostname": "Názov hostiteľa",
"Info": "Info",
"UpdateMechanismHelpText": "Použiť vstavaný Prowlarr aktualizátor alebo skript",
"Events": "Udalosť",
"Grabs": "Grab",
"Indexers": "Indexery",
"Password": "Heslo",
"Peers": "Peeri",
"Port": "Port",
"Title": "Názov",
"UI": "UI",
"Application": "Aplikácie",
"Protocol": "Protokol",
"Reload": "Obnoviť",
"Applications": "Aplikácie",
"Seeders": "Seederi",
"Connect": "Notifikácie",
"Details": "podrobnosti",
"Disabled": "zakázané",
"DownloadClient": "Klient na sťahovanie",
"DownloadClients": "Klient na sťahovanie",
"Enable": "Povoliť",
"Indexer": "Indexer",
"New": "Nový",
"Queued": "Fronta",
"RSS": "RSS"
}

View File

@@ -405,5 +405,13 @@
"Application": "Applikationer",
"Link": "Länkar",
"MappedDrivesRunningAsService": "Mappade nätverksenheter är inte tillgängliga när de körs som en Windows-tjänst. Se FAQ för mer information",
"No": "Nej"
"No": "Nej",
"ApplicationLongTermStatusCheckAllClientMessage": "Alla indexerare är inte tillgängliga på grund av fel i mer än 6 timmar",
"ApplicationLongTermStatusCheckSingleClientMessage": "Indexatorer är inte tillgängliga på grund av misslyckanden i mer än sex timmar: {0}",
"Duration": "Tid",
"Ended": "Avslutad",
"LastDuration": "lastDuration",
"LastExecution": "Senaste avrättningen",
"NextExecution": "Nästa utförande",
"Queued": "Köad"
}

View File

@@ -331,5 +331,12 @@
"No": "ไม่",
"Link": "ลิงค์",
"UnableToLoadIndexers": "ไม่สามารถโหลด Indexers",
"Yes": "ใช่"
"Yes": "ใช่",
"ApplicationLongTermStatusCheckAllClientMessage": "ตัวจัดทำดัชนีทั้งหมดไม่สามารถใช้งานได้เนื่องจากความล้มเหลวเป็นเวลานานกว่า 6 ชั่วโมง",
"ApplicationLongTermStatusCheckSingleClientMessage": "ดัชนีไม่พร้อมใช้งานเนื่องจากความล้มเหลวเป็นเวลานานกว่า 6 ชั่วโมง: {0}",
"Ended": "สิ้นสุดแล้ว",
"LastDuration": "lastDuration",
"LastExecution": "การดำเนินการล่าสุด",
"NextExecution": "การดำเนินการถัดไป",
"Queued": "อยู่ในคิว"
}

View File

@@ -334,5 +334,12 @@
"UnableToLoadIndexers": "Dizinleyiciler yüklenemiyor",
"Yes": "Evet",
"Link": "Bağlantılar",
"MappedDrivesRunningAsService": "Eşlenen ağ sürücüleri, bir Windows Hizmeti olarak çalışırken kullanılamaz. Daha fazla bilgi için lütfen SSS bölümüne bakın"
"MappedDrivesRunningAsService": "Eşlenen ağ sürücüleri, bir Windows Hizmeti olarak çalışırken kullanılamaz. Daha fazla bilgi için lütfen SSS bölümüne bakın",
"Ended": "Bitti",
"LastDuration": "lastDuration",
"LastExecution": "Son Yürütme",
"NextExecution": "Sonraki Yürütme",
"Queued": "Sıraya alındı",
"ApplicationLongTermStatusCheckAllClientMessage": "6 saatten uzun süren arızalar nedeniyle tüm dizinleyiciler kullanılamıyor",
"ApplicationLongTermStatusCheckSingleClientMessage": "6 saatten uzun süredir yaşanan arızalar nedeniyle dizinleyiciler kullanılamıyor: {0}"
}

View File

@@ -331,5 +331,12 @@
"MappedDrivesRunningAsService": "Các ổ đĩa mạng được ánh xạ không khả dụng khi chạy dưới dạng Dịch vụ Windows. Vui lòng xem Câu hỏi thường gặp để biết thêm thông tin",
"UnableToLoadIndexers": "Không thể tải Trình chỉ mục",
"Yes": "Đúng",
"NetCore": ".NET Core"
"NetCore": ".NET Core",
"Ended": "Đã kết thúc",
"ApplicationLongTermStatusCheckAllClientMessage": "Tất cả các trình lập chỉ mục không khả dụng do lỗi trong hơn 6 giờ",
"ApplicationLongTermStatusCheckSingleClientMessage": "Trình lập chỉ mục không khả dụng do lỗi trong hơn 6 giờ: {0}",
"LastDuration": "lastDuration",
"LastExecution": "Lần thực hiện cuối cùng",
"NextExecution": "Thực hiện tiếp theo",
"Queued": "Đã xếp hàng"
}

View File

@@ -1,15 +1,18 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using FluentValidation.Results;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Notifications.Webhook;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Notifiarr
{
public class Notifiarr : NotificationBase<NotifiarrSettings>
public class Notifiarr : WebhookBase<NotifiarrSettings>
{
private readonly INotifiarrProxy _proxy;
public Notifiarr(INotifiarrProxy proxy)
public Notifiarr(INotifiarrProxy proxy, IConfigFileProvider configFileProvider)
: base(configFileProvider)
{
_proxy = proxy;
}
@@ -18,36 +21,35 @@ namespace NzbDrone.Core.Notifications.Notifiarr
public override string Name => "Notifiarr";
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
var variables = new StringDictionary();
variables.Add("Prowlarr_EventType", "HealthIssue");
variables.Add("Prowlarr_Health_Issue_Level", healthCheck.Type.ToString() ?? string.Empty);
variables.Add("Prowlarr_Health_Issue_Message", healthCheck.Message);
variables.Add("Prowlarr_Health_Issue_Type", healthCheck.Source.Name);
variables.Add("Prowlarr_Health_Issue_Wiki", healthCheck.WikiUrl.ToString() ?? string.Empty);
_proxy.SendNotification(variables, Settings);
_proxy.SendNotification(BuildHealthPayload(healthCheck), Settings);
}
public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
{
var variables = new StringDictionary();
variables.Add("Prowlarr_EventType", "ApplicationUpdate");
variables.Add("Prowlarr_Update_Message", updateMessage.Message);
variables.Add("Prowlarr_Update_NewVersion", updateMessage.NewVersion.ToString());
variables.Add("Prowlarr_Update_PreviousVersion", updateMessage.PreviousVersion.ToString());
_proxy.SendNotification(variables, Settings);
_proxy.SendNotification(BuildApplicationUploadPayload(updateMessage), Settings);
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_proxy.Test(Settings));
failures.AddIfNotNull(SendWebhookTest());
return new ValidationResult(failures);
}
private ValidationFailure SendWebhookTest()
{
try
{
_proxy.SendNotification(BuildTestPayload(), Settings);
}
catch (NotifiarrException ex)
{
return new NzbDroneValidationFailure("APIKey", ex.Message);
}
return null;
}
}
}

View File

@@ -1,74 +1,45 @@
using System;
using System.Collections.Specialized;
using FluentValidation.Results;
using System.Net.Http;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Notifications.Webhook;
namespace NzbDrone.Core.Notifications.Notifiarr
{
public interface INotifiarrProxy
{
void SendNotification(StringDictionary message, NotifiarrSettings settings);
ValidationFailure Test(NotifiarrSettings settings);
void SendNotification(WebhookPayload payload, NotifiarrSettings settings);
}
public class NotifiarrProxy : INotifiarrProxy
{
private const string URL = "https://notifiarr.com";
private readonly IHttpClient _httpClient;
private readonly IConfigFileProvider _configFileProvider;
private readonly Logger _logger;
public NotifiarrProxy(IHttpClient httpClient, IConfigFileProvider configFileProvider, Logger logger)
public NotifiarrProxy(IHttpClient httpClient)
{
_httpClient = httpClient;
_configFileProvider = configFileProvider;
_logger = logger;
}
public void SendNotification(StringDictionary message, NotifiarrSettings settings)
public void SendNotification(WebhookPayload payload, NotifiarrSettings settings)
{
ProcessNotification(message, settings);
ProcessNotification(payload, settings);
}
public ValidationFailure Test(NotifiarrSettings settings)
private void ProcessNotification(WebhookPayload payload, NotifiarrSettings settings)
{
try
{
var variables = new StringDictionary();
variables.Add("Prowlarr_EventType", "Test");
var request = new HttpRequestBuilder(URL + "/api/v1/notification/prowlarr")
.Accept(HttpAccept.Json)
.SetHeader("X-API-Key", settings.APIKey)
.Build();
SendNotification(variables, settings);
return null;
}
catch (NotifiarrException ex)
{
return new ValidationFailure("APIKey", ex.Message);
}
catch (Exception ex)
{
_logger.Error(ex, ex.Message);
return new ValidationFailure("", "Unable to send test notification. Check the log for more details.");
}
}
request.Method = HttpMethod.Post;
private void ProcessNotification(StringDictionary message, NotifiarrSettings settings)
{
try
{
var instanceName = _configFileProvider.InstanceName;
var requestBuilder = new HttpRequestBuilder(URL + "/api/v1/notification/prowlarr").Post();
requestBuilder.AddFormParameter("instanceName", instanceName).Build();
requestBuilder.SetHeader("X-API-Key", settings.APIKey);
foreach (string key in message.Keys)
{
requestBuilder.AddFormParameter(key, message[key]);
}
var request = requestBuilder.Build();
request.Headers.ContentType = "application/json";
request.SetContent(payload.ToJson());
_httpClient.Post(request);
}
@@ -78,25 +49,20 @@ namespace NzbDrone.Core.Notifications.Notifiarr
switch ((int)responseCode)
{
case 401:
_logger.Error("Unauthorized", "HTTP 401 - API key is invalid");
throw new NotifiarrException("API key is invalid");
case 400:
_logger.Error("Invalid Request", "HTTP 400 - Unable to send notification. Ensure Prowlarr Integration is enabled & assigned a channel on Notifiarr");
throw new NotifiarrException("Unable to send notification. Ensure Prowlarr Integration is enabled & assigned a channel on Notifiarr");
case 502:
case 503:
case 504:
_logger.Error("Service Unavailable", "Unable to send notification. Service Unavailable");
throw new NotifiarrException("Unable to send notification. Service Unavailable", ex);
case 520:
case 521:
case 522:
case 523:
case 524:
_logger.Error(ex, "Cloudflare Related HTTP Error - Unable to send notification");
throw new NotifiarrException("Cloudflare Related HTTP Error - Unable to send notification", ex);
default:
_logger.Error(ex, "Unknown HTTP Error - Unable to send notification");
throw new NotifiarrException("Unknown HTTP Error - Unable to send notification", ex);
}
}

View File

@@ -1,15 +1,17 @@
using System.Collections.Generic;
using FluentValidation.Results;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Webhook
{
public class Webhook : NotificationBase<WebhookSettings>
public class Webhook : WebhookBase<WebhookSettings>
{
private readonly IWebhookProxy _proxy;
public Webhook(IWebhookProxy proxy)
public Webhook(IWebhookProxy proxy, IConfigFileProvider configFileProvider)
: base(configFileProvider)
{
_proxy = proxy;
}
@@ -18,29 +20,12 @@ namespace NzbDrone.Core.Notifications.Webhook
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
var payload = new WebhookHealthPayload
{
EventType = WebhookEventType.Health,
Level = healthCheck.Type,
Message = healthCheck.Message,
Type = healthCheck.Source.Name,
WikiUrl = healthCheck.WikiUrl?.ToString()
};
_proxy.SendWebhook(payload, Settings);
_proxy.SendWebhook(BuildHealthPayload(healthCheck), Settings);
}
public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
{
var payload = new WebhookApplicationUpdatePayload
{
EventType = WebhookEventType.ApplicationUpdate,
Message = updateMessage.Message,
PreviousVersion = updateMessage.PreviousVersion.ToString(),
NewVersion = updateMessage.NewVersion.ToString()
};
_proxy.SendWebhook(payload, Settings);
_proxy.SendWebhook(BuildApplicationUploadPayload(updateMessage), Settings);
}
public override string Name => "Webhook";
@@ -58,12 +43,7 @@ namespace NzbDrone.Core.Notifications.Webhook
{
try
{
var payload = new WebhookHealthPayload
{
EventType = WebhookEventType.Test
};
_proxy.SendWebhook(payload, Settings);
_proxy.SendWebhook(BuildTestPayload(), Settings);
}
catch (WebhookException ex)
{

View File

@@ -0,0 +1,51 @@
using NzbDrone.Core.Configuration;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Notifications.Webhook
{
public abstract class WebhookBase<TSettings> : NotificationBase<TSettings>
where TSettings : IProviderConfig, new()
{
private readonly IConfigFileProvider _configFileProvider;
protected WebhookBase(IConfigFileProvider configFileProvider)
: base()
{
_configFileProvider = configFileProvider;
}
protected WebhookHealthPayload BuildHealthPayload(HealthCheck.HealthCheck healthCheck)
{
return new WebhookHealthPayload
{
EventType = WebhookEventType.Health,
InstanceName = _configFileProvider.InstanceName,
Level = healthCheck.Type,
Message = healthCheck.Message,
Type = healthCheck.Source.Name,
WikiUrl = healthCheck.WikiUrl?.ToString()
};
}
protected WebhookApplicationUpdatePayload BuildApplicationUploadPayload(ApplicationUpdateMessage updateMessage)
{
return new WebhookApplicationUpdatePayload
{
EventType = WebhookEventType.ApplicationUpdate,
InstanceName = _configFileProvider.InstanceName,
Message = updateMessage.Message,
PreviousVersion = updateMessage.PreviousVersion.ToString(),
NewVersion = updateMessage.NewVersion.ToString()
};
}
protected WebhookPayload BuildTestPayload()
{
return new WebhookPayload
{
EventType = WebhookEventType.Test,
InstanceName = _configFileProvider.InstanceName
};
}
}
}

View File

@@ -3,5 +3,6 @@ namespace NzbDrone.Core.Notifications.Webhook
public class WebhookPayload
{
public WebhookEventType EventType { get; set; }
public string InstanceName { get; set; }
}
}

View File

@@ -1,30 +1,14 @@
using System.Net;
using System.Net.Sockets;
using FluentValidation;
using FluentValidation.Validators;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.Validation
{
public static class IpValidation
{
public static IRuleBuilderOptions<T, string> ValidIp4Address<T>(this IRuleBuilder<T, string> ruleBuilder)
public static IRuleBuilderOptions<T, string> ValidIpAddress<T>(this IRuleBuilder<T, string> ruleBuilder)
{
return ruleBuilder.Must(x =>
{
IPAddress parsedAddress;
if (!IPAddress.TryParse(x, out parsedAddress))
{
return false;
}
if (parsedAddress.Equals(IPAddress.Parse("255.255.255.255")))
{
return false;
}
return parsedAddress.AddressFamily == AddressFamily.InterNetwork;
}).WithMessage("Must contain wildcard (*) or a valid IPv4 Address");
return ruleBuilder.Must(x => x.IsValidIpAddress()).WithMessage("Must contain wildcard (*) or a valid IP Address");
}
public static IRuleBuilderOptions<T, string> NotListenAllIp4Address<T>(this IRuleBuilder<T, string> ruleBuilder)

View File

@@ -1,11 +1,15 @@
using System;
using System.Text.RegularExpressions;
using FluentValidation;
using FluentValidation.Validators;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.Validation
{
public static class RuleBuilderExtensions
{
private static readonly Regex HostRegex = new Regex("^[-_a-z0-9.]+$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public static IRuleBuilderOptions<T, int> ValidId<T>(this IRuleBuilder<T, int> ruleBuilder)
{
return ruleBuilder.SetValidator(new GreaterThanValidator(0));
@@ -24,13 +28,15 @@ namespace NzbDrone.Core.Validation
public static IRuleBuilderOptions<T, string> ValidHost<T>(this IRuleBuilder<T, string> ruleBuilder)
{
ruleBuilder.SetValidator(new NotEmptyValidator(null));
return ruleBuilder.SetValidator(new RegularExpressionValidator("^[-_a-z0-9.]+$", RegexOptions.IgnoreCase)).WithMessage("must be valid Host without http://");
return ruleBuilder.Must(x => HostRegex.IsMatch(x) || x.IsValidIpAddress()).WithMessage("must be valid Host without http://");
}
public static IRuleBuilderOptions<T, string> ValidRootUrl<T>(this IRuleBuilder<T, string> ruleBuilder)
{
ruleBuilder.SetValidator(new NotEmptyValidator(null));
return ruleBuilder.SetValidator(new RegularExpressionValidator("^https?://[-_a-z0-9.]+", RegexOptions.IgnoreCase)).WithMessage("must be valid URL that starts with http(s)://");
return ruleBuilder.Must(x => x.IsValidUrl() && x.StartsWith("http", StringComparison.InvariantCultureIgnoreCase)).WithMessage("must be valid URL that starts with http(s)://");
}
public static IRuleBuilderOptions<T, string> ValidUrlBase<T>(this IRuleBuilder<T, string> ruleBuilder, string example = "/sonarr")

View File

@@ -19,6 +19,8 @@ using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Instrumentation;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Host.AccessControl;
using NzbDrone.Http.Authentication;
using NzbDrone.SignalR;
@@ -215,6 +217,7 @@ namespace NzbDrone.Host
IConfigFileProvider configFileProvider,
IRuntimeInfo runtimeInfo,
IFirewallAdapter firewallAdapter,
IEventAggregator eventAggregator,
ProwlarrErrorPipeline errorHandler)
{
initializeLogger.Initialize();
@@ -236,6 +239,8 @@ namespace NzbDrone.Host
Console.CancelKeyPress += (sender, eventArgs) => NLog.LogManager.Configuration = null;
}
eventAggregator.PublishEvent(new ApplicationStartingEvent());
if (OsInfo.IsWindows && runtimeInfo.IsAdmin)
{
firewallAdapter.MakeAccessible();

View File

@@ -33,9 +33,9 @@ namespace Prowlarr.Api.V1.Config
_userService = userService;
SharedValidator.RuleFor(c => c.BindAddress)
.ValidIp4Address()
.ValidIpAddress()
.NotListenAllIp4Address()
.When(c => c.BindAddress != "*");
.When(c => c.BindAddress != "*" && c.BindAddress != "localhost");
SharedValidator.RuleFor(c => c.Port).ValidPort();

View File

@@ -126,39 +126,7 @@ namespace Prowlarr.Http.Extensions
remoteIP = remoteIP.MapToIPv4();
}
var remoteAddress = remoteIP.ToString();
// Only check if forwarded by a local network reverse proxy
if (remoteIP.IsLocalAddress())
{
var realIPHeader = request.Headers["X-Real-IP"];
if (realIPHeader.Any())
{
return realIPHeader.First().ToString();
}
var forwardedForHeader = request.Headers["X-Forwarded-For"];
if (forwardedForHeader.Any())
{
// Get the first address that was forwarded by a local IP to prevent remote clients faking another proxy
foreach (var forwardedForAddress in forwardedForHeader.SelectMany(v => v.Split(',')).Select(v => v.Trim()).Reverse())
{
if (!IPAddress.TryParse(forwardedForAddress, out remoteIP))
{
return remoteAddress;
}
if (!remoteIP.IsLocalAddress())
{
return forwardedForAddress;
}
remoteAddress = forwardedForAddress;
}
}
}
return remoteAddress;
return remoteIP.ToString();
}
public static string GetHostName(this HttpRequest request)

View File

@@ -6,6 +6,8 @@ using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using NLog;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Core.Datastore;
using NzbDrone.Http.REST.Attributes;
using Prowlarr.Http.Validation;
@@ -16,7 +18,9 @@ namespace Prowlarr.Http.REST
where TResource : RestResource, new()
{
private static readonly List<Type> VALIDATE_ID_ATTRIBUTES = new List<Type> { typeof(RestPutByIdAttribute), typeof(RestDeleteByIdAttribute) };
private static readonly Type DEPRECATED_ATTRIBUTE = typeof(ObsoleteAttribute);
private readonly Logger _logger;
protected ResourceValidator<TResource> PostValidator { get; private set; }
protected ResourceValidator<TResource> PutValidator { get; private set; }
protected ResourceValidator<TResource> SharedValidator { get; private set; }
@@ -31,6 +35,8 @@ namespace Prowlarr.Http.REST
protected RestController()
{
_logger = NzbDroneLogger.GetLogger(this);
PostValidator = new ResourceValidator<TResource>();
PutValidator = new ResourceValidator<TResource>();
SharedValidator = new ResourceValidator<TResource>();
@@ -57,12 +63,18 @@ namespace Prowlarr.Http.REST
foreach (var resource in resourceArgs)
{
// Map route Id to body resource if not set in request
if (Request.Method == "PUT" && resource.Id == 0 && context.RouteData.Values.TryGetValue("id", out var routeId))
{
resource.Id = Convert.ToInt32(routeId);
}
ValidateResource(resource, skipValidate, skipShared);
}
}
var attributes = descriptor.MethodInfo.CustomAttributes;
if (attributes.Any(x => VALIDATE_ID_ATTRIBUTES.Contains(x.GetType())) && !skipValidate)
if (attributes.Any(x => VALIDATE_ID_ATTRIBUTES.Contains(x.AttributeType)) && !skipValidate)
{
if (context.ActionArguments.TryGetValue("id", out var idObj))
{
@@ -70,6 +82,13 @@ namespace Prowlarr.Http.REST
}
}
var controllerAttributes = descriptor.ControllerTypeInfo.CustomAttributes;
if (controllerAttributes.Any(x => x.AttributeType == DEPRECATED_ATTRIBUTE) || attributes.Any(x => x.AttributeType == DEPRECATED_ATTRIBUTE))
{
_logger.Warn("API call made to deprecated endpoint from {0}", Request.Headers.UserAgent.ToString());
Response.Headers.Add("Deprecation", "true");
}
base.OnActionExecuting(context);
}