Compare commits

...

58 Commits

Author SHA1 Message Date
Qstick
dae21f22b9 Bump version to 0.4.8 2022-11-07 19:30:14 -06:00
Qstick
7ddbe09eca New: Base API info endpoint 2022-11-07 19:26:54 -06:00
bakerboy448
90e3c809c3 New: Notifiarr moved from webhook to API
New: Notifiarr Add Instance Name Support

Fixed: Notifiarr - Better HTTP Error Handling

also quiet sentry

move apikey to header from url
(cherry picked from commit 1db690ad39ec103c0f4dc89ac4545801ef95bec7)

Fixed: Improve Notifiarr Exception Handling and Validation Errors

(cherry picked from commit 6aaa024d71b939030950460ae986ada5bbae5ad7)
2022-11-07 19:23:16 -06:00
Weblate
ec8cf5f57a Translated using Weblate (Finnish)
Currently translated at 99.5% (462 of 464 strings)

Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translation: Servarr/Prowlarr
2022-11-07 19:22:26 -06:00
Bakerboy448
f4bbf2f8af Fixed: (Avistaz) Handle 429 Request Limit Reached 2022-11-04 09:55:11 -05:00
bakerboy448
ea98d41472 Update feature_request.yml
[skip ci]
2022-11-04 09:54:04 -05:00
bakerboy448
b8cb0fd291 update bug report template [skip ci] 2022-11-04 09:54:04 -05:00
Bakerboy448
d3dfa620ac Fix confusing session expired test message 2022-11-04 09:53:27 -05:00
Weblate
049668f307 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN))

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Dutch)

Currently translated at 87.0% (404 of 464 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (464 of 464 strings)

Translated using Weblate (Portuguese)

Currently translated at 78.7% (364 of 462 strings)

Translated using Weblate (Chinese (Traditional) (zh_TW))

Currently translated at 2.8% (13 of 462 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Thirrian <matthiaslantermann@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: korax1970 <duxlatronum@gmail.com>
Co-authored-by: libsu <libsu@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2022-11-04 09:48:54 -05:00
Yukine
c400575aac Fixed: (AnimeBytes) add delimiter to episode release 2022-11-04 09:48:14 -05:00
Yukine
6f122fb2e4 New: (AnimeBytes) add filename support for single episodes 2022-11-04 09:47:52 -05:00
Qstick
a9c210f8e7 Create CODE_OF_CONDUCT.md 2022-11-03 15:58:57 -05:00
ta264
1068ba8915 Use wildcard pattern now we have better bsd agent 2022-11-03 11:10:41 +00:00
ta264
635335d876 Revert "Temp disable BSD Tests"
This reverts commit 438ea380f5.
2022-11-03 11:10:41 +00:00
Qstick
2ed51cd933 Fixed: Nullref on Cardigann without login test 2022-10-30 18:06:19 -05:00
Qstick
b74c46c554 Ignore brotli test on osx 2022-10-30 13:25:34 -05:00
Qstick
7029e0d6ee Enable new Servarr build notifications 2022-10-25 21:59:08 -05:00
Qstick
438ea380f5 Temp disable BSD Tests 2022-10-25 21:54:19 -05:00
Qstick
4eec675d61 Fix Baker Problems 2022-10-25 21:51:48 -05:00
bakerboy448
0a9bd8287f New: Return 429 for Query and Grab Limits 2022-10-25 21:11:13 -05:00
Qstick
b583ac3a97 Fixed: (Cardigann) Rework login required logic
Fixes #1166
2022-10-25 20:21:27 -05:00
bakerboy448
4be41ff3fb fixup! 2022-10-18 10:49:12 -05:00
bakerboy448
b911f8cc08 Fix: (RetroFlix) Update URL to .club
Fixes #1159
2022-10-18 10:49:12 -05:00
Weblate
22face385f Translated using Weblate (Portuguese)
Currently translated at 78.7% (364 of 462 strings)

Translated using Weblate (Chinese (Traditional) (zh_TW))

Currently translated at 2.8% (13 of 462 strings)

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: korax1970 <duxlatronum@gmail.com>
Co-authored-by: libsu <libsu@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2022-10-14 22:22:40 -05:00
Qstick
3e700b63c2 New: Retry Postgres connection 3 times (with 5 second sleep) on Startup 2022-10-10 22:30:00 -05:00
Qstick
df0b8fc660 And another..... 2022-10-09 19:04:30 -05:00
Qstick
f96dbbfc21 Ensure FS doesn't fail when no proxy 2022-10-09 19:03:53 -05:00
Qstick
4a75f92cb5 Fixed: (FlareSolverr) Send non-auth global proxy when set
Fixes #1142
2022-10-09 18:33:41 -05:00
Qstick
dd05a9dbd4 Obsolete Anthelion C# Indexer 2022-10-09 10:34:40 -05:00
Qstick
e78b8d5346 New: Add long term Application status Healthcheck 2022-10-09 10:15:50 -05:00
Qstick
74a1d95ab7 Update NZBIndex Categories 2022-10-08 22:52:17 -05:00
Qstick
f929a7e62f New: (Indexer) NZBIndex 2022-10-08 22:14:46 -05:00
Qstick
e9e4248af4 New: (Indexer) RetroFlix 2022-10-08 19:18:12 -05:00
Yukine
9e3b43ef12 Fixed: (GreatPosterWall) correctly override Gazelle base method 2022-10-08 18:38:46 -05:00
Qstick
738a690aac Fixed: (Rarbg) Incorrect TVDB param logic
Fixes #1129

Co-Authored-By: bakerboy448 <55419169+bakerboy448@users.noreply.github.com>
2022-10-08 18:20:41 -05:00
Qstick
3b7c59e9bb Fixed: (Rarbg) More reliable token handling and retry
Fixes #1148
2022-10-08 18:11:22 -05:00
Qstick
b8ca28d955 Fixed: Explicitly forbid redirects on Gazelle search requests
Fixes #1144
2022-10-08 15:31:51 -05:00
Yukine
8797bb7d1c Remove unused Gazelle legacy code 2022-10-04 06:46:20 -05:00
Yukine
be430732f5 Fixed: (GreatPosterWall) move imdb id search to searchstr query param 2022-10-04 06:46:20 -05:00
h96kikh6
e7b1380b85 Fixed: (Indexer) HDSpace - Added new categories
Added new categories: 45 - HDTV 2160 -> Prowlarr: Movies/UHD (2045), 46 - Movies 2160 -> Prowlarr: TV/UHD (5045), 47 - Doc 2160 -> Prowlarr: TV/Documentary (5080), 48 - Animation 2160 -> Prowlarr: TV/Anime (5070), 49 - XXX 2160 -> Prowlarr: XXX/UHD (6045)
2022-10-02 11:08:51 -05:00
Qstick
c29735741c Optimize Indexer updates (v2) 2022-09-29 22:47:00 -05:00
Qstick
f56a13a375 Bump Mailkit to 3.4.1 2022-09-29 22:14:25 -05:00
Qstick
148d8ee249 Bump Sentry to 3.21.0 2022-09-29 22:14:25 -05:00
Qstick
3547028b96 Bump YamlDotNet to 12.0.1 2022-09-29 22:14:25 -05:00
Qstick
e4ffa1873e Fixed: Definition not updating if local file is missing 2022-09-29 22:13:21 -05:00
Qstick
2e85a21576 Fixed: (GazelleGames) Serialization error on empty response
Fixes #1137
2022-09-29 20:14:18 -05:00
Qstick
0a111e7572 Fixed: (Cardigann) Search path redirect
Fixes #1102
2022-09-26 21:13:57 -05:00
Qstick
25217c0ee8 Fixed: TypeError on Keyup in Firefox for IndexerIndex 2022-09-25 20:44:26 -05:00
Qstick
791592927c Purge old PTP Radarr check 2022-09-25 12:13:49 -05:00
Qstick
4137193a60 Fixed: (Avistaz) FL Only should be checkbox 2022-09-25 10:08:12 -05:00
Qstick
99816bfd36 Fix test error due to DryIOC update 2022-09-24 20:24:17 -05:00
Qstick
59e5b5bd52 Set PooledConnectionLifetime to 10 minutes
Setting PooledConnectinLifetime to a defined number will ensure we don't run into DNS refresh issues
2022-09-18 21:45:10 -05:00
Qstick
7fa0a2b33c Bump Swashbuckle to 6.4.0 2022-09-18 21:43:06 -05:00
Qstick
0593ca6b9e Bump DryIoc to 5.2.2 2022-09-18 21:40:32 -05:00
Qstick
06a26b5c87 Fixed: (RarBG) Don't disable indexer on temp rate limit
Fixes #1027
2022-09-18 15:56:31 -05:00
Weblate
dcae6dc151 Translated using Weblate (Slovak)
Currently translated at 12.5% (58 of 462 strings)

Translated using Weblate (Portuguese)

Currently translated at 78.3% (362 of 462 strings)

Translated using Weblate (Portuguese)

Currently translated at 78.3% (362 of 462 strings)

Translated using Weblate (Chinese (Traditional) (zh_TW))

Currently translated at 2.8% (13 of 462 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (462 of 462 strings)

Added translation using Weblate (Latvian)

Co-authored-by: Dainel Amendoeira <daniel@amendoeira.eu>
Co-authored-by: Gylesie <github-anon.dasheens@aleeas.com>
Co-authored-by: HiNesslio <chi.lio@shms-mail.ch>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2022-09-18 15:54:43 -05:00
Qstick
04e3ed0ffe Fixed: (Gazelle) Download fails if out of FL tokens
Fixes #1088
2022-09-18 15:39:09 -05:00
Qstick
1ed5ed9179 Bump version to 0.4.7 2022-09-18 14:55:58 -05:00
61 changed files with 2472 additions and 717 deletions

View File

@@ -5,9 +5,9 @@ body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
options:
- label: I have searched the existing issues
- label: I have searched the existing open and closed issues
required: true
- type: textarea
attributes:

View File

@@ -5,9 +5,9 @@ body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the feature you are requesting.
description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch.
options:
- label: I have searched the existing issues
- label: I have searched the existing open and closed issues
required: true
- type: textarea
attributes:

132
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,132 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
<development@prowlarr.com>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.4.6'
majorVersion: '0.4.8'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
@@ -748,7 +748,7 @@ stages:
inputs:
buildType: 'current'
artifactName: Packages
itemPattern: '/$(pattern)'
itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory)
- bash: |
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
@@ -1108,4 +1108,5 @@ stages:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey)
DISCORDTHREADID: $(discordThreadId)

View File

@@ -221,7 +221,7 @@ class IndexerIndex extends Component {
onKeyUp = (event) => {
const jumpBarItems = this.state.jumpBarItems.order;
if (event.path.length === 4) {
if (event.composedPath && event.composedPath().length === 4) {
if (event.keyCode === keyCodes.HOME && event.ctrlKey) {
this.setState({ jumpToCharacter: jumpBarItems[0] });
}

View File

@@ -212,6 +212,7 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
[Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")]
public void should_execute_get_using_brotli()
{
var request = new HttpRequest($"https://{_httpBinHost}/brotli");

View File

@@ -174,6 +174,7 @@ namespace NzbDrone.Common.Http.Dispatchers
PreAuthenticate = true,
MaxConnectionsPerServer = 12,
ConnectCallback = onConnect,
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
SslOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError

View File

@@ -4,13 +4,13 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="4.8.8" />
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Sentry" Version="3.19.0" />
<PackageReference Include="Sentry" Version="3.21.0" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

View File

@@ -0,0 +1,4 @@
{
"status": "success",
"response": []
}

View File

@@ -12,6 +12,7 @@ using NzbDrone.Core.Indexers.Definitions;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests
{
@@ -64,5 +65,19 @@ namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests
torrentInfo.DownloadVolumeFactor.Should().Be(1);
torrentInfo.UploadVolumeFactor.Should().Be(1);
}
[Test]
public async Task should_not_error_if_empty_response()
{
var recentFeed = ReadAllText(@"Files/Indexers/GazelleGames/recentfeed-empty.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(0);
}
}
}

View File

@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
};
Mocker.GetMock<IRarbgTokenProvider>()
.Setup(v => v.GetToken(It.IsAny<RarbgSettings>(), It.IsAny<string>()))
.Setup(v => v.GetToken(It.IsAny<RarbgSettings>()))
.Returns("validtoken");
}

View File

@@ -6,7 +6,7 @@
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="NBuilder" Version="6.1.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="YamlDotNet" Version="11.2.1" />
<PackageReference Include="YamlDotNet" Version="12.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />

View File

@@ -1,6 +1,7 @@
using System;
using System.Data.Common;
using System.Data.SQLite;
using System.Net.Sockets;
using NLog;
using Npgsql;
using NzbDrone.Common.Disk;
@@ -125,6 +126,37 @@ namespace NzbDrone.Core.Datastore
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://wiki.servarr.com/prowlarr/faq#i-am-getting-an-error-database-disk-image-is-malformed", e, fileName);
}
catch (NpgsqlException e)
{
if (e.InnerException is SocketException)
{
var retryCount = 3;
while (true)
{
Logger.Error(e, "Failure to connect to Postgres DB, {0} retries remaining", retryCount);
try
{
_migrationController.Migrate(connectionString, migrationContext);
}
catch (Exception ex)
{
if (--retryCount > 0)
{
System.Threading.Thread.Sleep(5000);
continue;
}
throw new ProwlarrStartupException(ex, "Error creating main database");
}
}
}
else
{
throw new ProwlarrStartupException(e, "Error creating main database");
}
}
catch (Exception e)
{
throw new ProwlarrStartupException(e, "Error creating main database");

View File

@@ -0,0 +1,59 @@
using System;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Applications;
using NzbDrone.Core.Localization;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderUpdatedEvent<IApplication>))]
[CheckOn(typeof(ProviderDeletedEvent<IApplication>))]
[CheckOn(typeof(ProviderStatusChangedEvent<IApplication>))]
public class ApplicationLongTermStatusCheck : HealthCheckBase
{
private readonly IApplicationFactory _providerFactory;
private readonly IApplicationStatusService _providerStatusService;
public ApplicationLongTermStatusCheck(IApplicationFactory providerFactory,
IApplicationStatusService providerStatusService,
ILocalizationService localizationService)
: base(localizationService)
{
_providerFactory = providerFactory;
_providerStatusService = providerStatusService;
}
public override HealthCheck Check()
{
var enabledProviders = _providerFactory.GetAvailableProviders();
var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(),
i => i.Definition.Id,
s => s.ProviderId,
(i, s) => new { Provider = i, Status = s })
.Where(p => p.Status.InitialFailure.HasValue &&
p.Status.InitialFailure.Value.Before(
DateTime.UtcNow.AddHours(-6)))
.ToList();
if (backOffProviders.Empty())
{
return new HealthCheck(GetType());
}
if (backOffProviders.Count == enabledProviders.Count)
{
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("ApplicationLongTermStatusCheckAllClientMessage"),
"#applications-are-unavailable-due-to-failures");
}
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("ApplicationLongTermStatusCheckSingleClientMessage"),
string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
"#applications-are-unavailable-due-to-failures");
}
}
}

