Compare commits

...

36 Commits

Author SHA1 Message Date
Bakerboy448
5e15054329 Avistaz response improvements 2023-01-05 15:40:22 -06:00
bakerboy448
3dfbfd07dd improve authentication required wording 2023-01-04 22:53:38 -06:00
Qstick
842df6913c New: Improve CF Detection
Co-Authored-By: Diego Heras <ngosang@hotmail.es>
2023-01-04 22:27:03 -06:00
Qstick
599eeb4c61 Bump Version to 1.1.0 2023-01-03 18:50:42 -06:00
Bogdan
da371dd921 Fixed: (Avistaz) Use type password for PID 2023-01-03 18:49:17 -06:00
Bogdan
fc25ba7ac0 Fixed: (Filelist) Use UTC in tests 2023-01-03 18:05:54 -06:00
Qstick
6e1bef13e2 Fixed: Correctly calculate UI age for some indexers 2023-01-02 23:28:36 -06:00
Qstick
72ee413411 Fixed: (BeyondHD) Assume Universal Time for publish dates 2023-01-02 23:23:56 -06:00
Qstick
e87b45b47e Fixed: (Filelist) Assume UTC+2 for API Dates 2023-01-02 23:23:17 -06:00
Qstick
cc841fe3d1 Remove Preview from Page Title 2023-01-02 18:06:34 -06:00
Qstick
264ffdcc26 Fixup Provider Tests 2023-01-02 17:41:41 -06:00
Qstick
5cc044aa8f Rarbg Rate Limit Tweaks, Additional back-off level 2023-01-02 17:16:46 -06:00
Bogdan
de2fd92b6f Fixed: (Avistaz) Workaround for fetching "retry-after" header not present when using "Accept: application/json" 2023-01-01 15:31:26 -06:00
Qstick
eff09c1f72 Treat Master as a valid branch 2022-12-31 19:32:06 -06:00
Qstick
9db888c9a3 Bump Version to 1.0.1 2022-12-31 18:49:38 -06:00
Qstick
bf78396164 Rewrite test to avoid 6 hours of failures and needing a change every year 2022-12-31 18:22:07 -06:00
Qstick
0e7eaa9221 Simplify logic in HandleRequest 2022-12-31 17:57:54 -06:00
Colin Gagnaire
5b82decc31 New: Add support for native Freebox Download Client 2022-12-31 17:34:17 -06:00
Mark McDowall
38ab533272 Fixed: Only log /proc/mounts exception once per process 2022-12-31 17:11:05 -06:00
Qstick
4914fcd5df Fixed: (UI) Category is None in history if only search by sub category 2022-12-31 17:03:27 -06:00
Qstick
858415b037 Fixed: (Cardigann) Query string gets first letter removed in request handling 2022-12-31 16:37:15 -06:00
Bogdan
43f4899324 New: (Indexer) Torrent Libble with 2FA and pagination 2022-12-31 16:22:06 -06:00
Bogdan
c60a94adfb Fixed: (RetroFlix) Set 5 days as MST, return 100 results and remove "[REQUESTED]" from title 2022-12-31 16:19:47 -06:00
Qstick
f386ddb806 Fixed: Sorting on mobile search UI 2022-12-31 16:17:54 -06:00
Qstick
4175c2577e Fixed: Link to release page from mobile search 2022-12-31 15:56:29 -06:00
Qstick
6ce9e5ceb9 Fixed: Release Grab not working on Mobile search 2022-12-30 18:28:55 -06:00
Bogdan
c15643be39 Fixed: (Cardigann) Allow use of template variables in fields selector 2022-12-30 17:58:24 -06:00
Bogdan
a58380031d Fixed: (Indexer) Added TvSearchParam.ImdbId to SpeedApp 2022-12-28 23:56:08 -06:00
Bogdan
73af5c9a72 Fixed: (Indexer) Changed FL to use internal flag 2022-12-28 23:54:48 -06:00
Bogdan
d556545e7f Fixed: Changed torznab parser to use additional attributes 2022-12-28 23:54:01 -06:00
Bogdan
affde5d7b7 Fixed: (Orpheus) Changed to use filters for categories, label and year 2022-12-27 22:16:49 -06:00
Bakerboy448
518c85dee2 Fixed: (Rarbg) Improve RateLimit Handling
Fixed: (Rarbg) Increase delay to 4s to reduce Rate Limiting

Fixes #1169
2022-12-26 18:26:43 -06:00
Bakerboy448
ba3a240707 Add TooManyRequestsException with var retryWait 2022-12-26 18:26:43 -06:00
Qstick
587a73f3d6 Fixed: (Newznab) Parsing of Ids from non-standard feeds
Fixes #1261
2022-12-26 18:17:50 -06:00
Weblate
ae8f017ca8 Translated using Weblate (Ukrainian)
Currently translated at 63.2% (298 of 471 strings)

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

Currently translated at 98.5% (464 of 471 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (471 of 471 strings)

Translated using Weblate (Bengali)

Currently translated at 0.8% (4 of 468 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (468 of 468 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: andrey4korop <andrey999@i.ua>
Co-authored-by: lhquark <lhquark@gmail.com>
Co-authored-by: saambd <me@salimrahman.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bn/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2022-12-26 17:52:43 -06:00
Bogdan
d9098b612e Fixed: (Nebulance) Changed MinimumSeedTime according to their H&R rules 2022-12-26 17:48:29 -06:00
54 changed files with 1646 additions and 144 deletions

View File

@@ -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)'

View File

@@ -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();

View File

@@ -35,9 +35,11 @@ $hoverScale: 1.05;
}
.title {
overflow: hidden;
width: 85%;
font-weight: 500;
font-size: 14px;
overflow-wrap: break-word;
}
.actions {

View File

@@ -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}>

View File

@@ -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}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;
}
}
}

View File

@@ -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);

View File

@@ -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")]

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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))
{

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,10 @@
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
{
public class FreeboxDownloadException : DownloadClientException
{
public FreeboxDownloadException(string message)
: base(message)
{
}
}
}

View File

@@ -0,0 +1,8 @@
namespace NzbDrone.Core.Download.Clients.FreeboxDownload
{
public enum FreeboxDownloadPriority
{
Last = 0,
First = 1
}
}

View File

@@ -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.");
}
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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";
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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";
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -31,6 +31,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
public enum ReleaseBranches
{
Master,
Develop,
Nightly
}

View File

@@ -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

View File

@@ -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),

View File

@@ -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");

View File

@@ -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);

View File

@@ -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;

View File

@@ -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")]

View File

@@ -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,

View File

@@ -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;

View File

@@ -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)

View File

@@ -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
}
};

View File

@@ -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; }
}
}

View File

@@ -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,

View 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; }
}
}

View File

@@ -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; }

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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>
{

View File

@@ -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>
{

View File

@@ -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();

View File

@@ -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;
}
}
}

View File

@@ -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",

View File

@@ -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."
}

View File

@@ -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 день. Заплановані завдання можуть не працювати належним чином, доки час не буде виправлено"
}

View File

@@ -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": "需要认证"
}

View File

@@ -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}");

View File

@@ -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?

View File

@@ -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,

View File

@@ -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>();