mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-17 21:44:48 -04:00
Compare commits
20 Commits
v0.4.9.208
...
v0.4.10.21
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b371f2d913 | ||
|
|
3ff3452e2d | ||
|
|
df13537e29 | ||
|
|
5d2fefde8f | ||
|
|
ffb3f83324 | ||
|
|
1c125733b2 | ||
|
|
2af7fac15e | ||
|
|
f172d17ecc | ||
|
|
c69843931e | ||
|
|
cd3e99ad87 | ||
|
|
1cce39b404 | ||
|
|
9b46ab73e4 | ||
|
|
a352c053ab | ||
|
|
b33e45d266 | ||
|
|
817d61de91 | ||
|
|
c7e5cc6462 | ||
|
|
25596fc2e8 | ||
|
|
9ff0b90626 | ||
|
|
4f4c011436 | ||
|
|
bd0115931f |
@@ -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)'
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
2578
src/NzbDrone.Core.Test/Files/Indexers/Orpheus/recentfeed.json
Normal file
2578
src/NzbDrone.Core.Test/Files/Indexers/Orpheus/recentfeed.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,4 @@
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Datastore\Migration\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
71
src/NzbDrone.Core/Datastore/Migration/022_orpheus_api.cs
Normal file
71
src/NzbDrone.Core/Datastore/Migration/022_orpheus_api.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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(""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -331,5 +331,12 @@
|
||||
"UnableToLoadIndexers": "تعذر تحميل المفهرسات",
|
||||
"Yes": "نعم",
|
||||
"GrabReleases": "انتزاع الإصدار",
|
||||
"No": "لا"
|
||||
"No": "لا",
|
||||
"Ended": "انتهى",
|
||||
"LastDuration": "المدة الماضية",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "جميع المفهرسات غير متوفرة بسبب الفشل لأكثر من 6 ساعات",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "المفهرسات غير متاحة بسبب الإخفاقات لأكثر من 6 ساعات: {0}",
|
||||
"LastExecution": "آخر تنفيذ",
|
||||
"NextExecution": "التنفيذ القادم",
|
||||
"Queued": "في قائمة الانتظار"
|
||||
}
|
||||
|
||||
@@ -331,5 +331,12 @@
|
||||
"UnableToLoadIndexers": "Индексаторите не могат да се заредят",
|
||||
"Yes": "Да",
|
||||
"ConnectionLostMessage": "Whisparr е загубил връзката си с бекенда и ще трябва да се презареди, за да възстанови функционалността.",
|
||||
"MappedDrivesRunningAsService": "Картографираните мрежови устройства не са налични, когато се изпълняват като услуга на Windows. Моля, вижте често задаваните въпроси за повече информация"
|
||||
"MappedDrivesRunningAsService": "Картографираните мрежови устройства не са налични, когато се изпълняват като услуга на Windows. Моля, вижте често задаваните въпроси за повече информация",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "Индексатори не са налични поради неуспехи за повече от 6 часа: {0}",
|
||||
"Ended": "Приключи",
|
||||
"LastExecution": "Последно изпълнение",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "Всички индексатори са недостъпни поради грешки за повече от 6 часа",
|
||||
"LastDuration": "lastDuration",
|
||||
"NextExecution": "Следващо изпълнение",
|
||||
"Queued": "На опашка"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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ě"
|
||||
}
|
||||
|
||||
@@ -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ø"
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
|
||||
@@ -334,5 +334,12 @@
|
||||
"MappedDrivesRunningAsService": "Οι αντιστοιχισμένες μονάδες δίσκου δικτύου δεν είναι διαθέσιμες κατά την εκτέλεση ως υπηρεσία Windows. Ανατρέξτε στις Συχνές Ερωτήσεις για περισσότερες πληροφορίες",
|
||||
"No": "Οχι",
|
||||
"UnableToLoadIndexers": "Δεν είναι δυνατή η φόρτωση του ευρετηρίου",
|
||||
"Yes": "Ναί"
|
||||
"Yes": "Ναί",
|
||||
"Ended": "Έληξε",
|
||||
"LastDuration": "τελευταία Διάρκεια",
|
||||
"LastExecution": "Τελευταία εκτέλεση",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "Όλοι οι δείκτες δεν είναι διαθέσιμοι λόγω αστοχιών για περισσότερο από 6 ώρες",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "Τα ευρετήρια δεν είναι διαθέσιμα λόγω αστοχιών για περισσότερο από 6 ώρες: {0}",
|
||||
"NextExecution": "Επόμενη εκτέλεση",
|
||||
"Queued": "Σε ουρά"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
|
||||
@@ -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é"
|
||||
}
|
||||
|
||||
@@ -337,5 +337,13 @@
|
||||
"NotificationTriggersHelpText": "בחר איזה אירועים יפעילו את ההתראה הזאת",
|
||||
"OnApplicationUpdate": "כשהאפליקציה מעדכנת גרסא",
|
||||
"OnApplicationUpdateHelpText": "כשהאפליקציה מעדכנת גרסא",
|
||||
"Database": "מסד נתונים"
|
||||
"Database": "מסד נתונים",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "אינדקסים לא זמינים עקב כשלים במשך יותר משש שעות: {0}",
|
||||
"Duration": "אורך",
|
||||
"Queued": "בתור",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "כל האינדקסים אינם זמינים עקב כשלים במשך יותר מ -6 שעות",
|
||||
"Ended": "הסתיים",
|
||||
"LastDuration": "lastDuration",
|
||||
"LastExecution": "ביצוע אחרון",
|
||||
"NextExecution": "הביצוע הבא"
|
||||
}
|
||||
|
||||
@@ -331,5 +331,12 @@
|
||||
"Link": "लिंक",
|
||||
"MappedDrivesRunningAsService": "विंडोज सर्विस के रूप में चलने पर मैप्ड नेटवर्क ड्राइव उपलब्ध नहीं हैं। अधिक जानकारी के लिए कृपया FAQ देखें",
|
||||
"No": "नहीं",
|
||||
"UnableToLoadIndexers": "अनुक्रमणिका लोड करने में असमर्थ"
|
||||
"UnableToLoadIndexers": "अनुक्रमणिका लोड करने में असमर्थ",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "6 घंटे से अधिक समय तक विफलताओं के कारण सभी सूचकांक अनुपलब्ध हैं",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "6 घंटे से अधिक समय तक विफलताओं के कारण सूचकांक उपलब्ध नहीं: {0}",
|
||||
"Ended": "समाप्त",
|
||||
"NextExecution": "अगला निष्पादन",
|
||||
"LastDuration": "lastDuration",
|
||||
"LastExecution": "अंतिम निष्पादन",
|
||||
"Queued": "कतारबद्ध"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -331,5 +331,12 @@
|
||||
"No": "番号",
|
||||
"UnableToLoadIndexers": "インデクサーを読み込めません",
|
||||
"Yes": "はい",
|
||||
"ConnectionLostMessage": "Whisparrはバックエンドへの接続を失ったため、機能を復元するには再ロードする必要があります。"
|
||||
"ConnectionLostMessage": "Whisparrはバックエンドへの接続を失ったため、機能を復元するには再ロードする必要があります。",
|
||||
"LastDuration": "lastDuration",
|
||||
"LastExecution": "最後の実行",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "6時間以上の障害のため、すべてのインデクサーが使用できなくなります",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "6時間以上の障害のため、インデクサーを使用できません:{0}",
|
||||
"Ended": "終了しました",
|
||||
"NextExecution": "次の実行",
|
||||
"Queued": "キューに入れられました"
|
||||
}
|
||||
|
||||
@@ -331,5 +331,12 @@
|
||||
"UnableToLoadIndexers": "인덱서를로드 할 수 없습니다.",
|
||||
"UpdateCheckStartupNotWritableMessage": "'{1}'사용자가 '{0}'시작 폴더에 쓸 수 없기 때문에 업데이트를 설치할 수 없습니다.",
|
||||
"Yes": "예",
|
||||
"GrabReleases": "그랩 릴리스"
|
||||
"GrabReleases": "그랩 릴리스",
|
||||
"NextExecution": "다음 실행",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "6 시간 이상 오류로 인해 인덱서를 사용할 수 없음 : {0}",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "6 시간 이상 오류로 인해 모든 인덱서를 사용할 수 없습니다.",
|
||||
"Ended": "종료",
|
||||
"LastDuration": "lastDuration",
|
||||
"LastExecution": "마지막 실행",
|
||||
"Queued": "대기 중"
|
||||
}
|
||||
|
||||
@@ -109,5 +109,6 @@
|
||||
"Updates": "Oppdater",
|
||||
"URLBase": "URL Base",
|
||||
"Details": "detaljer",
|
||||
"Info": "Info"
|
||||
"Info": "Info",
|
||||
"Queued": "Kø"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -351,5 +351,13 @@
|
||||
"InstanceName": "Имя экземпляра",
|
||||
"InstanceNameHelpText": "Имя экземпляра на вкладке и для имени приложения системного журнала",
|
||||
"Started": "Запущено",
|
||||
"Database": "База данных"
|
||||
"Database": "База данных",
|
||||
"Duration": "Длительность",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "Все индексаторы недоступны из-за ошибок за последние 6 часов: {0}",
|
||||
"Ended": "Закончился",
|
||||
"LastExecution": "Последнее выполнение",
|
||||
"NextExecution": "Следующее выполнение",
|
||||
"Queued": "В очереди",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "Все индексаторы недоступны из-за ошибок за последние 6 часов",
|
||||
"LastDuration": "Последняя длительность"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -331,5 +331,12 @@
|
||||
"No": "ไม่",
|
||||
"Link": "ลิงค์",
|
||||
"UnableToLoadIndexers": "ไม่สามารถโหลด Indexers",
|
||||
"Yes": "ใช่"
|
||||
"Yes": "ใช่",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "ตัวจัดทำดัชนีทั้งหมดไม่สามารถใช้งานได้เนื่องจากความล้มเหลวเป็นเวลานานกว่า 6 ชั่วโมง",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "ดัชนีไม่พร้อมใช้งานเนื่องจากความล้มเหลวเป็นเวลานานกว่า 6 ชั่วโมง: {0}",
|
||||
"Ended": "สิ้นสุดแล้ว",
|
||||
"LastDuration": "lastDuration",
|
||||
"LastExecution": "การดำเนินการล่าสุด",
|
||||
"NextExecution": "การดำเนินการถัดไป",
|
||||
"Queued": "อยู่ในคิว"
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
51
src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs
Normal file
51
src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,6 @@ namespace NzbDrone.Core.Notifications.Webhook
|
||||
public class WebhookPayload
|
||||
{
|
||||
public WebhookEventType EventType { get; set; }
|
||||
public string InstanceName { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user