View File

@@ -1,34 +0,0 @@
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.PassThePopcorn;
using NzbDrone.Core.Localization;
namespace NzbDrone.Core.HealthCheck.Checks
{
public class PTPOldSettingsCheck : HealthCheckBase
{
private readonly IIndexerFactory _indexerFactory;
public PTPOldSettingsCheck(IIndexerFactory indexerFactory, ILocalizationService localizationService)
: base(localizationService)
{
_indexerFactory = indexerFactory;
}
public override HealthCheck Check()
{
var ptpIndexers = _indexerFactory.All().Where(i => i.Settings.GetType() == typeof(PassThePopcornSettings));
var ptpIndexerOldSettings = ptpIndexers
.Where(i => (i.Settings as PassThePopcornSettings).APIUser.IsNullOrWhiteSpace()).Select(i => i.Name);
if (ptpIndexerOldSettings.Any())
{
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("PtpOldSettingsCheckMessage"), string.Join(", ", ptpIndexerOldSettings)));
}
return new HealthCheck(GetType());
}
}
}

View File

@@ -10,6 +10,7 @@ using NzbDrone.Common.Cache;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Http.CloudFlare;
using NzbDrone.Core.Localization;
@@ -20,10 +21,12 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
public class FlareSolverr : HttpIndexerProxyBase<FlareSolverrSettings>
{
private readonly ICached<string> _cache;
private readonly IHttpProxySettingsProvider _proxySettingsProvider;
public FlareSolverr(IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger, ILocalizationService localizationService, ICacheManager cacheManager)
public FlareSolverr(IHttpProxySettingsProvider proxySettingsProvider, IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger, ILocalizationService localizationService, ICacheManager cacheManager)
: base(cloudRequestBuilder, httpClient, logger, localizationService)
{
_proxySettingsProvider = proxySettingsProvider;
_cache = cacheManager.GetCache<string>(typeof(string), "UserAgent");
}
@@ -100,6 +103,10 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36";
var maxTimeout = Settings.RequestTimeout * 1000;
// Use Proxy if no credentials are set (creds not supported as of FS 2.2.9)
var proxySettings = _proxySettingsProvider.GetProxySettings();
var proxyUrl = proxySettings != null && proxySettings.Username.IsNullOrWhiteSpace() && proxySettings.Password.IsNullOrWhiteSpace() ? GetProxyUri(proxySettings) : null;
if (request.Method == HttpMethod.Get)
{
req = new FlareSolverrRequestGet
@@ -107,7 +114,11 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
Cmd = "request.get",
Url = url,
MaxTimeout = maxTimeout,
UserAgent = userAgent
UserAgent = userAgent,
Proxy = new FlareSolverrProxy
{
Url = proxyUrl?.AbsoluteUri
}
};
}
else if (request.Method == HttpMethod.Post)
@@ -130,7 +141,11 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
ContentLength = null
},
MaxTimeout = maxTimeout,
UserAgent = userAgent
UserAgent = userAgent,
Proxy = new FlareSolverrProxy
{
Url = proxyUrl?.AbsoluteUri
}
};
}
else if (contentTypeType.Contains("multipart/form-data")
@@ -191,38 +206,59 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
return new ValidationResult(failures);
}
public class FlareSolverrRequest
private Uri GetProxyUri(HttpProxySettings proxySettings)
{
switch (proxySettings.Type)
{
case ProxyType.Http:
return new Uri("http://" + proxySettings.Host + ":" + proxySettings.Port);
case ProxyType.Socks4:
return new Uri("socks4://" + proxySettings.Host + ":" + proxySettings.Port);
case ProxyType.Socks5:
return new Uri("socks5://" + proxySettings.Host + ":" + proxySettings.Port);
default:
return null;
}
}
private class FlareSolverrRequest
{
public string Cmd { get; set; }
public string Url { get; set; }
public string UserAgent { get; set; }
public Cookie[] Cookies { get; set; }
public FlareSolverrProxy Proxy { get; set; }
}
public class FlareSolverrRequestGet : FlareSolverrRequest
private class FlareSolverrRequestGet : FlareSolverrRequest
{
public string Headers { get; set; }
public int MaxTimeout { get; set; }
}
public class FlareSolverrRequestPost : FlareSolverrRequest
private class FlareSolverrRequestPost : FlareSolverrRequest
{
public string PostData { get; set; }
public int MaxTimeout { get; set; }
}
public class FlareSolverrRequestPostUrlEncoded : FlareSolverrRequestPost
private class FlareSolverrRequestPostUrlEncoded : FlareSolverrRequestPost
{
public HeadersPost Headers { get; set; }
}
public class HeadersPost
private class FlareSolverrProxy
{
public string Url { get; set; }
}
private class HeadersPost
{
public string ContentType { get; set; }
public string ContentLength { get; set; }
}
public class FlareSolverrResponse
private class FlareSolverrResponse
{
public string Status { get; set; }
public string Message { get; set; }
@@ -232,7 +268,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
public Solution Solution { get; set; }
}
public class Solution
private class Solution
{
public string Url { get; set; }
public string Status { get; set; }
@@ -242,7 +278,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
public string UserAgent { get; set; }
}
public class Cookie
private class Cookie
{
public string Name { get; set; }
public string Value { get; set; }
@@ -259,7 +295,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
public System.Net.Cookie ToCookieObj() => new System.Net.Cookie(Name, Value);
}
public class Headers
private class Headers
{
public string Status { get; set; }
public string Date { get; set; }

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using NLog;
using NzbDrone.Common.Cache;
@@ -282,44 +283,29 @@ namespace NzbDrone.Core.IndexerVersions
{
var startupFolder = _appFolderInfo.AppDataFolder;
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}");
var response = _httpClient.Get<List<CardigannMetaDefinition>>(request);
var currentDefs = _versionService.All().ToDictionary(x => x.DefinitionId, x => x.Sha);
try
{
EnsureDefinitionsFolder();
foreach (var def in response.Resource)
var definitionsFolder = Path.Combine(startupFolder, "Definitions");
var saveFile = Path.Combine(definitionsFolder, $"indexers.zip");
_httpClient.DownloadFile($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/package.zip", saveFile);
using (ZipArchive archive = ZipFile.OpenRead(saveFile))
{
try
{
var saveFile = Path.Combine(startupFolder, "Definitions", $"{def.File}.yml");
if (currentDefs.TryGetValue(def.Id, out var defSha) && defSha == def.Sha)
{
_logger.Trace("Indexer already up to date: {0}", def.File);
continue;
}
_httpClient.DownloadFile($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/{def.File}", saveFile);
_versionService.Upsert(new IndexerDefinitionVersion { Sha = def.Sha, DefinitionId = def.Id, File = def.File, LastUpdated = DateTime.UtcNow });
_cache.Remove(def.File);
_logger.Debug("Updated definition: {0}", def.File);
}
catch (Exception ex)
{
_logger.Error("Definition download failed: {0}, {1}", def.File, ex.Message);
}
archive.ExtractToDirectory(definitionsFolder, true);
}
_diskProvider.DeleteFile(saveFile);
_cache.Clear();
_logger.Debug("Updated indexer definitions");
}
catch (Exception ex)
{
_logger.Error(ex, "Definition download failed, error creating definitions folder in {0}", startupFolder);
_logger.Error(ex, "Definition update failed");
}
}
}

View File

@@ -321,9 +321,10 @@ namespace NzbDrone.Core.Indexers.Definitions
if (episode != null)
{
releaseInfo = episode is > 0 and < 10
var episodeString = episode is > 0 and < 10
? "0" + episode
: episode.ToString();
releaseInfo = $" - {episodeString}";
}
else
{
@@ -449,6 +450,37 @@ namespace NzbDrone.Core.Indexers.Definitions
// Additional 5 hours per GB
minimumSeedTime += (int)((size / 1000000000) * 18000);
if (_settings.UseFilenameForSingleEpisodes && torrent.FileCount == 1)
{
var fileName = torrent.Files.First().FileName;
var guid = new Uri(details + "&nh=" + StringUtil.Hash(fileName));
var release = new TorrentInfo
{
MinimumRatio = 1,
MinimumSeedTime = minimumSeedTime,
Title = fileName,
InfoUrl = details.AbsoluteUri,
Guid = guid.AbsoluteUri,
DownloadUrl = link.AbsoluteUri,
PublishDate = publishDate,
Categories = category,
Description = description,
Size = size,
Seeders = seeders,
Peers = peers,
Grabs = snatched,
Files = fileCount,
DownloadVolumeFactor = rawDownMultiplier,
UploadVolumeFactor = rawUpMultiplier,
};
torrentInfos.Add(release);
continue;
}
foreach (var title in synonyms)
{
var releaseTitle = groupName == "Movie" ?
@@ -510,6 +542,7 @@ namespace NzbDrone.Core.Indexers.Definitions
Passkey = "";
Username = "";
EnableSonarrCompatibility = true;
UseFilenameForSingleEpisodes = false;
}
[FieldDefinition(2, Label = "Passkey", HelpText = "Site Passkey", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
@@ -521,6 +554,9 @@ namespace NzbDrone.Core.Indexers.Definitions
[FieldDefinition(4, Label = "Enable Sonarr Compatibility", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr try to add Season information into Release names, without this Sonarr can't match any Seasons, but it has a lot of false positives as well")]
public bool EnableSonarrCompatibility { get; set; }
[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; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
@@ -678,10 +714,22 @@ namespace NzbDrone.Core.Indexers.Definitions
[JsonProperty("FileCount")]
public int FileCount { get; set; }
[JsonProperty("FileList")]
public List<File> Files { get; set; }
[JsonProperty("UploadTime")]
public DateTimeOffset UploadTime { get; set; }
}
public class File
{
[JsonProperty("filename")]
public string FileName { get; set; }
[JsonProperty("size")]
public string FileSize { get; set; }
}
public class EditionData
{
[JsonProperty("EditionTitle")]

View File

@@ -19,6 +19,7 @@ using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Definitions
{
[Obsolete("Moved to YML for Cardigann")]
public class Anthelion : TorrentIndexerBase<UserPassTorrentBaseSettings>
{
public override string Name => "Anthelion";

View File

@@ -29,6 +29,11 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
return torrentInfos.ToArray();
}
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
{
throw new RequestLimitReachedException(indexerResponse, "API Request Limit Reached");
}
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
{
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");

View File

@@ -36,7 +36,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
[FieldDefinition(4, Label = "PID", HelpText = "PID from My Account or My Profile page")]
public string Pid { get; set; }
[FieldDefinition(5, Label = "Freeleech Only", HelpText = "Search freeleech only")]
[FieldDefinition(5, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Search freeleech only")]
public bool FreeleechOnly { get; set; }
public override NzbDroneValidationResult Validate()

View File

@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
.ContainsIgnoreCase("login.php"))
{
CookiesUpdater(null, null);
throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Try testing the indexer in the settings.");
throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Recheck your cookie or credentials and try testing the indexer.");
}
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");

View File

@@ -945,6 +945,11 @@ namespace NzbDrone.Core.Indexers.Cardigann
public bool CheckIfLoginIsNeeded(HttpResponse response)
{
if (_definition.Login == null)
{
return false;
}
if (response.HasHttpRedirect)
{
var domainHint = GetRedirectDomainHint(response);
@@ -958,29 +963,21 @@ namespace NzbDrone.Core.Indexers.Cardigann
return true;
}
if (_definition.Login == null || _definition.Login.Test == null)
{
return false;
}
if (response.HasHttpError)
{
return true;
}
// Only run html test selector on html responses
if (response.Headers.ContentType?.Contains("text/html") ?? true)
if (_definition.Login.Test?.Selector != null && (response.Headers.ContentType?.Contains("text/html") ?? true))
{
var parser = new HtmlParser();
var document = parser.ParseDocument(response.Content);
if (_definition.Login.Test.Selector != null)
var selection = document.QuerySelectorAll(_definition.Login.Test.Selector);
if (selection.Length == 0)
{
var selection = document.QuerySelectorAll(_definition.Login.Test.Selector);
if (selection.Length == 0)
{
return true;
}
return true;
}
}
@@ -1124,6 +1121,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
var request = new CardigannRequest(requestbuilder.SetEncoding(_encoding).Build(), variables, searchPath);
request.HttpRequest.AllowAutoRedirect = searchPath.Followredirect;
yield return request;
}
}

View File

@@ -80,6 +80,30 @@ namespace NzbDrone.Core.Indexers.Gazelle
_logger.Debug("Gazelle authentication succeeded.");
}
public override async Task<byte[]> Download(Uri link)
{
var response = await base.Download(link);
if (response.Length >= 1
&& response[0] != 'd' // simple test for torrent vs HTML content
&& link.Query.Contains("usetoken=1"))
{
var html = Encoding.GetString(response);
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 with usetoken=0
var requestLinkNew = link.ToString().Replace("usetoken=1", "usetoken=0");
response = await base.Download(new Uri(requestLinkNew));
}
}
return response;
}
protected override bool CheckIfLoginNeeded(HttpResponse response)
{
if (response.HasHttpRedirect || (response.Content != null && response.Content.Contains("\"bad credentials\"")))

View File

@@ -30,22 +30,19 @@ namespace NzbDrone.Core.Indexers.Gazelle
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetRequest(string searchParameters)
protected IEnumerable<IndexerRequest> GetRequest(string searchParameters)
{
var filter = "";
if (searchParameters == null)
{
}
var request =
new IndexerRequest(
$"{APIUrl}?{searchParameters}{filter}",
$"{APIUrl}?{searchParameters}",
HttpAccept.Json);
request.HttpRequest.AllowAutoRedirect = false;
yield return request;
}
private string GetBasicSearchParameters(string searchTerm, int[] categories)
protected string GetBasicSearchParameters(string searchTerm, int[] categories)
{
var searchString = GetSearchTerm(searchTerm);
@@ -67,7 +64,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
return parameters;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
public virtual IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories);

View File

@@ -341,7 +341,18 @@ namespace NzbDrone.Core.Indexers.Definitions
return torrentInfos;
}
foreach (var result in jsonResponse.Resource.Response)
Dictionary<string, GazelleGamesGroup> response;
try
{
response = ((JObject)jsonResponse.Resource.Response).ToObject<Dictionary<string, GazelleGamesGroup>>();
}
catch
{
return torrentInfos;
}
foreach (var result in response)
{
Dictionary<string, GazelleGamesTorrent> torrents;
@@ -455,7 +466,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public class GazelleGamesResponse
{
public string Status { get; set; }
public Dictionary<string, GazelleGamesGroup> Response { get; set; }
public object Response { get; set; }
}
public class GazelleGamesGroup

View File

@@ -8,6 +8,7 @@ using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Gazelle;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
@@ -61,6 +62,20 @@ public class GreatPosterWall : Gazelle.Gazelle
public class GreatPosterWallRequestGenerator : GazelleRequestGenerator
{
protected override bool ImdbInTags => false;
public override IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories);
if (searchCriteria.ImdbId != null)
{
parameters += string.Format("&searchstr={0}", searchCriteria.FullImdbId);
}
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetRequest(parameters));
return pageableRequests;
}
}
public class GreatPosterWallParser : GazelleParser

View File

@@ -124,19 +124,24 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.MoviesBluRay, "Movie / Blu-ray");
caps.Categories.AddCategoryMapping(19, NewznabStandardCategory.MoviesHD, "Movie / 1080p");
caps.Categories.AddCategoryMapping(18, NewznabStandardCategory.MoviesHD, "Movie / 720p");
caps.Categories.AddCategoryMapping(46, NewznabStandardCategory.MoviesUHD, "Movie / 2160p");
caps.Categories.AddCategoryMapping(40, NewznabStandardCategory.MoviesHD, "Movie / Remux");
caps.Categories.AddCategoryMapping(16, NewznabStandardCategory.MoviesHD, "Movie / HD-DVD");
caps.Categories.AddCategoryMapping(41, NewznabStandardCategory.MoviesUHD, "Movie / 4K UHD");
caps.Categories.AddCategoryMapping(21, NewznabStandardCategory.TVHD, "TV Show / 720p HDTV");
caps.Categories.AddCategoryMapping(22, NewznabStandardCategory.TVHD, "TV Show / 1080p HDTV");
caps.Categories.AddCategoryMapping(45, NewznabStandardCategory.TVUHD, "TV Show / 2160p HDTV");
caps.Categories.AddCategoryMapping(24, NewznabStandardCategory.TVDocumentary, "Documentary / 720p");
caps.Categories.AddCategoryMapping(25, NewznabStandardCategory.TVDocumentary, "Documentary / 1080p");
caps.Categories.AddCategoryMapping(47, NewznabStandardCategory.TVDocumentary, "Documentary / 2160p");
caps.Categories.AddCategoryMapping(27, NewznabStandardCategory.TVAnime, "Animation / 720p");
caps.Categories.AddCategoryMapping(28, NewznabStandardCategory.TVAnime, "Animation / 1080p");
caps.Categories.AddCategoryMapping(48, NewznabStandardCategory.TVAnime, "Animation / 2160p");
caps.Categories.AddCategoryMapping(30, NewznabStandardCategory.AudioLossless, "Music / HQ Audio");
caps.Categories.AddCategoryMapping(31, NewznabStandardCategory.AudioVideo, "Music / Videos");
caps.Categories.AddCategoryMapping(33, NewznabStandardCategory.XXX, "XXX / 720p");
caps.Categories.AddCategoryMapping(34, NewznabStandardCategory.XXX, "XXX / 1080p");
caps.Categories.AddCategoryMapping(49, NewznabStandardCategory.XXX, "XXX / 2160p");
caps.Categories.AddCategoryMapping(36, NewznabStandardCategory.MoviesOther, "Trailers");
caps.Categories.AddCategoryMapping(37, NewznabStandardCategory.PC, "Software");
caps.Categories.AddCategoryMapping(38, NewznabStandardCategory.Other, "Others");

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using DryIoc;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.IndexerSearch.Definitions;
@@ -268,7 +269,10 @@ namespace NzbDrone.Core.Indexers.Newznab
parameters.Add("offset", searchCriteria.Offset.ToString());
}
yield return new IndexerRequest(string.Format("{0}&{1}", baseUrl, parameters.GetQueryString()), HttpAccept.Rss);
var request = new IndexerRequest(string.Format("{0}&{1}", baseUrl, parameters.GetQueryString()), HttpAccept.Rss);
request.HttpRequest.AllowAutoRedirect = true;
yield return request;
}
private static string NewsnabifyTitle(string title)

File diff suppressed because it is too large Load Diff

View File

@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
.ContainsIgnoreCase("login.php"))
{
CookiesUpdater(null, null);
throw new IndexerAuthException("We are being redirected to the PTP login page. Most likely your session expired or was killed. Try testing the indexer in the settings.");
throw new IndexerAuthException("We are being redirected to the PTP login page. Most likely your session expired or was killed. Recheck your cookie or credentials and try testing the indexer.");
}
if (indexerHttpResponse.StatusCode == HttpStatusCode.Forbidden)

View File

@@ -255,6 +255,8 @@ namespace NzbDrone.Core.Indexers.Definitions
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
request.HttpRequest.AllowAutoRedirect = false;
yield return request;
}

