Compare commits

...

62 Commits

Author SHA1 Message Date
Qstick a0d18c546e Bump version to 0.4.9 2022-11-12 20:03:35 -06:00
Qstick d935b0df82 Fix regression in release analytics service after debounce added
Fixes #1193
2022-11-10 17:39:14 -06:00
Qstick 9e37f69224 Fixed: (RetroFlix) Urls built with double slash
Fixes #1188
Closes #1192
2022-11-10 06:54:20 -06:00
Servarr 2805c4f18b Automated API Docs update 2022-11-07 20:22:57 -06:00
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
63 changed files with 2508 additions and 719 deletions
+2 -2
View File
@@ -5,9 +5,9 @@ body:
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Is there an existing issue for this? 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: options:
- label: I have searched the existing issues - label: I have searched the existing open and closed issues
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
+2 -2
View File
@@ -5,9 +5,9 @@ body:
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Is there an existing issue for this? 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: options:
- label: I have searched the existing issues - label: I have searched the existing open and closed issues
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
+132
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
+3 -2
View File
@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.4.6' majorVersion: '0.4.9'
minorVersion: $[counter('minorVersion', 1)] minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)' prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)' buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
@@ -748,7 +748,7 @@ stages:
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: Packages artifactName: Packages
itemPattern: '/$(pattern)' itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- bash: | - bash: |
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
@@ -1108,4 +1108,5 @@ stages:
SYSTEM_ACCESSTOKEN: $(System.AccessToken) SYSTEM_ACCESSTOKEN: $(System.AccessToken)
DISCORDCHANNELID: $(discordChannelId) DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey) DISCORDWEBHOOKKEY: $(discordWebhookKey)
DISCORDTHREADID: $(discordThreadId)
+1 -1
View File
@@ -221,7 +221,7 @@ class IndexerIndex extends Component {
onKeyUp = (event) => { onKeyUp = (event) => {
const jumpBarItems = this.state.jumpBarItems.order; 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) { if (event.keyCode === keyCodes.HOME && event.ctrlKey) {
this.setState({ jumpToCharacter: jumpBarItems[0] }); this.setState({ jumpToCharacter: jumpBarItems[0] });
} }
@@ -212,6 +212,7 @@ namespace NzbDrone.Common.Test.Http
} }
[Test] [Test]
[Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")]
public void should_execute_get_using_brotli() public void should_execute_get_using_brotli()
{ {
var request = new HttpRequest($"https://{_httpBinHost}/brotli"); var request = new HttpRequest($"https://{_httpBinHost}/brotli");
@@ -174,6 +174,7 @@ namespace NzbDrone.Common.Http.Dispatchers
PreAuthenticate = true, PreAuthenticate = true,
MaxConnectionsPerServer = 12, MaxConnectionsPerServer = 12,
ConnectCallback = onConnect, ConnectCallback = onConnect,
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
SslOptions = new SslClientAuthenticationOptions SslOptions = new SslClientAuthenticationOptions
{ {
RemoteCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError RemoteCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError
+2 -2
View File
@@ -4,13 +4,13 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants> <DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="5.0.1" /> <PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" /> <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="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" /> <PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" />
@@ -0,0 +1,4 @@
{
"status": "success",
"response": []
}
@@ -12,6 +12,7 @@ using NzbDrone.Core.Indexers.Definitions;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests
{ {
@@ -64,5 +65,19 @@ namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests
torrentInfo.DownloadVolumeFactor.Should().Be(1); torrentInfo.DownloadVolumeFactor.Should().Be(1);
torrentInfo.UploadVolumeFactor.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);
}
} }
} }
@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
}; };
Mocker.GetMock<IRarbgTokenProvider>() Mocker.GetMock<IRarbgTokenProvider>()
.Setup(v => v.GetToken(It.IsAny<RarbgSettings>(), It.IsAny<string>())) .Setup(v => v.GetToken(It.IsAny<RarbgSettings>()))
.Returns("validtoken"); .Returns("validtoken");
} }
@@ -6,7 +6,7 @@
<PackageReference Include="Dapper" Version="2.0.123" /> <PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="NBuilder" Version="6.1.0" /> <PackageReference Include="NBuilder" Version="6.1.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> <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>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" /> <ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />
+32
View File
@@ -1,6 +1,7 @@
using System; using System;
using System.Data.Common; using System.Data.Common;
using System.Data.SQLite; using System.Data.SQLite;
using System.Net.Sockets;
using NLog; using NLog;
using Npgsql; using Npgsql;
using NzbDrone.Common.Disk; 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); 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) catch (Exception e)
{ {
throw new ProwlarrStartupException(e, "Error creating main database"); throw new ProwlarrStartupException(e, "Error creating main database");
@@ -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");
}
}
}
@@ -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());
}
}
}
@@ -10,6 +10,7 @@ using NzbDrone.Common.Cache;
using NzbDrone.Common.Cloud; using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Core.Http.CloudFlare; using NzbDrone.Core.Http.CloudFlare;
using NzbDrone.Core.Localization; using NzbDrone.Core.Localization;
@@ -20,10 +21,12 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
public class FlareSolverr : HttpIndexerProxyBase<FlareSolverrSettings> public class FlareSolverr : HttpIndexerProxyBase<FlareSolverrSettings>
{ {
private readonly ICached<string> _cache; 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) : base(cloudRequestBuilder, httpClient, logger, localizationService)
{ {
_proxySettingsProvider = proxySettingsProvider;
_cache = cacheManager.GetCache<string>(typeof(string), "UserAgent"); _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 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; 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) if (request.Method == HttpMethod.Get)
{ {
req = new FlareSolverrRequestGet req = new FlareSolverrRequestGet
@@ -107,7 +114,11 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
Cmd = "request.get", Cmd = "request.get",
Url = url, Url = url,
MaxTimeout = maxTimeout, MaxTimeout = maxTimeout,
UserAgent = userAgent UserAgent = userAgent,
Proxy = new FlareSolverrProxy
{
Url = proxyUrl?.AbsoluteUri
}
}; };
} }
else if (request.Method == HttpMethod.Post) else if (request.Method == HttpMethod.Post)
@@ -130,7 +141,11 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
ContentLength = null ContentLength = null
}, },
MaxTimeout = maxTimeout, MaxTimeout = maxTimeout,
UserAgent = userAgent UserAgent = userAgent,
Proxy = new FlareSolverrProxy
{
Url = proxyUrl?.AbsoluteUri
}
}; };
} }
else if (contentTypeType.Contains("multipart/form-data") else if (contentTypeType.Contains("multipart/form-data")
@@ -191,38 +206,59 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
return new ValidationResult(failures); 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 Cmd { get; set; }
public string Url { get; set; } public string Url { get; set; }
public string UserAgent { get; set; } public string UserAgent { get; set; }
public Cookie[] Cookies { 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 string Headers { get; set; }
public int MaxTimeout { get; set; } public int MaxTimeout { get; set; }
} }
public class FlareSolverrRequestPost : FlareSolverrRequest private class FlareSolverrRequestPost : FlareSolverrRequest
{ {
public string PostData { get; set; } public string PostData { get; set; }
public int MaxTimeout { get; set; } public int MaxTimeout { get; set; }
} }
public class FlareSolverrRequestPostUrlEncoded : FlareSolverrRequestPost private class FlareSolverrRequestPostUrlEncoded : FlareSolverrRequestPost
{ {
public HeadersPost Headers { get; set; } 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 ContentType { get; set; }
public string ContentLength { get; set; } public string ContentLength { get; set; }
} }
public class FlareSolverrResponse private class FlareSolverrResponse
{ {
public string Status { get; set; } public string Status { get; set; }
public string Message { get; set; } public string Message { get; set; }
@@ -232,7 +268,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
public Solution Solution { get; set; } public Solution Solution { get; set; }
} }
public class Solution private class Solution
{ {
public string Url { get; set; } public string Url { get; set; }
public string Status { get; set; } public string Status { get; set; }
@@ -242,7 +278,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
public string UserAgent { get; set; } public string UserAgent { get; set; }
} }
public class Cookie private class Cookie
{ {
public string Name { get; set; } public string Name { get; set; }
public string Value { 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 System.Net.Cookie ToCookieObj() => new System.Net.Cookie(Name, Value);
} }
public class Headers private class Headers
{ {
public string Status { get; set; } public string Status { get; set; }
public string Date { get; set; } public string Date { get; set; }
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.IndexerSearch
public void HandleAsync(IndexerQueryEvent message) public void HandleAsync(IndexerQueryEvent message)
{ {
if (message.QueryResult?.Releases != null) if (_analyticsService.IsEnabled && message.QueryResult?.Releases != null)
{ {
lock (_pendingUpdates) lock (_pendingUpdates)
{ {
@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
@@ -282,44 +283,29 @@ namespace NzbDrone.Core.IndexerVersions
{ {
var startupFolder = _appFolderInfo.AppDataFolder; 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 try
{ {
EnsureDefinitionsFolder(); 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 archive.ExtractToDirectory(definitionsFolder, true);
{
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);
}
} }
_diskProvider.DeleteFile(saveFile);
_cache.Clear();
_logger.Debug("Updated indexer definitions");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "Definition download failed, error creating definitions folder in {0}", startupFolder); _logger.Error(ex, "Definition update failed");
} }
} }
} }
@@ -321,9 +321,10 @@ namespace NzbDrone.Core.Indexers.Definitions
if (episode != null) if (episode != null)
{ {
releaseInfo = episode is > 0 and < 10 var episodeString = episode is > 0 and < 10
? "0" + episode ? "0" + episode
: episode.ToString(); : episode.ToString();
releaseInfo = $" - {episodeString}";
} }
else else
{ {
@@ -449,6 +450,37 @@ namespace NzbDrone.Core.Indexers.Definitions
// Additional 5 hours per GB // Additional 5 hours per GB
minimumSeedTime += (int)((size / 1000000000) * 18000); 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) foreach (var title in synonyms)
{ {
var releaseTitle = groupName == "Movie" ? var releaseTitle = groupName == "Movie" ?
@@ -510,6 +542,7 @@ namespace NzbDrone.Core.Indexers.Definitions
Passkey = ""; Passkey = "";
Username = ""; Username = "";
EnableSonarrCompatibility = true; EnableSonarrCompatibility = true;
UseFilenameForSingleEpisodes = false;
} }
[FieldDefinition(2, Label = "Passkey", HelpText = "Site Passkey", Privacy = PrivacyLevel.Password, Type = FieldType.Password)] [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")] [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; } 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() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
@@ -678,10 +714,22 @@ namespace NzbDrone.Core.Indexers.Definitions
[JsonProperty("FileCount")] [JsonProperty("FileCount")]
public int FileCount { get; set; } public int FileCount { get; set; }
[JsonProperty("FileList")]
public List<File> Files { get; set; }
[JsonProperty("UploadTime")] [JsonProperty("UploadTime")]
public DateTimeOffset UploadTime { get; set; } 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 public class EditionData
{ {
[JsonProperty("EditionTitle")] [JsonProperty("EditionTitle")]
@@ -19,6 +19,7 @@ using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Definitions namespace NzbDrone.Core.Indexers.Definitions
{ {
[Obsolete("Moved to YML for Cardigann")]
public class Anthelion : TorrentIndexerBase<UserPassTorrentBaseSettings> public class Anthelion : TorrentIndexerBase<UserPassTorrentBaseSettings>
{ {
public override string Name => "Anthelion"; public override string Name => "Anthelion";
@@ -29,6 +29,11 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
return torrentInfos.ToArray(); return torrentInfos.ToArray();
} }
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
{
throw new RequestLimitReachedException(indexerResponse, "API Request Limit Reached");
}
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
{ {
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request"); throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
@@ -36,7 +36,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
[FieldDefinition(4, Label = "PID", HelpText = "PID from My Account or My Profile page")] [FieldDefinition(4, Label = "PID", HelpText = "PID from My Account or My Profile page")]
public string Pid { get; set; } 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 bool FreeleechOnly { get; set; }
public override NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
.ContainsIgnoreCase("login.php")) .ContainsIgnoreCase("login.php"))
{ {
CookiesUpdater(null, null); 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"); throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
@@ -945,6 +945,11 @@ namespace NzbDrone.Core.Indexers.Cardigann
public bool CheckIfLoginIsNeeded(HttpResponse response) public bool CheckIfLoginIsNeeded(HttpResponse response)
{ {
if (_definition.Login == null)
{
return false;
}
if (response.HasHttpRedirect) if (response.HasHttpRedirect)
{ {
var domainHint = GetRedirectDomainHint(response); var domainHint = GetRedirectDomainHint(response);
@@ -958,29 +963,21 @@ namespace NzbDrone.Core.Indexers.Cardigann
return true; return true;
} }
if (_definition.Login == null || _definition.Login.Test == null)
{
return false;
}
if (response.HasHttpError) if (response.HasHttpError)
{ {
return true; return true;
} }
// Only run html test selector on html responses // 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 parser = new HtmlParser();
var document = parser.ParseDocument(response.Content); 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); return true;
if (selection.Length == 0)
{
return true;
}
} }
} }
@@ -1124,6 +1121,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
var request = new CardigannRequest(requestbuilder.SetEncoding(_encoding).Build(), variables, searchPath); var request = new CardigannRequest(requestbuilder.SetEncoding(_encoding).Build(), variables, searchPath);
request.HttpRequest.AllowAutoRedirect = searchPath.Followredirect;
yield return request; yield return request;
} }
} }
@@ -80,6 +80,30 @@ namespace NzbDrone.Core.Indexers.Gazelle
_logger.Debug("Gazelle authentication succeeded."); _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) protected override bool CheckIfLoginNeeded(HttpResponse response)
{ {
if (response.HasHttpRedirect || (response.Content != null && response.Content.Contains("\"bad credentials\""))) if (response.HasHttpRedirect || (response.Content != null && response.Content.Contains("\"bad credentials\"")))
@@ -30,22 +30,19 @@ namespace NzbDrone.Core.Indexers.Gazelle
return pageableRequests; return pageableRequests;
} }
private IEnumerable<IndexerRequest> GetRequest(string searchParameters) protected IEnumerable<IndexerRequest> GetRequest(string searchParameters)
{ {
var filter = "";
if (searchParameters == null)
{
}
var request = var request =
new IndexerRequest( new IndexerRequest(
$"{APIUrl}?{searchParameters}{filter}", $"{APIUrl}?{searchParameters}",
HttpAccept.Json); HttpAccept.Json);
request.HttpRequest.AllowAutoRedirect = false;
yield return request; yield return request;
} }
private string GetBasicSearchParameters(string searchTerm, int[] categories) protected string GetBasicSearchParameters(string searchTerm, int[] categories)
{ {
var searchString = GetSearchTerm(searchTerm); var searchString = GetSearchTerm(searchTerm);
@@ -67,7 +64,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
return parameters; return parameters;
} }
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) public virtual IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{ {
var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories); var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories);
@@ -341,7 +341,18 @@ namespace NzbDrone.Core.Indexers.Definitions
return torrentInfos; 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; Dictionary<string, GazelleGamesTorrent> torrents;
@@ -455,7 +466,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public class GazelleGamesResponse public class GazelleGamesResponse
{ {
public string Status { get; set; } public string Status { get; set; }
public Dictionary<string, GazelleGamesGroup> Response { get; set; } public object Response { get; set; }
} }
public class GazelleGamesGroup public class GazelleGamesGroup
@@ -8,6 +8,7 @@ using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Gazelle; using NzbDrone.Core.Indexers.Gazelle;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
@@ -61,6 +62,20 @@ public class GreatPosterWall : Gazelle.Gazelle
public class GreatPosterWallRequestGenerator : GazelleRequestGenerator public class GreatPosterWallRequestGenerator : GazelleRequestGenerator
{ {
protected override bool ImdbInTags => false; 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 public class GreatPosterWallParser : GazelleParser
@@ -124,19 +124,24 @@ namespace NzbDrone.Core.Indexers.Definitions
caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.MoviesBluRay, "Movie / Blu-ray"); caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.MoviesBluRay, "Movie / Blu-ray");
caps.Categories.AddCategoryMapping(19, NewznabStandardCategory.MoviesHD, "Movie / 1080p"); caps.Categories.AddCategoryMapping(19, NewznabStandardCategory.MoviesHD, "Movie / 1080p");
caps.Categories.AddCategoryMapping(18, NewznabStandardCategory.MoviesHD, "Movie / 720p"); 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(40, NewznabStandardCategory.MoviesHD, "Movie / Remux");
caps.Categories.AddCategoryMapping(16, NewznabStandardCategory.MoviesHD, "Movie / HD-DVD"); caps.Categories.AddCategoryMapping(16, NewznabStandardCategory.MoviesHD, "Movie / HD-DVD");
caps.Categories.AddCategoryMapping(41, NewznabStandardCategory.MoviesUHD, "Movie / 4K UHD"); caps.Categories.AddCategoryMapping(41, NewznabStandardCategory.MoviesUHD, "Movie / 4K UHD");
caps.Categories.AddCategoryMapping(21, NewznabStandardCategory.TVHD, "TV Show / 720p HDTV"); caps.Categories.AddCategoryMapping(21, NewznabStandardCategory.TVHD, "TV Show / 720p HDTV");
caps.Categories.AddCategoryMapping(22, NewznabStandardCategory.TVHD, "TV Show / 1080p 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(24, NewznabStandardCategory.TVDocumentary, "Documentary / 720p");
caps.Categories.AddCategoryMapping(25, NewznabStandardCategory.TVDocumentary, "Documentary / 1080p"); 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(27, NewznabStandardCategory.TVAnime, "Animation / 720p");
caps.Categories.AddCategoryMapping(28, NewznabStandardCategory.TVAnime, "Animation / 1080p"); 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(30, NewznabStandardCategory.AudioLossless, "Music / HQ Audio");
caps.Categories.AddCategoryMapping(31, NewznabStandardCategory.AudioVideo, "Music / Videos"); caps.Categories.AddCategoryMapping(31, NewznabStandardCategory.AudioVideo, "Music / Videos");
caps.Categories.AddCategoryMapping(33, NewznabStandardCategory.XXX, "XXX / 720p"); caps.Categories.AddCategoryMapping(33, NewznabStandardCategory.XXX, "XXX / 720p");
caps.Categories.AddCategoryMapping(34, NewznabStandardCategory.XXX, "XXX / 1080p"); 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(36, NewznabStandardCategory.MoviesOther, "Trailers");
caps.Categories.AddCategoryMapping(37, NewznabStandardCategory.PC, "Software"); caps.Categories.AddCategoryMapping(37, NewznabStandardCategory.PC, "Software");
caps.Categories.AddCategoryMapping(38, NewznabStandardCategory.Other, "Others"); caps.Categories.AddCategoryMapping(38, NewznabStandardCategory.Other, "Others");
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using DryIoc;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
@@ -268,7 +269,10 @@ namespace NzbDrone.Core.Indexers.Newznab
parameters.Add("offset", searchCriteria.Offset.ToString()); 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) private static string NewsnabifyTitle(string title)
File diff suppressed because it is too large Load Diff
@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
.ContainsIgnoreCase("login.php")) .ContainsIgnoreCase("login.php"))
{ {
CookiesUpdater(null, null); 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) if (indexerHttpResponse.StatusCode == HttpStatusCode.Forbidden)
@@ -255,6 +255,8 @@ namespace NzbDrone.Core.Indexers.Definitions
var request = new IndexerRequest(searchUrl, HttpAccept.Html); var request = new IndexerRequest(searchUrl, HttpAccept.Html);
request.HttpRequest.AllowAutoRedirect = false;
yield return request; yield return request;
} }
@@ -1,6 +1,11 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http.Json;
using System.Threading.Tasks;
using System.Web;
using Newtonsoft.Json;
using NLog; using NLog;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@@ -8,7 +13,9 @@ using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions; using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Http.CloudFlare; using NzbDrone.Core.Http.CloudFlare;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Rarbg namespace NzbDrone.Core.Indexers.Rarbg
@@ -95,6 +102,57 @@ namespace NzbDrone.Core.Indexers.Rarbg
return caps; 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) public override object RequestAction(string action, IDictionary<string, string> query)
{ {
if (action == "checkCaptcha") if (action == "checkCaptcha")
@@ -40,9 +40,11 @@ namespace NzbDrone.Core.Indexers.Rarbg
if (jsonResponse.Resource.error_code.HasValue) if (jsonResponse.Resource.error_code.HasValue)
{ {
if (jsonResponse.Resource.error_code == 20 || jsonResponse.Resource.error_code == 8 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; return results;
} }
@@ -36,7 +36,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
{ {
requestBuilder.AddQueryParam("search_themoviedb", tmdbId); requestBuilder.AddQueryParam("search_themoviedb", tmdbId);
} }
else if (tvdbId.HasValue && tmdbId > 0) else if (tvdbId.HasValue && tvdbId > 0)
{ {
requestBuilder.AddQueryParam("search_tvdb", tvdbId); requestBuilder.AddQueryParam("search_tvdb", tvdbId);
} }
@@ -60,7 +60,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
} }
requestBuilder.AddQueryParam("limit", "100"); 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("format", "json_extended");
requestBuilder.AddQueryParam("app_id", BuildInfo.AppName); requestBuilder.AddQueryParam("app_id", BuildInfo.AppName);
@@ -69,42 +69,36 @@ namespace NzbDrone.Core.Indexers.Rarbg
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{ {
var request = GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories, searchCriteria.FullImdbId, searchCriteria.TmdbId); var pageableRequests = new IndexerPageableRequestChain();
return GetRequestChain(request, 2); pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories, searchCriteria.FullImdbId, searchCriteria.TmdbId));
return pageableRequests;
} }
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria) public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{ {
var request = GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories); var pageableRequests = new IndexerPageableRequestChain();
return GetRequestChain(request, 2); pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories));
return pageableRequests;
} }
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria) public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{ {
var request = GetRequest(searchCriteria.SanitizedTvSearchString, searchCriteria.Categories, searchCriteria.FullImdbId, tvdbId: searchCriteria.TvdbId); var pageableRequests = new IndexerPageableRequestChain();
return GetRequestChain(request, 2); pageableRequests.Add(GetRequest(searchCriteria.SanitizedTvSearchString, searchCriteria.Categories, searchCriteria.FullImdbId, tvdbId: searchCriteria.TvdbId));
return pageableRequests;
} }
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{ {
var request = GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories); var pageableRequests = new IndexerPageableRequestChain();
return GetRequestChain(request, 2); pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories));
return pageableRequests;
} }
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria) 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(); var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories));
for (int i = 0; i < retry; i++)
{
pageableRequests.AddTier(requests);
}
return pageableRequests; return pageableRequests;
} }
@@ -7,6 +7,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
{ {
public string error { get; set; } public string error { get; set; }
public int? error_code { get; set; } public int? error_code { get; set; }
public int? rate_limit { get; set; }
public List<RarbgTorrent> torrent_results { get; set; } public List<RarbgTorrent> torrent_results { get; set; }
} }
@@ -3,14 +3,14 @@ using Newtonsoft.Json.Linq;
using NLog; using NLog;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
namespace NzbDrone.Core.Indexers.Rarbg namespace NzbDrone.Core.Indexers.Rarbg
{ {
public interface IRarbgTokenProvider public interface IRarbgTokenProvider
{ {
string GetToken(RarbgSettings settings, string baseUrl); string GetToken(RarbgSettings settings);
void ExpireToken(RarbgSettings settings);
} }
public class RarbgTokenProvider : IRarbgTokenProvider public class RarbgTokenProvider : IRarbgTokenProvider
@@ -26,12 +26,17 @@ namespace NzbDrone.Core.Indexers.Rarbg
_logger = logger; _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) .WithRateLimit(3.0)
.Resource($"/pubapi_v2.php?get_token=get_token&app_id={BuildInfo.AppName}") .Resource($"/pubapi_v2.php?get_token=get_token&app_id={BuildInfo.AppName}")
.Accept(HttpAccept.Json); .Accept(HttpAccept.Json);
@@ -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;
}
}
}
@@ -194,6 +194,8 @@ namespace NzbDrone.Core.Indexers.Definitions
var request = new IndexerRequest(searchUrl, HttpAccept.Html); var request = new IndexerRequest(searchUrl, HttpAccept.Html);
request.HttpRequest.AllowAutoRedirect = false;
yield return request; yield return request;
} }
@@ -1480,6 +1480,8 @@ namespace NzbDrone.Core.Indexers.Definitions
var request = new IndexerRequest(searchUrl, HttpAccept.Html); var request = new IndexerRequest(searchUrl, HttpAccept.Html);
request.HttpRequest.AllowAutoRedirect = false;
yield return request; yield return request;
} }
@@ -1,187 +1,28 @@
using System;
using System.Collections.Generic; 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 NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration; 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.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions namespace NzbDrone.Core.Indexers.Definitions
{ {
public class SpeedApp : TorrentIndexerBase<SpeedAppSettings> public class SpeedApp : SpeedAppBase
{ {
public override string Name => "SpeedApp.io"; public override string Name => "SpeedApp.io";
public override string[] IndexerUrls => new string[] { "https://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 Description => "SpeedApp is a ROMANIAN Private Torrent Tracker for MOVIES / TV / GENERAL";
public override string Language => "ro-RO"; 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 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) 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() protected override IndexerCapabilities SetCapabilities()
{
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()
{ {
var caps = new IndexerCapabilities var caps = new IndexerCapabilities
{ {
@@ -253,356 +94,4 @@ namespace NzbDrone.Core.Indexers.Definitions
return caps; 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; }
}
} }
@@ -0,0 +1,531 @@
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 LoginUrl => Settings.BaseUrl + "api/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; }
}
}
@@ -355,8 +355,6 @@ namespace NzbDrone.Core.Indexers
request.HttpRequest.LogResponseContent = true; request.HttpRequest.LogResponseContent = true;
} }
request.HttpRequest.AllowAutoRedirect = FollowRedirect;
var originalUrl = request.Url; var originalUrl = request.Url;
Cookies = GetCookies(); Cookies = GetCookies();
@@ -23,6 +23,8 @@
"AppDataDirectory": "AppData directory", "AppDataDirectory": "AppData directory",
"AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update", "AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update",
"Application": "Application", "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", "Applications": "Applications",
"ApplicationStatusCheckAllClientMessage": "All applications are unavailable due to failures", "ApplicationStatusCheckAllClientMessage": "All applications are unavailable due to failures",
"ApplicationStatusCheckSingleClientMessage": "Applications unavailable due to failures: {0}", "ApplicationStatusCheckSingleClientMessage": "Applications unavailable due to failures: {0}",
+7 -7
View File
@@ -55,7 +55,7 @@
"UpdateCheckStartupTranslocationMessage": "Päivitystä ei voi asentaa, koska käynnistyskansio '{0}' sijaitsee 'App Translocation' -kansiossa.", "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}'.", "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.", "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ä", "Enable": "Käytä",
"UI": "Käyttöliittymä", "UI": "Käyttöliittymä",
"UrlBaseHelpText": "Käänteisen välityspalvelimen tuki (esim. 'http://[host]:[port]/[urlBase]'). Käytä oletusta jättämällä tyhjäksi.", "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", "Username": "Käyttäjätunnus",
"YesCancel": "Kyllä, peruuta", "YesCancel": "Kyllä, peruuta",
"NoTagsHaveBeenAddedYet": "Tunnisteita ei ole vielä lisätty.", "NoTagsHaveBeenAddedYet": "Tunnisteita ei ole vielä lisätty.",
"ApplyTags": "Toimenpide tunnisteille", "ApplyTags": "Tunnistetoimenpide",
"Authentication": "Todennus", "Authentication": "Todennus",
"AuthenticationMethodHelpText": "Vaadi käyttäjätunnus ja salasana.", "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", "Close": "Sulje",
"DeleteNotification": "Poista kytkentä", "DeleteNotification": "Poista kytkentä",
"Docker": "Docker", "Docker": "Docker",
@@ -186,7 +186,7 @@
"Discord": "Discord", "Discord": "Discord",
"Donations": "Lahjoitukset", "Donations": "Lahjoitukset",
"Edit": "Muokkaa", "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ä", "Enabled": "Käytössä",
"EventType": "Tapahtumatyyppi", "EventType": "Tapahtumatyyppi",
"Exception": "Poikkeus", "Exception": "Poikkeus",
@@ -224,7 +224,7 @@
"Wiki": "Wiki", "Wiki": "Wiki",
"ApplyTagsHelpTexts1": "Tunnisteisiin kohdistettavat toimenpiteet:", "ApplyTagsHelpTexts1": "Tunnisteisiin kohdistettavat toimenpiteet:",
"ApplyTagsHelpTexts2": " 'Lisää' syötetyt tunnisteet aiempiin tunnisteisiin", "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", "Port": "Portti",
"AreYouSureYouWantToResetYourAPIKey": "Haluatko varmasti uudistaa API-avaimesi?", "AreYouSureYouWantToResetYourAPIKey": "Haluatko varmasti uudistaa API-avaimesi?",
"Automatic": "Automaattinen", "Automatic": "Automaattinen",
@@ -243,7 +243,7 @@
"Cancel": "Peruuta", "Cancel": "Peruuta",
"CancelPendingTask": "Haluatko varmasti perua tämän odottavan tehtävän?", "CancelPendingTask": "Haluatko varmasti perua tämän odottavan tehtävän?",
"CertificateValidation": "Varmenteen vahvistus", "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", "ChangeHasNotBeenSavedYet": "Muutosta ei ole vielä tallennettu",
"Clear": "Tyhjennä", "Clear": "Tyhjennä",
"CloneProfile": "Kloonaa profiili", "CloneProfile": "Kloonaa profiili",
@@ -402,7 +402,7 @@
"Filters": "Suodattimet", "Filters": "Suodattimet",
"OnGrab": "Kun elokuva siepataan", "OnGrab": "Kun elokuva siepataan",
"OnHealthIssue": "Kun havaitaan kuntoon liittyvä ongelma", "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.", "HistoryCleanupDaysHelpTextWarning": "Tässä määritettyä aikaa vanhemmat tiedostot poistetaan automaattisesti roskakorista pysyvästi.",
"TestAllIndexers": "Testaa tietolähteet", "TestAllIndexers": "Testaa tietolähteet",
"UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent-tiedon ilmoitti sovellus, joka kommunikoi API:n kanssa", "UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent-tiedon ilmoitti sovellus, joka kommunikoi API:n kanssa",
+3 -1
View File
@@ -460,5 +460,7 @@
"Parameters": "Paraméterek", "Parameters": "Paraméterek",
"Queued": "Sorba helyezve", "Queued": "Sorba helyezve",
"Started": "Elkezdődött", "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"
} }
@@ -0,0 +1 @@
{}
+2 -2
View File
@@ -301,8 +301,8 @@
"IndexerLongTermStatusCheckAllClientMessage": "Alle indexeerders zijn niet beschikbaar vanwege storingen gedurende meer dan 6 uur", "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", "ClearHistoryMessageText": "Weet je zeker dat je alle geschiedenis van Prowlarr wilt verwijderen",
"ClearHistory": "Geschiedenis verwijderen", "ClearHistory": "Geschiedenis verwijderen",
"ApplicationStatusCheckSingleClientMessage": "Applicaties niet toegankelijk door fouten", "ApplicationStatusCheckSingleClientMessage": "Applicaties onbeschikbaar door fouten",
"ApplicationStatusCheckAllClientMessage": "Alle applicaties niet toegankelijk door fouten", "ApplicationStatusCheckAllClientMessage": "Alle applicaties onbeschikbaar door fouten",
"AllIndexersHiddenDueToFilter": "Alle indexeerders zijn verborgen door actieve filter", "AllIndexersHiddenDueToFilter": "Alle indexeerders zijn verborgen door actieve filter",
"AddToDownloadClient": "Release toevoegen aan download client", "AddToDownloadClient": "Release toevoegen aan download client",
"AddNewIndexer": "Voeg nieuwe Indexeerder Toe", "AddNewIndexer": "Voeg nieuwe Indexeerder Toe",
+6 -3
View File
@@ -153,7 +153,7 @@
"NetCore": ".NET", "NetCore": ".NET",
"Mode": "Modo", "Mode": "Modo",
"Mechanism": "Mecanismo", "Mechanism": "Mecanismo",
"Logs": "Logs", "Logs": "Registos",
"LogLevel": "Nível de log", "LogLevel": "Nível de log",
"Interval": "Intervalo", "Interval": "Intervalo",
"IndexerFlags": "Sinalizadores do indexador", "IndexerFlags": "Sinalizadores do indexador",
@@ -392,7 +392,7 @@
"UserAgentProvidedByTheAppThatCalledTheAPI": "Par Utilizador-Agente fornecido pela aplicação que chamou a API", "UserAgentProvidedByTheAppThatCalledTheAPI": "Par Utilizador-Agente fornecido pela aplicação que chamou a API",
"OnApplicationUpdate": "Quando a aplicação atualizar", "OnApplicationUpdate": "Quando a aplicação atualizar",
"OnApplicationUpdateHelpText": "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", "HistoryCleanupDaysHelpTextWarning": "Ficheiros na reciclagem serão eliminados automaticamente após o número de dias selecionado",
"Application": "Aplicações", "Application": "Aplicações",
"Link": "Ligações", "Link": "Ligações",
@@ -401,5 +401,8 @@
"UnableToLoadIndexers": "Não foi possível carregar os indexadores", "UnableToLoadIndexers": "Não foi possível carregar os indexadores",
"Yes": "Sim", "Yes": "Sim",
"GrabReleases": "Capturar versão", "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"
} }
@@ -460,5 +460,7 @@
"GrabTitle": "Obter Título", "GrabTitle": "Obter Título",
"LastDuration": "Última Duração", "LastDuration": "Última Duração",
"NextExecution": "Próxima Execuçã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}"
} }
+13 -1
View File
@@ -44,5 +44,17 @@
"AuthenticationMethodHelpText": "Vyžadovať používateľské meno a heslo pre prístup k Radarru", "AuthenticationMethodHelpText": "Vyžadovať používateľské meno a heslo pre prístup k Radarru",
"BackupFolderHelpText": "Relatívne cesty budú v priečinku AppData Radarru", "BackupFolderHelpText": "Relatívne cesty budú v priečinku AppData Radarru",
"BranchUpdate": "Vetva, ktorá sa má použiť k aktualizácií 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é"
} }
@@ -282,7 +282,7 @@
"RemovedFromTaskQueue": "从任务队列中移除", "RemovedFromTaskQueue": "从任务队列中移除",
"RemoveFilter": "移除过滤条件", "RemoveFilter": "移除过滤条件",
"RemovingTag": "移除标签", "RemovingTag": "移除标签",
"RestartRequiredHelpTextWarning": "重启生效", "RestartRequiredHelpTextWarning": "需要重新启动才能生效",
"RestoreBackup": "恢复备份", "RestoreBackup": "恢复备份",
"Result": "结果", "Result": "结果",
"Retention": "保留", "Retention": "保留",
@@ -460,5 +460,7 @@
"Parameters": "参数", "Parameters": "参数",
"Queued": "队列中", "Queued": "队列中",
"Started": "已开始", "Started": "已开始",
"LastDuration": "上一次用时" "LastDuration": "上一次用时",
"ApplicationLongTermStatusCheckAllClientMessage": "由于故障超过6小时,所有程序都不可用",
"ApplicationLongTermStatusCheckSingleClientMessage": "由于故障超过6小时而无法使用的程序:{0}"
} }
@@ -1,7 +1,7 @@
{ {
"About": "關於", "About": "關於",
"Add": "新增", "Add": "新增",
"Added": "新增", "Added": "新增",
"Actions": "執行", "Actions": "執行",
"Age": "年齡", "Age": "年齡",
"AddIndexer": "新增索引", "AddIndexer": "新增索引",
@@ -11,5 +11,6 @@
"AddIndexerProxy": "新增索引器代理", "AddIndexerProxy": "新增索引器代理",
"AddingTag": "新增標籤", "AddingTag": "新增標籤",
"All": "全部", "All": "全部",
"AddRemoveOnly": "僅限新增或移除" "AddRemoveOnly": "僅限新增或移除",
"AcceptConfirmationModal": "接受確認模式"
} }
@@ -1,9 +1,10 @@
using System; using System;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Net;
using FluentValidation.Results; using FluentValidation.Results;
using NLog; using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.Notifications.Notifiarr namespace NzbDrone.Core.Notifications.Notifiarr
{ {
@@ -15,27 +16,21 @@ namespace NzbDrone.Core.Notifications.Notifiarr
public class NotifiarrProxy : INotifiarrProxy 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 IHttpClient _httpClient;
private readonly IConfigFileProvider _configFileProvider;
private readonly Logger _logger; private readonly Logger _logger;
public NotifiarrProxy(IHttpClient httpClient, Logger logger) public NotifiarrProxy(IHttpClient httpClient, IConfigFileProvider configFileProvider, Logger logger)
{ {
_httpClient = httpClient; _httpClient = httpClient;
_configFileProvider = configFileProvider;
_logger = logger; _logger = logger;
} }
public void SendNotification(StringDictionary message, NotifiarrSettings settings) public void SendNotification(StringDictionary message, NotifiarrSettings settings)
{ {
try
{
ProcessNotification(message, settings); 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) public ValidationFailure Test(NotifiarrSettings settings)
@@ -48,21 +43,14 @@ namespace NzbDrone.Core.Notifications.Notifiarr
SendNotification(variables, settings); SendNotification(variables, settings);
return null; return null;
} }
catch (HttpException ex) catch (NotifiarrException ex)
{ {
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) return new ValidationFailure("APIKey", ex.Message);
{
_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");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "Unable to send test notification: " + ex.Message); _logger.Error(ex, ex.Message);
return new ValidationFailure("", "Unable to send test notification"); return new ValidationFailure("", "Unable to send test notification. Check the log for more details.");
} }
} }
@@ -70,8 +58,10 @@ namespace NzbDrone.Core.Notifications.Notifiarr
{ {
try try
{ {
var requestBuilder = new HttpRequestBuilder(URL).Post(); var instanceName = _configFileProvider.InstanceName;
requestBuilder.AddFormParameter("api", settings.APIKey).Build(); 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) foreach (string key in message.Keys)
{ {
@@ -84,13 +74,31 @@ namespace NzbDrone.Core.Notifications.Notifiarr
} }
catch (HttpException ex) catch (HttpException ex)
{ {
if (ex.Response.StatusCode == HttpStatusCode.BadRequest) var responseCode = ex.Response.StatusCode;
switch ((int)responseCode)
{ {
_logger.Error(ex, "API key is invalid"); case 401:
throw; _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);
} }
} }
} }
+2 -2
View File
@@ -6,7 +6,7 @@
<PackageReference Include="AngleSharp.Xml" Version="0.17.0" /> <PackageReference Include="AngleSharp.Xml" Version="0.17.0" />
<PackageReference Include="Dapper" Version="2.0.123" /> <PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="FluentMigrator.Runner" Version="3.3.2" /> <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="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" /> <PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="Npgsql" Version="5.0.11" /> <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.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Text.Json" Version="6.0.5" /> <PackageReference Include="System.Text.Json" Version="6.0.5" />
<PackageReference Include="MonoTorrent" Version="2.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" /> <PackageReference Include="AngleSharp" Version="0.17.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+3 -3
View File
@@ -7,9 +7,9 @@
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" /> <PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" 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="Swashbuckle.AspNetCore.SwaggerGen" Version="6.4.0" />
<PackageReference Include="DryIoc.dll" Version="4.8.8" /> <PackageReference Include="DryIoc.dll" Version="5.2.2" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.0" /> <PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.1.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\NzbDrone.Common\Prowlarr.Common.csproj" /> <ProjectReference Include="..\NzbDrone.Common\Prowlarr.Common.csproj" />
@@ -96,11 +96,16 @@ namespace NzbDrone.Test.Common.AutoMoq
return null; return null;
} }
if (serviceType == typeof(System.Text.Json.Serialization.JsonConverter))
{
return null;
}
// get the Mock object for the abstract class or interface // get the Mock object for the abstract class or interface
if (serviceType.IsInterface || serviceType.IsAbstract) if (serviceType.IsInterface || serviceType.IsAbstract)
{ {
var mockType = typeof(Mock<>).MakeGenericType(serviceType); var mockType = typeof(Mock<>).MakeGenericType(serviceType);
var mockFactory = new DelegateFactory(r => var mockFactory = DelegateFactory.Of(r =>
{ {
var mock = (Mock)r.Resolve(mockType); var mock = (Mock)r.Resolve(mockType);
SetMock(serviceType, mock); SetMock(serviceType, mock);
+2 -2
View File
@@ -4,8 +4,8 @@
<TargetFrameworks>net6.0</TargetFrameworks> <TargetFrameworks>net6.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DryIoc.dll" Version="4.8.8" /> <PackageReference Include="DryIoc.dll" Version="5.2.2" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.0" /> <PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.1.0" />
<PackageReference Include="NLog" Version="5.0.1" /> <PackageReference Include="NLog" Version="5.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -131,7 +131,7 @@ namespace NzbDrone.Api.V1.Indexers
//TODO Optimize this so it's not called here and in NzbSearchService (for manual search) //TODO Optimize this so it's not called here and in NzbSearchService (for manual search)
if (_indexerLimitService.AtQueryLimit(indexerDef)) 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) switch (requestType)
@@ -171,7 +171,7 @@ namespace NzbDrone.Api.V1.Indexers
if (_indexerLimitService.AtDownloadLimit(indexerDef)) 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()) if (link.IsNullOrWhiteSpace() || file.IsNullOrWhiteSpace())
+36
View File
@@ -27,6 +27,25 @@
} }
], ],
"paths": { "paths": {
"/api": {
"get": {
"tags": [
"ApiInfo"
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiInfoResource"
}
}
}
}
}
}
},
"/api/v1/applications/{id}": { "/api/v1/applications/{id}": {
"get": { "get": {
"tags": [ "tags": [
@@ -4406,6 +4425,23 @@
}, },
"components": { "components": {
"schemas": { "schemas": {
"ApiInfoResource": {
"type": "object",
"properties": {
"current": {
"type": "string",
"nullable": true
},
"deprecated": {
"type": "array",
"items": {
"type": "string"
},
"nullable": true
}
},
"additionalProperties": false
},
"AppProfileResource": { "AppProfileResource": {
"type": "object", "type": "object",
"properties": { "properties": {
+23
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>()
};
}
}
}
+14
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; }
}
}