mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-18 21:55:12 -04:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0d18c546e | |||
| d935b0df82 | |||
| 9e37f69224 | |||
| 2805c4f18b | |||
| dae21f22b9 | |||
| 7ddbe09eca | |||
| 90e3c809c3 | |||
| ec8cf5f57a | |||
| f4bbf2f8af | |||
| ea98d41472 | |||
| b8cb0fd291 | |||
| d3dfa620ac | |||
| 049668f307 | |||
| c400575aac | |||
| 6f122fb2e4 | |||
| a9c210f8e7 | |||
| 1068ba8915 | |||
| 635335d876 | |||
| 2ed51cd933 | |||
| b74c46c554 | |||
| 7029e0d6ee | |||
| 438ea380f5 | |||
| 4eec675d61 | |||
| 0a9bd8287f | |||
| b583ac3a97 | |||
| 4be41ff3fb | |||
| b911f8cc08 | |||
| 22face385f | |||
| 3e700b63c2 | |||
| df0b8fc660 | |||
| f96dbbfc21 | |||
| 4a75f92cb5 | |||
| dd05a9dbd4 | |||
| e78b8d5346 | |||
| 74a1d95ab7 | |||
| f929a7e62f | |||
| e9e4248af4 | |||
| 9e3b43ef12 | |||
| 738a690aac | |||
| 3b7c59e9bb | |||
| b8ca28d955 | |||
| 8797bb7d1c | |||
| be430732f5 | |||
| e7b1380b85 | |||
| c29735741c | |||
| f56a13a375 | |||
| 148d8ee249 | |||
| 3547028b96 | |||
| e4ffa1873e | |||
| 2e85a21576 | |||
| 0a111e7572 | |||
| 25217c0ee8 | |||
| 791592927c | |||
| 4137193a60 | |||
| 99816bfd36 | |||
| 59e5b5bd52 | |||
| 7fa0a2b33c | |||
| 0593ca6b9e | |||
| 06a26b5c87 | |||
| dcae6dc151 | |||
| 04e3ed0ffe | |||
| 1ed5ed9179 |
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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}",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
|
{}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user