View File

@@ -1,6 +1,11 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Json;
using System.Threading.Tasks;
using System.Web;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
@@ -8,7 +13,9 @@ using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Http.CloudFlare;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Rarbg
@@ -95,6 +102,57 @@ namespace NzbDrone.Core.Indexers.Rarbg
return caps;
}
protected override async Task<IndexerQueryResult> FetchPage(IndexerRequest request, IParseIndexerResponse parser)
{
var response = await FetchIndexerResponse(request);
// try and recover from token or rate limit errors
var jsonResponse = new HttpResponse<RarbgResponse>(response.HttpResponse);
if (jsonResponse.Resource.error_code.HasValue)
{
if (jsonResponse.Resource.error_code == 4 || jsonResponse.Resource.error_code == 2)
{
_logger.Debug("Invalid or expired token, refreshing token from Rarbg");
_tokenProvider.ExpireToken(Settings);
var newToken = _tokenProvider.GetToken(Settings);
var qs = HttpUtility.ParseQueryString(request.HttpRequest.Url.Query);
qs.Set("token", newToken);
request.HttpRequest.Url = request.Url.SetQuery(qs.GetQueryString());
response = await FetchIndexerResponse(request);
}
else if (jsonResponse.Resource.error_code == 5 || jsonResponse.Resource.rate_limit.HasValue)
{
_logger.Debug("Rarbg rate limit hit, retying request");
response = await FetchIndexerResponse(request);
}
}
try
{
var releases = parser.ParseResponse(response).ToList();
if (releases.Count == 0)
{
_logger.Trace(response.Content);
}
return new IndexerQueryResult
{
Releases = releases,
Response = response.HttpResponse
};
}
catch (Exception ex)
{
ex.WithData(response.HttpResponse, 128 * 1024);
_logger.Trace("Unexpected Response content ({0} bytes): {1}", response.HttpResponse.ResponseData.Length, response.HttpResponse.Content);
throw;
}
}
public override object RequestAction(string action, IDictionary<string, string> query)
{
if (action == "checkCaptcha")

View File

@@ -40,9 +40,11 @@ namespace NzbDrone.Core.Indexers.Rarbg
if (jsonResponse.Resource.error_code.HasValue)
{
if (jsonResponse.Resource.error_code == 20 || jsonResponse.Resource.error_code == 8
|| jsonResponse.Resource.error_code == 9 || jsonResponse.Resource.error_code == 10)
|| jsonResponse.Resource.error_code == 9 || jsonResponse.Resource.error_code == 10
|| jsonResponse.Resource.error_code == 5 || jsonResponse.Resource.error_code == 13
|| jsonResponse.Resource.error_code == 14)
{
// No results or imdbid not found
// No results, rate limit, or imdbid/tvdb not found
return results;
}

View File

@@ -36,7 +36,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
{
requestBuilder.AddQueryParam("search_themoviedb", tmdbId);
}
else if (tvdbId.HasValue && tmdbId > 0)
else if (tvdbId.HasValue && tvdbId > 0)
{
requestBuilder.AddQueryParam("search_tvdb", tvdbId);
}
@@ -60,7 +60,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
}
requestBuilder.AddQueryParam("limit", "100");
requestBuilder.AddQueryParam("token", _tokenProvider.GetToken(Settings, Settings.BaseUrl));
requestBuilder.AddQueryParam("token", _tokenProvider.GetToken(Settings));
requestBuilder.AddQueryParam("format", "json_extended");
requestBuilder.AddQueryParam("app_id", BuildInfo.AppName);
@@ -69,42 +69,36 @@ namespace NzbDrone.Core.Indexers.Rarbg
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var request = GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories, searchCriteria.FullImdbId, searchCriteria.TmdbId);
return GetRequestChain(request, 2);
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories, searchCriteria.FullImdbId, searchCriteria.TmdbId));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var request = GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
return GetRequestChain(request, 2);
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var request = GetRequest(searchCriteria.SanitizedTvSearchString, searchCriteria.Categories, searchCriteria.FullImdbId, tvdbId: searchCriteria.TvdbId);
return GetRequestChain(request, 2);
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetRequest(searchCriteria.SanitizedTvSearchString, searchCriteria.Categories, searchCriteria.FullImdbId, tvdbId: searchCriteria.TvdbId));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var request = GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
return GetRequestChain(request, 2);
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var request = GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
return GetRequestChain(request, 2);
}
private IndexerPageableRequestChain GetRequestChain(IEnumerable<IndexerRequest> requests, int retry)
{
var pageableRequests = new IndexerPageableRequestChain();
for (int i = 0; i < retry; i++)
{
pageableRequests.AddTier(requests);
}
pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories));
return pageableRequests;
}

