mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-16 21:35:04 -04:00
Compare commits
36 Commits
v1.0.0.217
...
ratelimit-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e15054329 | ||
|
|
3dfbfd07dd | ||
|
|
842df6913c | ||
|
|
599eeb4c61 | ||
|
|
da371dd921 | ||
|
|
fc25ba7ac0 | ||
|
|
6e1bef13e2 | ||
|
|
72ee413411 | ||
|
|
e87b45b47e | ||
|
|
cc841fe3d1 | ||
|
|
264ffdcc26 | ||
|
|
5cc044aa8f | ||
|
|
de2fd92b6f | ||
|
|
eff09c1f72 | ||
|
|
9db888c9a3 | ||
|
|
bf78396164 | ||
|
|
0e7eaa9221 | ||
|
|
5b82decc31 | ||
|
|
38ab533272 | ||
|
|
4914fcd5df | ||
|
|
858415b037 | ||
|
|
43f4899324 | ||
|
|
c60a94adfb | ||
|
|
f386ddb806 | ||
|
|
4175c2577e | ||
|
|
6ce9e5ceb9 | ||
|
|
c15643be39 | ||
|
|
a58380031d | ||
|
|
73af5c9a72 | ||
|
|
d556545e7f | ||
|
|
affde5d7b7 | ||
|
|
518c85dee2 | ||
|
|
ba3a240707 | ||
|
|
587a73f3d6 | ||
|
|
ae8f017ca8 | ||
|
|
d9098b612e |
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.0.0'
|
||||
majorVersion: '1.1.0'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
|
||||
@@ -14,7 +14,7 @@ function CapabilitiesLabel(props) {
|
||||
let filteredList = categories.filter((item) => item.id < 100000);
|
||||
|
||||
if (categoryFilter.length > 0) {
|
||||
filteredList = filteredList.filter((item) => categoryFilter.includes(item.id));
|
||||
filteredList = filteredList.filter((item) => categoryFilter.includes(item.id) || (item.subCategories && item.subCategories.some((r) => categoryFilter.includes(r.id))));
|
||||
}
|
||||
|
||||
const nameList = filteredList.map((item) => item.name).sort();
|
||||
|
||||
@@ -35,9 +35,11 @@ $hoverScale: 1.05;
|
||||
}
|
||||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
width: 85%;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import CategoryLabel from 'Search/Table/CategoryLabel';
|
||||
@@ -52,12 +53,17 @@ class SearchIndexOverview extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditSeriesPress = () => {
|
||||
this.setState({ isEditSeriesModalOpen: true });
|
||||
};
|
||||
onGrabPress = () => {
|
||||
const {
|
||||
guid,
|
||||
indexerId,
|
||||
onGrabPress
|
||||
} = this.props;
|
||||
|
||||
onEditSeriesModalClose = () => {
|
||||
this.setState({ isEditSeriesModalOpen: false });
|
||||
onGrabPress({
|
||||
guid,
|
||||
indexerId
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
@@ -66,6 +72,7 @@ class SearchIndexOverview extends Component {
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
infoUrl,
|
||||
protocol,
|
||||
downloadUrl,
|
||||
categories,
|
||||
@@ -91,10 +98,16 @@ class SearchIndexOverview extends Component {
|
||||
<div className={styles.info} style={{ height: contentHeight }}>
|
||||
<div className={styles.titleRow}>
|
||||
<div className={styles.title}>
|
||||
<TextTruncate
|
||||
line={2}
|
||||
text={title}
|
||||
/>
|
||||
<Link
|
||||
to={infoUrl}
|
||||
title={title}
|
||||
>
|
||||
<TextTruncate
|
||||
line={2}
|
||||
text={title}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
|
||||
@@ -49,7 +49,7 @@ class SearchIndexOverviews extends Component {
|
||||
this._grid &&
|
||||
(prevState.width !== width ||
|
||||
prevState.rowHeight !== rowHeight ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||
hasDifferentItemsOrOrder(prevProps.items, items, 'guid')
|
||||
)
|
||||
) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
@@ -93,7 +93,6 @@ class SearchIndexOverviews extends Component {
|
||||
cellRenderer = ({ key, rowIndex, style }) => {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
@@ -117,7 +116,6 @@ class SearchIndexOverviews extends Component {
|
||||
<SearchIndexItemConnector
|
||||
key={release.guid}
|
||||
component={SearchIndexOverview}
|
||||
sortKey={sortKey}
|
||||
rowHeight={rowHeight}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
|
||||
@@ -41,7 +41,7 @@ const mapDispatchToProps = {
|
||||
dispatchExecuteCommand: executeCommand
|
||||
};
|
||||
|
||||
class MovieIndexItemConnector extends Component {
|
||||
class SearchIndexItemConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -66,9 +66,9 @@ class MovieIndexItemConnector extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
MovieIndexItemConnector.propTypes = {
|
||||
SearchIndexItemConnector.propTypes = {
|
||||
guid: PropTypes.string,
|
||||
component: PropTypes.elementType.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieIndexItemConnector);
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(SearchIndexItemConnector);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
||||
|
||||
<meta name="description" content="Prowlarr (Preview)" />
|
||||
<meta name="description" content="Prowlarr" />
|
||||
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
@@ -50,7 +50,7 @@
|
||||
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
|
||||
<!-- webpack bundles head -->
|
||||
|
||||
<title>Prowlarr (Preview)</title>
|
||||
<title>Prowlarr</title>
|
||||
|
||||
<!--
|
||||
The super basic styling for .root will live here,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Common.Http
|
||||
{
|
||||
@@ -25,5 +25,11 @@ namespace NzbDrone.Common.Http
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TooManyRequestsException(HttpRequest request, HttpResponse response, TimeSpan retryWait)
|
||||
: base(request, response)
|
||||
{
|
||||
RetryAfter = retryWait;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,15 +28,15 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||
[Test]
|
||||
public void should_return_warning_when_branch_not_valid()
|
||||
{
|
||||
GivenValidBranch("master");
|
||||
GivenValidBranch("test");
|
||||
|
||||
Subject.Check().ShouldBeWarning();
|
||||
}
|
||||
|
||||
[TestCase("Develop")]
|
||||
[TestCase("develop")]
|
||||
[TestCase("nightly")]
|
||||
[TestCase("Nightly")]
|
||||
[TestCase("develop")]
|
||||
[TestCase("master")]
|
||||
public void should_return_no_warning_when_branch_valid(string branch)
|
||||
{
|
||||
GivenValidBranch(branch);
|
||||
|
||||
@@ -71,12 +71,12 @@ namespace NzbDrone.Core.Test.IndexerTests.CardigannTests
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("{{ .Today.Year }}", "2022")]
|
||||
public void should_handle_variables_statements(string template, string expected)
|
||||
[TestCase("{{ .Today.Year }}")]
|
||||
public void should_handle_variables_statements(string template)
|
||||
{
|
||||
var result = Subject.ApplyGoTemplateText(template, _variables);
|
||||
|
||||
result.Should().Be(expected);
|
||||
result.Should().Be(DateTime.Now.Year.ToString());
|
||||
}
|
||||
|
||||
[TestCase("{{if .False }}0{{else}}1{{end}}", "1")]
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 22:20:19"));
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 20:20:19").ToUniversalTime());
|
||||
torrentInfo.Size.Should().Be(8300512414);
|
||||
torrentInfo.InfoHash.Should().Be(null);
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
|
||||
@@ -90,7 +90,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
|
||||
var status = Subject.GetBlockedProviders().FirstOrDefault();
|
||||
status.Should().NotBeNull();
|
||||
status.DisabledTill.Should().HaveValue();
|
||||
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500);
|
||||
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(1), 500);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -133,7 +133,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
|
||||
var status = Subject.GetBlockedProviders().FirstOrDefault();
|
||||
status.Should().NotBeNull();
|
||||
status.DisabledTill.Should().HaveValue();
|
||||
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(15), 500);
|
||||
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -160,7 +160,7 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
|
||||
status.Should().NotBeNull();
|
||||
|
||||
origStatus.EscalationLevel.Should().Be(3);
|
||||
status.DisabledTill.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500);
|
||||
status.DisabledTill.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(1), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Configuration
|
||||
var releaseInfoPath = Path.Combine(bin, "release_info");
|
||||
|
||||
PackageUpdateMechanism = UpdateMechanism.BuiltIn;
|
||||
DefaultBranch = "develop";
|
||||
DefaultBranch = "master";
|
||||
|
||||
if (Path.GetFileName(bin) == "bin" && diskProvider.FileExists(packageInfoPath))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||
{
|
||||
public static class EncodingForBase64
|
||||
{
|
||||
public static string EncodeBase64(this string text)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] textAsBytes = System.Text.Encoding.UTF8.GetBytes(text);
|
||||
return System.Convert.ToBase64String(textAsBytes);
|
||||
}
|
||||
|
||||
public static string DecodeBase64(this string encodedText)
|
||||
{
|
||||
if (encodedText == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] textAsBytes = System.Convert.FromBase64String(encodedText);
|
||||
return System.Text.Encoding.UTF8.GetString(textAsBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||
{
|
||||
public class FreeboxDownloadException : DownloadClientException
|
||||
{
|
||||
public FreeboxDownloadException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||
{
|
||||
public enum FreeboxDownloadPriority
|
||||
{
|
||||
Last = 0,
|
||||
First = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||
{
|
||||
public interface IFreeboxDownloadProxy
|
||||
{
|
||||
void Authenticate(FreeboxDownloadSettings settings);
|
||||
string AddTaskFromUrl(string url, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings);
|
||||
string AddTaskFromFile(string fileName, byte[] fileContent, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings);
|
||||
void DeleteTask(string id, bool deleteData, FreeboxDownloadSettings settings);
|
||||
FreeboxDownloadConfiguration GetDownloadConfiguration(FreeboxDownloadSettings settings);
|
||||
List<FreeboxDownloadTask> GetTasks(FreeboxDownloadSettings settings);
|
||||
}
|
||||
|
||||
public class FreeboxDownloadProxy : IFreeboxDownloadProxy
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
private ICached<string> _authSessionTokenCache;
|
||||
|
||||
public FreeboxDownloadProxy(ICacheManager cacheManager, IHttpClient httpClient, Logger logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_authSessionTokenCache = cacheManager.GetCache<string>(GetType(), "authSessionToken");
|
||||
}
|
||||
|
||||
public void Authenticate(FreeboxDownloadSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/login").Build();
|
||||
|
||||
var response = ProcessRequest<FreeboxLogin>(request, settings);
|
||||
|
||||
if (response.Result.LoggedIn == false)
|
||||
{
|
||||
throw new DownloadClientAuthenticationException("Not logged");
|
||||
}
|
||||
}
|
||||
|
||||
public string AddTaskFromUrl(string url, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/downloads/add").Post();
|
||||
request.Headers.ContentType = "application/x-www-form-urlencoded";
|
||||
|
||||
request.AddFormParameter("download_url", System.Web.HttpUtility.UrlPathEncode(url));
|
||||
|
||||
if (!directory.IsNullOrWhiteSpace())
|
||||
{
|
||||
request.AddFormParameter("download_dir", directory);
|
||||
}
|
||||
|
||||
var response = ProcessRequest<FreeboxDownloadTask>(request.Build(), settings);
|
||||
|
||||
SetTorrentSettings(response.Result.Id, addPaused, addFirst, settings);
|
||||
|
||||
return response.Result.Id;
|
||||
}
|
||||
|
||||
public string AddTaskFromFile(string fileName, byte[] fileContent, string directory, bool addPaused, bool addFirst, FreeboxDownloadSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/downloads/add").Post();
|
||||
|
||||
request.AddFormUpload("download_file", fileName, fileContent, "multipart/form-data");
|
||||
|
||||
if (directory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
request.AddFormParameter("download_dir", directory);
|
||||
}
|
||||
|
||||
var response = ProcessRequest<FreeboxDownloadTask>(request.Build(), settings);
|
||||
|
||||
SetTorrentSettings(response.Result.Id, addPaused, addFirst, settings);
|
||||
|
||||
return response.Result.Id;
|
||||
}
|
||||
|
||||
public void DeleteTask(string id, bool deleteData, FreeboxDownloadSettings settings)
|
||||
{
|
||||
var uri = "/downloads/" + id;
|
||||
|
||||
if (deleteData == true)
|
||||
{
|
||||
uri += "/erase";
|
||||
}
|
||||
|
||||
var request = BuildRequest(settings).Resource(uri).Build();
|
||||
|
||||
request.Method = HttpMethod.Delete;
|
||||
|
||||
ProcessRequest<string>(request, settings);
|
||||
}
|
||||
|
||||
public FreeboxDownloadConfiguration GetDownloadConfiguration(FreeboxDownloadSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/downloads/config/").Build();
|
||||
|
||||
return ProcessRequest<FreeboxDownloadConfiguration>(request, settings).Result;
|
||||
}
|
||||
|
||||
public List<FreeboxDownloadTask> GetTasks(FreeboxDownloadSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/downloads/").Build();
|
||||
|
||||
return ProcessRequest<List<FreeboxDownloadTask>>(request, settings).Result;
|
||||
}
|
||||
|
||||
private static string BuildCachedHeaderKey(FreeboxDownloadSettings settings)
|
||||
{
|
||||
return $"{settings.Host}:{settings.AppId}:{settings.AppToken}";
|
||||
}
|
||||
|
||||
private void SetTorrentSettings(string id, bool addPaused, bool addFirst, FreeboxDownloadSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/downloads/" + id).Build();
|
||||
|
||||
request.Method = HttpMethod.Put;
|
||||
|
||||
var body = new Dictionary<string, object> { };
|
||||
|
||||
if (addPaused)
|
||||
{
|
||||
body.Add("status", FreeboxDownloadTaskStatus.Stopped.ToString().ToLower());
|
||||
}
|
||||
|
||||
if (addFirst)
|
||||
{
|
||||
body.Add("queue_pos", "1");
|
||||
}
|
||||
|
||||
if (body.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
request.SetContent(body.ToJson());
|
||||
|
||||
ProcessRequest<FreeboxDownloadTask>(request, settings);
|
||||
}
|
||||
|
||||
private string GetSessionToken(HttpRequestBuilder requestBuilder, FreeboxDownloadSettings settings, bool force = false)
|
||||
{
|
||||
var sessionToken = _authSessionTokenCache.Find(BuildCachedHeaderKey(settings));
|
||||
|
||||
if (sessionToken == null || force)
|
||||
{
|
||||
_authSessionTokenCache.Remove(BuildCachedHeaderKey(settings));
|
||||
|
||||
_logger.Debug($"Client needs a new Session Token to reach the API with App ID '{settings.AppId}'");
|
||||
|
||||
// Obtaining a Session Token (from official documentation):
|
||||
// To protect the app_token secret, it will never be used directly to authenticate the
|
||||
// application, instead the API will provide a challenge the app will combine to its
|
||||
// app_token to open a session and get a session_token.
|
||||
// The validity of the session_token is limited in time and the app will have to renew
|
||||
// this session_token once in a while.
|
||||
|
||||
// Retrieving the 'challenge' value (it changes frequently and have a limited time validity)
|
||||
// needed to build password
|
||||
var challengeRequest = requestBuilder.Resource("/login").Build();
|
||||
challengeRequest.Method = HttpMethod.Get;
|
||||
|
||||
var challenge = ProcessRequest<FreeboxLogin>(challengeRequest, settings).Result.Challenge;
|
||||
|
||||
// The password is computed using the 'challenge' value and the 'app_token' ('App Token' setting)
|
||||
var enc = System.Text.Encoding.ASCII;
|
||||
var hmac = new HMACSHA1(enc.GetBytes(settings.AppToken));
|
||||
hmac.Initialize();
|
||||
var buffer = enc.GetBytes(challenge);
|
||||
var password = System.BitConverter.ToString(hmac.ComputeHash(buffer)).Replace("-", "").ToLower();
|
||||
|
||||
// Both 'app_id' ('App ID' setting) and computed password are set to get a Session Token
|
||||
var sessionRequest = requestBuilder.Resource("/login/session").Post().Build();
|
||||
var body = new Dictionary<string, object>
|
||||
{
|
||||
{ "app_id", settings.AppId },
|
||||
{ "password", password }
|
||||
};
|
||||
sessionRequest.SetContent(body.ToJson());
|
||||
|
||||
sessionToken = ProcessRequest<FreeboxLogin>(sessionRequest, settings).Result.SessionToken;
|
||||
|
||||
_authSessionTokenCache.Set(BuildCachedHeaderKey(settings), sessionToken);
|
||||
|
||||
_logger.Debug($"New Session Token stored in cache for App ID '{settings.AppId}', ready to reach API");
|
||||
}
|
||||
|
||||
return sessionToken;
|
||||
}
|
||||
|
||||
private HttpRequestBuilder BuildRequest(FreeboxDownloadSettings settings, bool authentication = true)
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.ApiUrl)
|
||||
{
|
||||
LogResponseContent = true
|
||||
};
|
||||
|
||||
requestBuilder.Headers.ContentType = "application/json";
|
||||
|
||||
if (authentication == true)
|
||||
{
|
||||
requestBuilder.SetHeader("X-Fbx-App-Auth", GetSessionToken(requestBuilder, settings));
|
||||
}
|
||||
|
||||
return requestBuilder;
|
||||
}
|
||||
|
||||
private FreeboxResponse<T> ProcessRequest<T>(HttpRequest request, FreeboxDownloadSettings settings)
|
||||
{
|
||||
request.LogResponseContent = true;
|
||||
request.SuppressHttpError = true;
|
||||
|
||||
HttpResponse response;
|
||||
|
||||
try
|
||||
{
|
||||
response = _httpClient.Execute(request);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
throw new DownloadClientUnavailableException($"Unable to reach Freebox API. Verify 'Host', 'Port' or 'Use SSL' settings. (Error: {ex.Message})", ex);
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
throw new DownloadClientUnavailableException("Unable to connect to Freebox API, please check your settings", ex);
|
||||
}
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Forbidden || response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
_authSessionTokenCache.Remove(BuildCachedHeaderKey(settings));
|
||||
|
||||
var responseContent = Json.Deserialize<FreeboxResponse<FreeboxLogin>>(response.Content);
|
||||
|
||||
var msg = $"Authentication to Freebox API failed. Reason: {responseContent.GetErrorDescription()}";
|
||||
_logger.Error(msg);
|
||||
throw new DownloadClientAuthenticationException(msg);
|
||||
}
|
||||
else if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
throw new FreeboxDownloadException("Unable to reach Freebox API. Verify 'API URL' setting for base URL and version.");
|
||||
}
|
||||
else if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
var responseContent = Json.Deserialize<FreeboxResponse<T>>(response.Content);
|
||||
|
||||
if (responseContent.Success)
|
||||
{
|
||||
return responseContent;
|
||||
}
|
||||
else
|
||||
{
|
||||
var msg = $"Freebox API returned error: {responseContent.GetErrorDescription()}";
|
||||
_logger.Error(msg);
|
||||
throw new DownloadClientException(msg);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new DownloadClientException("Unable to connect to Freebox, please check your settings.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||
{
|
||||
public class FreeboxDownloadSettingsValidator : AbstractValidator<FreeboxDownloadSettings>
|
||||
{
|
||||
public FreeboxDownloadSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Host).ValidHost();
|
||||
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
|
||||
RuleFor(c => c.ApiUrl).NotEmpty()
|
||||
.WithMessage("'API URL' must not be empty.");
|
||||
RuleFor(c => c.ApiUrl).ValidUrlBase();
|
||||
RuleFor(c => c.AppId).NotEmpty()
|
||||
.WithMessage("'App ID' must not be empty.");
|
||||
RuleFor(c => c.AppToken).NotEmpty()
|
||||
.WithMessage("'App Token' must not be empty.");
|
||||
RuleFor(c => c.Category).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase)
|
||||
.WithMessage("Allowed characters a-z and -");
|
||||
RuleFor(c => c.DestinationDirectory).IsValidPath()
|
||||
.When(c => c.DestinationDirectory.IsNotNullOrWhiteSpace());
|
||||
RuleFor(c => c.DestinationDirectory).Empty()
|
||||
.When(c => c.Category.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Cannot use 'Category' and 'Destination Directory' at the same time.");
|
||||
RuleFor(c => c.Category).Empty()
|
||||
.When(c => c.DestinationDirectory.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Cannot use 'Category' and 'Destination Directory' at the same time.");
|
||||
}
|
||||
}
|
||||
|
||||
public class FreeboxDownloadSettings : IProviderConfig
|
||||
{
|
||||
private static readonly FreeboxDownloadSettingsValidator Validator = new FreeboxDownloadSettingsValidator();
|
||||
|
||||
public FreeboxDownloadSettings()
|
||||
{
|
||||
Host = "mafreebox.freebox.fr";
|
||||
Port = 443;
|
||||
UseSsl = true;
|
||||
ApiUrl = "/api/v1/";
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox, HelpText = "Hostname or host IP address of the Freebox, defaults to 'mafreebox.freebox.fr' (will only work if on same network)")]
|
||||
public string Host { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox, HelpText = "Port used to access Freebox interface, defaults to '443'")]
|
||||
public int Port { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secured connection when connecting to Freebox API")]
|
||||
public bool UseSsl { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "API URL", Type = FieldType.Textbox, Advanced = true, HelpText = "Define Freebox API base URL with API version, eg http://[host]:[port]/[api_base_url]/[api_version]/, defaults to '/api/v1/'")]
|
||||
public string ApiUrl { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "App ID", Type = FieldType.Textbox, HelpText = "App ID given when creating access to Freebox API (ie 'app_id')")]
|
||||
public string AppId { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "App Token", Type = FieldType.Password, Privacy = PrivacyLevel.Password, HelpText = "App token retrieved when creating access to Freebox API (ie 'app_token')")]
|
||||
public string AppToken { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Destination Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default Freebox download location")]
|
||||
public string DestinationDirectory { get; set; }
|
||||
|
||||
[FieldDefinition(7, Label = "Default Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Prowlarr avoids conflicts with unrelated non-Prowlarr downloads (will create a [category] subdirectory in the output directory)")]
|
||||
public string Category { get; set; }
|
||||
|
||||
[FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(FreeboxDownloadPriority), HelpText = "Priority to use when grabbing")]
|
||||
public int Priority { get; set; }
|
||||
|
||||
[FieldDefinition(10, Label = "Add Paused", Type = FieldType.Checkbox)]
|
||||
public bool AddPaused { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload.Responses
|
||||
{
|
||||
public class FreeboxDownloadConfiguration
|
||||
{
|
||||
[JsonProperty(PropertyName = "download_dir")]
|
||||
public string DownloadDirectory { get; set; }
|
||||
public string DecodedDownloadDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
return DownloadDirectory.DecodeBase64();
|
||||
}
|
||||
set
|
||||
{
|
||||
DownloadDirectory = value.EncodeBase64();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload.Responses
|
||||
{
|
||||
public enum FreeboxDownloadTaskType
|
||||
{
|
||||
Bt,
|
||||
Nzb,
|
||||
Http,
|
||||
Ftp
|
||||
}
|
||||
|
||||
public enum FreeboxDownloadTaskStatus
|
||||
{
|
||||
Unknown,
|
||||
Stopped,
|
||||
Queued,
|
||||
Starting,
|
||||
Downloading,
|
||||
Stopping,
|
||||
Error,
|
||||
Done,
|
||||
Checking,
|
||||
Repairing,
|
||||
Extracting,
|
||||
Seeding,
|
||||
Retry
|
||||
}
|
||||
|
||||
public enum FreeboxDownloadTaskIoPriority
|
||||
{
|
||||
Low,
|
||||
Normal,
|
||||
High
|
||||
}
|
||||
|
||||
public class FreeboxDownloadTask
|
||||
{
|
||||
private static readonly Dictionary<string, string> Descriptions;
|
||||
|
||||
[JsonProperty(PropertyName = "id")]
|
||||
public string Id { get; set; }
|
||||
[JsonProperty(PropertyName = "name")]
|
||||
public string Name { get; set; }
|
||||
[JsonProperty(PropertyName = "download_dir")]
|
||||
public string DownloadDirectory { get; set; }
|
||||
public string DecodedDownloadDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
return DownloadDirectory.DecodeBase64();
|
||||
}
|
||||
set
|
||||
{
|
||||
DownloadDirectory = value.EncodeBase64();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "info_hash")]
|
||||
public string InfoHash { get; set; }
|
||||
[JsonProperty(PropertyName = "queue_pos")]
|
||||
public int QueuePosition { get; set; }
|
||||
[JsonConverter(typeof(UnderscoreStringEnumConverter), FreeboxDownloadTaskStatus.Unknown)]
|
||||
public FreeboxDownloadTaskStatus Status { get; set; }
|
||||
[JsonProperty(PropertyName = "eta")]
|
||||
public long Eta { get; set; }
|
||||
[JsonProperty(PropertyName = "error")]
|
||||
public string Error { get; set; }
|
||||
[JsonProperty(PropertyName = "type")]
|
||||
public string Type { get; set; }
|
||||
[JsonProperty(PropertyName = "io_priority")]
|
||||
public string IoPriority { get; set; }
|
||||
[JsonProperty(PropertyName = "stop_ratio")]
|
||||
public long StopRatio { get; set; }
|
||||
[JsonProperty(PropertyName = "piece_length")]
|
||||
public long PieceLength { get; set; }
|
||||
[JsonProperty(PropertyName = "created_ts")]
|
||||
public long CreatedTimestamp { get; set; }
|
||||
[JsonProperty(PropertyName = "size")]
|
||||
public long Size { get; set; }
|
||||
[JsonProperty(PropertyName = "rx_pct")]
|
||||
public long ReceivedPrct { get; set; }
|
||||
[JsonProperty(PropertyName = "rx_bytes")]
|
||||
public long ReceivedBytes { get; set; }
|
||||
[JsonProperty(PropertyName = "rx_rate")]
|
||||
public long ReceivedRate { get; set; }
|
||||
[JsonProperty(PropertyName = "tx_pct")]
|
||||
public long TransmittedPrct { get; set; }
|
||||
[JsonProperty(PropertyName = "tx_bytes")]
|
||||
public long TransmittedBytes { get; set; }
|
||||
[JsonProperty(PropertyName = "tx_rate")]
|
||||
public long TransmittedRate { get; set; }
|
||||
|
||||
static FreeboxDownloadTask()
|
||||
{
|
||||
Descriptions = new Dictionary<string, string>
|
||||
{
|
||||
{ "internal", "Internal error." },
|
||||
{ "disk_full", "The disk is full." },
|
||||
{ "unknown", "Unknown error." },
|
||||
{ "parse_error", "Parse error." },
|
||||
{ "unknown_host", "Unknown host." },
|
||||
{ "timeout", "Timeout." },
|
||||
{ "bad_authentication", "Invalid credentials." },
|
||||
{ "connection_refused", "Remote host refused connection." },
|
||||
{ "bt_tracker_error", "Unable to announce on tracker." },
|
||||
{ "bt_missing_files", "Missing torrent files." },
|
||||
{ "bt_file_error", "Error accessing torrent files." },
|
||||
{ "missing_ctx_file", "Error accessing task context file." },
|
||||
{ "nzb_no_group", "Cannot find the requested group on server." },
|
||||
{ "nzb_not_found", "Article not fount on the server." },
|
||||
{ "nzb_invalid_crc", "Invalid article CRC." },
|
||||
{ "nzb_invalid_size", "Invalid article size." },
|
||||
{ "nzb_invalid_filename", "Invalid filename." },
|
||||
{ "nzb_open_failed", "Error opening." },
|
||||
{ "nzb_write_failed", "Error writing." },
|
||||
{ "nzb_missing_size", "Missing article size." },
|
||||
{ "nzb_decode_error", "Article decoding error." },
|
||||
{ "nzb_missing_segments", "Missing article segments." },
|
||||
{ "nzb_error", "Other nzb error." },
|
||||
{ "nzb_authentication_required", "Nzb server need authentication." }
|
||||
};
|
||||
}
|
||||
|
||||
public string GetErrorDescription()
|
||||
{
|
||||
if (Descriptions.ContainsKey(Error))
|
||||
{
|
||||
return Descriptions[Error];
|
||||
}
|
||||
|
||||
return $"{Error} - Unknown error";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload.Responses
|
||||
{
|
||||
public class FreeboxLogin
|
||||
{
|
||||
[JsonProperty(PropertyName = "logged_in")]
|
||||
public bool LoggedIn { get; set; }
|
||||
[JsonProperty(PropertyName = "challenge")]
|
||||
public string Challenge { get; set; }
|
||||
[JsonProperty(PropertyName = "password_salt")]
|
||||
public string PasswordSalt { get; set; }
|
||||
[JsonProperty(PropertyName = "password_set")]
|
||||
public bool PasswordSet { get; set; }
|
||||
[JsonProperty(PropertyName = "session_token")]
|
||||
public string SessionToken { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload.Responses
|
||||
{
|
||||
public class FreeboxResponse<T>
|
||||
{
|
||||
private static readonly Dictionary<string, string> Descriptions;
|
||||
|
||||
[JsonProperty(PropertyName = "success")]
|
||||
public bool Success { get; set; }
|
||||
[JsonProperty(PropertyName = "msg")]
|
||||
public string Message { get; set; }
|
||||
[JsonProperty(PropertyName = "error_code")]
|
||||
public string ErrorCode { get; set; }
|
||||
[JsonProperty(PropertyName = "result")]
|
||||
public T Result { get; set; }
|
||||
|
||||
static FreeboxResponse()
|
||||
{
|
||||
Descriptions = new Dictionary<string, string>
|
||||
{
|
||||
// Common errors
|
||||
{ "invalid_request", "Your request is invalid." },
|
||||
{ "invalid_api_version", "Invalid API base url or unknown API version." },
|
||||
{ "internal_error", "Internal error." },
|
||||
|
||||
// Login API errors
|
||||
{ "auth_required", "Invalid session token, or no session token sent." },
|
||||
{ "invalid_token", "The app token you are trying to use is invalid or has been revoked." },
|
||||
{ "pending_token", "The app token you are trying to use has not been validated by user yet." },
|
||||
{ "insufficient_rights", "Your app permissions does not allow accessing this API." },
|
||||
{ "denied_from_external_ip", "You are trying to get an app_token from a remote IP." },
|
||||
{ "ratelimited", "Too many auth error have been made from your IP." },
|
||||
{ "new_apps_denied", "New application token request has been disabled." },
|
||||
{ "apps_denied", "API access from apps has been disabled." },
|
||||
|
||||
// Download API errors
|
||||
{ "task_not_found", "No task was found with the given id." },
|
||||
{ "invalid_operation", "Attempt to perform an invalid operation." },
|
||||
{ "invalid_file", "Error with the download file (invalid format ?)." },
|
||||
{ "invalid_url", "URL is invalid." },
|
||||
{ "not_implemented", "Method not implemented." },
|
||||
{ "out_of_memory", "No more memory available to perform the requested action." },
|
||||
{ "invalid_task_type", "The task type is invalid." },
|
||||
{ "hibernating", "The downloader is hibernating." },
|
||||
{ "need_bt_stopped_done", "This action is only valid for Bittorrent task in stopped or done state." },
|
||||
{ "bt_tracker_not_found", "Attempt to access an invalid tracker object." },
|
||||
{ "too_many_tasks", "Too many tasks." },
|
||||
{ "invalid_address", "Invalid peer address." },
|
||||
{ "port_conflict", "Port conflict when setting config." },
|
||||
{ "invalid_priority", "Invalid priority." },
|
||||
{ "ctx_file_error", "Failed to initialize task context file (need to check disk)." },
|
||||
{ "exists", "Same task already exists." },
|
||||
{ "port_outside_range", "Incoming port is not available for this customer." }
|
||||
};
|
||||
}
|
||||
|
||||
public string GetErrorDescription()
|
||||
{
|
||||
if (Descriptions.ContainsKey(ErrorCode))
|
||||
{
|
||||
return Descriptions[ErrorCode];
|
||||
}
|
||||
|
||||
return $"{ErrorCode} - Unknown error";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Download.Clients.FreeboxDownload.Responses;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
|
||||
{
|
||||
public class TorrentFreeboxDownload : TorrentClientBase<FreeboxDownloadSettings>
|
||||
{
|
||||
private readonly IFreeboxDownloadProxy _proxy;
|
||||
|
||||
public TorrentFreeboxDownload(IFreeboxDownloadProxy proxy,
|
||||
ITorrentFileInfoReader torrentFileInfoReader,
|
||||
IHttpClient httpClient,
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
Logger logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, logger)
|
||||
{
|
||||
_proxy = proxy;
|
||||
}
|
||||
|
||||
public override string Name => "Freebox Download";
|
||||
|
||||
public override bool SupportsCategories => true;
|
||||
|
||||
protected IEnumerable<FreeboxDownloadTask> GetTorrents()
|
||||
{
|
||||
return _proxy.GetTasks(Settings).Where(v => v.Type.ToLower() == FreeboxDownloadTaskType.Bt.ToString().ToLower());
|
||||
}
|
||||
|
||||
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
|
||||
{
|
||||
return _proxy.AddTaskFromUrl(magnetLink,
|
||||
GetDownloadDirectory(release).EncodeBase64(),
|
||||
ToBePaused(),
|
||||
ToBeQueuedFirst(),
|
||||
Settings);
|
||||
}
|
||||
|
||||
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
|
||||
{
|
||||
return _proxy.AddTaskFromFile(filename,
|
||||
fileContent,
|
||||
GetDownloadDirectory(release).EncodeBase64(),
|
||||
ToBePaused(),
|
||||
ToBeQueuedFirst(),
|
||||
Settings);
|
||||
}
|
||||
|
||||
protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink)
|
||||
{
|
||||
return _proxy.AddTaskFromUrl(torrentLink,
|
||||
GetDownloadDirectory(release).EncodeBase64(),
|
||||
ToBePaused(),
|
||||
ToBeQueuedFirst(),
|
||||
Settings);
|
||||
}
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
try
|
||||
{
|
||||
_proxy.Authenticate(Settings);
|
||||
}
|
||||
catch (DownloadClientUnavailableException ex)
|
||||
{
|
||||
failures.Add(new ValidationFailure("Host", ex.Message));
|
||||
failures.Add(new ValidationFailure("Port", ex.Message));
|
||||
}
|
||||
catch (DownloadClientAuthenticationException ex)
|
||||
{
|
||||
failures.Add(new ValidationFailure("AppId", ex.Message));
|
||||
failures.Add(new ValidationFailure("AppToken", ex.Message));
|
||||
}
|
||||
catch (FreeboxDownloadException ex)
|
||||
{
|
||||
failures.Add(new ValidationFailure("ApiUrl", ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ValidateCategories(List<ValidationFailure> failures)
|
||||
{
|
||||
base.ValidateCategories(failures);
|
||||
|
||||
foreach (var label in Categories)
|
||||
{
|
||||
if (!Regex.IsMatch(label.ClientCategory, "^\\.?[-a-z]*$"))
|
||||
{
|
||||
failures.AddIfNotNull(new ValidationFailure(string.Empty, "Mapped Categories allowed characters a-z and -"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetDownloadDirectory(ReleaseInfo release)
|
||||
{
|
||||
if (Settings.DestinationDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return Settings.DestinationDirectory.TrimEnd('/');
|
||||
}
|
||||
|
||||
var destDir = _proxy.GetDownloadConfiguration(Settings).DecodedDownloadDirectory.TrimEnd('/');
|
||||
|
||||
if (Settings.Category.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var category = GetCategoryForRelease(release) ?? Settings.Category;
|
||||
|
||||
destDir = $"{destDir}/{category}";
|
||||
}
|
||||
|
||||
return destDir;
|
||||
}
|
||||
|
||||
private bool ToBeQueuedFirst()
|
||||
{
|
||||
if (Settings.Priority == (int)FreeboxDownloadPriority.First)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool ToBePaused()
|
||||
{
|
||||
return Settings.AddPaused;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
public enum ReleaseBranches
|
||||
{
|
||||
Master,
|
||||
Develop,
|
||||
Nightly
|
||||
}
|
||||
|
||||
@@ -28,7 +28,11 @@ namespace NzbDrone.Core.Http.CloudFlare
|
||||
if (response.StatusCode.Equals(HttpStatusCode.ServiceUnavailable) ||
|
||||
response.StatusCode.Equals(HttpStatusCode.Forbidden))
|
||||
{
|
||||
return true; // Defected CloudFlare and DDoS-GUARD
|
||||
var responseHtml = response.Content;
|
||||
if (responseHtml.Contains("<title>Just a moment...") || responseHtml.Contains("<title>DDOS-GUARD"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// detect Custom CloudFlare for EbookParadijs, Film-Paleis, MuziekFabriek and Puur-Hollands
|
||||
|
||||
@@ -93,12 +93,13 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
r.Languages == null ? null : from c in r.Languages select GetNabElement("language", c, protocol),
|
||||
r.Subs == null ? null : from c in r.Subs select GetNabElement("subs", c, protocol),
|
||||
r.Genres == null ? null : GetNabElement("genre", string.Join(", ", r.Genres), protocol),
|
||||
GetNabElement("rageid", r.TvRageId, protocol),
|
||||
GetNabElement("tvdbid", r.TvdbId, protocol),
|
||||
GetNabElement("imdb", r.ImdbId.ToString("D7"), protocol),
|
||||
GetNabElement("tmdbid", r.TmdbId, protocol),
|
||||
GetNabElement("traktid", r.TraktId, protocol),
|
||||
GetNabElement("doubanid", r.DoubanId, protocol),
|
||||
r.TvRageId == 0 ? null : GetNabElement("rageid", r.TvRageId, protocol),
|
||||
r.TvdbId == 0 ? null : GetNabElement("tvdbid", r.TvdbId, protocol),
|
||||
r.ImdbId == 0 ? null : GetNabElement("imdb", r.ImdbId.ToString("D7"), protocol),
|
||||
r.TmdbId == 0 ? null : GetNabElement("tmdbid", r.TmdbId, protocol),
|
||||
r.TraktId == 0 ? null : GetNabElement("traktid", r.TraktId, protocol),
|
||||
r.TvMazeId == 0 ? null : GetNabElement("tvmazeid", r.TvMazeId, protocol),
|
||||
r.DoubanId == 0 ? null : GetNabElement("doubanid", r.DoubanId, protocol),
|
||||
GetNabElement("seeders", t.Seeders, protocol),
|
||||
GetNabElement("files", r.Files, protocol),
|
||||
GetNabElement("grabs", r.Grabs, protocol),
|
||||
|
||||
@@ -98,6 +98,12 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
var jsonResponse = new HttpResponse<AvistazErrorResponse>(ex.Response);
|
||||
return new ValidationFailure(string.Empty, jsonResponse.Resource?.Message ?? "Unauthorized request to indexer");
|
||||
}
|
||||
else if (ex.Response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
_logger.Warn(ex, "Too Many Requests");
|
||||
|
||||
return new ValidationFailure(string.Empty, "Too Many Requests");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn(ex, "Unable to connect to indexer");
|
||||
|
||||
@@ -24,24 +24,26 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
{
|
||||
var torrentInfos = new List<TorrentInfo>();
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.NotFound)
|
||||
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
|
||||
{
|
||||
return torrentInfos.ToArray();
|
||||
}
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
throw new RequestLimitReachedException(indexerResponse, "API Request Limit Reached");
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
|
||||
}
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
|
||||
}
|
||||
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
// No results found
|
||||
return torrentInfos.ToArray();
|
||||
}
|
||||
|
||||
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}");
|
||||
HttpResponse<AvistazErrorResponse> jsonErrorResponse = new HttpResponse<AvistazErrorResponse>(indexerResponse.HttpResponse);
|
||||
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new IndexerAuthException(string.Empty, jsonErrorResponse.Resource?.Message ?? "Unauthorized request to indexer");
|
||||
}
|
||||
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
|
||||
}
|
||||
|
||||
var jsonResponse = new HttpResponse<AvistazResponse>(indexerResponse.HttpResponse);
|
||||
|
||||
@@ -73,7 +73,8 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
{
|
||||
var searchUrl = SearchUrl + "?" + searchParameters.GetQueryString();
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Json);
|
||||
// TODO: Change to HttpAccept.Json after they fix the issue with missing headers
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
|
||||
request.HttpRequest.Headers.Add("Authorization", $"Bearer {Settings.Token}");
|
||||
|
||||
yield return request;
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "PID", HelpText = "PID from My Account or My Profile page")]
|
||||
[FieldDefinition(4, Label = "PID", HelpText = "PID from My Account or My Profile page", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
public string Pid { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Search freeleech only")]
|
||||
|
||||
@@ -223,7 +223,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
InfoUrl = details,
|
||||
Guid = details,
|
||||
Categories = _categories.MapTrackerCatDescToNewznab(row.Category),
|
||||
PublishDate = DateTime.Parse(row.CreatedAt, CultureInfo.InvariantCulture),
|
||||
PublishDate = DateTime.Parse(row.CreatedAt, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
|
||||
Size = row.Size,
|
||||
Grabs = row.Grabs,
|
||||
Seeders = row.Seeders,
|
||||
|
||||
@@ -132,20 +132,22 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
if (selector.Selector != null)
|
||||
{
|
||||
if (dom.Matches(selector.Selector))
|
||||
var selectorSelector = ApplyGoTemplateText(selector.Selector, variables);
|
||||
|
||||
if (dom.Matches(selectorSelector))
|
||||
{
|
||||
selection = dom;
|
||||
}
|
||||
else
|
||||
{
|
||||
selection = QuerySelector(dom, selector.Selector);
|
||||
selection = QuerySelector(dom, selectorSelector);
|
||||
}
|
||||
|
||||
if (selection == null)
|
||||
{
|
||||
if (required)
|
||||
{
|
||||
throw new Exception(string.Format("Selector \"{0}\" didn't match {1}", selector.Selector, dom.ToHtmlPretty()));
|
||||
throw new Exception(string.Format("Selector \"{0}\" didn't match {1}", selectorSelector, dom.ToHtmlPretty()));
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -716,15 +716,12 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
if (queryCollection.Count > 0)
|
||||
{
|
||||
if (!requestLinkStr.Contains("?"))
|
||||
if (!requestLinkStr.Contains('?'))
|
||||
{
|
||||
// TODO Need Encoding here if we add it back
|
||||
requestLinkStr += "?" + queryCollection.GetQueryString(separator: request.Queryseparator).Substring(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
requestLinkStr += queryCollection.GetQueryString(separator: request.Queryseparator);
|
||||
requestLinkStr += "?";
|
||||
}
|
||||
|
||||
requestLinkStr += queryCollection.GetQueryString(_encoding, separator: request.Queryseparator);
|
||||
}
|
||||
|
||||
var httpRequest = new HttpRequestBuilder(requestLinkStr)
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace NzbDrone.Core.Indexers.FileList
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
TvSearchParam.Q, TvSearchParam.ImdbId, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
@@ -55,6 +55,7 @@ namespace NzbDrone.Core.Indexers.FileList
|
||||
},
|
||||
Flags = new List<IndexerFlag>
|
||||
{
|
||||
IndexerFlag.Internal,
|
||||
IndexerFlag.FreeLeech
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,12 +16,13 @@ namespace NzbDrone.Core.Indexers.FileList
|
||||
public uint Files { get; set; }
|
||||
[JsonProperty(PropertyName = "imdb")]
|
||||
public string ImdbId { get; set; }
|
||||
public bool Internal { get; set; }
|
||||
[JsonProperty(PropertyName = "freeleech")]
|
||||
public bool FreeLeech { get; set; }
|
||||
[JsonProperty(PropertyName = "doubleup")]
|
||||
public bool DoubleUp { get; set; }
|
||||
[JsonProperty(PropertyName = "upload_date")]
|
||||
public DateTime UploadDate { get; set; }
|
||||
public string UploadDate { get; set; }
|
||||
public string Category { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,11 @@ namespace NzbDrone.Core.Indexers.FileList
|
||||
|
||||
var flags = new List<IndexerFlag>();
|
||||
|
||||
if (result.Internal)
|
||||
{
|
||||
flags.Add(IndexerFlag.Internal);
|
||||
}
|
||||
|
||||
var imdbId = 0;
|
||||
if (result.ImdbId != null && result.ImdbId.Length > 2)
|
||||
{
|
||||
@@ -57,7 +62,7 @@ namespace NzbDrone.Core.Indexers.FileList
|
||||
InfoUrl = GetInfoUrl(id),
|
||||
Seeders = result.Seeders,
|
||||
Peers = result.Leechers + result.Seeders,
|
||||
PublishDate = result.UploadDate,
|
||||
PublishDate = DateTime.Parse(result.UploadDate + " +0200"),
|
||||
ImdbId = imdbId,
|
||||
IndexerFlags = flags,
|
||||
Files = (int)result.Files,
|
||||
|
||||
372
src/NzbDrone.Core/Indexers/Definitions/Libble.cs
Normal file
372
src/NzbDrone.Core/Indexers/Definitions/Libble.cs
Normal file
@@ -0,0 +1,372 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Html.Parser;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
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;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
internal class Libble : TorrentIndexerBase<LibbleSettings>
|
||||
{
|
||||
public override string Name => "Libble";
|
||||
public override string[] IndexerUrls => new string[] { "https://libble.me/" };
|
||||
public override string Description => "Libble is a Private Torrent Tracker for MUSIC";
|
||||
private string LoginUrl => Settings.BaseUrl + "login.php";
|
||||
public override string Language => "en-US";
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override int PageSize => 50;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public Libble(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new LibbleRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new LibbleParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override async Task DoLogin()
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
||||
{
|
||||
Method = HttpMethod.Post,
|
||||
AllowAutoRedirect = true
|
||||
};
|
||||
|
||||
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
|
||||
|
||||
var cookies = Cookies;
|
||||
|
||||
Cookies = null;
|
||||
var authLoginRequest = requestBuilder
|
||||
.AddFormParameter("username", Settings.Username)
|
||||
.AddFormParameter("password", Settings.Password)
|
||||
.AddFormParameter("code", Settings.TwoFactorAuthCode)
|
||||
.AddFormParameter("keeplogged", "1")
|
||||
.AddFormParameter("login", "Login")
|
||||
.SetHeader("Content-Type", "multipart/form-data")
|
||||
.Build();
|
||||
|
||||
var headers = new NameValueCollection
|
||||
{
|
||||
{ "Referer", LoginUrl }
|
||||
};
|
||||
|
||||
authLoginRequest.Headers.Add(headers);
|
||||
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
var errorMessage = dom.QuerySelector("#loginform > .warning")?.TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException($"Libble authentication failed. Error: \"{errorMessage}\"");
|
||||
}
|
||||
|
||||
cookies = response.GetCookies();
|
||||
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
|
||||
|
||||
_logger.Debug("Libble authentication succeeded.");
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
return !httpResponse.Content.Contains("logout.php");
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
MusicSearchParam.Q, MusicSearchParam.Artist, MusicSearchParam.Album, MusicSearchParam.Label, MusicSearchParam.Year, MusicSearchParam.Genre
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.Audio);
|
||||
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.Audio);
|
||||
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.AudioVideo);
|
||||
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class LibbleRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public LibbleSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
|
||||
public LibbleRequestGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria, NameValueCollection parameters)
|
||||
{
|
||||
var term = searchCriteria.SanitizedSearchTerm.Trim();
|
||||
|
||||
parameters.Add("order_by", "time");
|
||||
parameters.Add("order_way", "desc");
|
||||
parameters.Add("searchstr", term);
|
||||
|
||||
var queryCats = Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
|
||||
|
||||
if (queryCats.Count > 0)
|
||||
{
|
||||
foreach (var cat in queryCats)
|
||||
{
|
||||
parameters.Add($"filter_cat[{cat}]", "1");
|
||||
}
|
||||
}
|
||||
|
||||
if (searchCriteria.Offset.HasValue && searchCriteria.Limit.HasValue && searchCriteria.Offset > 0 && searchCriteria.Limit > 0)
|
||||
{
|
||||
var page = (int)(searchCriteria.Offset / searchCriteria.Limit) + 1;
|
||||
parameters.Add("page", page.ToString());
|
||||
}
|
||||
|
||||
var searchUrl = string.Format("{0}/torrents.php?{1}", Settings.BaseUrl.TrimEnd('/'), parameters.GetQueryString());
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
|
||||
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
if (searchCriteria.Artist.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("artistname", searchCriteria.Artist);
|
||||
}
|
||||
|
||||
if (searchCriteria.Album.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("groupname", searchCriteria.Album);
|
||||
}
|
||||
|
||||
if (searchCriteria.Label.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("recordlabel", searchCriteria.Label);
|
||||
}
|
||||
|
||||
if (searchCriteria.Year.HasValue)
|
||||
{
|
||||
parameters.Add("year", searchCriteria.Year.ToString());
|
||||
}
|
||||
|
||||
if (searchCriteria.Genre.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("taglist", searchCriteria.Genre);
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class LibbleParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly LibbleSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
public LibbleParser(LibbleSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
var rows = doc.QuerySelectorAll("table#torrent_table > tbody > tr.group:has(strong > a[href*=\"torrents.php?id=\"])");
|
||||
|
||||
var releaseYearRegex = new Regex(@"\[(\d{4})\]$");
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var albumLinkNode = row.QuerySelector("strong > a[href*=\"torrents.php?id=\"]");
|
||||
var groupId = ParseUtil.GetArgumentFromQueryString(albumLinkNode.GetAttribute("href"), "id");
|
||||
|
||||
var artistsNodes = row.QuerySelectorAll("strong > a[href*=\"artist.php?id=\"]");
|
||||
|
||||
var releaseArtist = "Various Artists";
|
||||
if (artistsNodes.Count() > 0)
|
||||
{
|
||||
releaseArtist = artistsNodes.Select(artist => artist.TextContent.Trim()).ToList().Join(", ");
|
||||
}
|
||||
|
||||
var releaseAlbumName = row.QuerySelector("strong > a[href*=\"torrents.php?id=\"]")?.TextContent.Trim();
|
||||
|
||||
var title = row.QuerySelector("td:nth-child(4) > strong")?.TextContent.Trim();
|
||||
var releaseAlbumYear = releaseYearRegex.Match(title);
|
||||
|
||||
var releaseDescription = row.QuerySelector("div.tags")?.TextContent.Trim();
|
||||
var releaseThumbnailUrl = row.QuerySelector(".thumbnail")?.GetAttribute("title").Trim();
|
||||
|
||||
var releaseGenres = new List<string>();
|
||||
if (!string.IsNullOrEmpty(releaseDescription))
|
||||
{
|
||||
releaseGenres = releaseGenres.Union(releaseDescription.Split(',').Select(tag => tag.Trim()).ToList()).ToList();
|
||||
}
|
||||
|
||||
var cat = row.QuerySelector("td.cats_col div.cat_icon")?.GetAttribute("class").Trim();
|
||||
|
||||
var matchCategory = Regex.Match(cat, @"\bcats_(.*?)\b");
|
||||
if (matchCategory.Success)
|
||||
{
|
||||
cat = matchCategory.Groups[1].Value.Trim();
|
||||
}
|
||||
|
||||
var category = new List<IndexerCategory>
|
||||
{
|
||||
cat switch
|
||||
{
|
||||
"music" => NewznabStandardCategory.Audio,
|
||||
"libblemixtapes" => NewznabStandardCategory.Audio,
|
||||
"musicvideos" => NewznabStandardCategory.AudioVideo,
|
||||
_ => NewznabStandardCategory.Other,
|
||||
}
|
||||
};
|
||||
|
||||
var releaseRows = doc.QuerySelectorAll(string.Format("table#torrent_table > tbody > tr.group_torrent.groupid_{0}:has(a[href*=\"torrents.php?id=\"])", groupId));
|
||||
|
||||
foreach (var releaseRow in releaseRows)
|
||||
{
|
||||
var release = new TorrentInfo();
|
||||
|
||||
var detailsNode = releaseRow.QuerySelector("a[href^=\"torrents.php?id=\"]");
|
||||
var downloadLink = _settings.BaseUrl + releaseRow.QuerySelector("a[href^=\"torrents.php?action=download&id=\"]").GetAttribute("href").Trim();
|
||||
|
||||
var releaseTags = detailsNode.FirstChild.TextContent.Trim(' ', '/');
|
||||
|
||||
release.Title = string.Format("{0} - {1} {2} {3}", releaseArtist, releaseAlbumName, releaseAlbumYear, releaseTags).Trim();
|
||||
release.Categories = category;
|
||||
release.Description = releaseDescription;
|
||||
release.Genres = releaseGenres;
|
||||
release.PosterUrl = releaseThumbnailUrl;
|
||||
|
||||
release.InfoUrl = _settings.BaseUrl + detailsNode.GetAttribute("href").Trim();
|
||||
release.DownloadUrl = downloadLink;
|
||||
release.Guid = release.InfoUrl;
|
||||
|
||||
release.Size = ParseUtil.GetBytes(releaseRow.QuerySelector("td:nth-child(4)").TextContent.Trim());
|
||||
release.Files = ParseUtil.CoerceInt(releaseRow.QuerySelector("td:nth-child(2)").TextContent);
|
||||
release.Grabs = ParseUtil.CoerceInt(releaseRow.QuerySelector("td:nth-child(5)").TextContent);
|
||||
release.Seeders = ParseUtil.CoerceInt(releaseRow.QuerySelector("td:nth-child(6)").TextContent);
|
||||
release.Peers = release.Seeders + ParseUtil.CoerceInt(releaseRow.QuerySelector("td:nth-child(7)").TextContent);
|
||||
|
||||
release.MinimumRatio = 1;
|
||||
release.MinimumSeedTime = 259200; // 72 hours
|
||||
|
||||
try
|
||||
{
|
||||
release.PublishDate = DateTime.ParseExact(
|
||||
releaseRow.QuerySelector("td:nth-child(3) > span[title]").GetAttribute("title").Trim(),
|
||||
"MMM dd yyyy, HH:mm",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
|
||||
switch (releaseRow.QuerySelector("a[href^=\"torrents.php?id=\"] strong")?.TextContent.Trim())
|
||||
{
|
||||
case "Neutral!":
|
||||
release.DownloadVolumeFactor = 0;
|
||||
release.UploadVolumeFactor = 0;
|
||||
break;
|
||||
case "Freeleech!":
|
||||
release.DownloadVolumeFactor = 0;
|
||||
release.UploadVolumeFactor = 1;
|
||||
break;
|
||||
default:
|
||||
release.DownloadVolumeFactor = 1;
|
||||
release.UploadVolumeFactor = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
torrentInfos.Add(release);
|
||||
}
|
||||
}
|
||||
|
||||
return torrentInfos.ToArray();
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class LibbleSettings : UserPassTorrentBaseSettings
|
||||
{
|
||||
public LibbleSettings()
|
||||
{
|
||||
TwoFactorAuthCode = "";
|
||||
}
|
||||
|
||||
[FieldDefinition(4, Label = "2FA code", Type = FieldType.Textbox, HelpText = "Only fill in the <b>2FA code</b> box if you have enabled <b>2FA</b> on the Libble Web Site. Otherwise just leave it empty.")]
|
||||
public string TwoFactorAuthCode { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -49,9 +49,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId, TvSearchParam.TvMazeId
|
||||
}
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId, TvSearchParam.TvMazeId
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TV);
|
||||
@@ -208,12 +208,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Categories = new List<IndexerCategory> { TvCategoryFromQualityParser.ParseTvShowQuality(row.ReleaseTitle) },
|
||||
Size = ParseUtil.CoerceLong(row.Size),
|
||||
Files = row.FileList.Length,
|
||||
PublishDate = DateTime.Parse(row.PublishDateUtc, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal),
|
||||
PublishDate = DateTime.Parse(row.PublishDateUtc, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
|
||||
Grabs = ParseUtil.CoerceInt(row.Snatch),
|
||||
Seeders = ParseUtil.CoerceInt(row.Seed),
|
||||
Peers = ParseUtil.CoerceInt(row.Seed) + ParseUtil.CoerceInt(row.Leech),
|
||||
MinimumRatio = 0, // ratioless
|
||||
MinimumSeedTime = 86400, // 24 hours
|
||||
MinimumSeedTime = row.Category.ToLower() == "season" ? 432000 : 86400, // 120 hours for seasons and 24 hours for episodes
|
||||
DownloadVolumeFactor = 0, // ratioless tracker
|
||||
UploadVolumeFactor = 1
|
||||
};
|
||||
@@ -272,6 +272,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
[JsonProperty(PropertyName = "rls_name")]
|
||||
public string ReleaseTitle { get; set; }
|
||||
public string Title { get; set; }
|
||||
[JsonProperty(PropertyName = "cat")]
|
||||
public string Category { get; set; }
|
||||
public string Size { get; set; }
|
||||
public string Seed { get; set; }
|
||||
public string Leech { get; set; }
|
||||
|
||||
@@ -100,12 +100,14 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
}
|
||||
|
||||
releaseInfo = base.ProcessItem(item, releaseInfo);
|
||||
releaseInfo.ImdbId = GetIntAttribute(item, "imdb");
|
||||
releaseInfo.TmdbId = GetIntAttribute(item, "tmdbid");
|
||||
releaseInfo.TvdbId = GetIntAttribute(item, "tvdbid");
|
||||
releaseInfo.TvRageId = GetIntAttribute(item, "rageid");
|
||||
releaseInfo.Grabs = GetIntAttribute(item, "grabs");
|
||||
releaseInfo.Files = GetIntAttribute(item, "files");
|
||||
releaseInfo.ImdbId = GetIntAttribute(item, new[] { "imdb", "imdbid" });
|
||||
releaseInfo.TmdbId = GetIntAttribute(item, new[] { "tmdbid", "tmdb" });
|
||||
releaseInfo.TvdbId = GetIntAttribute(item, new[] { "tvdbid", "tvdb" });
|
||||
releaseInfo.TvMazeId = GetIntAttribute(item, new[] { "tvmazeid", "tvmaze" });
|
||||
releaseInfo.TraktId = GetIntAttribute(item, new[] { "traktid", "trakt" });
|
||||
releaseInfo.TvRageId = GetIntAttribute(item, new[] { "rageid" });
|
||||
releaseInfo.Grabs = GetIntAttribute(item, new[] { "grabs" });
|
||||
releaseInfo.Files = GetIntAttribute(item, new[] { "files" });
|
||||
releaseInfo.PosterUrl = GetPosterUrl(item);
|
||||
|
||||
return releaseInfo;
|
||||
@@ -206,14 +208,17 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
return url;
|
||||
}
|
||||
|
||||
protected virtual int GetIntAttribute(XElement item, string attribute)
|
||||
protected virtual int GetIntAttribute(XElement item, string[] attributes)
|
||||
{
|
||||
var idString = TryGetNewznabAttribute(item, attribute);
|
||||
int idInt;
|
||||
|
||||
if (!idString.IsNullOrWhiteSpace() && int.TryParse(idString, out idInt))
|
||||
foreach (var attr in attributes)
|
||||
{
|
||||
return idInt;
|
||||
var idString = TryGetNewznabAttribute(item, attr);
|
||||
int idInt;
|
||||
|
||||
if (!idString.IsNullOrWhiteSpace() && int.TryParse(idString, out idInt))
|
||||
{
|
||||
return idInt;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FluentValidation;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
@@ -126,8 +128,29 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
pageableRequests.Add(GetRequest(string.Format("&artistname={0}&groupname={1}", searchCriteria.Artist, searchCriteria.Album)));
|
||||
if (searchCriteria.Artist.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("artistname", searchCriteria.Artist);
|
||||
}
|
||||
|
||||
if (searchCriteria.Album.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("groupname", searchCriteria.Album);
|
||||
}
|
||||
|
||||
if (searchCriteria.Label.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parameters.Add("recordlabel", searchCriteria.Label);
|
||||
}
|
||||
|
||||
if (searchCriteria.Year.HasValue)
|
||||
{
|
||||
parameters.Add("year", searchCriteria.Year.ToString());
|
||||
}
|
||||
|
||||
pageableRequests.Add(GetRequest(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -135,8 +158,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm));
|
||||
pageableRequests.Add(GetRequest(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -154,16 +178,34 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
var parameters = new NameValueCollection();
|
||||
|
||||
pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm));
|
||||
pageableRequests.Add(GetRequest(searchCriteria, parameters));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetRequest(string searchParameters)
|
||||
private IEnumerable<IndexerRequest> GetRequest(SearchCriteriaBase searchCriteria, NameValueCollection parameters)
|
||||
{
|
||||
var term = searchCriteria.SanitizedSearchTerm.Trim();
|
||||
|
||||
parameters.Add("action", "browse");
|
||||
parameters.Add("order_by", "time");
|
||||
parameters.Add("order_way", "desc");
|
||||
parameters.Add("searchstr", term);
|
||||
|
||||
var queryCats = Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories);
|
||||
|
||||
if (queryCats.Count > 0)
|
||||
{
|
||||
foreach (var cat in queryCats)
|
||||
{
|
||||
parameters.Add($"filter_cat[{cat}]", "1");
|
||||
}
|
||||
}
|
||||
|
||||
var req = RequestBuilder()
|
||||
.Resource($"ajax.php?action=browse&searchstr={searchParameters}")
|
||||
.Resource($"ajax.php?{parameters.GetQueryString()}")
|
||||
.Build();
|
||||
|
||||
yield return new IndexerRequest(req);
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
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;
|
||||
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;
|
||||
@@ -34,7 +29,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
||||
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(2);
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(4);
|
||||
|
||||
public Rarbg(IRarbgTokenProvider tokenProvider, IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
@@ -106,7 +101,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
||||
{
|
||||
var response = await FetchIndexerResponse(request);
|
||||
|
||||
// try and recover from token or rate limit errors
|
||||
// try and recover from token errors
|
||||
var jsonResponse = new HttpResponse<RarbgResponse>(response.HttpResponse);
|
||||
|
||||
if (jsonResponse.Resource.error_code.HasValue)
|
||||
@@ -123,9 +118,9 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
||||
request.HttpRequest.Url = request.Url.SetQuery(qs.GetQueryString());
|
||||
response = await FetchIndexerResponse(request);
|
||||
}
|
||||
else if (jsonResponse.Resource.error_code == 5 || jsonResponse.Resource.rate_limit.HasValue)
|
||||
else if (jsonResponse.Resource.error_code == 5)
|
||||
{
|
||||
_logger.Debug("Rarbg rate limit hit, retying request");
|
||||
_logger.Debug("Rarbg temp rate limit hit, retrying request");
|
||||
response = await FetchIndexerResponse(request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,16 +23,18 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var results = new List<ReleaseInfo>();
|
||||
var responseCode = (int)indexerResponse.HttpResponse.StatusCode;
|
||||
|
||||
switch (indexerResponse.HttpResponse.StatusCode)
|
||||
switch (responseCode)
|
||||
{
|
||||
default:
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Indexer API call returned an unexpected StatusCode [{0}]", indexerResponse.HttpResponse.StatusCode);
|
||||
}
|
||||
|
||||
case (int)HttpStatusCode.TooManyRequests:
|
||||
throw new TooManyRequestsException(indexerResponse.HttpRequest, indexerResponse.HttpResponse, TimeSpan.FromMinutes(2));
|
||||
case 520:
|
||||
throw new TooManyRequestsException(indexerResponse.HttpRequest, indexerResponse.HttpResponse, TimeSpan.FromMinutes(3));
|
||||
case (int)HttpStatusCode.OK:
|
||||
break;
|
||||
default:
|
||||
throw new IndexerException(indexerResponse, "Indexer API call returned an unexpected StatusCode [{0}]", responseCode);
|
||||
}
|
||||
|
||||
var jsonResponse = new HttpResponse<RarbgResponse>(indexerResponse.HttpResponse);
|
||||
|
||||
@@ -9,14 +9,12 @@ 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);
|
||||
protected override int MinimumSeedTime => 432000; // 120 hours
|
||||
|
||||
public RetroFlix(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger, IIndexerRepository indexerRepository)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger, indexerRepository)
|
||||
@@ -29,15 +27,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q,
|
||||
TvSearchParam.Season,
|
||||
TvSearchParam.Ep,
|
||||
TvSearchParam.ImdbId
|
||||
TvSearchParam.Q, TvSearchParam.ImdbId, TvSearchParam.Season, TvSearchParam.Ep,
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q,
|
||||
MovieSearchParam.ImdbId
|
||||
MovieSearchParam.Q, MovieSearchParam.ImdbId,
|
||||
},
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
|
||||
@@ -8,14 +8,10 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public class SpeedApp : SpeedAppBase
|
||||
{
|
||||
public override string Name => "SpeedApp.io";
|
||||
|
||||
public override string[] IndexerUrls => new string[] { "https://speedapp.io/" };
|
||||
public override string[] LegacyUrls => new string[] { "https://speedapp.io" };
|
||||
|
||||
public override string Description => "SpeedApp is a ROMANIAN Private Torrent Tracker for MOVIES / TV / GENERAL";
|
||||
|
||||
public override string Language => "ro-RO";
|
||||
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
|
||||
public SpeedApp(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger, IIndexerRepository indexerRepository)
|
||||
@@ -29,14 +25,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q,
|
||||
TvSearchParam.Season,
|
||||
TvSearchParam.Ep,
|
||||
TvSearchParam.Q, TvSearchParam.ImdbId, TvSearchParam.Season, TvSearchParam.Ep,
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q,
|
||||
MovieSearchParam.ImdbId,
|
||||
MovieSearchParam.Q, MovieSearchParam.ImdbId,
|
||||
},
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using FluentValidation;
|
||||
using Newtonsoft.Json;
|
||||
@@ -21,7 +22,6 @@ 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
|
||||
@@ -29,13 +29,10 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public abstract class SpeedAppBase : TorrentIndexerBase<SpeedAppSettings>
|
||||
{
|
||||
private string LoginUrl => Settings.BaseUrl + "api/login";
|
||||
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
protected virtual int MinimumSeedTime => 172800; // 48 hours
|
||||
private IIndexerRepository _indexerRepository;
|
||||
|
||||
public SpeedAppBase(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger, IIndexerRepository indexerRepository)
|
||||
@@ -51,7 +48,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new SpeedAppParser(Settings, Capabilities.Categories);
|
||||
return new SpeedAppParser(Settings, Capabilities.Categories, MinimumSeedTime);
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
@@ -229,7 +226,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null, int? season = null, string episode = null)
|
||||
{
|
||||
var qc = new NameValueCollection();
|
||||
var qc = new NameValueCollection()
|
||||
{
|
||||
{ "itemsPerPage", "100" },
|
||||
{ "sort", "torrent.createdAt" },
|
||||
{ "direction", "desc" }
|
||||
};
|
||||
|
||||
if (imdbId.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
@@ -274,13 +276,15 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
private readonly SpeedAppSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
private readonly int _minimumSeedTime;
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
public SpeedAppParser(SpeedAppSettings settings, IndexerCapabilitiesCategories categories)
|
||||
public SpeedAppParser(SpeedAppSettings settings, IndexerCapabilitiesCategories categories, int minimumSeedTime)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
_minimumSeedTime = minimumSeedTime;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
@@ -300,11 +304,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return jsonResponse.Resource.Select(torrent => new TorrentInfo
|
||||
{
|
||||
Guid = torrent.Id.ToString(),
|
||||
Title = torrent.Name,
|
||||
Title = Regex.Replace(torrent.Name, @"(?i:\[REQUESTED\])", "").Trim(' ', '.'),
|
||||
Description = torrent.ShortDescription,
|
||||
Size = torrent.Size,
|
||||
ImdbId = ParseUtil.GetImdbID(torrent.ImdbId).GetValueOrDefault(),
|
||||
DownloadUrl = $"{_settings.BaseUrl}/api/torrent/{torrent.Id}/download",
|
||||
DownloadUrl = $"{_settings.BaseUrl}api/torrent/{torrent.Id}/download",
|
||||
PosterUrl = torrent.Poster,
|
||||
InfoUrl = torrent.Url,
|
||||
Grabs = torrent.TimesCompleted,
|
||||
@@ -314,7 +318,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Seeders = torrent.Seeders,
|
||||
Peers = torrent.Leechers + torrent.Seeders,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 172800,
|
||||
MinimumSeedTime = _minimumSeedTime,
|
||||
DownloadVolumeFactor = torrent.DownloadVolumeFactor,
|
||||
UploadVolumeFactor = torrent.UploadVolumeFactor,
|
||||
}).ToArray();
|
||||
|
||||
@@ -74,8 +74,18 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||
torrentInfo.DownloadVolumeFactor = downloadFactor;
|
||||
torrentInfo.UploadVolumeFactor = uploadFactor;
|
||||
|
||||
torrentInfo.ImdbId = GetIntAttribute(item, new[] { "imdb", "imdbid" });
|
||||
torrentInfo.TmdbId = GetIntAttribute(item, new[] { "tmdbid", "tmdb" });
|
||||
torrentInfo.TvdbId = GetIntAttribute(item, new[] { "tvdbid", "tvdb" });
|
||||
torrentInfo.TvMazeId = GetIntAttribute(item, new[] { "tvmazeid", "tvmaze" });
|
||||
torrentInfo.TraktId = GetIntAttribute(item, new[] { "traktid", "trakt" });
|
||||
torrentInfo.TvRageId = GetIntAttribute(item, new[] { "rageid" });
|
||||
torrentInfo.Grabs = GetIntAttribute(item, new[] { "grabs" });
|
||||
torrentInfo.Files = GetIntAttribute(item, new[] { "files" });
|
||||
|
||||
torrentInfo.IndexerFlags = GetFlags(item);
|
||||
torrentInfo.PosterUrl = GetPosterUrl(item);
|
||||
torrentInfo.InfoHash = GetInfoHash(item);
|
||||
}
|
||||
|
||||
return torrentInfo;
|
||||
@@ -287,5 +297,21 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
protected virtual int GetIntAttribute(XElement item, string[] attributes)
|
||||
{
|
||||
foreach (var attr in attributes)
|
||||
{
|
||||
var idString = TryGetTorznabAttribute(item, attr);
|
||||
int idInt;
|
||||
|
||||
if (!idString.IsNullOrWhiteSpace() && int.TryParse(idString, out idInt))
|
||||
{
|
||||
return idInt;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"AuthenticationMethodHelpText": "Require Username and Password to access Prowlarr",
|
||||
"AuthenticationRequired": "Authentication Required",
|
||||
"AuthenticationRequiredHelpText": "Change which requests authentication is required for. Do not change unless you understand the risks.",
|
||||
"AuthenticationRequiredWarning": "To prevent remote access without authentication, Prowlarr now requires authentication to be enabled. You can optionally disable authentication from local addresses.",
|
||||
"AuthenticationRequiredWarning": "To prevent remote access without authentication, Prowlarr now requires authentication to be enabled. Configure your authentication method and credentials. You can optionally disable authentication from local addresses. Refer to the FAQ for additional information.",
|
||||
"Automatic": "Automatic",
|
||||
"AutomaticSearch": "Automatic Search",
|
||||
"Backup": "Backup",
|
||||
|
||||
@@ -466,5 +466,8 @@
|
||||
"AreYouSureYouWantToDeleteCategory": "Tem certeza de que deseja excluir a categoria mapeada?",
|
||||
"DeleteClientCategory": "Excluir Categoria de Cliente de Download",
|
||||
"DownloadClientCategory": "Categoria de Download do Cliente",
|
||||
"MappedCategories": "Categorias Mapeadas"
|
||||
"MappedCategories": "Categorias Mapeadas",
|
||||
"AuthenticationRequired": "Autenticação Requerida",
|
||||
"AuthenticationRequiredHelpText": "Altera para quais solicitações a autenticação é necessária. Não mude a menos que você entenda os riscos.",
|
||||
"AuthenticationRequiredWarning": "Para impedir o acesso remoto sem autenticação, o Prowlarr agora exige que a autenticação seja habilitada. Você pode, opcionalmente, desabilitar a autenticação de endereços locais."
|
||||
}
|
||||
|
||||
@@ -70,5 +70,238 @@
|
||||
"DeleteApplicationMessageText": "Ви впевнені, що хочете видалити клієнт завантаження '{0}'?",
|
||||
"DeleteTagMessageText": "Ви впевнені, що хочете видалити тег {0} ?",
|
||||
"DeleteIndexerProxyMessageText": "Ви впевнені, що хочете видалити тег {0} ?",
|
||||
"DeleteNotificationMessageText": "Ви впевнені, що хочете видалити клієнт завантаження '{0}'?"
|
||||
"DeleteNotificationMessageText": "Ви впевнені, що хочете видалити клієнт завантаження '{0}'?",
|
||||
"YesCancel": "Так, скасувати",
|
||||
"InstanceName": "Ім'я екземпляра",
|
||||
"Interval": "Інтервал",
|
||||
"PendingChangesStayReview": "Залишайтеся та переглядайте зміни",
|
||||
"ShowSearch": "Показати пошук",
|
||||
"SSLCertPasswordHelpText": "Пароль для файлу pfx",
|
||||
"TestAll": "Перевірити все",
|
||||
"Type": "Тип",
|
||||
"UnableToLoadDownloadClients": "Не вдалося завантажити клієнти для завантаження",
|
||||
"UnableToLoadGeneralSettings": "Не вдалося завантажити загальні налаштування",
|
||||
"UnableToLoadHistory": "Не вдалося завантажити історію",
|
||||
"UnableToLoadIndexers": "Не вдалося завантажити індексатори",
|
||||
"UnableToLoadNotifications": "Не вдалося завантажити сповіщення",
|
||||
"UnableToLoadTags": "Не вдалося завантажити теги",
|
||||
"UpdateAutomaticallyHelpText": "Автоматичне завантаження та встановлення оновлень. Ви все ще зможете встановити з System: Updates",
|
||||
"Uptime": "Час роботи",
|
||||
"URLBase": "URL-адреса",
|
||||
"DownloadClients": "Клієнти завантажувачів",
|
||||
"DownloadClientSettings": "Налаштування клієнта завантажувача",
|
||||
"DownloadClientStatusCheckSingleClientMessage": "Завантаження клієнтів недоступне через помилки: {0}",
|
||||
"Edit": "Редагувати",
|
||||
"EditIndexer": "Редагувати індексатор",
|
||||
"Docker": "Docker",
|
||||
"NoLeaveIt": "Ні, залиште це",
|
||||
"NoLogFiles": "Немає файлів журналу",
|
||||
"NoTagsHaveBeenAddedYet": "Теги ще не додано",
|
||||
"NotificationTriggers": "Тригери сповіщень",
|
||||
"Ok": "Гаразд",
|
||||
"LastWriteTime": "Час останнього запису",
|
||||
"Presets": "Предустановки",
|
||||
"RestoreBackup": "Відновлення резервної копії",
|
||||
"Result": "Результат",
|
||||
"Retention": "Утримання",
|
||||
"OAuthPopupMessage": "Ваш браузер блокує спливаючі вікна",
|
||||
"Title": "Назва",
|
||||
"Today": "Сьогодні",
|
||||
"Tomorrow": "Завтра",
|
||||
"Torrents": "Торренти",
|
||||
"OnHealthIssueHelpText": "Про питання здоров'я",
|
||||
"OpenBrowserOnStart": "Відкрийте браузер при запуску",
|
||||
"Options": "Опції",
|
||||
"PackageVersion": "Версія пакета",
|
||||
"Protocol": "Протокол",
|
||||
"Proxy": "Проксі",
|
||||
"ProxyCheckFailedToTestMessage": "Не вдалося перевірити проксі: {0}",
|
||||
"ProxyCheckResolveIpMessage": "Не вдалося визначити IP-адресу для налаштованого проксі-сервера {0}",
|
||||
"ProxyType": "Тип проксі",
|
||||
"ProxyUsernameHelpText": "Вам потрібно лише ввести ім’я користувача та пароль, якщо вони потрібні. В іншому випадку залиште їх порожніми.",
|
||||
"Reset": "Скинути",
|
||||
"RemoveFilter": "Видалити фільтр",
|
||||
"RemovingTag": "Видалення мітки",
|
||||
"ResetAPIKey": "Скинути ключ API",
|
||||
"Restart": "Перезавантажити",
|
||||
"RestartNow": "Перезавантажити зараз",
|
||||
"RestartRequiredHelpTextWarning": "Щоб набуло чинності, потрібно перезапустити",
|
||||
"Search": "Пошук",
|
||||
"SendAnonymousUsageData": "Надсилати анонімні дані про використання",
|
||||
"SetTags": "Встановити теги",
|
||||
"Settings": "Налаштування",
|
||||
"SettingsEnableColorImpairedModeHelpText": "Змінений стиль, щоб користувачі з вадами кольору могли краще розрізняти кольорову кодовану інформацію",
|
||||
"Source": "Джерело",
|
||||
"Shutdown": "Вимкнення",
|
||||
"Size": "Розмір",
|
||||
"Sort": "Сортування",
|
||||
"UILanguage": "Мова інтерфейсу користувача",
|
||||
"UISettings": "Налаштування інтерфейсу користувача",
|
||||
"EnableSSL": "Увімкнути SSL",
|
||||
"HomePage": "Домашня сторінка",
|
||||
"Hostname": "Ім'я хоста",
|
||||
"DeleteNotification": "Видалити сповіщення",
|
||||
"IndexerStatusCheckSingleClientMessage": "Індексатори недоступні через помилки: {0}",
|
||||
"Info": "Інформація",
|
||||
"InstanceNameHelpText": "Ім’я екземпляра на вкладці та ім’я програми Syslog",
|
||||
"InteractiveSearch": "Інтерактивний пошук",
|
||||
"KeyboardShortcuts": "Гарячі клавіши",
|
||||
"Language": "Мова",
|
||||
"LastDuration": "Остання тривалість",
|
||||
"Mode": "Режим",
|
||||
"MoreInfo": "Більше інформації",
|
||||
"Name": "Ім'я",
|
||||
"NoChanges": "Жодних змін",
|
||||
"NoUpdatesAreAvailable": "Немає оновлень",
|
||||
"OnApplicationUpdate": "Оновлення програми",
|
||||
"OnApplicationUpdateHelpText": "Оновлення програми",
|
||||
"OnHealthIssue": "Про питання здоров'я",
|
||||
"Password": "Пароль",
|
||||
"PendingChangesDiscardChanges": "Відкинути зміни та залишити",
|
||||
"Port": "Порт",
|
||||
"PortNumber": "Номер порту",
|
||||
"Priority": "Пріоритет",
|
||||
"ProxyBypassFilterHelpText": "Використовуйте «,» як роздільник і «*». як символ підстановки для субдоменів",
|
||||
"PtpOldSettingsCheckMessage": "Наведені нижче індексатори PassThePopcorn мають застарілі налаштування та їх потрібно оновити: {0}",
|
||||
"Queue": "Черга",
|
||||
"Queued": "У черзі",
|
||||
"ReadTheWikiForMoreInformation": "Читайте Wiki для отримання додаткової інформації",
|
||||
"Reload": "Перезавантажити",
|
||||
"RemovedFromTaskQueue": "Видалено з черги завдань",
|
||||
"Restore": "Відновлення",
|
||||
"RSSIsNotSupportedWithThisIndexer": "Цей індексатор не підтримує RSS",
|
||||
"Save": "Зберегти",
|
||||
"SaveChanges": "Зберегти зміни",
|
||||
"SaveSettings": "Зберегти зміни",
|
||||
"ScriptPath": "Шлях сценарію",
|
||||
"Security": "Безпека",
|
||||
"SelectAll": "Вибрати все",
|
||||
"SettingsLongDateFormat": "Довгий формат дати",
|
||||
"SettingsShortDateFormat": "Короткий формат дати",
|
||||
"SettingsShowRelativeDates": "Показати відносні дати",
|
||||
"SettingsTimeFormat": "Формат часу",
|
||||
"ShowAdvanced": "Показати Додатково",
|
||||
"ShownClickToHide": "Показано, натисніть, щоб приховати",
|
||||
"ShowSearchHelpText": "Показувати кнопку пошуку при наведенні",
|
||||
"TagCannotBeDeletedWhileInUse": "Неможливо видалити під час використання",
|
||||
"TestAllClients": "Перевірте всіх клієнтів",
|
||||
"TestAllIndexers": "Перевірити всі індексатори",
|
||||
"Time": "Час",
|
||||
"UILanguageHelpTextWarning": "Потрібно перезавантажити браузер",
|
||||
"UnableToAddANewIndexerPleaseTryAgain": "Не вдалося додати новий індексатор, спробуйте ще раз.",
|
||||
"UnableToAddANewNotificationPleaseTryAgain": "Не вдалося додати нове сповіщення, спробуйте ще раз.",
|
||||
"UnableToLoadBackups": "Не вдалося завантажити резервні копії",
|
||||
"UnableToLoadUISettings": "Не вдалося завантажити налаштування інтерфейсу користувача",
|
||||
"UnsavedChanges": "Незбережені зміни",
|
||||
"UnselectAll": "Скасувати вибір усіх",
|
||||
"UpdateCheckStartupNotWritableMessage": "Неможливо встановити оновлення, оскільки папка запуску \"{0}\" не може бути записана користувачем \"{1}\".",
|
||||
"UpdateCheckStartupTranslocationMessage": "Неможливо встановити оновлення, оскільки папка запуску \"{0}\" знаходиться в папці переміщення програми.",
|
||||
"UpdateCheckUINotWritableMessage": "Неможливо встановити оновлення, оскільки папка інтерфейсу користувача \"{0}\" не може бути записана користувачем \"{1}\".",
|
||||
"Updates": "Оновлення",
|
||||
"UrlBaseHelpText": "Для підтримки зворотного проксі-сервера значення за умовчанням порожнє",
|
||||
"UseProxy": "Використовуйте проксі",
|
||||
"Yesterday": "Вчора",
|
||||
"Duration": "Тривалість",
|
||||
"EnableAutomaticSearch": "Увімкнути автоматичний пошук",
|
||||
"EnableInteractiveSearch": "Увімкнути інтерактивний пошук",
|
||||
"EnableSslHelpText": " Щоб набуло чинності, потрібно перезапустити роботу від імені адміністратора",
|
||||
"Ended": "Завершено",
|
||||
"Error": "Помилка",
|
||||
"ErrorLoadingContents": "Помилка завантаження вмісту",
|
||||
"EventType": "Тип події",
|
||||
"Exception": "Виняток",
|
||||
"ExistingTag": "Існуючий тег",
|
||||
"Failed": "Не вдалося",
|
||||
"FeatureRequests": "Запити щодо функцій",
|
||||
"Events": "Події",
|
||||
"Fixed": "Виправлено",
|
||||
"Filters": "Фільтри",
|
||||
"Files": "Файли",
|
||||
"Filter": "Фільтр",
|
||||
"FocusSearchBox": "Перейти до вікна пошуку",
|
||||
"Folder": "Папка",
|
||||
"General": "Загальний",
|
||||
"GeneralSettings": "Загальні налаштування",
|
||||
"Health": "Здоров'я",
|
||||
"HiddenClickToShow": "Приховано, натисніть, щоб показати",
|
||||
"HideAdvanced": "Сховати додаткові",
|
||||
"History": "Історія",
|
||||
"IgnoredAddresses": "Ігноровані адреси",
|
||||
"IllRestartLater": "Я перезапущу пізніше",
|
||||
"IncludeHealthWarningsHelpText": "Включайте попередження про здоров’я",
|
||||
"Indexer": "Індексатор",
|
||||
"IndexerPriority": "Пріоритет індексатора",
|
||||
"LogLevel": "Рівень журналу",
|
||||
"Level": "Рівень",
|
||||
"Logs": "Журнали",
|
||||
"New": "Новий",
|
||||
"NoChange": "Без змін",
|
||||
"NextExecution": "Наступне виконання",
|
||||
"NoBackupsAreAvailable": "Немає резервних копій",
|
||||
"NoLinks": "Немає посилань",
|
||||
"OpenThisModal": "Відкрийте цей модальний вікно",
|
||||
"ProxyCheckBadRequestMessage": "Не вдалося перевірити проксі. Код стану: {0}",
|
||||
"ReleaseStatus": "Статус випуску",
|
||||
"Refresh": "Оновити",
|
||||
"RefreshMovie": "Оновити фільм",
|
||||
"System": "Система",
|
||||
"TableOptions": "Параметри таблиці",
|
||||
"Test": "Тест",
|
||||
"Tags": "Теги",
|
||||
"TagsSettingsSummary": "Перегляньте всі теги та те, як вони використовуються. Невикористані теги можна видалити",
|
||||
"Tasks": "Задачі",
|
||||
"Version": "Версія",
|
||||
"View": "Переглянути",
|
||||
"Username": "Ім'я користувача",
|
||||
"Warn": "Попередити",
|
||||
"Mechanism": "Механізм",
|
||||
"Message": "Повідомлення",
|
||||
"MIA": "MIA",
|
||||
"Enabled": "Увімкнено",
|
||||
"EnableInteractiveSearchHelpText": "Буде використано, коли використовується інтерактивний пошук",
|
||||
"Indexers": "Індексатори",
|
||||
"HealthNoIssues": "Немає проблем із вашою конфігурацією",
|
||||
"IndexerFlags": "Прапори індексатора",
|
||||
"IndexerLongTermStatusCheckAllClientMessage": "Усі індексатори недоступні через збої більше 6 годин",
|
||||
"IndexerLongTermStatusCheckSingleClientMessage": "Індексатори недоступні через збої більше 6 годин: {0}",
|
||||
"IndexerStatusCheckAllClientMessage": "Усі індексатори недоступні через збої",
|
||||
"LastExecution": "Останнє виконання",
|
||||
"LogFiles": "Файли журналів",
|
||||
"LogLevelTraceHelpTextWarning": "Журнал трасування слід увімкнути лише тимчасово",
|
||||
"Manual": "Інструкція",
|
||||
"MappedDrivesRunningAsService": "Підключені мережеві диски недоступні під час роботи як служби Windows. Щоб отримати додаткову інформацію, перегляньте FAQ",
|
||||
"MovieIndexScrollBottom": "Індекс фільму: прокрутка внизу",
|
||||
"MovieIndexScrollTop": "Індекс фільму: прокрутка вгору",
|
||||
"NotificationTriggersHelpText": "Виберіть, які події мають викликати це сповіщення",
|
||||
"PageSize": "Розмір сторінки",
|
||||
"PageSizeHelpText": "Кількість елементів для показу на кожній сторінці",
|
||||
"PendingChangesMessage": "У вас є незбережені зміни. Ви впевнені, що бажаєте залишити цю сторінку?",
|
||||
"ProxyPasswordHelpText": "Вам потрібно лише ввести ім’я користувача та пароль, якщо вони потрібні. В іншому випадку залиште їх порожніми.",
|
||||
"Scheduled": "За розкладом",
|
||||
"SettingsEnableColorImpairedMode": "Увімкнути режим із порушенням кольору",
|
||||
"SettingsShowRelativeDatesHelpText": "Показати відносні (сьогодні/вчора/тощо) або абсолютні дати",
|
||||
"TagIsNotUsedAndCanBeDeleted": "Тег не використовується і може бути видалений",
|
||||
"UnableToAddANewDownloadClientPleaseTryAgain": "Не вдається додати новий клієнт для завантаження, повторіть спробу.",
|
||||
"UpdateScriptPathHelpText": "Шлях до спеціального сценарію, який приймає витягнутий пакет оновлення та обробляє решту процесу оновлення",
|
||||
"DeleteTag": "Видалити тег",
|
||||
"Details": "Подробиці",
|
||||
"Disabled": "Вимкнено",
|
||||
"Discord": "Discord",
|
||||
"DownloadClient": "Клієнт завантажувача",
|
||||
"Donations": "Пожертви",
|
||||
"DownloadClientStatusCheckAllClientMessage": "Усі клієнти завантаження недоступні через збої",
|
||||
"Enable": "Увімкнути",
|
||||
"Filename": "Ім'я файлу",
|
||||
"Host": "Хост",
|
||||
"PriorityHelpText": "Надайте пріоритет кільком клієнтам завантаження. Round-Robin використовується для клієнтів з однаковим пріоритетом.",
|
||||
"SSLCertPathHelpText": "Шлях до файлу pfx",
|
||||
"SSLPort": "Порт SSL",
|
||||
"Started": "Розпочато",
|
||||
"StartTypingOrSelectAPathBelow": "Почніть вводити текст або виберіть шлях нижче",
|
||||
"StartupDirectory": "Каталог запуску",
|
||||
"Status": "Статус",
|
||||
"Style": "Стиль",
|
||||
"SuggestTranslationChange": "Запропонуйте зміну перекладу",
|
||||
"TableOptionsColumnsMessage": "Виберіть, які стовпці відображаються та в якому порядку вони відображаються",
|
||||
"SystemTimeCheckMessage": "Системний час вимкнено більш ніж на 1 день. Заплановані завдання можуть не працювати належним чином, доки час не буде виправлено"
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
"BranchUpdateMechanism": "外部更新机制使用的分支",
|
||||
"BranchUpdate": "更新Prowlarr的分支",
|
||||
"Branch": "分支",
|
||||
"BindAddressHelpText": "有效的 IP4 地址或以'*'代表所有地址",
|
||||
"BindAddressHelpText": "有效的 IP 地址,localhost,或以'*'代表所有地址",
|
||||
"BindAddress": "绑定地址",
|
||||
"BeforeUpdate": "更新前",
|
||||
"Backups": "备份",
|
||||
@@ -462,5 +462,6 @@
|
||||
"Started": "已开始",
|
||||
"LastDuration": "上一次用时",
|
||||
"ApplicationLongTermStatusCheckAllClientMessage": "由于故障超过6小时,所有程序都不可用",
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "由于故障超过6小时而无法使用的程序:{0}"
|
||||
"ApplicationLongTermStatusCheckSingleClientMessage": "由于故障超过6小时而无法使用的程序:{0}",
|
||||
"AuthenticationRequired": "需要认证"
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace NzbDrone.Core.Parser
|
||||
if (DateTimeRoutines.TryParseDateOrTime(
|
||||
str, dtFormat, out DateTimeRoutines.ParsedDateTime dt))
|
||||
{
|
||||
return dt.DateTime.ToUniversalTime();
|
||||
return dt.DateTime;
|
||||
}
|
||||
|
||||
throw new InvalidDateException($"FromFuzzyTime parsing failed for string {str}");
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace NzbDrone.Core.Parser.Model
|
||||
public int ImdbId { get; set; }
|
||||
public int TmdbId { get; set; }
|
||||
public int TraktId { get; set; }
|
||||
public int TvMazeId { get; set; }
|
||||
public int DoubanId { get; set; }
|
||||
public int Year { get; set; }
|
||||
public string Author { get; set; }
|
||||
@@ -60,7 +61,7 @@ namespace NzbDrone.Core.Parser.Model
|
||||
|
||||
public int Age
|
||||
{
|
||||
get { return DateTime.UtcNow.Subtract(PublishDate).Days; }
|
||||
get { return DateTime.UtcNow.Subtract(PublishDate.ToUniversalTime()).Days; }
|
||||
|
||||
//This prevents manually downloading a release from blowing up in mono
|
||||
//TODO: Is there a better way?
|
||||
@@ -69,7 +70,7 @@ namespace NzbDrone.Core.Parser.Model
|
||||
|
||||
public double AgeHours
|
||||
{
|
||||
get { return DateTime.UtcNow.Subtract(PublishDate).TotalHours; }
|
||||
get { return DateTime.UtcNow.Subtract(PublishDate.ToUniversalTime()).TotalHours; }
|
||||
|
||||
//This prevents manually downloading a release from blowing up in mono
|
||||
//TODO: Is there a better way?
|
||||
@@ -78,7 +79,7 @@ namespace NzbDrone.Core.Parser.Model
|
||||
|
||||
public double AgeMinutes
|
||||
{
|
||||
get { return DateTime.UtcNow.Subtract(PublishDate).TotalMinutes; }
|
||||
get { return DateTime.UtcNow.Subtract(PublishDate.ToUniversalTime()).TotalMinutes; }
|
||||
|
||||
//This prevents manually downloading a release from blowing up in mono
|
||||
//TODO: Is there a better way?
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
namespace NzbDrone.Core.ThingiProvider.Status
|
||||
namespace NzbDrone.Core.ThingiProvider.Status
|
||||
{
|
||||
public static class EscalationBackOff
|
||||
{
|
||||
public static readonly int[] Periods =
|
||||
{
|
||||
0,
|
||||
60,
|
||||
5 * 60,
|
||||
15 * 60,
|
||||
30 * 60,
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace NzbDrone.Mono.Disk
|
||||
|
||||
private static Dictionary<string, bool> _fileSystems;
|
||||
|
||||
private bool _hasLoggedProcMountFailure = false;
|
||||
|
||||
public ProcMountProvider(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -45,7 +47,11 @@ namespace NzbDrone.Mono.Disk
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug(ex, "Failed to retrieve mounts from {0}", PROC_MOUNTS_FILENAME);
|
||||
if (!_hasLoggedProcMountFailure)
|
||||
{
|
||||
_logger.Debug(ex, "Failed to retrieve mounts from {0}", PROC_MOUNTS_FILENAME);
|
||||
_hasLoggedProcMountFailure = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new List<IMount>();
|
||||
|
||||
Reference in New Issue
Block a user