mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-03-05 13:40:08 -05:00
Compare commits
58 Commits
v0.4.6.196
...
v0.4.8.207
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -5,9 +5,9 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
- label: I have searched the existing open and closed issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -5,9 +5,9 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the feature you are requesting.
|
||||
description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
- label: I have searched the existing open and closed issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
132
CODE_OF_CONDUCT.md
Normal file
132
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
<development@prowlarr.com>.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '0.4.6'
|
||||
majorVersion: '0.4.8'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
@@ -748,7 +748,7 @@ stages:
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: Packages
|
||||
itemPattern: '/$(pattern)'
|
||||
itemPattern: '**/$(pattern)'
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
- bash: |
|
||||
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
|
||||
@@ -1108,4 +1108,5 @@ stages:
|
||||
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
||||
DISCORDCHANNELID: $(discordChannelId)
|
||||
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
||||
DISCORDTHREADID: $(discordThreadId)
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ class IndexerIndex extends Component {
|
||||
|
||||
onKeyUp = (event) => {
|
||||
const jumpBarItems = this.state.jumpBarItems.order;
|
||||
if (event.path.length === 4) {
|
||||
if (event.composedPath && event.composedPath().length === 4) {
|
||||
if (event.keyCode === keyCodes.HOME && event.ctrlKey) {
|
||||
this.setState({ jumpToCharacter: jumpBarItems[0] });
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ namespace NzbDrone.Common.Test.Http
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")]
|
||||
public void should_execute_get_using_brotli()
|
||||
{
|
||||
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
|
||||
|
||||
@@ -174,6 +174,7 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
PreAuthenticate = true,
|
||||
MaxConnectionsPerServer = 12,
|
||||
ConnectCallback = onConnect,
|
||||
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
|
||||
SslOptions = new SslClientAuthenticationOptions
|
||||
{
|
||||
RemoteCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DryIoc.dll" Version="4.8.8" />
|
||||
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="NLog" Version="5.0.1" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
|
||||
<PackageReference Include="Sentry" Version="3.19.0" />
|
||||
<PackageReference Include="Sentry" Version="3.21.0" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "success",
|
||||
"response": []
|
||||
}
|
||||
@@ -12,6 +12,7 @@ using NzbDrone.Core.Indexers.Definitions;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests
|
||||
{
|
||||
@@ -64,5 +65,19 @@ namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests
|
||||
torrentInfo.DownloadVolumeFactor.Should().Be(1);
|
||||
torrentInfo.UploadVolumeFactor.Should().Be(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task should_not_error_if_empty_response()
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/GazelleGames/recentfeed-empty.json");
|
||||
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
|
||||
};
|
||||
|
||||
Mocker.GetMock<IRarbgTokenProvider>()
|
||||
.Setup(v => v.GetToken(It.IsAny<RarbgSettings>(), It.IsAny<string>()))
|
||||
.Setup(v => v.GetToken(It.IsAny<RarbgSettings>()))
|
||||
.Returns("validtoken");
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="NBuilder" Version="6.1.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="YamlDotNet" Version="11.2.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="12.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Data.Common;
|
||||
using System.Data.SQLite;
|
||||
using System.Net.Sockets;
|
||||
using NLog;
|
||||
using Npgsql;
|
||||
using NzbDrone.Common.Disk;
|
||||
@@ -125,6 +126,37 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://wiki.servarr.com/prowlarr/faq#i-am-getting-an-error-database-disk-image-is-malformed", e, fileName);
|
||||
}
|
||||
catch (NpgsqlException e)
|
||||
{
|
||||
if (e.InnerException is SocketException)
|
||||
{
|
||||
var retryCount = 3;
|
||||
|
||||
while (true)
|
||||
{
|
||||
Logger.Error(e, "Failure to connect to Postgres DB, {0} retries remaining", retryCount);
|
||||
|
||||
try
|
||||
{
|
||||
_migrationController.Migrate(connectionString, migrationContext);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (--retryCount > 0)
|
||||
{
|
||||
System.Threading.Thread.Sleep(5000);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new ProwlarrStartupException(ex, "Error creating main database");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ProwlarrStartupException(e, "Error creating main database");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ProwlarrStartupException(e, "Error creating main database");
|
||||
|
||||
@@ -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.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Http.Proxy;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Http.CloudFlare;
|
||||
using NzbDrone.Core.Localization;
|
||||
@@ -20,10 +21,12 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
public class FlareSolverr : HttpIndexerProxyBase<FlareSolverrSettings>
|
||||
{
|
||||
private readonly ICached<string> _cache;
|
||||
private readonly IHttpProxySettingsProvider _proxySettingsProvider;
|
||||
|
||||
public FlareSolverr(IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger, ILocalizationService localizationService, ICacheManager cacheManager)
|
||||
public FlareSolverr(IHttpProxySettingsProvider proxySettingsProvider, IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger, ILocalizationService localizationService, ICacheManager cacheManager)
|
||||
: base(cloudRequestBuilder, httpClient, logger, localizationService)
|
||||
{
|
||||
_proxySettingsProvider = proxySettingsProvider;
|
||||
_cache = cacheManager.GetCache<string>(typeof(string), "UserAgent");
|
||||
}
|
||||
|
||||
@@ -100,6 +103,10 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36";
|
||||
var maxTimeout = Settings.RequestTimeout * 1000;
|
||||
|
||||
// Use Proxy if no credentials are set (creds not supported as of FS 2.2.9)
|
||||
var proxySettings = _proxySettingsProvider.GetProxySettings();
|
||||
var proxyUrl = proxySettings != null && proxySettings.Username.IsNullOrWhiteSpace() && proxySettings.Password.IsNullOrWhiteSpace() ? GetProxyUri(proxySettings) : null;
|
||||
|
||||
if (request.Method == HttpMethod.Get)
|
||||
{
|
||||
req = new FlareSolverrRequestGet
|
||||
@@ -107,7 +114,11 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
Cmd = "request.get",
|
||||
Url = url,
|
||||
MaxTimeout = maxTimeout,
|
||||
UserAgent = userAgent
|
||||
UserAgent = userAgent,
|
||||
Proxy = new FlareSolverrProxy
|
||||
{
|
||||
Url = proxyUrl?.AbsoluteUri
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (request.Method == HttpMethod.Post)
|
||||
@@ -130,7 +141,11 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
ContentLength = null
|
||||
},
|
||||
MaxTimeout = maxTimeout,
|
||||
UserAgent = userAgent
|
||||
UserAgent = userAgent,
|
||||
Proxy = new FlareSolverrProxy
|
||||
{
|
||||
Url = proxyUrl?.AbsoluteUri
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (contentTypeType.Contains("multipart/form-data")
|
||||
@@ -191,38 +206,59 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
return new ValidationResult(failures);
|
||||
}
|
||||
|
||||
public class FlareSolverrRequest
|
||||
private Uri GetProxyUri(HttpProxySettings proxySettings)
|
||||
{
|
||||
switch (proxySettings.Type)
|
||||
{
|
||||
case ProxyType.Http:
|
||||
return new Uri("http://" + proxySettings.Host + ":" + proxySettings.Port);
|
||||
case ProxyType.Socks4:
|
||||
return new Uri("socks4://" + proxySettings.Host + ":" + proxySettings.Port);
|
||||
case ProxyType.Socks5:
|
||||
return new Uri("socks5://" + proxySettings.Host + ":" + proxySettings.Port);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private class FlareSolverrRequest
|
||||
{
|
||||
public string Cmd { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string UserAgent { get; set; }
|
||||
public Cookie[] Cookies { get; set; }
|
||||
public FlareSolverrProxy Proxy { get; set; }
|
||||
}
|
||||
|
||||
public class FlareSolverrRequestGet : FlareSolverrRequest
|
||||
private class FlareSolverrRequestGet : FlareSolverrRequest
|
||||
{
|
||||
public string Headers { get; set; }
|
||||
public int MaxTimeout { get; set; }
|
||||
}
|
||||
|
||||
public class FlareSolverrRequestPost : FlareSolverrRequest
|
||||
private class FlareSolverrRequestPost : FlareSolverrRequest
|
||||
{
|
||||
public string PostData { get; set; }
|
||||
public int MaxTimeout { get; set; }
|
||||
}
|
||||
|
||||
public class FlareSolverrRequestPostUrlEncoded : FlareSolverrRequestPost
|
||||
private class FlareSolverrRequestPostUrlEncoded : FlareSolverrRequestPost
|
||||
{
|
||||
public HeadersPost Headers { get; set; }
|
||||
}
|
||||
|
||||
public class HeadersPost
|
||||
private class FlareSolverrProxy
|
||||
{
|
||||
public string Url { get; set; }
|
||||
}
|
||||
|
||||
private class HeadersPost
|
||||
{
|
||||
public string ContentType { get; set; }
|
||||
public string ContentLength { get; set; }
|
||||
}
|
||||
|
||||
public class FlareSolverrResponse
|
||||
private class FlareSolverrResponse
|
||||
{
|
||||
public string Status { get; set; }
|
||||
public string Message { get; set; }
|
||||
@@ -232,7 +268,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
public Solution Solution { get; set; }
|
||||
}
|
||||
|
||||
public class Solution
|
||||
private class Solution
|
||||
{
|
||||
public string Url { get; set; }
|
||||
public string Status { get; set; }
|
||||
@@ -242,7 +278,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
public string UserAgent { get; set; }
|
||||
}
|
||||
|
||||
public class Cookie
|
||||
private class Cookie
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
@@ -259,7 +295,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
public System.Net.Cookie ToCookieObj() => new System.Net.Cookie(Name, Value);
|
||||
}
|
||||
|
||||
public class Headers
|
||||
private class Headers
|
||||
{
|
||||
public string Status { get; set; }
|
||||
public string Date { get; set; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
@@ -282,44 +283,29 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
{
|
||||
var startupFolder = _appFolderInfo.AppDataFolder;
|
||||
|
||||
var request = new HttpRequest($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}");
|
||||
var response = _httpClient.Get<List<CardigannMetaDefinition>>(request);
|
||||
|
||||
var currentDefs = _versionService.All().ToDictionary(x => x.DefinitionId, x => x.Sha);
|
||||
|
||||
try
|
||||
{
|
||||
EnsureDefinitionsFolder();
|
||||
|
||||
foreach (var def in response.Resource)
|
||||
var definitionsFolder = Path.Combine(startupFolder, "Definitions");
|
||||
var saveFile = Path.Combine(definitionsFolder, $"indexers.zip");
|
||||
|
||||
_httpClient.DownloadFile($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/package.zip", saveFile);
|
||||
|
||||
using (ZipArchive archive = ZipFile.OpenRead(saveFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
var saveFile = Path.Combine(startupFolder, "Definitions", $"{def.File}.yml");
|
||||
|
||||
if (currentDefs.TryGetValue(def.Id, out var defSha) && defSha == def.Sha)
|
||||
{
|
||||
_logger.Trace("Indexer already up to date: {0}", def.File);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
_httpClient.DownloadFile($"https://indexers.prowlarr.com/{DEFINITION_BRANCH}/{DEFINITION_VERSION}/{def.File}", saveFile);
|
||||
|
||||
_versionService.Upsert(new IndexerDefinitionVersion { Sha = def.Sha, DefinitionId = def.Id, File = def.File, LastUpdated = DateTime.UtcNow });
|
||||
|
||||
_cache.Remove(def.File);
|
||||
_logger.Debug("Updated definition: {0}", def.File);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error("Definition download failed: {0}, {1}", def.File, ex.Message);
|
||||
}
|
||||
archive.ExtractToDirectory(definitionsFolder, true);
|
||||
}
|
||||
|
||||
_diskProvider.DeleteFile(saveFile);
|
||||
|
||||
_cache.Clear();
|
||||
|
||||
_logger.Debug("Updated indexer definitions");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Definition download failed, error creating definitions folder in {0}", startupFolder);
|
||||
_logger.Error(ex, "Definition update failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,9 +321,10 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
if (episode != null)
|
||||
{
|
||||
releaseInfo = episode is > 0 and < 10
|
||||
var episodeString = episode is > 0 and < 10
|
||||
? "0" + episode
|
||||
: episode.ToString();
|
||||
releaseInfo = $" - {episodeString}";
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -449,6 +450,37 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
// Additional 5 hours per GB
|
||||
minimumSeedTime += (int)((size / 1000000000) * 18000);
|
||||
|
||||
if (_settings.UseFilenameForSingleEpisodes && torrent.FileCount == 1)
|
||||
{
|
||||
var fileName = torrent.Files.First().FileName;
|
||||
|
||||
var guid = new Uri(details + "&nh=" + StringUtil.Hash(fileName));
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = minimumSeedTime,
|
||||
Title = fileName,
|
||||
InfoUrl = details.AbsoluteUri,
|
||||
Guid = guid.AbsoluteUri,
|
||||
DownloadUrl = link.AbsoluteUri,
|
||||
PublishDate = publishDate,
|
||||
Categories = category,
|
||||
Description = description,
|
||||
Size = size,
|
||||
Seeders = seeders,
|
||||
Peers = peers,
|
||||
Grabs = snatched,
|
||||
Files = fileCount,
|
||||
DownloadVolumeFactor = rawDownMultiplier,
|
||||
UploadVolumeFactor = rawUpMultiplier,
|
||||
};
|
||||
|
||||
torrentInfos.Add(release);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var title in synonyms)
|
||||
{
|
||||
var releaseTitle = groupName == "Movie" ?
|
||||
@@ -510,6 +542,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Passkey = "";
|
||||
Username = "";
|
||||
EnableSonarrCompatibility = true;
|
||||
UseFilenameForSingleEpisodes = false;
|
||||
}
|
||||
|
||||
[FieldDefinition(2, Label = "Passkey", HelpText = "Site Passkey", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
@@ -521,6 +554,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
[FieldDefinition(4, Label = "Enable Sonarr Compatibility", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr try to add Season information into Release names, without this Sonarr can't match any Seasons, but it has a lot of false positives as well")]
|
||||
public bool EnableSonarrCompatibility { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Use Filenames for Single Episodes", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr replace AnimeBytes release names with the actual filename, this currently only works for single episode releases")]
|
||||
public bool UseFilenameForSingleEpisodes { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
@@ -678,10 +714,22 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
[JsonProperty("FileCount")]
|
||||
public int FileCount { get; set; }
|
||||
|
||||
[JsonProperty("FileList")]
|
||||
public List<File> Files { get; set; }
|
||||
|
||||
[JsonProperty("UploadTime")]
|
||||
public DateTimeOffset UploadTime { get; set; }
|
||||
}
|
||||
|
||||
public class File
|
||||
{
|
||||
[JsonProperty("filename")]
|
||||
public string FileName { get; set; }
|
||||
|
||||
[JsonProperty("size")]
|
||||
public string FileSize { get; set; }
|
||||
}
|
||||
|
||||
public class EditionData
|
||||
{
|
||||
[JsonProperty("EditionTitle")]
|
||||
|
||||
@@ -19,6 +19,7 @@ using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
[Obsolete("Moved to YML for Cardigann")]
|
||||
public class Anthelion : TorrentIndexerBase<UserPassTorrentBaseSettings>
|
||||
{
|
||||
public override string Name => "Anthelion";
|
||||
|
||||
@@ -29,6 +29,11 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
return torrentInfos.ToArray();
|
||||
}
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
throw new RequestLimitReachedException(indexerResponse, "API Request Limit Reached");
|
||||
}
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
[FieldDefinition(4, Label = "PID", HelpText = "PID from My Account or My Profile page")]
|
||||
public string Pid { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Freeleech Only", HelpText = "Search freeleech only")]
|
||||
[FieldDefinition(5, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Search freeleech only")]
|
||||
public bool FreeleechOnly { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
.ContainsIgnoreCase("login.php"))
|
||||
{
|
||||
CookiesUpdater(null, null);
|
||||
throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Try testing the indexer in the settings.");
|
||||
throw new IndexerException(indexerResponse, "We are being redirected to the login page. Most likely your session expired or was killed. Recheck your cookie or credentials and try testing the indexer.");
|
||||
}
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
|
||||
|
||||
@@ -945,6 +945,11 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
public bool CheckIfLoginIsNeeded(HttpResponse response)
|
||||
{
|
||||
if (_definition.Login == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (response.HasHttpRedirect)
|
||||
{
|
||||
var domainHint = GetRedirectDomainHint(response);
|
||||
@@ -958,29 +963,21 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_definition.Login == null || _definition.Login.Test == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (response.HasHttpError)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Only run html test selector on html responses
|
||||
if (response.Headers.ContentType?.Contains("text/html") ?? true)
|
||||
if (_definition.Login.Test?.Selector != null && (response.Headers.ContentType?.Contains("text/html") ?? true))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var document = parser.ParseDocument(response.Content);
|
||||
|
||||
if (_definition.Login.Test.Selector != null)
|
||||
var selection = document.QuerySelectorAll(_definition.Login.Test.Selector);
|
||||
if (selection.Length == 0)
|
||||
{
|
||||
var selection = document.QuerySelectorAll(_definition.Login.Test.Selector);
|
||||
if (selection.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1124,6 +1121,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
var request = new CardigannRequest(requestbuilder.SetEncoding(_encoding).Build(), variables, searchPath);
|
||||
|
||||
request.HttpRequest.AllowAutoRedirect = searchPath.Followredirect;
|
||||
|
||||
yield return request;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,30 @@ namespace NzbDrone.Core.Indexers.Gazelle
|
||||
_logger.Debug("Gazelle authentication succeeded.");
|
||||
}
|
||||
|
||||
public override async Task<byte[]> Download(Uri link)
|
||||
{
|
||||
var response = await base.Download(link);
|
||||
|
||||
if (response.Length >= 1
|
||||
&& response[0] != 'd' // simple test for torrent vs HTML content
|
||||
&& link.Query.Contains("usetoken=1"))
|
||||
{
|
||||
var html = Encoding.GetString(response);
|
||||
if (html.Contains("You do not have any freeleech tokens left.")
|
||||
|| html.Contains("You do not have enough freeleech tokens")
|
||||
|| html.Contains("This torrent is too large.")
|
||||
|| html.Contains("You cannot use tokens here"))
|
||||
{
|
||||
// download again with usetoken=0
|
||||
var requestLinkNew = link.ToString().Replace("usetoken=1", "usetoken=0");
|
||||
|
||||
response = await base.Download(new Uri(requestLinkNew));
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse response)
|
||||
{
|
||||
if (response.HasHttpRedirect || (response.Content != null && response.Content.Contains("\"bad credentials\"")))
|
||||
|
||||
@@ -30,22 +30,19 @@ namespace NzbDrone.Core.Indexers.Gazelle
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetRequest(string searchParameters)
|
||||
protected IEnumerable<IndexerRequest> GetRequest(string searchParameters)
|
||||
{
|
||||
var filter = "";
|
||||
if (searchParameters == null)
|
||||
{
|
||||
}
|
||||
|
||||
var request =
|
||||
new IndexerRequest(
|
||||
$"{APIUrl}?{searchParameters}{filter}",
|
||||
$"{APIUrl}?{searchParameters}",
|
||||
HttpAccept.Json);
|
||||
|
||||
request.HttpRequest.AllowAutoRedirect = false;
|
||||
|
||||
yield return request;
|
||||
}
|
||||
|
||||
private string GetBasicSearchParameters(string searchTerm, int[] categories)
|
||||
protected string GetBasicSearchParameters(string searchTerm, int[] categories)
|
||||
{
|
||||
var searchString = GetSearchTerm(searchTerm);
|
||||
|
||||
@@ -67,7 +64,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
public virtual IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories);
|
||||
|
||||
|
||||
@@ -341,7 +341,18 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return torrentInfos;
|
||||
}
|
||||
|
||||
foreach (var result in jsonResponse.Resource.Response)
|
||||
Dictionary<string, GazelleGamesGroup> response;
|
||||
|
||||
try
|
||||
{
|
||||
response = ((JObject)jsonResponse.Resource.Response).ToObject<Dictionary<string, GazelleGamesGroup>>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return torrentInfos;
|
||||
}
|
||||
|
||||
foreach (var result in response)
|
||||
{
|
||||
Dictionary<string, GazelleGamesTorrent> torrents;
|
||||
|
||||
@@ -455,7 +466,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public class GazelleGamesResponse
|
||||
{
|
||||
public string Status { get; set; }
|
||||
public Dictionary<string, GazelleGamesGroup> Response { get; set; }
|
||||
public object Response { get; set; }
|
||||
}
|
||||
|
||||
public class GazelleGamesGroup
|
||||
|
||||
@@ -8,6 +8,7 @@ using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Gazelle;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -61,6 +62,20 @@ public class GreatPosterWall : Gazelle.Gazelle
|
||||
public class GreatPosterWallRequestGenerator : GazelleRequestGenerator
|
||||
{
|
||||
protected override bool ImdbInTags => false;
|
||||
|
||||
public override IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var parameters = GetBasicSearchParameters(searchCriteria.SearchTerm, searchCriteria.Categories);
|
||||
|
||||
if (searchCriteria.ImdbId != null)
|
||||
{
|
||||
parameters += string.Format("&searchstr={0}", searchCriteria.FullImdbId);
|
||||
}
|
||||
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
pageableRequests.Add(GetRequest(parameters));
|
||||
return pageableRequests;
|
||||
}
|
||||
}
|
||||
|
||||
public class GreatPosterWallParser : GazelleParser
|
||||
|
||||
@@ -124,19 +124,24 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.MoviesBluRay, "Movie / Blu-ray");
|
||||
caps.Categories.AddCategoryMapping(19, NewznabStandardCategory.MoviesHD, "Movie / 1080p");
|
||||
caps.Categories.AddCategoryMapping(18, NewznabStandardCategory.MoviesHD, "Movie / 720p");
|
||||
caps.Categories.AddCategoryMapping(46, NewznabStandardCategory.MoviesUHD, "Movie / 2160p");
|
||||
caps.Categories.AddCategoryMapping(40, NewznabStandardCategory.MoviesHD, "Movie / Remux");
|
||||
caps.Categories.AddCategoryMapping(16, NewznabStandardCategory.MoviesHD, "Movie / HD-DVD");
|
||||
caps.Categories.AddCategoryMapping(41, NewznabStandardCategory.MoviesUHD, "Movie / 4K UHD");
|
||||
caps.Categories.AddCategoryMapping(21, NewznabStandardCategory.TVHD, "TV Show / 720p HDTV");
|
||||
caps.Categories.AddCategoryMapping(22, NewznabStandardCategory.TVHD, "TV Show / 1080p HDTV");
|
||||
caps.Categories.AddCategoryMapping(45, NewznabStandardCategory.TVUHD, "TV Show / 2160p HDTV");
|
||||
caps.Categories.AddCategoryMapping(24, NewznabStandardCategory.TVDocumentary, "Documentary / 720p");
|
||||
caps.Categories.AddCategoryMapping(25, NewznabStandardCategory.TVDocumentary, "Documentary / 1080p");
|
||||
caps.Categories.AddCategoryMapping(47, NewznabStandardCategory.TVDocumentary, "Documentary / 2160p");
|
||||
caps.Categories.AddCategoryMapping(27, NewznabStandardCategory.TVAnime, "Animation / 720p");
|
||||
caps.Categories.AddCategoryMapping(28, NewznabStandardCategory.TVAnime, "Animation / 1080p");
|
||||
caps.Categories.AddCategoryMapping(48, NewznabStandardCategory.TVAnime, "Animation / 2160p");
|
||||
caps.Categories.AddCategoryMapping(30, NewznabStandardCategory.AudioLossless, "Music / HQ Audio");
|
||||
caps.Categories.AddCategoryMapping(31, NewznabStandardCategory.AudioVideo, "Music / Videos");
|
||||
caps.Categories.AddCategoryMapping(33, NewznabStandardCategory.XXX, "XXX / 720p");
|
||||
caps.Categories.AddCategoryMapping(34, NewznabStandardCategory.XXX, "XXX / 1080p");
|
||||
caps.Categories.AddCategoryMapping(49, NewznabStandardCategory.XXX, "XXX / 2160p");
|
||||
caps.Categories.AddCategoryMapping(36, NewznabStandardCategory.MoviesOther, "Trailers");
|
||||
caps.Categories.AddCategoryMapping(37, NewznabStandardCategory.PC, "Software");
|
||||
caps.Categories.AddCategoryMapping(38, NewznabStandardCategory.Other, "Others");
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using DryIoc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
@@ -268,7 +269,10 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
parameters.Add("offset", searchCriteria.Offset.ToString());
|
||||
}
|
||||
|
||||
yield return new IndexerRequest(string.Format("{0}&{1}", baseUrl, parameters.GetQueryString()), HttpAccept.Rss);
|
||||
var request = new IndexerRequest(string.Format("{0}&{1}", baseUrl, parameters.GetQueryString()), HttpAccept.Rss);
|
||||
request.HttpRequest.AllowAutoRedirect = true;
|
||||
|
||||
yield return request;
|
||||
}
|
||||
|
||||
private static string NewsnabifyTitle(string title)
|
||||
|
||||
1190
src/NzbDrone.Core/Indexers/Definitions/NzbIndex.cs
Normal file
1190
src/NzbDrone.Core/Indexers/Definitions/NzbIndex.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
.ContainsIgnoreCase("login.php"))
|
||||
{
|
||||
CookiesUpdater(null, null);
|
||||
throw new IndexerAuthException("We are being redirected to the PTP login page. Most likely your session expired or was killed. Try testing the indexer in the settings.");
|
||||
throw new IndexerAuthException("We are being redirected to the PTP login page. Most likely your session expired or was killed. Recheck your cookie or credentials and try testing the indexer.");
|
||||
}
|
||||
|
||||
if (indexerHttpResponse.StatusCode == HttpStatusCode.Forbidden)
|
||||
|
||||
@@ -255,6 +255,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
|
||||
|
||||
request.HttpRequest.AllowAutoRedirect = false;
|
||||
|
||||
yield return request;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@@ -8,7 +13,9 @@ using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.Http.CloudFlare;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Rarbg
|
||||
@@ -95,6 +102,57 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
||||
return caps;
|
||||
}
|
||||
|
||||
protected override async Task<IndexerQueryResult> FetchPage(IndexerRequest request, IParseIndexerResponse parser)
|
||||
{
|
||||
var response = await FetchIndexerResponse(request);
|
||||
|
||||
// try and recover from token or rate limit errors
|
||||
var jsonResponse = new HttpResponse<RarbgResponse>(response.HttpResponse);
|
||||
|
||||
if (jsonResponse.Resource.error_code.HasValue)
|
||||
{
|
||||
if (jsonResponse.Resource.error_code == 4 || jsonResponse.Resource.error_code == 2)
|
||||
{
|
||||
_logger.Debug("Invalid or expired token, refreshing token from Rarbg");
|
||||
_tokenProvider.ExpireToken(Settings);
|
||||
var newToken = _tokenProvider.GetToken(Settings);
|
||||
|
||||
var qs = HttpUtility.ParseQueryString(request.HttpRequest.Url.Query);
|
||||
qs.Set("token", newToken);
|
||||
|
||||
request.HttpRequest.Url = request.Url.SetQuery(qs.GetQueryString());
|
||||
response = await FetchIndexerResponse(request);
|
||||
}
|
||||
else if (jsonResponse.Resource.error_code == 5 || jsonResponse.Resource.rate_limit.HasValue)
|
||||
{
|
||||
_logger.Debug("Rarbg rate limit hit, retying request");
|
||||
response = await FetchIndexerResponse(request);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var releases = parser.ParseResponse(response).ToList();
|
||||
|
||||
if (releases.Count == 0)
|
||||
{
|
||||
_logger.Trace(response.Content);
|
||||
}
|
||||
|
||||
return new IndexerQueryResult
|
||||
{
|
||||
Releases = releases,
|
||||
Response = response.HttpResponse
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ex.WithData(response.HttpResponse, 128 * 1024);
|
||||
_logger.Trace("Unexpected Response content ({0} bytes): {1}", response.HttpResponse.ResponseData.Length, response.HttpResponse.Content);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||
{
|
||||
if (action == "checkCaptcha")
|
||||
|
||||
@@ -40,9 +40,11 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
||||
if (jsonResponse.Resource.error_code.HasValue)
|
||||
{
|
||||
if (jsonResponse.Resource.error_code == 20 || jsonResponse.Resource.error_code == 8
|
||||
|| jsonResponse.Resource.error_code == 9 || jsonResponse.Resource.error_code == 10)
|
||||
|| jsonResponse.Resource.error_code == 9 || jsonResponse.Resource.error_code == 10
|
||||
|| jsonResponse.Resource.error_code == 5 || jsonResponse.Resource.error_code == 13
|
||||
|| jsonResponse.Resource.error_code == 14)
|
||||
{
|
||||
// No results or imdbid not found
|
||||
// No results, rate limit, or imdbid/tvdb not found
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
||||
{
|
||||
requestBuilder.AddQueryParam("search_themoviedb", tmdbId);
|
||||
}
|
||||
else if (tvdbId.HasValue && tmdbId > 0)
|
||||
else if (tvdbId.HasValue && tvdbId > 0)
|
||||
{
|
||||
requestBuilder.AddQueryParam("search_tvdb", tvdbId);
|
||||
}
|
||||
@@ -60,7 +60,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
||||
}
|
||||
|
||||
requestBuilder.AddQueryParam("limit", "100");
|
||||
requestBuilder.AddQueryParam("token", _tokenProvider.GetToken(Settings, Settings.BaseUrl));
|
||||
requestBuilder.AddQueryParam("token", _tokenProvider.GetToken(Settings));
|
||||
requestBuilder.AddQueryParam("format", "json_extended");
|
||||
requestBuilder.AddQueryParam("app_id", BuildInfo.AppName);
|
||||
|
||||
@@ -69,42 +69,36 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var request = GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories, searchCriteria.FullImdbId, searchCriteria.TmdbId);
|
||||
return GetRequestChain(request, 2);
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories, searchCriteria.FullImdbId, searchCriteria.TmdbId));
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var request = GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
|
||||
return GetRequestChain(request, 2);
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories));
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var request = GetRequest(searchCriteria.SanitizedTvSearchString, searchCriteria.Categories, searchCriteria.FullImdbId, tvdbId: searchCriteria.TvdbId);
|
||||
return GetRequestChain(request, 2);
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
pageableRequests.Add(GetRequest(searchCriteria.SanitizedTvSearchString, searchCriteria.Categories, searchCriteria.FullImdbId, tvdbId: searchCriteria.TvdbId));
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var request = GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
|
||||
return GetRequestChain(request, 2);
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories));
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var request = GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories);
|
||||
return GetRequestChain(request, 2);
|
||||
}
|
||||
|
||||
private IndexerPageableRequestChain GetRequestChain(IEnumerable<IndexerRequest> requests, int retry)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
for (int i = 0; i < retry; i++)
|
||||
{
|
||||
pageableRequests.AddTier(requests);
|
||||
}
|
||||
pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
||||
{
|
||||
public string error { get; set; }
|
||||
public int? error_code { get; set; }
|
||||
public int? rate_limit { get; set; }
|
||||
public List<RarbgTorrent> torrent_results { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Rarbg
|
||||
{
|
||||
public interface IRarbgTokenProvider
|
||||
{
|
||||
string GetToken(RarbgSettings settings, string baseUrl);
|
||||
string GetToken(RarbgSettings settings);
|
||||
void ExpireToken(RarbgSettings settings);
|
||||
}
|
||||
|
||||
public class RarbgTokenProvider : IRarbgTokenProvider
|
||||
@@ -26,12 +26,17 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string GetToken(RarbgSettings settings, string baseUrl)
|
||||
public void ExpireToken(RarbgSettings settings)
|
||||
{
|
||||
return _tokenCache.Get(baseUrl,
|
||||
_tokenCache.Remove(settings.BaseUrl);
|
||||
}
|
||||
|
||||
public string GetToken(RarbgSettings settings)
|
||||
{
|
||||
return _tokenCache.Get(settings.BaseUrl,
|
||||
() =>
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(baseUrl.Trim('/'))
|
||||
var requestBuilder = new HttpRequestBuilder(settings.BaseUrl.Trim('/'))
|
||||
.WithRateLimit(3.0)
|
||||
.Resource($"/pubapi_v2.php?get_token=get_token&app_id={BuildInfo.AppName}")
|
||||
.Accept(HttpAccept.Json);
|
||||
|
||||
62
src/NzbDrone.Core/Indexers/Definitions/RetroFlix.cs
Normal file
62
src/NzbDrone.Core/Indexers/Definitions/RetroFlix.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public class RetroFlix : SpeedAppBase
|
||||
{
|
||||
public override string Name => "RetroFlix";
|
||||
|
||||
public override string[] IndexerUrls => new string[] { "https://retroflix.club/" };
|
||||
public override string[] LegacyUrls => new string[] { "https://retroflix.net/" };
|
||||
|
||||
public override string Description => "Private Torrent Tracker for Classic Movies / TV / General Releases";
|
||||
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(2.1);
|
||||
|
||||
public RetroFlix(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger, IIndexerRepository indexerRepository)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger, indexerRepository)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q,
|
||||
TvSearchParam.Season,
|
||||
TvSearchParam.Ep,
|
||||
TvSearchParam.ImdbId
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q,
|
||||
MovieSearchParam.ImdbId
|
||||
},
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
MusicSearchParam.Q,
|
||||
},
|
||||
BookSearchParams = new List<BookSearchParam>
|
||||
{
|
||||
BookSearchParam.Q,
|
||||
},
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(401, NewznabStandardCategory.Movies, "Movies");
|
||||
caps.Categories.AddCategoryMapping(402, NewznabStandardCategory.TV, "TV Series");
|
||||
caps.Categories.AddCategoryMapping(406, NewznabStandardCategory.AudioVideo, "Music Videos");
|
||||
caps.Categories.AddCategoryMapping(407, NewznabStandardCategory.TVSport, "Sports");
|
||||
caps.Categories.AddCategoryMapping(409, NewznabStandardCategory.Books, "Books");
|
||||
caps.Categories.AddCategoryMapping(408, NewznabStandardCategory.Audio, "HQ Audio");
|
||||
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,6 +194,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
|
||||
|
||||
request.HttpRequest.AllowAutoRedirect = false;
|
||||
|
||||
yield return request;
|
||||
}
|
||||
|
||||
|
||||
@@ -1480,6 +1480,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
|
||||
|
||||
request.HttpRequest.AllowAutoRedirect = false;
|
||||
|
||||
yield return request;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,187 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FluentValidation;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public class SpeedApp : TorrentIndexerBase<SpeedAppSettings>
|
||||
public class SpeedApp : SpeedAppBase
|
||||
{
|
||||
public override string Name => "SpeedApp.io";
|
||||
|
||||
public override string[] IndexerUrls => new string[] { "https://speedapp.io" };
|
||||
|
||||
private string ApiUrl => $"{Settings.BaseUrl}/api";
|
||||
|
||||
private string LoginUrl => $"{ApiUrl}/login";
|
||||
|
||||
public override string Description => "SpeedApp is a ROMANIAN Private Torrent Tracker for MOVIES / TV / GENERAL";
|
||||
|
||||
public override string Language => "ro-RO";
|
||||
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
private IIndexerRepository _indexerRepository;
|
||||
|
||||
public SpeedApp(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger, IIndexerRepository indexerRepository)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger, indexerRepository)
|
||||
{
|
||||
_indexerRepository = indexerRepository;
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new SpeedAppRequestGenerator(Capabilities, Settings);
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new SpeedAppParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
return Settings.ApiKey.IsNullOrWhiteSpace() || httpResponse.StatusCode == HttpStatusCode.Unauthorized;
|
||||
}
|
||||
|
||||
protected override async Task DoLogin()
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
||||
{
|
||||
LogResponseContent = true,
|
||||
AllowAutoRedirect = true,
|
||||
Method = HttpMethod.Post,
|
||||
};
|
||||
|
||||
var request = requestBuilder.Build();
|
||||
|
||||
var data = new SpeedAppAuthenticationRequest
|
||||
{
|
||||
Email = Settings.Email,
|
||||
Password = Settings.Password
|
||||
};
|
||||
|
||||
request.SetContent(JsonConvert.SerializeObject(data));
|
||||
|
||||
request.Headers.ContentType = MediaTypeNames.Application.Json;
|
||||
|
||||
var response = await ExecuteAuth(request);
|
||||
|
||||
var statusCode = (int)response.StatusCode;
|
||||
|
||||
if (statusCode is < 200 or > 299)
|
||||
{
|
||||
throw new HttpException(response);
|
||||
}
|
||||
|
||||
var parsedResponse = JsonConvert.DeserializeObject<SpeedAppAuthenticationResponse>(response.Content);
|
||||
|
||||
Settings.ApiKey = parsedResponse.Token;
|
||||
|
||||
if (Definition.Id > 0)
|
||||
{
|
||||
_indexerRepository.UpdateSettings((IndexerDefinition)Definition);
|
||||
}
|
||||
|
||||
_logger.Debug("SpeedApp authentication succeeded.");
|
||||
}
|
||||
|
||||
protected override void ModifyRequest(IndexerRequest request)
|
||||
{
|
||||
request.HttpRequest.Headers.Set("Authorization", $"Bearer {Settings.ApiKey}");
|
||||
}
|
||||
|
||||
public override async Task<byte[]> Download(Uri link)
|
||||
{
|
||||
Cookies = GetCookies();
|
||||
|
||||
if (link.Scheme == "magnet")
|
||||
{
|
||||
ValidateMagnet(link.OriginalString);
|
||||
return Encoding.UTF8.GetBytes(link.OriginalString);
|
||||
}
|
||||
|
||||
var requestBuilder = new HttpRequestBuilder(link.AbsoluteUri);
|
||||
|
||||
if (Cookies != null)
|
||||
{
|
||||
requestBuilder.SetCookies(Cookies);
|
||||
}
|
||||
|
||||
var request = requestBuilder.Build();
|
||||
request.AllowAutoRedirect = FollowRedirect;
|
||||
request.Headers.Set("Authorization", $"Bearer {Settings.ApiKey}");
|
||||
|
||||
byte[] torrentData;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
|
||||
torrentData = response.ResponseData;
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
if (ex.Response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
_logger.Error(ex, "Downloading torrent file for release failed since it no longer exists ({0})", link.AbsoluteUri);
|
||||
throw new ReleaseUnavailableException("Downloading torrent failed", ex);
|
||||
}
|
||||
|
||||
if (ex.Response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
_logger.Error("API Grab Limit reached for {0}", link.AbsoluteUri);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Error(ex, "Downloading torrent file for release failed ({0})", link.AbsoluteUri);
|
||||
}
|
||||
|
||||
throw new ReleaseDownloadException("Downloading torrent failed", ex);
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
_logger.Error(ex, "Downloading torrent file for release failed ({0})", link.AbsoluteUri);
|
||||
|
||||
throw new ReleaseDownloadException("Downloading torrent failed", ex);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_indexerStatusService.RecordFailure(Definition.Id);
|
||||
_logger.Error("Downloading torrent failed");
|
||||
throw;
|
||||
}
|
||||
|
||||
return torrentData;
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
protected override IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
@@ -253,356 +94,4 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class SpeedAppRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
private IndexerCapabilities Capabilities { get; }
|
||||
|
||||
private SpeedAppSettings Settings { get; }
|
||||
|
||||
public SpeedAppRequestGenerator(IndexerCapabilities capabilities, SpeedAppSettings settings)
|
||||
{
|
||||
Capabilities = capabilities;
|
||||
Settings = settings;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
return GetSearch(searchCriteria, searchCriteria.FullImdbId);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
return GetSearch(searchCriteria);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
return GetSearch(searchCriteria, searchCriteria.FullImdbId, searchCriteria.Season, searchCriteria.Episode);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
return GetSearch(searchCriteria);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
return GetSearch(searchCriteria);
|
||||
}
|
||||
|
||||
private IndexerPageableRequestChain GetSearch(SearchCriteriaBase searchCriteria, string imdbId = null, int? season = null, string episode = null)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories, imdbId, season, episode));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null, int? season = null, string episode = null)
|
||||
{
|
||||
var qc = new NameValueCollection();
|
||||
|
||||
if (imdbId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
qc.Add("imdbId", imdbId);
|
||||
}
|
||||
else
|
||||
{
|
||||
qc.Add("search", term);
|
||||
}
|
||||
|
||||
if (season != null)
|
||||
{
|
||||
qc.Add("season", season.Value.ToString());
|
||||
}
|
||||
|
||||
if (episode != null)
|
||||
{
|
||||
qc.Add("episode", episode);
|
||||
}
|
||||
|
||||
var cats = Capabilities.Categories.MapTorznabCapsToTrackers(categories);
|
||||
|
||||
if (cats.Count > 0)
|
||||
{
|
||||
foreach (var cat in cats)
|
||||
{
|
||||
qc.Add("categories[]", cat);
|
||||
}
|
||||
}
|
||||
|
||||
var searchUrl = Settings.BaseUrl + "/api/torrent?" + qc.GetQueryString(duplicateKeysIfMulti: true);
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Json);
|
||||
|
||||
request.HttpRequest.Headers.Set("Authorization", $"Bearer {Settings.ApiKey}");
|
||||
|
||||
yield return request;
|
||||
}
|
||||
}
|
||||
|
||||
public class SpeedAppParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly SpeedAppSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
public SpeedAppParser(SpeedAppSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
|
||||
}
|
||||
|
||||
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
|
||||
}
|
||||
|
||||
var jsonResponse = new HttpResponse<List<SpeedAppTorrent>>(indexerResponse.HttpResponse);
|
||||
|
||||
return jsonResponse.Resource.Select(torrent => new TorrentInfo
|
||||
{
|
||||
Guid = torrent.Id.ToString(),
|
||||
Title = torrent.Name,
|
||||
Description = torrent.ShortDescription,
|
||||
Size = torrent.Size,
|
||||
ImdbId = ParseUtil.GetImdbID(torrent.ImdbId).GetValueOrDefault(),
|
||||
DownloadUrl = $"{_settings.BaseUrl}/api/torrent/{torrent.Id}/download",
|
||||
PosterUrl = torrent.Poster,
|
||||
InfoUrl = torrent.Url,
|
||||
Grabs = torrent.TimesCompleted,
|
||||
PublishDate = torrent.CreatedAt,
|
||||
Categories = _categories.MapTrackerCatToNewznab(torrent.Category.Id.ToString()),
|
||||
InfoHash = null,
|
||||
Seeders = torrent.Seeders,
|
||||
Peers = torrent.Leechers + torrent.Seeders,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 172800,
|
||||
DownloadVolumeFactor = torrent.DownloadVolumeFactor,
|
||||
UploadVolumeFactor = torrent.UploadVolumeFactor,
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public class SpeedAppSettingsValidator : AbstractValidator<SpeedAppSettings>
|
||||
{
|
||||
public SpeedAppSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Email).NotEmpty();
|
||||
RuleFor(c => c.Password).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class SpeedAppSettings : NoAuthTorrentBaseSettings
|
||||
{
|
||||
private static readonly SpeedAppSettingsValidator Validator = new ();
|
||||
|
||||
public SpeedAppSettings()
|
||||
{
|
||||
Email = "";
|
||||
Password = "";
|
||||
}
|
||||
|
||||
[FieldDefinition(2, Label = "Email", HelpText = "Site Email", Privacy = PrivacyLevel.UserName)]
|
||||
public string Email { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "API Key", Hidden = HiddenType.Hidden)]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
|
||||
public class SpeedAppCategory
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class SpeedAppCountry
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("flag_image")]
|
||||
public string FlagImage { get; set; }
|
||||
}
|
||||
|
||||
public class SpeedAppUploadedBy
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("username")]
|
||||
public string Username { get; set; }
|
||||
|
||||
[JsonProperty("email")]
|
||||
public string Email { get; set; }
|
||||
|
||||
[JsonProperty("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonProperty("class")]
|
||||
public int Class { get; set; }
|
||||
|
||||
[JsonProperty("avatar")]
|
||||
public string Avatar { get; set; }
|
||||
|
||||
[JsonProperty("uploaded")]
|
||||
public int Uploaded { get; set; }
|
||||
|
||||
[JsonProperty("downloaded")]
|
||||
public int Downloaded { get; set; }
|
||||
|
||||
[JsonProperty("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
[JsonProperty("country")]
|
||||
public SpeedAppCountry Country { get; set; }
|
||||
|
||||
[JsonProperty("passkey")]
|
||||
public string Passkey { get; set; }
|
||||
|
||||
[JsonProperty("invites")]
|
||||
public int Invites { get; set; }
|
||||
|
||||
[JsonProperty("timezone")]
|
||||
public string Timezone { get; set; }
|
||||
|
||||
[JsonProperty("hit_and_run_count")]
|
||||
public int HitAndRunCount { get; set; }
|
||||
|
||||
[JsonProperty("snatch_count")]
|
||||
public int SnatchCount { get; set; }
|
||||
|
||||
[JsonProperty("need_seed")]
|
||||
public int NeedSeed { get; set; }
|
||||
|
||||
[JsonProperty("average_seed_time")]
|
||||
public int AverageSeedTime { get; set; }
|
||||
|
||||
[JsonProperty("free_leech_tokens")]
|
||||
public int FreeLeechTokens { get; set; }
|
||||
|
||||
[JsonProperty("double_upload_tokens")]
|
||||
public int DoubleUploadTokens { get; set; }
|
||||
}
|
||||
|
||||
public class SpeedAppTag
|
||||
{
|
||||
[JsonProperty("translated_name")]
|
||||
public string TranslatedName { get; set; }
|
||||
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("match_list")]
|
||||
public List<string> MatchList { get; set; }
|
||||
|
||||
[JsonProperty("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class SpeedAppTorrent
|
||||
{
|
||||
[JsonProperty("download_volume_factor")]
|
||||
public float DownloadVolumeFactor { get; set; }
|
||||
|
||||
[JsonProperty("upload_volume_factor")]
|
||||
public float UploadVolumeFactor { get; set; }
|
||||
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonProperty("category")]
|
||||
public SpeedAppCategory Category { get; set; }
|
||||
|
||||
[JsonProperty("size")]
|
||||
public long Size { get; set; }
|
||||
|
||||
[JsonProperty("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonProperty("times_completed")]
|
||||
public int TimesCompleted { get; set; }
|
||||
|
||||
[JsonProperty("leechers")]
|
||||
public int Leechers { get; set; }
|
||||
|
||||
[JsonProperty("seeders")]
|
||||
public int Seeders { get; set; }
|
||||
|
||||
[JsonProperty("uploaded_by")]
|
||||
public SpeedAppUploadedBy UploadedBy { get; set; }
|
||||
|
||||
[JsonProperty("short_description")]
|
||||
public string ShortDescription { get; set; }
|
||||
|
||||
[JsonProperty("poster")]
|
||||
public string Poster { get; set; }
|
||||
|
||||
[JsonProperty("season")]
|
||||
public int Season { get; set; }
|
||||
|
||||
[JsonProperty("episode")]
|
||||
public int Episode { get; set; }
|
||||
|
||||
[JsonProperty("tags")]
|
||||
public List<SpeedAppTag> Tags { get; set; }
|
||||
|
||||
[JsonProperty("imdb_id")]
|
||||
public string ImdbId { get; set; }
|
||||
}
|
||||
|
||||
public class SpeedAppAuthenticationRequest
|
||||
{
|
||||
[JsonProperty("username")]
|
||||
public string Email { get; set; }
|
||||
|
||||
[JsonProperty("password")]
|
||||
public string Password { get; set; }
|
||||
}
|
||||
|
||||
public class SpeedAppAuthenticationResponse
|
||||
{
|
||||
[JsonProperty("token")]
|
||||
public string Token { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
533
src/NzbDrone.Core/Indexers/Definitions/SpeedApp/SpeedAppBase.cs
Normal file
533
src/NzbDrone.Core/Indexers/Definitions/SpeedApp/SpeedAppBase.cs
Normal file
@@ -0,0 +1,533 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FluentValidation;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public abstract class SpeedAppBase : TorrentIndexerBase<SpeedAppSettings>
|
||||
{
|
||||
private string ApiUrl => $"{Settings.BaseUrl}/api";
|
||||
|
||||
private string LoginUrl => $"{ApiUrl}/login";
|
||||
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
private IIndexerRepository _indexerRepository;
|
||||
|
||||
public SpeedAppBase(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger, IIndexerRepository indexerRepository)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
{
|
||||
_indexerRepository = indexerRepository;
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new SpeedAppRequestGenerator(Capabilities, Settings);
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new SpeedAppParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
return Settings.ApiKey.IsNullOrWhiteSpace() || httpResponse.StatusCode == HttpStatusCode.Unauthorized;
|
||||
}
|
||||
|
||||
protected override async Task DoLogin()
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
||||
{
|
||||
LogResponseContent = true,
|
||||
AllowAutoRedirect = true,
|
||||
Method = HttpMethod.Post,
|
||||
};
|
||||
|
||||
var request = requestBuilder.Build();
|
||||
|
||||
var data = new SpeedAppAuthenticationRequest
|
||||
{
|
||||
Email = Settings.Email,
|
||||
Password = Settings.Password
|
||||
};
|
||||
|
||||
request.SetContent(JsonConvert.SerializeObject(data));
|
||||
|
||||
request.Headers.ContentType = MediaTypeNames.Application.Json;
|
||||
|
||||
var response = await ExecuteAuth(request);
|
||||
|
||||
var statusCode = (int)response.StatusCode;
|
||||
|
||||
if (statusCode is < 200 or > 299)
|
||||
{
|
||||
throw new HttpException(response);
|
||||
}
|
||||
|
||||
var parsedResponse = JsonConvert.DeserializeObject<SpeedAppAuthenticationResponse>(response.Content);
|
||||
|
||||
Settings.ApiKey = parsedResponse.Token;
|
||||
|
||||
if (Definition.Id > 0)
|
||||
{
|
||||
_indexerRepository.UpdateSettings((IndexerDefinition)Definition);
|
||||
}
|
||||
|
||||
_logger.Debug("SpeedApp authentication succeeded.");
|
||||
}
|
||||
|
||||
protected override void ModifyRequest(IndexerRequest request)
|
||||
{
|
||||
request.HttpRequest.Headers.Set("Authorization", $"Bearer {Settings.ApiKey}");
|
||||
}
|
||||
|
||||
public override async Task<byte[]> Download(Uri link)
|
||||
{
|
||||
Cookies = GetCookies();
|
||||
|
||||
if (link.Scheme == "magnet")
|
||||
{
|
||||
ValidateMagnet(link.OriginalString);
|
||||
return Encoding.UTF8.GetBytes(link.OriginalString);
|
||||
}
|
||||
|
||||
var requestBuilder = new HttpRequestBuilder(link.AbsoluteUri);
|
||||
|
||||
if (Cookies != null)
|
||||
{
|
||||
requestBuilder.SetCookies(Cookies);
|
||||
}
|
||||
|
||||
var request = requestBuilder.Build();
|
||||
request.AllowAutoRedirect = FollowRedirect;
|
||||
request.Headers.Set("Authorization", $"Bearer {Settings.ApiKey}");
|
||||
|
||||
byte[] torrentData;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
|
||||
torrentData = response.ResponseData;
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
if (ex.Response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
_logger.Error(ex, "Downloading torrent file for release failed since it no longer exists ({0})", link.AbsoluteUri);
|
||||
throw new ReleaseUnavailableException("Downloading torrent failed", ex);
|
||||
}
|
||||
|
||||
if (ex.Response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
_logger.Error("API Grab Limit reached for {0}", link.AbsoluteUri);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Error(ex, "Downloading torrent file for release failed ({0})", link.AbsoluteUri);
|
||||
}
|
||||
|
||||
throw new ReleaseDownloadException("Downloading torrent failed", ex);
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
_logger.Error(ex, "Downloading torrent file for release failed ({0})", link.AbsoluteUri);
|
||||
|
||||
throw new ReleaseDownloadException("Downloading torrent failed", ex);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_indexerStatusService.RecordFailure(Definition.Id);
|
||||
_logger.Error("Downloading torrent failed");
|
||||
throw;
|
||||
}
|
||||
|
||||
return torrentData;
|
||||
}
|
||||
|
||||
protected virtual IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities();
|
||||
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class SpeedAppRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
private IndexerCapabilities Capabilities { get; }
|
||||
|
||||
private SpeedAppSettings Settings { get; }
|
||||
|
||||
public SpeedAppRequestGenerator(IndexerCapabilities capabilities, SpeedAppSettings settings)
|
||||
{
|
||||
Capabilities = capabilities;
|
||||
Settings = settings;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
return GetSearch(searchCriteria, searchCriteria.FullImdbId);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
return GetSearch(searchCriteria);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
return GetSearch(searchCriteria, searchCriteria.FullImdbId, searchCriteria.Season, searchCriteria.Episode);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
return GetSearch(searchCriteria);
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
return GetSearch(searchCriteria);
|
||||
}
|
||||
|
||||
private IndexerPageableRequestChain GetSearch(SearchCriteriaBase searchCriteria, string imdbId = null, int? season = null, string episode = null)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests($"{searchCriteria.SanitizedSearchTerm}", searchCriteria.Categories, imdbId, season, episode));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null, int? season = null, string episode = null)
|
||||
{
|
||||
var qc = new NameValueCollection();
|
||||
|
||||
if (imdbId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
qc.Add("imdbId", imdbId);
|
||||
}
|
||||
else
|
||||
{
|
||||
qc.Add("search", term);
|
||||
}
|
||||
|
||||
if (season != null)
|
||||
{
|
||||
qc.Add("season", season.Value.ToString());
|
||||
}
|
||||
|
||||
if (episode != null)
|
||||
{
|
||||
qc.Add("episode", episode);
|
||||
}
|
||||
|
||||
var cats = Capabilities.Categories.MapTorznabCapsToTrackers(categories);
|
||||
|
||||
if (cats.Count > 0)
|
||||
{
|
||||
foreach (var cat in cats)
|
||||
{
|
||||
qc.Add("categories[]", cat);
|
||||
}
|
||||
}
|
||||
|
||||
var searchUrl = Settings.BaseUrl + "/api/torrent?" + qc.GetQueryString(duplicateKeysIfMulti: true);
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Json);
|
||||
|
||||
request.HttpRequest.Headers.Set("Authorization", $"Bearer {Settings.ApiKey}");
|
||||
|
||||
yield return request;
|
||||
}
|
||||
}
|
||||
|
||||
public class SpeedAppParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly SpeedAppSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
public SpeedAppParser(SpeedAppSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
|
||||
}
|
||||
|
||||
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
|
||||
}
|
||||
|
||||
var jsonResponse = new HttpResponse<List<SpeedAppTorrent>>(indexerResponse.HttpResponse);
|
||||
|
||||
return jsonResponse.Resource.Select(torrent => new TorrentInfo
|
||||
{
|
||||
Guid = torrent.Id.ToString(),
|
||||
Title = torrent.Name,
|
||||
Description = torrent.ShortDescription,
|
||||
Size = torrent.Size,
|
||||
ImdbId = ParseUtil.GetImdbID(torrent.ImdbId).GetValueOrDefault(),
|
||||
DownloadUrl = $"{_settings.BaseUrl}/api/torrent/{torrent.Id}/download",
|
||||
PosterUrl = torrent.Poster,
|
||||
InfoUrl = torrent.Url,
|
||||
Grabs = torrent.TimesCompleted,
|
||||
PublishDate = torrent.CreatedAt,
|
||||
Categories = _categories.MapTrackerCatToNewznab(torrent.Category.Id.ToString()),
|
||||
InfoHash = null,
|
||||
Seeders = torrent.Seeders,
|
||||
Peers = torrent.Leechers + torrent.Seeders,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 172800,
|
||||
DownloadVolumeFactor = torrent.DownloadVolumeFactor,
|
||||
UploadVolumeFactor = torrent.UploadVolumeFactor,
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public class SpeedAppSettingsValidator : AbstractValidator<SpeedAppSettings>
|
||||
{
|
||||
public SpeedAppSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Email).NotEmpty();
|
||||
RuleFor(c => c.Password).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class SpeedAppSettings : NoAuthTorrentBaseSettings
|
||||
{
|
||||
private static readonly SpeedAppSettingsValidator Validator = new ();
|
||||
|
||||
public SpeedAppSettings()
|
||||
{
|
||||
Email = "";
|
||||
Password = "";
|
||||
}
|
||||
|
||||
[FieldDefinition(2, Label = "Email", HelpText = "Site Email", Privacy = PrivacyLevel.UserName)]
|
||||
public string Email { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "API Key", Hidden = HiddenType.Hidden)]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
|
||||
public class SpeedAppCategory
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class SpeedAppCountry
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("flag_image")]
|
||||
public string FlagImage { get; set; }
|
||||
}
|
||||
|
||||
public class SpeedAppUploadedBy
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("username")]
|
||||
public string Username { get; set; }
|
||||
|
||||
[JsonProperty("email")]
|
||||
public string Email { get; set; }
|
||||
|
||||
[JsonProperty("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonProperty("class")]
|
||||
public int Class { get; set; }
|
||||
|
||||
[JsonProperty("avatar")]
|
||||
public string Avatar { get; set; }
|
||||
|
||||
[JsonProperty("uploaded")]
|
||||
public int Uploaded { get; set; }
|
||||
|
||||
[JsonProperty("downloaded")]
|
||||
public int Downloaded { get; set; }
|
||||
|
||||
[JsonProperty("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
[JsonProperty("country")]
|
||||
public SpeedAppCountry Country { get; set; }
|
||||
|
||||
[JsonProperty("passkey")]
|
||||
public string Passkey { get; set; }
|
||||
|
||||
[JsonProperty("invites")]
|
||||
public int Invites { get; set; }
|
||||
|
||||
[JsonProperty("timezone")]
|
||||
public string Timezone { get; set; }
|
||||
|
||||
[JsonProperty("hit_and_run_count")]
|
||||
public int HitAndRunCount { get; set; }
|
||||
|
||||
[JsonProperty("snatch_count")]
|
||||
public int SnatchCount { get; set; }
|
||||
|
||||
[JsonProperty("need_seed")]
|
||||
public int NeedSeed { get; set; }
|
||||
|
||||
[JsonProperty("average_seed_time")]
|
||||
public int AverageSeedTime { get; set; }
|
||||
|
||||
[JsonProperty("free_leech_tokens")]
|
||||
public int FreeLeechTokens { get; set; }
|
||||
|
||||
[JsonProperty("double_upload_tokens")]
|
||||
public int DoubleUploadTokens { get; set; }
|
||||
}
|
||||
|
||||
public class SpeedAppTag
|
||||
{
|
||||
[JsonProperty("translated_name")]
|
||||
public string TranslatedName { get; set; }
|
||||
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("match_list")]
|
||||
public List<string> MatchList { get; set; }
|
||||
|
||||
[JsonProperty("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class SpeedAppTorrent
|
||||
{
|
||||
[JsonProperty("download_volume_factor")]
|
||||
public float DownloadVolumeFactor { get; set; }
|
||||
|
||||
[JsonProperty("upload_volume_factor")]
|
||||
public float UploadVolumeFactor { get; set; }
|
||||
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonProperty("category")]
|
||||
public SpeedAppCategory Category { get; set; }
|
||||
|
||||
[JsonProperty("size")]
|
||||
public long Size { get; set; }
|
||||
|
||||
[JsonProperty("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonProperty("times_completed")]
|
||||
public int TimesCompleted { get; set; }
|
||||
|
||||
[JsonProperty("leechers")]
|
||||
public int Leechers { get; set; }
|
||||
|
||||
[JsonProperty("seeders")]
|
||||
public int Seeders { get; set; }
|
||||
|
||||
[JsonProperty("uploaded_by")]
|
||||
public SpeedAppUploadedBy UploadedBy { get; set; }
|
||||
|
||||
[JsonProperty("short_description")]
|
||||
public string ShortDescription { get; set; }
|
||||
|
||||
[JsonProperty("poster")]
|
||||
public string Poster { get; set; }
|
||||
|
||||
[JsonProperty("season")]
|
||||
public int Season { get; set; }
|
||||
|
||||
[JsonProperty("episode")]
|
||||
public int Episode { get; set; }
|
||||
|
||||
[JsonProperty("tags")]
|
||||
public List<SpeedAppTag> Tags { get; set; }
|
||||
|
||||
[JsonProperty("imdb_id")]
|
||||
public string ImdbId { get; set; }
|
||||
}
|
||||
|
||||
public class SpeedAppAuthenticationRequest
|
||||
{
|
||||
[JsonProperty("username")]
|
||||
public string Email { get; set; }
|
||||
|
||||
[JsonProperty("password")]
|
||||
public string Password { get; set; }
|
||||
}
|
||||
|
||||
public class SpeedAppAuthenticationResponse
|
||||
{
|
||||
[JsonProperty("token")]
|
||||
public string Token { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -355,8 +355,6 @@ namespace NzbDrone.Core.Indexers
|
||||
request.HttpRequest.LogResponseContent = true;
|
||||
}
|
||||
|
||||
request.HttpRequest.AllowAutoRedirect = FollowRedirect;
|
||||
|
||||
var originalUrl = request.Url;
|
||||
|
||||
Cookies = GetCookies();
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
"AppDataDirectory": "AppData directory",
|
||||
"AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update",
|
||||
"Application": "Application",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "All applications are unavailable due to failures for more than 6 hours",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "Applications unavailable due to failures for more than 6 hours: {0}",
|
||||
"Applications": "Applications",
|
||||
"ApplicationStatusCheckAllClientMessage": "All applications are unavailable due to failures",
|
||||
"ApplicationStatusCheckSingleClientMessage": "Applications unavailable due to failures: {0}",
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"UpdateCheckStartupTranslocationMessage": "Päivitystä ei voi asentaa, koska käynnistyskansio '{0}' sijaitsee 'App Translocation' -kansiossa.",
|
||||
"UpdateCheckUINotWritableMessage": "Päivitystä ei voi asentaa, koska käyttäjällä '{1}' ei ole kirjoitusoikeutta käyttöliittymäkansioon '{0}'.",
|
||||
"UpdateMechanismHelpText": "Käytä Prowlarrin sisäänrakennettua päivitystoimintoa tai omaa komentosarjaasi.",
|
||||
"ApplyTagsHelpTexts3": "– 'Poista' ainoastaan syötetyt tunnisteet",
|
||||
"ApplyTagsHelpTexts3": "- \"Poista\" tyhjentää syötetyt tunnisteet.",
|
||||
"Enable": "Käytä",
|
||||
"UI": "Käyttöliittymä",
|
||||
"UrlBaseHelpText": "Käänteisen välityspalvelimen tuki (esim. 'http://[host]:[port]/[urlBase]'). Käytä oletusta jättämällä tyhjäksi.",
|
||||
@@ -71,10 +71,10 @@
|
||||
"Username": "Käyttäjätunnus",
|
||||
"YesCancel": "Kyllä, peruuta",
|
||||
"NoTagsHaveBeenAddedYet": "Tunnisteita ei ole vielä lisätty.",
|
||||
"ApplyTags": "Toimenpide tunnisteille",
|
||||
"ApplyTags": "Tunnistetoimenpide",
|
||||
"Authentication": "Todennus",
|
||||
"AuthenticationMethodHelpText": "Vaadi käyttäjätunnus ja salasana.",
|
||||
"BindAddressHelpText": "Toimiva IPv4-osoite tai jokerimerkkinä '*' (tähti) kaikille yhteyksille.",
|
||||
"BindAddressHelpText": "Toimiva IPv4-osoite tai '*' (tähti) kaikille yhteyksille.",
|
||||
"Close": "Sulje",
|
||||
"DeleteNotification": "Poista kytkentä",
|
||||
"Docker": "Docker",
|
||||
@@ -186,7 +186,7 @@
|
||||
"Discord": "Discord",
|
||||
"Donations": "Lahjoitukset",
|
||||
"Edit": "Muokkaa",
|
||||
"EnableAutomaticSearchHelpText": "Profiilia käytetään automaattihaun yhteydessä, kun sellainen suoritetaan käyttöliittymästä tai Prowlarin toimesta.",
|
||||
"EnableAutomaticSearchHelpText": "Profiilia käytetään automaattihaun yhteydessä, kun haku suoritetaan käyttöliittymästä tai Prowlarrin toimesta.",
|
||||
"Enabled": "Käytössä",
|
||||
"EventType": "Tapahtumatyyppi",
|
||||
"Exception": "Poikkeus",
|
||||
@@ -224,7 +224,7 @@
|
||||
"Wiki": "Wiki",
|
||||
"ApplyTagsHelpTexts1": "Tunnisteisiin kohdistettavat toimenpiteet:",
|
||||
"ApplyTagsHelpTexts2": "– 'Lisää' syötetyt tunnisteet aiempiin tunnisteisiin",
|
||||
"ApplyTagsHelpTexts4": "– 'Korvaa' kaikki aiemmat tunnisteet tai poista kaikki tunnisteet jättämällä tyhjäksi",
|
||||
"ApplyTagsHelpTexts4": "- \"Korvaa\" nykyiset tunnisteet syötetyillä tai tyhjennä kaikki tunnisteet jättämällä tyhjäksi.",
|
||||
"Port": "Portti",
|
||||
"AreYouSureYouWantToResetYourAPIKey": "Haluatko varmasti uudistaa API-avaimesi?",
|
||||
"Automatic": "Automaattinen",
|
||||
@@ -243,7 +243,7 @@
|
||||
"Cancel": "Peruuta",
|
||||
"CancelPendingTask": "Haluatko varmasti perua tämän odottavan tehtävän?",
|
||||
"CertificateValidation": "Varmenteen vahvistus",
|
||||
"CertificateValidationHelpText": "Valitse HTTPS-varmenteen vahvistuksen tarkkuus. Älä muuta, jollet ymmärrä tähän liittyviä riskejä.",
|
||||
"CertificateValidationHelpText": "Muuta HTTPS-varmennevahvistuksen tarkkuutta. Älä muuta, jollet ymmärrä tähän liittyviä riskejä.",
|
||||
"ChangeHasNotBeenSavedYet": "Muutosta ei ole vielä tallennettu",
|
||||
"Clear": "Tyhjennä",
|
||||
"CloneProfile": "Kloonaa profiili",
|
||||
@@ -402,7 +402,7 @@
|
||||
"Filters": "Suodattimet",
|
||||
"OnGrab": "Kun elokuva siepataan",
|
||||
"OnHealthIssue": "Kun havaitaan kuntoon liittyvä ongelma",
|
||||
"HistoryCleanupDaysHelpText": "Älä tyhjennä automaattisesti asettamalla arvoksi '0'.",
|
||||
"HistoryCleanupDaysHelpText": "Poista automaattinen tyhjennys käytöstä asettamalla arvoksi '0'.",
|
||||
"HistoryCleanupDaysHelpTextWarning": "Tässä määritettyä aikaa vanhemmat tiedostot poistetaan automaattisesti roskakorista pysyvästi.",
|
||||
"TestAllIndexers": "Testaa tietolähteet",
|
||||
"UserAgentProvidedByTheAppThatCalledTheAPI": "User-Agent-tiedon ilmoitti sovellus, joka kommunikoi API:n kanssa",
|
||||
|
||||
@@ -460,5 +460,7 @@
|
||||
"Parameters": "Paraméterek",
|
||||
"Queued": "Sorba helyezve",
|
||||
"Started": "Elkezdődött",
|
||||
"NextExecution": "Következő végrehajtás"
|
||||
"NextExecution": "Következő végrehajtás",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "Az alkamazások elérhetetlenek több mint 6 órája az alábbi hiba miatt: {0}",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "Az összes alkalmazás elérhetetlen több mint 6 órája meghibásodás miatt"
|
||||
}
|
||||
|
||||
1
src/NzbDrone.Core/Localization/Core/lv.json
Normal file
1
src/NzbDrone.Core/Localization/Core/lv.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -301,8 +301,8 @@
|
||||
"IndexerLongTermStatusCheckAllClientMessage": "Alle indexeerders zijn niet beschikbaar vanwege storingen gedurende meer dan 6 uur",
|
||||
"ClearHistoryMessageText": "Weet je zeker dat je alle geschiedenis van Prowlarr wilt verwijderen",
|
||||
"ClearHistory": "Geschiedenis verwijderen",
|
||||
"ApplicationStatusCheckSingleClientMessage": "Applicaties niet toegankelijk door fouten",
|
||||
"ApplicationStatusCheckAllClientMessage": "Alle applicaties niet toegankelijk door fouten",
|
||||
"ApplicationStatusCheckSingleClientMessage": "Applicaties onbeschikbaar door fouten",
|
||||
"ApplicationStatusCheckAllClientMessage": "Alle applicaties onbeschikbaar door fouten",
|
||||
"AllIndexersHiddenDueToFilter": "Alle indexeerders zijn verborgen door actieve filter",
|
||||
"AddToDownloadClient": "Release toevoegen aan download client",
|
||||
"AddNewIndexer": "Voeg nieuwe Indexeerder Toe",
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
"NetCore": ".NET",
|
||||
"Mode": "Modo",
|
||||
"Mechanism": "Mecanismo",
|
||||
"Logs": "Logs",
|
||||
"Logs": "Registos",
|
||||
"LogLevel": "Nível de log",
|
||||
"Interval": "Intervalo",
|
||||
"IndexerFlags": "Sinalizadores do indexador",
|
||||
@@ -392,7 +392,7 @@
|
||||
"UserAgentProvidedByTheAppThatCalledTheAPI": "Par Utilizador-Agente fornecido pela aplicação que chamou a API",
|
||||
"OnApplicationUpdate": "Quando a aplicação atualizar",
|
||||
"OnApplicationUpdateHelpText": "Quando a aplicação atualizar",
|
||||
"Database": "base de dados",
|
||||
"Database": "Base de dados",
|
||||
"HistoryCleanupDaysHelpTextWarning": "Ficheiros na reciclagem serão eliminados automaticamente após o número de dias selecionado",
|
||||
"Application": "Aplicações",
|
||||
"Link": "Ligações",
|
||||
@@ -401,5 +401,8 @@
|
||||
"UnableToLoadIndexers": "Não foi possível carregar os indexadores",
|
||||
"Yes": "Sim",
|
||||
"GrabReleases": "Capturar versão",
|
||||
"InstanceName": "Nome da Instancia"
|
||||
"InstanceName": "Nome da Instancia",
|
||||
"InstanceNameHelpText": "Nome da instância na aba e nome da aplicação para Syslog",
|
||||
"UnableToLoadIndexerProxies": "Incapaz de ler o indexador de proxies",
|
||||
"UnableToLoadApplicationList": "Não foi possível carregar a lista de aplicações"
|
||||
}
|
||||
|
||||
@@ -460,5 +460,7 @@
|
||||
"GrabTitle": "Obter Título",
|
||||
"LastDuration": "Última Duração",
|
||||
"NextExecution": "Próxima Execução",
|
||||
"Started": "Iniciado"
|
||||
"Started": "Iniciado",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "Todos os aplicativos estão indisponíveis devido a falhas por mais de 6 horas",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "Aplicativos indisponíveis devido a falhas por mais de 6 horas: {0}"
|
||||
}
|
||||
|
||||
@@ -44,5 +44,17 @@
|
||||
"AuthenticationMethodHelpText": "Vyžadovať používateľské meno a heslo pre prístup k Radarru",
|
||||
"BackupFolderHelpText": "Relatívne cesty budú v priečinku AppData Radarru",
|
||||
"BranchUpdate": "Vetva, ktorá sa má použiť k aktualizácií Radarru",
|
||||
"DeleteDownloadClientMessageText": "Naozaj chcete zmazať značku formátu {0} ?"
|
||||
"DeleteDownloadClientMessageText": "Naozaj chcete zmazať značku formátu {0} ?",
|
||||
"ChangeHasNotBeenSavedYet": "Zmena ešte nebola uložená",
|
||||
"Clear": "Vymazať",
|
||||
"Close": "Zatvoriť",
|
||||
"CertificateValidation": "Overenie certifikátu",
|
||||
"CloneProfile": "Klonovať profil",
|
||||
"BindAddress": "Viazať adresu",
|
||||
"CancelPendingTask": "Naozaj chcete zrušiť túto prebiehajúcu úlohu?",
|
||||
"ClientPriority": "Priorita klienta",
|
||||
"CloseCurrentModal": "Zatvoriť aktuálne okno",
|
||||
"Columns": "Stĺpce",
|
||||
"Component": "Komponent",
|
||||
"ConnectionLost": "Spojenie prerušené"
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"RemovedFromTaskQueue": "从任务队列中移除",
|
||||
"RemoveFilter": "移除过滤条件",
|
||||
"RemovingTag": "移除标签",
|
||||
"RestartRequiredHelpTextWarning": "重启生效",
|
||||
"RestartRequiredHelpTextWarning": "需要重新启动才能生效",
|
||||
"RestoreBackup": "恢复备份",
|
||||
"Result": "结果",
|
||||
"Retention": "保留",
|
||||
@@ -460,5 +460,7 @@
|
||||
"Parameters": "参数",
|
||||
"Queued": "队列中",
|
||||
"Started": "已开始",
|
||||
"LastDuration": "上一次用时"
|
||||
"LastDuration": "上一次用时",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "由于故障超过6小时,所有程序都不可用",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "由于故障超过6小时而无法使用的程序:{0}"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"About": "關於",
|
||||
"Add": "新增",
|
||||
"Added": "以新增",
|
||||
"Added": "已新增",
|
||||
"Actions": "執行",
|
||||
"Age": "年齡",
|
||||
"AddIndexer": "新增索引",
|
||||
@@ -11,5 +11,6 @@
|
||||
"AddIndexerProxy": "新增索引器代理",
|
||||
"AddingTag": "新增標籤",
|
||||
"All": "全部",
|
||||
"AddRemoveOnly": "僅限新增或移除"
|
||||
"AddRemoveOnly": "僅限新增或移除",
|
||||
"AcceptConfirmationModal": "接受確認模式"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Net;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Notifiarr
|
||||
{
|
||||
@@ -15,27 +16,21 @@ namespace NzbDrone.Core.Notifications.Notifiarr
|
||||
|
||||
public class NotifiarrProxy : INotifiarrProxy
|
||||
{
|
||||
private const string URL = "https://notifiarr.com/notifier.php";
|
||||
private const string URL = "https://notifiarr.com";
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public NotifiarrProxy(IHttpClient httpClient, Logger logger)
|
||||
public NotifiarrProxy(IHttpClient httpClient, IConfigFileProvider configFileProvider, Logger logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_configFileProvider = configFileProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void SendNotification(StringDictionary message, NotifiarrSettings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessNotification(message, settings);
|
||||
}
|
||||
catch (NotifiarrException ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send notification");
|
||||
throw new NotifiarrException("Unable to send notification");
|
||||
}
|
||||
}
|
||||
|
||||
public ValidationFailure Test(NotifiarrSettings settings)
|
||||
@@ -48,21 +43,14 @@ namespace NzbDrone.Core.Notifications.Notifiarr
|
||||
SendNotification(variables, settings);
|
||||
return null;
|
||||
}
|
||||
catch (HttpException ex)
|
||||
catch (NotifiarrException ex)
|
||||
{
|
||||
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
_logger.Error(ex, "API key is invalid: " + ex.Message);
|
||||
return new ValidationFailure("APIKey", "API key is invalid");
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Unable to send test message: " + ex.Message);
|
||||
return new ValidationFailure("APIKey", "Unable to send test notification");
|
||||
return new ValidationFailure("APIKey", ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send test notification: " + ex.Message);
|
||||
return new ValidationFailure("", "Unable to send test notification");
|
||||
_logger.Error(ex, ex.Message);
|
||||
return new ValidationFailure("", "Unable to send test notification. Check the log for more details.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,8 +58,10 @@ namespace NzbDrone.Core.Notifications.Notifiarr
|
||||
{
|
||||
try
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(URL).Post();
|
||||
requestBuilder.AddFormParameter("api", settings.APIKey).Build();
|
||||
var instanceName = _configFileProvider.InstanceName;
|
||||
var requestBuilder = new HttpRequestBuilder(URL + "/api/v1/notification/prowlarr").Post();
|
||||
requestBuilder.AddFormParameter("instanceName", instanceName).Build();
|
||||
requestBuilder.SetHeader("X-API-Key", settings.APIKey);
|
||||
|
||||
foreach (string key in message.Keys)
|
||||
{
|
||||
@@ -84,13 +74,31 @@ namespace NzbDrone.Core.Notifications.Notifiarr
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
|
||||
var responseCode = ex.Response.StatusCode;
|
||||
switch ((int)responseCode)
|
||||
{
|
||||
_logger.Error(ex, "API key is invalid");
|
||||
throw;
|
||||
case 401:
|
||||
_logger.Error("Unauthorized", "HTTP 401 - API key is invalid");
|
||||
throw new NotifiarrException("API key is invalid");
|
||||
case 400:
|
||||
_logger.Error("Invalid Request", "HTTP 400 - Unable to send notification. Ensure Prowlarr Integration is enabled & assigned a channel on Notifiarr");
|
||||
throw new NotifiarrException("Unable to send notification. Ensure Prowlarr Integration is enabled & assigned a channel on Notifiarr");
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
_logger.Error("Service Unavailable", "Unable to send notification. Service Unavailable");
|
||||
throw new NotifiarrException("Unable to send notification. Service Unavailable", ex);
|
||||
case 520:
|
||||
case 521:
|
||||
case 522:
|
||||
case 523:
|
||||
case 524:
|
||||
_logger.Error(ex, "Cloudflare Related HTTP Error - Unable to send notification");
|
||||
throw new NotifiarrException("Cloudflare Related HTTP Error - Unable to send notification", ex);
|
||||
default:
|
||||
_logger.Error(ex, "Unknown HTTP Error - Unable to send notification");
|
||||
throw new NotifiarrException("Unknown HTTP Error - Unable to send notification", ex);
|
||||
}
|
||||
|
||||
throw new NotifiarrException("Unable to send notification", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<PackageReference Include="AngleSharp.Xml" Version="0.17.0" />
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="FluentMigrator.Runner" Version="3.3.2" />
|
||||
<PackageReference Include="MailKit" Version="3.3.0" />
|
||||
<PackageReference Include="MailKit" Version="3.4.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="Npgsql" Version="5.0.11" />
|
||||
@@ -21,7 +21,7 @@
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.5" />
|
||||
<PackageReference Include="MonoTorrent" Version="2.0.5" />
|
||||
<PackageReference Include="YamlDotNet" Version="11.2.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="12.0.1" />
|
||||
<PackageReference Include="AngleSharp" Version="0.17.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.3.1" />
|
||||
<PackageReference Include="DryIoc.dll" Version="4.8.8" />
|
||||
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.4.0" />
|
||||
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
|
||||
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.1.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Common\Prowlarr.Common.csproj" />
|
||||
|
||||
@@ -96,11 +96,16 @@ namespace NzbDrone.Test.Common.AutoMoq
|
||||
return null;
|
||||
}
|
||||
|
||||
if (serviceType == typeof(System.Text.Json.Serialization.JsonConverter))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// get the Mock object for the abstract class or interface
|
||||
if (serviceType.IsInterface || serviceType.IsAbstract)
|
||||
{
|
||||
var mockType = typeof(Mock<>).MakeGenericType(serviceType);
|
||||
var mockFactory = new DelegateFactory(r =>
|
||||
var mockFactory = DelegateFactory.Of(r =>
|
||||
{
|
||||
var mock = (Mock)r.Resolve(mockType);
|
||||
SetMock(serviceType, mock);
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DryIoc.dll" Version="4.8.8" />
|
||||
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.0" />
|
||||
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
|
||||
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.1.0" />
|
||||
<PackageReference Include="NLog" Version="5.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -131,7 +131,7 @@ namespace NzbDrone.Api.V1.Indexers
|
||||
//TODO Optimize this so it's not called here and in NzbSearchService (for manual search)
|
||||
if (_indexerLimitService.AtQueryLimit(indexerDef))
|
||||
{
|
||||
return Content(CreateErrorXML(500, $"Request limit reached ({((IIndexerSettings)indexer.Definition.Settings).BaseSettings.QueryLimit})"), "application/rss+xml");
|
||||
return Content(CreateErrorXML(429, $"Request limit reached ({((IIndexerSettings)indexer.Definition.Settings).BaseSettings.QueryLimit})"), "application/rss+xml");
|
||||
}
|
||||
|
||||
switch (requestType)
|
||||
@@ -171,7 +171,7 @@ namespace NzbDrone.Api.V1.Indexers
|
||||
|
||||
if (_indexerLimitService.AtDownloadLimit(indexerDef))
|
||||
{
|
||||
throw new BadRequestException("Grab limit reached");
|
||||
return Content(CreateErrorXML(429, $"Grab limit reached ({((IIndexerSettings)indexer.Definition.Settings).BaseSettings.GrabLimit})"), "application/rss+xml");
|
||||
}
|
||||
|
||||
if (link.IsNullOrWhiteSpace() || file.IsNullOrWhiteSpace())
|
||||
|
||||
23
src/Prowlarr.Http/ApiInfoController.cs
Normal file
23
src/Prowlarr.Http/ApiInfoController.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Prowlarr.Http
|
||||
{
|
||||
public class ApiInfoController : Controller
|
||||
{
|
||||
public ApiInfoController()
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("/api")]
|
||||
[Produces("application/json")]
|
||||
public ApiInfoResource GetApiInfo()
|
||||
{
|
||||
return new ApiInfoResource
|
||||
{
|
||||
Current = "v1",
|
||||
Deprecated = new List<string>()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/Prowlarr.Http/ApiInfoResource.cs
Normal file
14
src/Prowlarr.Http/ApiInfoResource.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Prowlarr.Http
|
||||
{
|
||||
public class ApiInfoResource
|
||||
{
|
||||
public string Current { get; set; }
|
||||
public List<string> Deprecated { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user