View File

@@ -7,6 +7,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
{
public string error { get; set; }
public int? error_code { get; set; }
public int? rate_limit { get; set; }
public List<RarbgTorrent> torrent_results { get; set; }
}

View File

@@ -3,14 +3,14 @@ using Newtonsoft.Json.Linq;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Indexers.Rarbg
{
public interface IRarbgTokenProvider
{
string GetToken(RarbgSettings settings, string baseUrl);
string GetToken(RarbgSettings settings);
void ExpireToken(RarbgSettings settings);
}
public class RarbgTokenProvider : IRarbgTokenProvider
@@ -26,12 +26,17 @@ namespace NzbDrone.Core.Indexers.Rarbg
_logger = logger;
}
public string GetToken(RarbgSettings settings, string baseUrl)
public void ExpireToken(RarbgSettings settings)
{
return _tokenCache.Get(baseUrl,
_tokenCache.Remove(settings.BaseUrl);
}
public string GetToken(RarbgSettings settings)
{
return _tokenCache.Get(settings.BaseUrl,
() =>
{
var requestBuilder = new HttpRequestBuilder(baseUrl.Trim('/'))
var requestBuilder = new HttpRequestBuilder(settings.BaseUrl.Trim('/'))
.WithRateLimit(3.0)
.Resource($"/pubapi_v2.php?get_token=get_token&app_id={BuildInfo.AppName}")
.Accept(HttpAccept.Json);

View File

@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using NLog;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Indexers.Definitions
{
public class RetroFlix : SpeedAppBase
{
public override string Name => "RetroFlix";
public override string[] IndexerUrls => new string[] { "https://retroflix.club/" };
public override string[] LegacyUrls => new string[] { "https://retroflix.net/" };
public override string Description => "Private Torrent Tracker for Classic Movies / TV / General Releases";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override TimeSpan RateLimit => TimeSpan.FromSeconds(2.1);
public RetroFlix(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger, IIndexerRepository indexerRepository)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger, indexerRepository)
{
}
protected override IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q,
TvSearchParam.Season,
TvSearchParam.Ep,
TvSearchParam.ImdbId
},
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q,
MovieSearchParam.ImdbId
},
MusicSearchParams = new List<MusicSearchParam>
{
MusicSearchParam.Q,
},
BookSearchParams = new List<BookSearchParam>
{
BookSearchParam.Q,
},
};
caps.Categories.AddCategoryMapping(401, NewznabStandardCategory.Movies, "Movies");
caps.Categories.AddCategoryMapping(402, NewznabStandardCategory.TV, "TV Series");
caps.Categories.AddCategoryMapping(406, NewznabStandardCategory.AudioVideo, "Music Videos");
caps.Categories.AddCategoryMapping(407, NewznabStandardCategory.TVSport, "Sports");
caps.Categories.AddCategoryMapping(409, NewznabStandardCategory.Books, "Books");
caps.Categories.AddCategoryMapping(408, NewznabStandardCategory.Audio, "HQ Audio");
return caps;
}
}
}

View File

@@ -194,6 +194,8 @@ namespace NzbDrone.Core.Indexers.Definitions
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
request.HttpRequest.AllowAutoRedirect = false;
yield return request;
}

View File

@@ -1480,6 +1480,8 @@ namespace NzbDrone.Core.Indexers.Definitions
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
request.HttpRequest.AllowAutoRedirect = false;
yield return request;
}

View File

@@ -1,187 +1,28 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
using FluentValidation;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Indexers.Exceptions;
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.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{
public class SpeedApp : TorrentIndexerBase<SpeedAppSettings>
public class SpeedApp : SpeedAppBase
{
public override string Name => "SpeedApp.io";
public override string[] IndexerUrls => new string[] { "https://speedapp.io" };
private string ApiUrl => $"{Settings.BaseUrl}/api";
private string LoginUrl => $"{ApiUrl}/login";
public override string Description => "SpeedApp is a ROMANIAN Private Torrent Tracker for MOVIES / TV / GENERAL";
public override string Language => "ro-RO";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
private IIndexerRepository _indexerRepository;
public SpeedApp(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger, IIndexerRepository indexerRepository)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger, indexerRepository)
{
_indexerRepository = indexerRepository;
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new SpeedAppRequestGenerator(Capabilities, Settings);
}
public override IParseIndexerResponse GetParser()
{
return new SpeedAppParser(Settings, Capabilities.Categories);
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
return Settings.ApiKey.IsNullOrWhiteSpace() || httpResponse.StatusCode == HttpStatusCode.Unauthorized;
}
protected override async Task DoLogin()
{
var requestBuilder = new HttpRequestBuilder(LoginUrl)
{
LogResponseContent = true,
AllowAutoRedirect = true,
Method = HttpMethod.Post,
};
var request = requestBuilder.Build();
var data = new SpeedAppAuthenticationRequest
{
Email = Settings.Email,
Password = Settings.Password
};
request.SetContent(JsonConvert.SerializeObject(data));
request.Headers.ContentType = MediaTypeNames.Application.Json;
var response = await ExecuteAuth(request);
var statusCode = (int)response.StatusCode;
if (statusCode is < 200 or > 299)
{
throw new HttpException(response);
}
var parsedResponse = JsonConvert.DeserializeObject<SpeedAppAuthenticationResponse>(response.Content);
Settings.ApiKey = parsedResponse.Token;
if (Definition.Id > 0)
{
_indexerRepository.UpdateSettings((IndexerDefinition)Definition);
}
_logger.Debug("SpeedApp authentication succeeded.");
}
protected override void ModifyRequest(IndexerRequest request)
{
request.HttpRequest.Headers.Set("Authorization", $"Bearer {Settings.ApiKey}");
}
public override async Task<byte[]> Download(Uri link)
{
Cookies = GetCookies();
if (link.Scheme == "magnet")
{
ValidateMagnet(link.OriginalString);
return Encoding.UTF8.GetBytes(link.OriginalString);
}
var requestBuilder = new HttpRequestBuilder(link.AbsoluteUri);
if (Cookies != null)
{
requestBuilder.SetCookies(Cookies);
}
var request = requestBuilder.Build();
request.AllowAutoRedirect = FollowRedirect;
request.Headers.Set("Authorization", $"Bearer {Settings.ApiKey}");
byte[] torrentData;
try
{
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
torrentData = response.ResponseData;
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.NotFound)
{
_logger.Error(ex, "Downloading torrent file for release failed since it no longer exists ({0})", link.AbsoluteUri);
throw new ReleaseUnavailableException("Downloading torrent failed", ex);
}
if (ex.Response.StatusCode == HttpStatusCode.TooManyRequests)
{
_logger.Error("API Grab Limit reached for {0}", link.AbsoluteUri);
}
else
{
_logger.Error(ex, "Downloading torrent file for release failed ({0})", link.AbsoluteUri);
}
throw new ReleaseDownloadException("Downloading torrent failed", ex);
}
catch (WebException ex)
{
_logger.Error(ex, "Downloading torrent file for release failed ({0})", link.AbsoluteUri);
throw new ReleaseDownloadException("Downloading torrent failed", ex);
}
catch (Exception)
{
_indexerStatusService.RecordFailure(Definition.Id);
_logger.Error("Downloading torrent failed");
throw;
}
return torrentData;
}
private IndexerCapabilities SetCapabilities()
protected override IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
@@ -253,356 +94,4 @@ namespace NzbDrone.Core.Indexers.Definitions
return caps;
}
}
public class SpeedAppRequestGenerator : IIndexerRequestGenerator
{
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
private IndexerCapabilities Capabilities { get; }
private SpeedAppSettings Settings { get; }
public SpeedAppRequestGenerator(IndexerCapabilities capabilities, SpeedAppSettings settings)
{
Capabilities = capabilities;
Settings = settings;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
return GetSearch(searchCriteria, searchCriteria.FullImdbId);
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
return GetSearch(searchCriteria);
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
return GetSearch(searchCriteria, searchCriteria.FullImdbId, searchCriteria.Season, searchCriteria.Episode);
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
return GetSearch(searchCriteria);
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
return GetSearch(searchCriteria);
}
private IndexerPageableRequestChain GetSearch(SearchCriteriaBase searchCriteria, string imdbId = null, int? season = null, string episode = null)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories, imdbId, season, episode));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null, int? season = null, string episode = null)
{
var qc = new NameValueCollection();
if (imdbId.IsNotNullOrWhiteSpace())
{
qc.Add("imdbId", imdbId);
}
else
{
qc.Add("search", term);
}
if (season != null)
{
qc.Add("season", season.Value.ToString());
}
if (episode != null)
{
qc.Add("episode", episode);
}
var cats = Capabilities.Categories.MapTorznabCapsToTrackers(categories);
if (cats.Count > 0)
{
foreach (var cat in cats)
{
qc.Add("categories[]", cat);
}
}
var searchUrl = Settings.BaseUrl + "/api/torrent?" + qc.GetQueryString(duplicateKeysIfMulti: true);
var request = new IndexerRequest(searchUrl, HttpAccept.Json);
request.HttpRequest.Headers.Set("Authorization", $"Bearer {Settings.ApiKey}");
yield return request;
}
}
public class SpeedAppParser : IParseIndexerResponse
{
private readonly SpeedAppSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
public SpeedAppParser(SpeedAppSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
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<List<SpeedAppTorrent>>(indexerResponse.HttpResponse);
return jsonResponse.Resource.Select(torrent => new TorrentInfo
{
Guid = torrent.Id.ToString(),
Title = torrent.Name,
Description = torrent.ShortDescription,
Size = torrent.Size,
ImdbId = ParseUtil.GetImdbID(torrent.ImdbId).GetValueOrDefault(),
DownloadUrl = $"{_settings.BaseUrl}/api/torrent/{torrent.Id}/download",
PosterUrl = torrent.Poster,
InfoUrl = torrent.Url,
Grabs = torrent.TimesCompleted,
PublishDate = torrent.CreatedAt,
Categories = _categories.MapTrackerCatToNewznab(torrent.Category.Id.ToString()),
InfoHash = null,
Seeders = torrent.Seeders,
Peers = torrent.Leechers + torrent.Seeders,
MinimumRatio = 1,
MinimumSeedTime = 172800,
DownloadVolumeFactor = torrent.DownloadVolumeFactor,
UploadVolumeFactor = torrent.UploadVolumeFactor,
}).ToArray();
}
}
public class SpeedAppSettingsValidator : AbstractValidator<SpeedAppSettings>
{
public SpeedAppSettingsValidator()
{
RuleFor(c => c.Email).NotEmpty();
RuleFor(c => c.Password).NotEmpty();
}
}
public class SpeedAppSettings : NoAuthTorrentBaseSettings
{
private static readonly SpeedAppSettingsValidator Validator = new ();
public SpeedAppSettings()
{
Email = "";
Password = "";
}
[FieldDefinition(2, Label = "Email", HelpText = "Site Email", Privacy = PrivacyLevel.UserName)]
public string Email { get; set; }
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
public string Password { get; set; }
[FieldDefinition(4, Label = "API Key", Hidden = HiddenType.Hidden)]
public string ApiKey { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
public class SpeedAppCategory
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}
public class SpeedAppCountry
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("flag_image")]
public string FlagImage { get; set; }
}
public class SpeedAppUploadedBy
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("username")]
public string Username { get; set; }
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("created_at")]
public DateTime CreatedAt { get; set; }
[JsonProperty("class")]
public int Class { get; set; }
[JsonProperty("avatar")]
public string Avatar { get; set; }
[JsonProperty("uploaded")]
public int Uploaded { get; set; }
[JsonProperty("downloaded")]
public int Downloaded { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("country")]
public SpeedAppCountry Country { get; set; }
[JsonProperty("passkey")]
public string Passkey { get; set; }
[JsonProperty("invites")]
public int Invites { get; set; }
[JsonProperty("timezone")]
public string Timezone { get; set; }
[JsonProperty("hit_and_run_count")]
public int HitAndRunCount { get; set; }
[JsonProperty("snatch_count")]
public int SnatchCount { get; set; }
[JsonProperty("need_seed")]
public int NeedSeed { get; set; }
[JsonProperty("average_seed_time")]
public int AverageSeedTime { get; set; }
[JsonProperty("free_leech_tokens")]
public int FreeLeechTokens { get; set; }
[JsonProperty("double_upload_tokens")]
public int DoubleUploadTokens { get; set; }
}
public class SpeedAppTag
{
[JsonProperty("translated_name")]
public string TranslatedName { get; set; }
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("match_list")]
public List<string> MatchList { get; set; }
[JsonProperty("created_at")]
public DateTime CreatedAt { get; set; }
}
public class SpeedAppTorrent
{
[JsonProperty("download_volume_factor")]
public float DownloadVolumeFactor { get; set; }
[JsonProperty("upload_volume_factor")]
public float UploadVolumeFactor { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("category")]
public SpeedAppCategory Category { get; set; }
[JsonProperty("size")]
public long Size { get; set; }
[JsonProperty("created_at")]
public DateTime CreatedAt { get; set; }
[JsonProperty("times_completed")]
public int TimesCompleted { get; set; }
[JsonProperty("leechers")]
public int Leechers { get; set; }
[JsonProperty("seeders")]
public int Seeders { get; set; }
[JsonProperty("uploaded_by")]
public SpeedAppUploadedBy UploadedBy { get; set; }
[JsonProperty("short_description")]
public string ShortDescription { get; set; }
[JsonProperty("poster")]
public string Poster { get; set; }
[JsonProperty("season")]
public int Season { get; set; }
[JsonProperty("episode")]
public int Episode { get; set; }
[JsonProperty("tags")]
public List<SpeedAppTag> Tags { get; set; }
[JsonProperty("imdb_id")]
public string ImdbId { get; set; }
}
public class SpeedAppAuthenticationRequest
{
[JsonProperty("username")]
public string Email { get; set; }
[JsonProperty("password")]
public string Password { get; set; }
}
public class SpeedAppAuthenticationResponse
{
[JsonProperty("token")]
public string Token { get; set; }
}
}

View File

@@ -0,0 +1,533 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
using FluentValidation;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Indexers.Exceptions;
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.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{
public abstract class SpeedAppBase : TorrentIndexerBase<SpeedAppSettings>
{
private string ApiUrl => $"{Settings.BaseUrl}/api";
private string LoginUrl => $"{ApiUrl}/login";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerCapabilities Capabilities => SetCapabilities();
private IIndexerRepository _indexerRepository;
public SpeedAppBase(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger, IIndexerRepository indexerRepository)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
_indexerRepository = indexerRepository;
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new SpeedAppRequestGenerator(Capabilities, Settings);
}
public override IParseIndexerResponse GetParser()
{
return new SpeedAppParser(Settings, Capabilities.Categories);
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
return Settings.ApiKey.IsNullOrWhiteSpace() || httpResponse.StatusCode == HttpStatusCode.Unauthorized;
}
protected override async Task DoLogin()
{
var requestBuilder = new HttpRequestBuilder(LoginUrl)
{
LogResponseContent = true,
AllowAutoRedirect = true,
Method = HttpMethod.Post,
};
var request = requestBuilder.Build();
var data = new SpeedAppAuthenticationRequest
{
Email = Settings.Email,
Password = Settings.Password
};
request.SetContent(JsonConvert.SerializeObject(data));
request.Headers.ContentType = MediaTypeNames.Application.Json;
var response = await ExecuteAuth(request);
var statusCode = (int)response.StatusCode;
if (statusCode is < 200 or > 299)
{
throw new HttpException(response);
}
var parsedResponse = JsonConvert.DeserializeObject<SpeedAppAuthenticationResponse>(response.Content);
Settings.ApiKey = parsedResponse.Token;
if (Definition.Id > 0)
{
_indexerRepository.UpdateSettings((IndexerDefinition)Definition);
}
_logger.Debug("SpeedApp authentication succeeded.");
}
protected override void ModifyRequest(IndexerRequest request)
{
request.HttpRequest.Headers.Set("Authorization", $"Bearer {Settings.ApiKey}");
}
public override async Task<byte[]> Download(Uri link)
{
Cookies = GetCookies();
if (link.Scheme == "magnet")
{
ValidateMagnet(link.OriginalString);
return Encoding.UTF8.GetBytes(link.OriginalString);
}
var requestBuilder = new HttpRequestBuilder(link.AbsoluteUri);
if (Cookies != null)
{
requestBuilder.SetCookies(Cookies);
}
var request = requestBuilder.Build();
request.AllowAutoRedirect = FollowRedirect;
request.Headers.Set("Authorization", $"Bearer {Settings.ApiKey}");
byte[] torrentData;
try
{
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
torrentData = response.ResponseData;
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.NotFound)
{
_logger.Error(ex, "Downloading torrent file for release failed since it no longer exists ({0})", link.AbsoluteUri);
throw new ReleaseUnavailableException("Downloading torrent failed", ex);
}
if (ex.Response.StatusCode == HttpStatusCode.TooManyRequests)
{
_logger.Error("API Grab Limit reached for {0}", link.AbsoluteUri);
}
else
{
_logger.Error(ex, "Downloading torrent file for release failed ({0})", link.AbsoluteUri);
}
throw new ReleaseDownloadException("Downloading torrent failed", ex);
}
catch (WebException ex)
{
_logger.Error(ex, "Downloading torrent file for release failed ({0})", link.AbsoluteUri);
throw new ReleaseDownloadException("Downloading torrent failed", ex);
}
catch (Exception)
{
_indexerStatusService.RecordFailure(Definition.Id);
_logger.Error("Downloading torrent failed");
throw;
}
return torrentData;
}
protected virtual IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities();
return caps;
}
}
public class SpeedAppRequestGenerator : IIndexerRequestGenerator
{
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
private IndexerCapabilities Capabilities { get; }
private SpeedAppSettings Settings { get; }
public SpeedAppRequestGenerator(IndexerCapabilities capabilities, SpeedAppSettings settings)
{
Capabilities = capabilities;
Settings = settings;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
return GetSearch(searchCriteria, searchCriteria.FullImdbId);
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
return GetSearch(searchCriteria);
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
return GetSearch(searchCriteria, searchCriteria.FullImdbId, searchCriteria.Season, searchCriteria.Episode);
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
return GetSearch(searchCriteria);
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
return GetSearch(searchCriteria);
}
private IndexerPageableRequestChain GetSearch(SearchCriteriaBase searchCriteria, string imdbId = null, int? season = null, string episode = null)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories, imdbId, season, episode));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null, int? season = null, string episode = null)
{
var qc = new NameValueCollection();
if (imdbId.IsNotNullOrWhiteSpace())
{
qc.Add("imdbId", imdbId);
}
else
{
qc.Add("search", term);
}
if (season != null)
{
qc.Add("season", season.Value.ToString());
}
if (episode != null)
{
qc.Add("episode", episode);
}
var cats = Capabilities.Categories.MapTorznabCapsToTrackers(categories);
if (cats.Count > 0)
{
foreach (var cat in cats)
{
qc.Add("categories[]", cat);
}
}
var searchUrl = Settings.BaseUrl + "/api/torrent?" + qc.GetQueryString(duplicateKeysIfMulti: true);
var request = new IndexerRequest(searchUrl, HttpAccept.Json);
request.HttpRequest.Headers.Set("Authorization", $"Bearer {Settings.ApiKey}");
yield return request;
}
}
public class SpeedAppParser : IParseIndexerResponse
{
private readonly SpeedAppSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
public SpeedAppParser(SpeedAppSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
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<List<SpeedAppTorrent>>(indexerResponse.HttpResponse);
return jsonResponse.Resource.Select(torrent => new TorrentInfo
{
Guid = torrent.Id.ToString(),
Title = torrent.Name,
Description = torrent.ShortDescription,
Size = torrent.Size,
ImdbId = ParseUtil.GetImdbID(torrent.ImdbId).GetValueOrDefault(),
DownloadUrl = $"{_settings.BaseUrl}/api/torrent/{torrent.Id}/download",
PosterUrl = torrent.Poster,
InfoUrl = torrent.Url,
Grabs = torrent.TimesCompleted,
PublishDate = torrent.CreatedAt,
Categories = _categories.MapTrackerCatToNewznab(torrent.Category.Id.ToString()),
InfoHash = null,
Seeders = torrent.Seeders,
Peers = torrent.Leechers + torrent.Seeders,
MinimumRatio = 1,
MinimumSeedTime = 172800,
DownloadVolumeFactor = torrent.DownloadVolumeFactor,
UploadVolumeFactor = torrent.UploadVolumeFactor,
}).ToArray();
}
}
public class SpeedAppSettingsValidator : AbstractValidator<SpeedAppSettings>
{
public SpeedAppSettingsValidator()
{
RuleFor(c => c.Email).NotEmpty();
RuleFor(c => c.Password).NotEmpty();
}
}
public class SpeedAppSettings : NoAuthTorrentBaseSettings
{
private static readonly SpeedAppSettingsValidator Validator = new ();
public SpeedAppSettings()
{
Email = "";
Password = "";
}
[FieldDefinition(2, Label = "Email", HelpText = "Site Email", Privacy = PrivacyLevel.UserName)]
public string Email { get; set; }
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
public string Password { get; set; }
[FieldDefinition(4, Label = "API Key", Hidden = HiddenType.Hidden)]
public string ApiKey { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
public class SpeedAppCategory
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}
public class SpeedAppCountry
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("flag_image")]
public string FlagImage { get; set; }
}
public class SpeedAppUploadedBy
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("username")]
public string Username { get; set; }
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("created_at")]
public DateTime CreatedAt { get; set; }
[JsonProperty("class")]
public int Class { get; set; }
[JsonProperty("avatar")]
public string Avatar { get; set; }
[JsonProperty("uploaded")]
public int Uploaded { get; set; }
[JsonProperty("downloaded")]
public int Downloaded { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("country")]
public SpeedAppCountry Country { get; set; }
[JsonProperty("passkey")]
public string Passkey { get; set; }
[JsonProperty("invites")]
public int Invites { get; set; }
[JsonProperty("timezone")]
public string Timezone { get; set; }
[JsonProperty("hit_and_run_count")]
public int HitAndRunCount { get; set; }
[JsonProperty("snatch_count")]
public int SnatchCount { get; set; }
[JsonProperty("need_seed")]
public int NeedSeed { get; set; }
[JsonProperty("average_seed_time")]
public int AverageSeedTime { get; set; }
[JsonProperty("free_leech_tokens")]
public int FreeLeechTokens { get; set; }
[JsonProperty("double_upload_tokens")]
public int DoubleUploadTokens { get; set; }
}
public class SpeedAppTag
{
[JsonProperty("translated_name")]
public string TranslatedName { get; set; }
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("match_list")]
public List<string> MatchList { get; set; }
[JsonProperty("created_at")]
public DateTime CreatedAt { get; set; }
}
public class SpeedAppTorrent
{
[JsonProperty("download_volume_factor")]
public float DownloadVolumeFactor { get; set; }
[JsonProperty("upload_volume_factor")]
public float UploadVolumeFactor { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("category")]
public SpeedAppCategory Category { get; set; }
[JsonProperty("size")]
public long Size { get; set; }
[JsonProperty("created_at")]
public DateTime CreatedAt { get; set; }
[JsonProperty("times_completed")]
public int TimesCompleted { get; set; }
[JsonProperty("leechers")]
public int Leechers { get; set; }
[JsonProperty("seeders")]
public int Seeders { get; set; }
[JsonProperty("uploaded_by")]
public SpeedAppUploadedBy UploadedBy { get; set; }
[JsonProperty("short_description")]
public string ShortDescription { get; set; }
[JsonProperty("poster")]
public string Poster { get; set; }
[JsonProperty("season")]
public int Season { get; set; }
[JsonProperty("episode")]
public int Episode { get; set; }
[JsonProperty("tags")]
public List<SpeedAppTag> Tags { get; set; }
[JsonProperty("imdb_id")]
public string ImdbId { get; set; }
}
public class SpeedAppAuthenticationRequest
{
[JsonProperty("username")]
public string Email { get; set; }
[JsonProperty("password")]
public string Password { get; set; }
}
public class SpeedAppAuthenticationResponse
{
[JsonProperty("token")]
public string Token { get; set; }
}
}

View File

@@ -355,8 +355,6 @@ namespace NzbDrone.Core.Indexers
request.HttpRequest.LogResponseContent = true;
}
request.HttpRequest.AllowAutoRedirect = FollowRedirect;
var originalUrl = request.Url;
Cookies = GetCookies();

View File

@@ -23,6 +23,8 @@
"AppDataDirectory": "AppData directory",
"AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update",
"Application": "Application",
"ApplicationLongTermStatusCheckAllClientMessage": "All applications are unavailable due to failures for more than 6 hours",
"ApplicationLongTermStatusCheckSingleClientMessage": "Applications unavailable due to failures for more than 6 hours: {0}",
"Applications": "Applications",
"ApplicationStatusCheckAllClientMessage": "All applications are unavailable due to failures",
"ApplicationStatusCheckSingleClientMessage": "Applications unavailable due to failures: {0}",

View File

@@ -55,7 +55,7 @@
"UpdateCheckStartupTranslocationMessage": "Päivitystä ei voi asentaa, koska käynnistyskansio '{0}' sijaitsee 'App Translocation' -kansiossa.",
"UpdateCheckUINotWritableMessage": "Päivitystä ei voi asentaa, koska käyttäjällä '{1}' ei ole kirjoitusoikeutta käyttöliittymäkansioon '{0}'.",
"UpdateMechanismHelpText": "Käytä Prowlarrin sisäänrakennettua päivitystoimintoa tai omaa komentosarjaasi.",
"ApplyTagsHelpTexts3": " 'Poista' ainoastaan syötetyt tunnisteet",
"ApplyTagsHelpTexts3": "- \"Poista\" tyhjentää syötetyt tunnisteet.",
"Enable": "Käytä",
"UI": "Käyttöliittymä",
"UrlBaseHelpText": "Käänteisen välityspalvelimen tuki (esim. 'http://[host]:[port]/[urlBase]'). Käytä oletusta jättämällä tyhjäksi.",
@@ -71,10 +71,10 @@
"Username": "Käyttäjätunnus",
"YesCancel": "Kyllä, peruuta",
"NoTagsHaveBeenAddedYet": "Tunnisteita ei ole vielä lisätty.",
"ApplyTags": "Toimenpide tunnisteille",
"ApplyTags": "Tunnistetoimenpide",
"Authentication": "Todennus",
"AuthenticationMethodHelpText": "Vaadi käyttäjätunnus ja salasana.",
"BindAddressHelpText": "Toimiva IPv4-osoite tai jokerimerkkinä '*' (tähti) kaikille yhteyksille.",
"BindAddressHelpText": "Toimiva IPv4-osoite tai '*' (tähti) kaikille yhteyksille.",
"Close": "Sulje",
"DeleteNotification": "Poista kytkentä",
"Docker": "Docker",
@@ -186,7 +186,7 @@
"Discord": "Discord",
"Donations": "Lahjoitukset",
"Edit": "Muokkaa",
"EnableAutomaticSearchHelpText": "Profiilia käytetään automaattihaun yhteydessä, kun sellainen suoritetaan käyttöliittymästä tai Prowlarin toimesta.",
"EnableAutomaticSearchHelpText": "Profiilia käytetään automaattihaun yhteydessä, kun haku suoritetaan käyttöliittymästä tai Prowlarrin toimesta.",
"Enabled": "Käytössä",
"EventType": "Tapahtumatyyppi",
"Exception": "Poikkeus",
@@ -224,7 +224,7 @@
"Wiki": "Wiki",
"ApplyTagsHelpTexts1": "Tunnisteisiin kohdistettavat toimenpiteet:",
"ApplyTagsHelpTexts2": " 'Lisää' syötetyt tunnisteet aiempiin tunnisteisiin",
"ApplyTagsHelpTexts4": " 'Korvaa' kaikki aiemmat tunnisteet tai poista kaikki tunnisteet jättämällä tyhjäksi",
"ApplyTagsHelpTexts4": "- \"Korvaa\" nykyiset tunnisteet syötetyillä tai tyhjennä kaikki tunnisteet jättämällä tyhjäksi.",
"Port": "Portti",
"AreYouSureYouWantToResetYourAPIKey": "Haluatko varmasti uudistaa API-avaimesi?",
"Automatic": "Automaattinen",
@@ -243,7 +243,7 @@
"Cancel": "Peruuta",
"CancelPendingTask": "Haluatko varmasti perua tämän odottavan tehtävän?",
"CertificateValidation": "Varmenteen vahvistus",
"CertificateValidationHelpText": "Valitse HTTPS-varmenteen vahvistuksen tarkkuus. Älä muuta, jollet ymmärrä tähän liittyviä riskejä.",
"CertificateValidationHelpText": "Muuta HTTPS-varmennevahvistuksen tarkkuutta. Älä muuta, jollet ymmärrä tähän liittyviä riskejä.",
"ChangeHasNotBeenSavedYet": "Muutosta ei ole vielä tallennettu",
"Clear": "Tyhjennä",
"CloneProfile": "Kloonaa profiili",
@@ -402,7 +402,7 @@
"Filters": "Suodattimet",
"OnGrab": "Kun elokuva siepataan",
"OnHealthIssue": "Kun havaitaan kuntoon liittyvä ongelma",
"HistoryCleanupDaysHelpText": "Älä tyhjennä automaattisesti asettamalla arvoksi '0'.",
"HistoryCleanupDaysHelpText": "Poista automaattinen tyhjennys käytöstä asettamalla arvoksi '0'.",
"HistoryCleanupDaysHelpTextWarning": "Tässä määritettyä aikaa vanhemmat tiedostot poistetaan automaattisesti roskakorista pysyvästi.",
"TestAllIndexers": "Testaa tietolähteet",
"UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent-tiedon ilmoitti sovellus, joka kommunikoi API:n kanssa",

View File

@@ -460,5 +460,7 @@
"Parameters": "Paraméterek",
"Queued": "Sorba helyezve",
"Started": "Elkezdődött",
"NextExecution": "Következő végrehajtás"
"NextExecution": "Következő végrehajtás",
"ApplicationLongTermStatusCheckSingleClientMessage": "Az alkamazások elérhetetlenek több mint 6 órája az alábbi hiba miatt: {0}",
"ApplicationLongTermStatusCheckAllClientMessage": "Az összes alkalmazás elérhetetlen több mint 6 órája meghibásodás miatt"
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -301,8 +301,8 @@
"IndexerLongTermStatusCheckAllClientMessage": "Alle indexeerders zijn niet beschikbaar vanwege storingen gedurende meer dan 6 uur",
"ClearHistoryMessageText": "Weet je zeker dat je alle geschiedenis van Prowlarr wilt verwijderen",
"ClearHistory": "Geschiedenis verwijderen",
"ApplicationStatusCheckSingleClientMessage": "Applicaties niet toegankelijk door fouten",
"ApplicationStatusCheckAllClientMessage": "Alle applicaties niet toegankelijk door fouten",
"ApplicationStatusCheckSingleClientMessage": "Applicaties onbeschikbaar door fouten",
"ApplicationStatusCheckAllClientMessage": "Alle applicaties onbeschikbaar door fouten",
"AllIndexersHiddenDueToFilter": "Alle indexeerders zijn verborgen door actieve filter",
"AddToDownloadClient": "Release toevoegen aan download client",
"AddNewIndexer": "Voeg nieuwe Indexeerder Toe",

View File

@@ -153,7 +153,7 @@
"NetCore": ".NET",
"Mode": "Modo",
"Mechanism": "Mecanismo",
"Logs": "Logs",
"Logs": "Registos",
"LogLevel": "Nível de log",
"Interval": "Intervalo",
"IndexerFlags": "Sinalizadores do indexador",
@@ -392,7 +392,7 @@
"UserAgentProvidedByTheAppThatCalledTheAPI": "Par Utilizador-Agente fornecido pela aplicação que chamou a API",
"OnApplicationUpdate": "Quando a aplicação atualizar",
"OnApplicationUpdateHelpText": "Quando a aplicação atualizar",
"Database": "base de dados",
"Database": "Base de dados",
"HistoryCleanupDaysHelpTextWarning": "Ficheiros na reciclagem serão eliminados automaticamente após o número de dias selecionado",
"Application": "Aplicações",
"Link": "Ligações",
@@ -401,5 +401,8 @@
"UnableToLoadIndexers": "Não foi possível carregar os indexadores",
"Yes": "Sim",
"GrabReleases": "Capturar versão",
"InstanceName": "Nome da Instancia"
"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"
}

View File

@@ -460,5 +460,7 @@
"GrabTitle": "Obter Título",
"LastDuration": "Última Duração",
"NextExecution": "Próxima Execução",
"Started": "Iniciado"
"Started": "Iniciado",
"ApplicationLongTermStatusCheckAllClientMessage": "Todos os aplicativos estão indisponíveis devido a falhas por mais de 6 horas",
"ApplicationLongTermStatusCheckSingleClientMessage": "Aplicativos indisponíveis devido a falhas por mais de 6 horas: {0}"
}

View File

@@ -44,5 +44,17 @@
"AuthenticationMethodHelpText": "Vyžadovať používateľské meno a heslo pre prístup k Radarru",
"BackupFolderHelpText": "Relatívne cesty budú v priečinku AppData Radarru",
"BranchUpdate": "Vetva, ktorá sa má použiť k aktualizácií Radarru",
"DeleteDownloadClientMessageText": "Naozaj chcete zmazať značku formátu {0} ?"
"DeleteDownloadClientMessageText": "Naozaj chcete zmazať značku formátu {0} ?",
"ChangeHasNotBeenSavedYet": "Zmena ešte nebola uložená",
"Clear": "Vymazať",
"Close": "Zatvoriť",
"CertificateValidation": "Overenie certifikátu",
"CloneProfile": "Klonovať profil",
"BindAddress": "Viazať adresu",
"CancelPendingTask": "Naozaj chcete zrušiť túto prebiehajúcu úlohu?",
"ClientPriority": "Priorita klienta",
"CloseCurrentModal": "Zatvoriť aktuálne okno",
"Columns": "Stĺpce",
"Component": "Komponent",
"ConnectionLost": "Spojenie prerušené"
}

View File

@@ -282,7 +282,7 @@
"RemovedFromTaskQueue": "从任务队列中移除",
"RemoveFilter": "移除过滤条件",
"RemovingTag": "移除标签",
"RestartRequiredHelpTextWarning": "重启生效",
"RestartRequiredHelpTextWarning": "需要重新启动才能生效",
"RestoreBackup": "恢复备份",
"Result": "结果",
"Retention": "保留",
@@ -460,5 +460,7 @@
"Parameters": "参数",
"Queued": "队列中",
"Started": "已开始",
"LastDuration": "上一次用时"
"LastDuration": "上一次用时",
"ApplicationLongTermStatusCheckAllClientMessage": "由于故障超过6小时所有程序都不可用",
"ApplicationLongTermStatusCheckSingleClientMessage": "由于故障超过6小时而无法使用的程序{0}"
}

View File

@@ -1,7 +1,7 @@
{
"About": "關於",
"Add": "新增",
"Added": "新增",
"Added": "新增",
"Actions": "執行",
"Age": "年齡",
"AddIndexer": "新增索引",
@@ -11,5 +11,6 @@
"AddIndexerProxy": "新增索引器代理",
"AddingTag": "新增標籤",
"All": "全部",
"AddRemoveOnly": "僅限新增或移除"
"AddRemoveOnly": "僅限新增或移除",
"AcceptConfirmationModal": "接受確認模式"
}

View File

@@ -1,9 +1,10 @@
using System;
using System;
using System.Collections.Specialized;
using System.Net;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.Notifications.Notifiarr
{
@@ -15,27 +16,21 @@ namespace NzbDrone.Core.Notifications.Notifiarr
public class NotifiarrProxy : INotifiarrProxy
{
private const string URL = "https://notifiarr.com/notifier.php";
private const string URL = "https://notifiarr.com";
private readonly IHttpClient _httpClient;
private readonly IConfigFileProvider _configFileProvider;
private readonly Logger _logger;
public NotifiarrProxy(IHttpClient httpClient, Logger logger)
public NotifiarrProxy(IHttpClient httpClient, IConfigFileProvider configFileProvider, Logger logger)
{
_httpClient = httpClient;
_configFileProvider = configFileProvider;
_logger = logger;
}
public void SendNotification(StringDictionary message, NotifiarrSettings settings)
{
try
{
ProcessNotification(message, settings);
}
catch (NotifiarrException ex)
{
_logger.Error(ex, "Unable to send notification");
throw new NotifiarrException("Unable to send notification");
}
}
public ValidationFailure Test(NotifiarrSettings settings)
@@ -48,21 +43,14 @@ namespace NzbDrone.Core.Notifications.Notifiarr
SendNotification(variables, settings);
return null;
}
catch (HttpException ex)
catch (NotifiarrException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
{
_logger.Error(ex, "API key is invalid: " + ex.Message);
return new ValidationFailure("APIKey", "API key is invalid");
}
_logger.Error(ex, "Unable to send test message: " + ex.Message);
return new ValidationFailure("APIKey", "Unable to send test notification");
return new ValidationFailure("APIKey", ex.Message);
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test notification: " + ex.Message);
return new ValidationFailure("", "Unable to send test notification");
_logger.Error(ex, ex.Message);
return new ValidationFailure("", "Unable to send test notification. Check the log for more details.");
}
}
@@ -70,8 +58,10 @@ namespace NzbDrone.Core.Notifications.Notifiarr
{
try
{
var requestBuilder = new HttpRequestBuilder(URL).Post();
requestBuilder.AddFormParameter("api", settings.APIKey).Build();
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)
{
@@ -84,13 +74,31 @@ namespace NzbDrone.Core.Notifications.Notifiarr
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
var responseCode = ex.Response.StatusCode;
switch ((int)responseCode)
{
_logger.Error(ex, "API key is invalid");
throw;
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);
}
throw new NotifiarrException("Unable to send notification", ex);
}
}
}

View File

@@ -6,7 +6,7 @@
<PackageReference Include="AngleSharp.Xml" Version="0.17.0" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="FluentMigrator.Runner" Version="3.3.2" />
<PackageReference Include="MailKit" Version="3.3.0" />
<PackageReference Include="MailKit" Version="3.4.1" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="Npgsql" Version="5.0.11" />
@@ -21,7 +21,7 @@
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Text.Json" Version="6.0.5" />
<PackageReference Include="MonoTorrent" Version="2.0.5" />
<PackageReference Include="YamlDotNet" Version="11.2.1" />
<PackageReference Include="YamlDotNet" Version="12.0.1" />
<PackageReference Include="AngleSharp" Version="0.17.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -7,9 +7,9 @@
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.3.1" />
<PackageReference Include="DryIoc.dll" Version="4.8.8" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.4.0" />
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Common\Prowlarr.Common.csproj" />

View File

@@ -96,11 +96,16 @@ namespace NzbDrone.Test.Common.AutoMoq
return null;
}
if (serviceType == typeof(System.Text.Json.Serialization.JsonConverter))
{
return null;
}
// get the Mock object for the abstract class or interface
if (serviceType.IsInterface || serviceType.IsAbstract)
{
var mockType = typeof(Mock<>).MakeGenericType(serviceType);
var mockFactory = new DelegateFactory(r =>
var mockFactory = DelegateFactory.Of(r =>
{
var mock = (Mock)r.Resolve(mockType);
SetMock(serviceType, mock);

View File

@@ -4,8 +4,8 @@
<TargetFrameworks>net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="4.8.8" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.0" />
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.1.0" />
<PackageReference Include="NLog" Version="5.0.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -131,7 +131,7 @@ namespace NzbDrone.Api.V1.Indexers
//TODO Optimize this so it's not called here and in NzbSearchService (for manual search)
if (_indexerLimitService.AtQueryLimit(indexerDef))
{
return Content(CreateErrorXML(500, $"Request limit reached ({((IIndexerSettings)indexer.Definition.Settings).BaseSettings.QueryLimit})"), "application/rss+xml");
return Content(CreateErrorXML(429, $"Request limit reached ({((IIndexerSettings)indexer.Definition.Settings).BaseSettings.QueryLimit})"), "application/rss+xml");
}
switch (requestType)
@@ -171,7 +171,7 @@ namespace NzbDrone.Api.V1.Indexers
if (_indexerLimitService.AtDownloadLimit(indexerDef))
{
throw new BadRequestException("Grab limit reached");
return Content(CreateErrorXML(429, $"Grab limit reached ({((IIndexerSettings)indexer.Definition.Settings).BaseSettings.GrabLimit})"), "application/rss+xml");
}
if (link.IsNullOrWhiteSpace() || file.IsNullOrWhiteSpace())

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
namespace Prowlarr.Http
{
public class ApiInfoController : Controller
{
public ApiInfoController()
{
}
[HttpGet("/api")]
[Produces("application/json")]
public ApiInfoResource GetApiInfo()
{
return new ApiInfoResource
{
Current = "v1",
Deprecated = new List<string>()
};
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Prowlarr.Http
{
public class ApiInfoResource
{
public string Current { get; set; }
public List<string> Deprecated { get; set; }
}
}