Compare commits

..

25 Commits

Author SHA1 Message Date
Bogdan
75efbd45e1 Fixed: Calculating seed time for qBittorrent
(cherry picked from commit 1b3ff64cc521396f9f1623617052c497649325a8)
2023-09-08 04:17:10 +03:00
Weblate
00cac507ad Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: DavidJares <david.jares@me.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-09-07 11:58:51 +03:00
Bogdan
c4850505b0 New: Add Plex Media Server notifications 2023-09-07 10:18:24 +03:00
Bogdan
75213c86a1 Bump dotnet to 6.0.21 2023-09-05 16:30:21 +03:00
Bogdan
b8c3a42643 Migrate to merged proposals now included in babel/present-env
Closes #2899
2023-09-05 03:03:33 +03:00
Bogdan
8acb034aa6 Use not allowed cursor for disabled select options
(cherry picked from commit 229a4bba05d1f42089aa92b1d938747e152590b2)
2023-09-05 02:58:09 +03:00
Bogdan
889d32552b Update UI dev packages 2023-09-01 14:29:20 +03:00
Bogdan
adc5f4db97 Fixed: Increase timeout when downloading updates
(cherry picked from commit 467ce70291c793042ffb3ed8942c42e7bc1424d5)
2023-09-01 04:05:16 +03:00
Weblate
9d08050f96 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: He Zhu <zhuhe202@qq.com>
Co-authored-by: monopolo11 <bernardorn21@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-08-31 20:59:43 +03:00
Weblate
f8cffbb4cf Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: DavidJares <david.jares@me.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: He Zhu <zhuhe202@qq.com>
Co-authored-by: Renan da Mota Ciciliato <renanciciliato@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: brokje1988 <brokje1988@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-08-29 18:12:42 +03:00
Weblate
14aeb66142 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: He Zhu <zhuhe202@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-08-27 19:22:06 +03:00
Bogdan
37e8e11e31 Ensure the correct icons are spinning when refreshing authors and books 2023-08-25 21:51:52 +03:00
Bogdan
bdb2f14936 Prevent NullRef in GetChangedAuthors when metadata is down 2023-08-25 21:49:15 +03:00
Mark McDowall
a97af657be Improved UI error messages (stack trace and version)
(cherry picked from commit 37c355da51b654cea7309678c32a83a5cbe43d1f)

Closes #2207
2023-08-24 21:15:47 +03:00
Servarr
301127e6dc Automated API Docs update 2023-08-24 00:44:35 +03:00
Bogdan
1f95bcae4e New: Async HttpClient
(cherry picked from commit 0feee191462dd3e5dde66e476e8b4b46a85ec4f0)
2023-08-24 00:38:31 +03:00
Bogdan
29118cda45 New: Use HTTP/2 in HttpClient
(cherry picked from commit 78593f428acc578785f9ecfdd41fbf2443d93d84)
2023-08-24 00:38:31 +03:00
Bogdan
09beaa939d Fixed: (FileList) Prevent double query escaping in search requests 2023-08-24 00:38:31 +03:00
Bogdan
2107624f1c Prevent health checks warnings for disabled notifications
(cherry picked from commit 5a7f42a63e25d6abdb187c37e92a908a6b85fb4d)
2023-08-23 04:58:22 +03:00
Weblate
c1c2076e5c Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2023-08-23 04:57:55 +03:00
Bogdan
c31a797bd8 Revert "Switch to Parallel.ForEach for list processing with MaxParallelism"
This reverts commit ebb2b4eca3.
2023-08-22 06:03:14 +03:00
Qstick
ebb2b4eca3 Switch to Parallel.ForEach for list processing with MaxParallelism
(cherry picked from commit 0f93e04186f24abdb0cf0b3ba6a3505fda834e06)
2023-08-21 04:44:59 +03:00
Qstick
3ec5d9b9fe Use default MemoryAllocator for ImageSharp resizing
(cherry picked from commit c1a3a8249befde0a1b68e7845d5d2346066457a1)
2023-08-21 04:42:59 +03:00
Qstick
1ad84a7c44 Fixed: Ignore case when comparing torrent infohash
(cherry picked from commit 7986488c6d1687b0810b3bcac2c1dae725e770ac)
2023-08-20 16:10:26 -05:00
Bogdan
9d67c18254 Bump version to 0.3.4 2023-08-20 12:24:38 +03:00
106 changed files with 2858 additions and 1466 deletions

View File

@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.3.3'
majorVersion: '0.3.4'
minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.408'
dotnetVersion: '6.0.413'
nodeVersion: '16.X'
innoVersion: '6.2.0'
windowsImage: 'windows-2022'

View File

@@ -4,14 +4,14 @@ module.exports = {
plugins: [
// Stage 1
'@babel/plugin-proposal-export-default-from',
['@babel/plugin-proposal-optional-chaining', { loose }],
['@babel/plugin-proposal-nullish-coalescing-operator', { loose }],
['@babel/plugin-transform-optional-chaining', { loose }],
['@babel/plugin-transform-nullish-coalescing-operator', { loose }],
// Stage 2
'@babel/plugin-proposal-export-namespace-from',
'@babel/plugin-transform-export-namespace-from',
// Stage 3
['@babel/plugin-proposal-class-properties', { loose }],
['@babel/plugin-transform-class-properties', { loose }],
'@babel/plugin-syntax-dynamic-import'
],
env: {

View File

@@ -16,7 +16,7 @@ import AuthorIndex from './AuthorIndex';
function createMapStateToProps() {
return createSelector(
createAuthorClientSideCollectionItemsSelector('authorIndex'),
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.BULK_REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
@@ -24,17 +24,17 @@ function createMapStateToProps() {
(
author,
isRefreshingAuthor,
isRssSyncExecuting,
isOrganizingAuthor,
isRetaggingAuthor,
isRssSyncExecuting,
dimensionsState
) => {
return {
...author,
isRefreshingAuthor,
isRssSyncExecuting,
isOrganizingAuthor,
isRetaggingAuthor,
isRssSyncExecuting,
isSmallScreen: dimensionsState.isSmallScreen
};
}

View File

@@ -16,8 +16,8 @@ import BookIndex from './BookIndex';
function createMapStateToProps() {
return createSelector(
createBookClientSideCollectionItemsSelector('bookIndex'),
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.REFRESH_BOOK),
createCommandExecutingSelector(commandNames.BULK_REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.BULK_REFRESH_BOOK),
createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_BOOK_SEARCH),
createCommandExecutingSelector(commandNames.MISSING_BOOK_SEARCH),

View File

@@ -25,6 +25,10 @@
white-space: pre-wrap;
}
.version {
margin-top: 20px;
}
@media only screen and (max-width: $breakpointMedium) {
.image {
height: 250px;

View File

@@ -6,6 +6,7 @@ interface CssExports {
'image': string;
'imageContainer': string;
'message': string;
'version': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,60 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import styles from './ErrorBoundaryError.css';
function ErrorBoundaryError(props) {
const {
className,
messageClassName,
detailsClassName,
message,
error,
info
} = props;
return (
<div className={className}>
<div className={messageClassName}>
{message}
</div>
<div className={styles.imageContainer}>
<img
className={styles.image}
src={`${window.Readarr.urlBase}/Content/Images/error.png`}
/>
</div>
<details className={detailsClassName}>
{
error &&
<div>
{error.toString()}
</div>
}
<div className={styles.info}>
{info.componentStack}
</div>
</details>
</div>
);
}
ErrorBoundaryError.propTypes = {
className: PropTypes.string.isRequired,
messageClassName: PropTypes.string.isRequired,
detailsClassName: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
error: PropTypes.object.isRequired,
info: PropTypes.object.isRequired
};
ErrorBoundaryError.defaultProps = {
className: styles.container,
messageClassName: styles.message,
detailsClassName: styles.details,
message: 'There was an error loading this content'
};
export default ErrorBoundaryError;

View File

@@ -0,0 +1,77 @@
import React, { useEffect, useState } from 'react';
import StackTrace from 'stacktrace-js';
import translate from 'Utilities/String/translate';
import styles from './ErrorBoundaryError.css';
interface ErrorBoundaryErrorProps {
className: string;
messageClassName: string;
detailsClassName: string;
message: string;
error: Error;
info: {
componentStack: string;
};
}
function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
const {
className = styles.container,
messageClassName = styles.message,
detailsClassName = styles.details,
message = translate('ErrorLoadingContent'),
error,
info,
} = props;
const [detailedError, setDetailedError] = useState<
StackTrace.StackFrame[] | null
>(null);
useEffect(() => {
if (error) {
StackTrace.fromError(error).then((de) => {
setDetailedError(de);
});
} else {
setDetailedError(null);
}
}, [error, setDetailedError]);
return (
<div className={className}>
<div className={messageClassName}>{message}</div>
<div className={styles.imageContainer}>
<img
className={styles.image}
src={`${window.Readarr.urlBase}/Content/Images/error.png`}
/>
</div>
<details className={detailsClassName}>
{error ? <div>{error.message}</div> : null}
{detailedError ? (
detailedError.map((d, index) => {
return (
<div key={index}>
{` at ${d.functionName} (${d.fileName}:${d.lineNumber}:${d.columnNumber})`}
</div>
);
})
) : (
<div>{info.componentStack}</div>
)}
{
<div className={styles.version}>
Version: {window.Readarr.version}
</div>
}
</details>
</div>
);
}
export default ErrorBoundaryError;

View File

@@ -9,6 +9,10 @@
&:hover {
background-color: var(--inputHoverBackgroundColor);
}
&.isDisabled {
cursor: not-allowed;
}
}
.optionCheck {

View File

@@ -30,7 +30,7 @@
"@fortawesome/free-regular-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/react-fontawesome": "0.2.0",
"@microsoft/signalr": "6.0.16",
"@microsoft/signalr": "6.0.21",
"@sentry/browser": "7.51.2",
"@sentry/integrations": "7.51.2",
"@types/node": "18.16.16",
@@ -83,30 +83,27 @@
"redux-localstorage": "0.4.1",
"redux-thunk": "2.3.0",
"reselect": "4.1.8",
"stacktrace-js": "2.0.2",
"typescript": "4.9.5"
},
"devDependencies": {
"@babel/core": "7.22.9",
"@babel/eslint-parser": "7.22.9",
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/core": "7.22.11",
"@babel/eslint-parser": "7.22.11",
"@babel/plugin-proposal-export-default-from": "7.22.5",
"@babel/plugin-proposal-export-namespace-from": "7.18.9",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-optional-chaining": "7.21.0",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.22.9",
"@babel/preset-env": "7.22.15",
"@babel/preset-react": "7.22.5",
"@babel/preset-typescript": "7.22.5",
"@babel/preset-typescript": "7.22.11",
"@types/lodash": "4.14.197",
"@types/redux-actions": "2.6.2",
"@typescript-eslint/eslint-plugin": "6.0.0",
"@typescript-eslint/parser": "6.0.0",
"@typescript-eslint/eslint-plugin": "6.5.0",
"@typescript-eslint/parser": "6.5.0",
"autoprefixer": "10.4.14",
"babel-loader": "9.1.3",
"babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.31.1",
"css-loader": "6.7.3",
"core-js": "3.32.1",
"css-loader": "6.8.1",
"css-modules-typescript-loader": "4.0.1",
"eslint": "8.44.0",
"eslint-config-prettier": "8.8.0",
@@ -120,9 +117,9 @@
"file-loader": "6.2.0",
"filemanager-webpack-plugin": "8.0.0",
"fork-ts-checker-webpack-plugin": "8.0.0",
"html-webpack-plugin": "5.5.1",
"html-webpack-plugin": "5.5.3",
"loader-utils": "^3.2.1",
"mini-css-extract-plugin": "2.7.5",
"mini-css-extract-plugin": "2.7.6",
"postcss": "8.4.23",
"postcss-color-function": "4.1.0",
"postcss-loader": "7.3.0",
@@ -135,14 +132,14 @@
"rimraf": "4.4.1",
"run-sequence": "2.2.1",
"streamqueue": "1.1.2",
"style-loader": "3.3.2",
"stylelint": "15.10.1",
"style-loader": "3.3.3",
"stylelint": "15.10.3",
"stylelint-order": "6.0.3",
"terser-webpack-plugin": "5.3.9",
"ts-loader": "9.4.3",
"ts-loader": "9.4.4",
"typescript-plugin-css-modules": "5.0.1",
"url-loader": "4.1.1",
"webpack": "5.88.1",
"webpack": "5.88.2",
"webpack-cli": "5.1.4",
"webpack-livereload-plugin": "3.0.2",
"worker-loader": "3.0.8"

View File

@@ -4,7 +4,7 @@
<PackageVersion Include="AutoFixture" Version="4.17.0" />
<PackageVersion Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" PrivateAssets="all" />
<PackageVersion Include="Dapper" Version="2.0.123" />
<PackageVersion Include="DryIoc.dll" Version="5.4.0" />
<PackageVersion Include="DryIoc.dll" Version="5.4.1" />
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
<PackageVersion Include="Equ" Version="2.3.0" />
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
@@ -16,7 +16,7 @@
<PackageVersion Include="ImpromptuInterface" Version="7.0.1" />
<PackageVersion Include="LazyCache" Version="2.4.0" />
<PackageVersion Include="Mailkit" Version="3.6.0" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.16" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.21" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
@@ -43,7 +43,7 @@
<PackageVersion Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" />
<PackageVersion Include="Sentry" Version="3.31.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.0.1" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.0.2" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
<PackageVersion Include="System.Buffers" Version="4.5.1" />
@@ -57,10 +57,10 @@
<PackageVersion Include="System.Resources.Extensions" Version="6.0.0" />
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageVersion Include="System.Text.Json" Version="6.0.7" />
<PackageVersion Include="System.Text.Json" Version="6.0.8" />
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
<PackageVersion Include="TagLibSharp-Lidarr" Version="2.2.0.19" />
</ItemGroup>
</Project>
</Project>

View File

@@ -6,6 +6,7 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NLog;
@@ -114,21 +115,21 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_execute_simple_get()
public async Task should_execute_simple_get()
{
var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Execute(request);
var response = await Subject.ExecuteAsync(request);
response.Content.Should().NotBeNullOrWhiteSpace();
}
[Test]
public void should_execute_https_get()
public async Task should_execute_https_get()
{
var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Execute(request);
var response = await Subject.ExecuteAsync(request);
response.Content.Should().NotBeNullOrWhiteSpace();
}
@@ -140,47 +141,47 @@ namespace NzbDrone.Common.Test.Http
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(validationType);
var request = new HttpRequest($"https://expired.badssl.com");
Assert.Throws<HttpRequestException>(() => Subject.Execute(request));
Assert.ThrowsAsync<HttpRequestException>(async () => await Subject.ExecuteAsync(request));
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void bad_ssl_should_pass_if_remote_validation_disabled()
public async Task bad_ssl_should_pass_if_remote_validation_disabled()
{
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Disabled);
var request = new HttpRequest($"https://expired.badssl.com");
Subject.Execute(request);
await Subject.ExecuteAsync(request);
ExceptionVerification.ExpectedErrors(0);
}
[Test]
public void should_execute_typed_get()
public async Task should_execute_typed_get()
{
var request = new HttpRequest($"https://{_httpBinHost}/get?test=1");
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Url.EndsWith("/get?test=1");
response.Resource.Args.Should().Contain("test", "1");
}
[Test]
public void should_execute_simple_post()
public async Task should_execute_simple_post()
{
var message = "{ my: 1 }";
var request = new HttpRequest($"https://{_httpBinHost}/post");
request.SetContent(message);
var response = Subject.Post<HttpBinResource>(request);
var response = await Subject.PostAsync<HttpBinResource>(request);
response.Resource.Data.Should().Be(message);
}
[Test]
public void should_execute_post_with_content_type()
public async Task should_execute_post_with_content_type()
{
var message = "{ my: 1 }";
@@ -188,17 +189,16 @@ namespace NzbDrone.Common.Test.Http
request.SetContent(message);
request.Headers.ContentType = "application/json";
var response = Subject.Post<HttpBinResource>(request);
var response = await Subject.PostAsync<HttpBinResource>(request);
response.Resource.Data.Should().Be(message);
}
[Test]
public void should_execute_get_using_gzip()
public async Task should_execute_get_using_gzip()
{
var request = new HttpRequest($"https://{_httpBinHost}/gzip");
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("gzip");
@@ -208,11 +208,10 @@ namespace NzbDrone.Common.Test.Http
[Test]
[Platform(Exclude = "MacOsX", Reason = "Azure agent update prevents brotli on OSX")]
public void should_execute_get_using_brotli()
public async Task should_execute_get_using_brotli()
{
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("br");
@@ -230,7 +229,7 @@ namespace NzbDrone.Common.Test.Http
{
var request = new HttpRequest($"https://{_httpBinHost}/status/{statusCode}");
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
var exception = Assert.ThrowsAsync<HttpException>(async () => await Subject.GetAsync<HttpBinResource>(request));
((int)exception.Response.StatusCode).Should().Be(statusCode);
@@ -243,7 +242,7 @@ namespace NzbDrone.Common.Test.Http
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.NotFound };
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
Assert.ThrowsAsync<HttpException>(async () => await Subject.GetAsync<HttpBinResource>(request));
ExceptionVerification.IgnoreWarns();
}
@@ -253,7 +252,7 @@ namespace NzbDrone.Common.Test.Http
{
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
var exception = Assert.ThrowsAsync<HttpException>(async () => await Subject.GetAsync<HttpBinResource>(request));
ExceptionVerification.ExpectedWarns(1);
}
@@ -264,28 +263,28 @@ namespace NzbDrone.Common.Test.Http
var request = new HttpRequest($"https://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.LogHttpError = false;
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
Assert.ThrowsAsync<HttpException>(async () => await Subject.GetAsync<HttpBinResource>(request));
ExceptionVerification.ExpectedWarns(0);
}
[Test]
public void should_not_follow_redirects_when_not_in_production()
public async Task should_not_follow_redirects_when_not_in_production()
{
var request = new HttpRequest($"https://{_httpBinHost}/redirect/1");
Subject.Get(request);
await Subject.GetAsync(request);
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void should_follow_redirects()
public async Task should_follow_redirects()
{
var request = new HttpRequest($"https://{_httpBinHost}/redirect/1");
request.AllowAutoRedirect = true;
var response = Subject.Get(request);
var response = await Subject.GetAsync(request);
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -293,12 +292,12 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_not_follow_redirects()
public async Task should_not_follow_redirects()
{
var request = new HttpRequest($"https://{_httpBinHost}/redirect/1");
request.AllowAutoRedirect = false;
var response = Subject.Get(request);
var response = await Subject.GetAsync(request);
response.StatusCode.Should().Be(HttpStatusCode.Found);
@@ -306,14 +305,14 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_follow_redirects_to_https()
public async Task should_follow_redirects_to_https()
{
var request = new HttpRequestBuilder($"https://{_httpBinHost}/redirect-to")
.AddQueryParam("url", $"https://readarr.com/")
.Build();
request.AllowAutoRedirect = true;
var response = Subject.Get(request);
var response = await Subject.GetAsync(request);
response.StatusCode.Should().Be(HttpStatusCode.OK);
response.Content.Should().Contain("Readarr");
@@ -327,17 +326,17 @@ namespace NzbDrone.Common.Test.Http
var request = new HttpRequest($"https://{_httpBinHost}/redirect/6");
request.AllowAutoRedirect = true;
Assert.Throws<WebException>(() => Subject.Get(request));
Assert.ThrowsAsync<WebException>(async () => await Subject.GetAsync(request));
ExceptionVerification.ExpectedErrors(0);
}
[Test]
public void should_send_user_agent()
public async Task should_send_user_agent()
{
var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("User-Agent");
@@ -347,24 +346,24 @@ namespace NzbDrone.Common.Test.Http
}
[TestCase("Accept", "text/xml, text/rss+xml, application/rss+xml")]
public void should_send_headers(string header, string value)
public async Task should_send_headers(string header, string value)
{
var request = new HttpRequest($"https://{_httpBinHost}/get");
request.Headers.Add(header, value);
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers[header].ToString().Should().Be(value);
}
[Test]
public void should_download_file()
public async Task should_download_file()
{
var file = GetTempFilePath();
var url = "https://readarr.com/img/slider/artistdetails.png";
Subject.DownloadFile(url, file);
await Subject.DownloadFileAsync(url, file);
var fileInfo = new FileInfo(file);
fileInfo.Exists.Should().BeTrue();
@@ -372,7 +371,7 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_download_file_with_redirect()
public async Task should_download_file_with_redirect()
{
var file = GetTempFilePath();
@@ -380,7 +379,7 @@ namespace NzbDrone.Common.Test.Http
.AddQueryParam("url", $"https://readarr.com/img/slider/artistdetails.png")
.Build();
Subject.DownloadFile(request.Url.FullUri, file);
await Subject.DownloadFileAsync(request.Url.FullUri, file);
ExceptionVerification.ExpectedErrors(0);
@@ -394,7 +393,7 @@ namespace NzbDrone.Common.Test.Http
{
var file = GetTempFilePath();
Assert.Throws<HttpException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file));
Assert.ThrowsAsync<HttpException>(async () => await Subject.DownloadFileAsync("https://download.sonarr.tv/wrongpath", file));
File.Exists(file).Should().BeFalse();
@@ -402,7 +401,7 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_not_write_redirect_content_to_stream()
public async Task should_not_write_redirect_content_to_stream()
{
var file = GetTempFilePath();
@@ -412,7 +411,7 @@ namespace NzbDrone.Common.Test.Http
request.AllowAutoRedirect = false;
request.ResponseStream = fileStream;
var response = Subject.Get(request);
var response = await Subject.GetAsync(request);
response.StatusCode.Should().Be(HttpStatusCode.Moved);
}
@@ -427,12 +426,12 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_send_cookie()
public async Task should_send_cookie()
{
var request = new HttpRequest($"https://{_httpBinHost}/get");
request.Cookies["my"] = "cookie";
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("Cookie");
@@ -461,13 +460,13 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_preserve_cookie_during_session()
public async Task should_preserve_cookie_during_session()
{
GivenOldCookie();
var request = new HttpRequest($"https://{_httpBinHost2}/get");
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("Cookie");
@@ -477,30 +476,30 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_not_send_cookie_to_other_host()
public async Task should_not_send_cookie_to_other_host()
{
GivenOldCookie();
var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().NotContainKey("Cookie");
}
[Test]
public void should_not_store_request_cookie()
public async Task should_not_store_request_cookie()
{
var requestGet = new HttpRequest($"https://{_httpBinHost}/get");
requestGet.Cookies.Add("my", "cookie");
requestGet.AllowAutoRedirect = false;
requestGet.StoreRequestCookie = false;
requestGet.StoreResponseCookie = false;
var responseGet = Subject.Get<HttpBinResource>(requestGet);
var responseGet = await Subject.GetAsync<HttpBinResource>(requestGet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.AllowAutoRedirect = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().BeEmpty();
@@ -508,18 +507,18 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_store_request_cookie()
public async Task should_store_request_cookie()
{
var requestGet = new HttpRequest($"https://{_httpBinHost}/get");
requestGet.Cookies.Add("my", "cookie");
requestGet.AllowAutoRedirect = false;
requestGet.StoreRequestCookie.Should().BeTrue();
requestGet.StoreResponseCookie = false;
var responseGet = Subject.Get<HttpBinResource>(requestGet);
var responseGet = await Subject.GetAsync<HttpBinResource>(requestGet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.AllowAutoRedirect = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -527,7 +526,7 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_delete_request_cookie()
public async Task should_delete_request_cookie()
{
var requestDelete = new HttpRequest($"https://{_httpBinHost}/cookies/delete?my");
requestDelete.Cookies.Add("my", "cookie");
@@ -536,13 +535,13 @@ namespace NzbDrone.Common.Test.Http
requestDelete.StoreResponseCookie = false;
// Delete and redirect since that's the only way to check the internal temporary cookie container
var responseCookies = Subject.Get<HttpCookieResource>(requestDelete);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestDelete);
responseCookies.Resource.Cookies.Should().BeEmpty();
}
[Test]
public void should_clear_request_cookie()
public async Task should_clear_request_cookie()
{
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies");
requestSet.Cookies.Add("my", "cookie");
@@ -550,7 +549,7 @@ namespace NzbDrone.Common.Test.Http
requestSet.StoreRequestCookie = true;
requestSet.StoreResponseCookie = false;
var responseSet = Subject.Get<HttpCookieResource>(requestSet);
var responseSet = await Subject.GetAsync<HttpCookieResource>(requestSet);
var requestClear = new HttpRequest($"https://{_httpBinHost}/cookies");
requestClear.Cookies.Add("my", null);
@@ -558,24 +557,24 @@ namespace NzbDrone.Common.Test.Http
requestClear.StoreRequestCookie = true;
requestClear.StoreResponseCookie = false;
var responseClear = Subject.Get<HttpCookieResource>(requestClear);
var responseClear = await Subject.GetAsync<HttpCookieResource>(requestClear);
responseClear.Resource.Cookies.Should().BeEmpty();
}
[Test]
public void should_not_store_response_cookie()
public async Task should_not_store_response_cookie()
{
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = false;
requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie.Should().BeFalse();
var responseSet = Subject.Get(requestSet);
var responseSet = await Subject.GetAsync(requestSet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().BeEmpty();
@@ -583,18 +582,18 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_store_response_cookie()
public async Task should_store_response_cookie()
{
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = false;
requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet);
var responseSet = await Subject.GetAsync(requestSet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -602,13 +601,13 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_temp_store_response_cookie()
public async Task should_temp_store_response_cookie()
{
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = true;
requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie.Should().BeFalse();
var responseSet = Subject.Get<HttpCookieResource>(requestSet);
var responseSet = await Subject.GetAsync<HttpCookieResource>(requestSet);
// Set and redirect since that's the only way to check the internal temporary cookie container
responseSet.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -617,7 +616,7 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_overwrite_response_cookie()
public async Task should_overwrite_response_cookie()
{
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.Cookies.Add("my", "oldcookie");
@@ -625,11 +624,11 @@ namespace NzbDrone.Common.Test.Http
requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet);
var responseSet = await Subject.GetAsync(requestSet);
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -637,7 +636,7 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_overwrite_temp_response_cookie()
public async Task should_overwrite_temp_response_cookie()
{
var requestSet = new HttpRequest($"https://{_httpBinHost}/cookies/set?my=cookie");
requestSet.Cookies.Add("my", "oldcookie");
@@ -645,13 +644,13 @@ namespace NzbDrone.Common.Test.Http
requestSet.StoreRequestCookie = true;
requestSet.StoreResponseCookie = false;
var responseSet = Subject.Get<HttpCookieResource>(requestSet);
var responseSet = await Subject.GetAsync<HttpCookieResource>(requestSet);
responseSet.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "oldcookie");
@@ -659,14 +658,14 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_not_delete_response_cookie()
public async Task should_not_delete_response_cookie()
{
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.Cookies.Add("my", "cookie");
requestCookies.AllowAutoRedirect = false;
requestCookies.StoreRequestCookie = true;
requestCookies.StoreResponseCookie = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -675,13 +674,13 @@ namespace NzbDrone.Common.Test.Http
requestDelete.StoreRequestCookie = false;
requestDelete.StoreResponseCookie = false;
var responseDelete = Subject.Get(requestDelete);
var responseDelete = await Subject.GetAsync(requestDelete);
requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.StoreRequestCookie = false;
requestCookies.StoreResponseCookie = false;
responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -689,14 +688,14 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_delete_response_cookie()
public async Task should_delete_response_cookie()
{
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.Cookies.Add("my", "cookie");
requestCookies.AllowAutoRedirect = false;
requestCookies.StoreRequestCookie = true;
requestCookies.StoreResponseCookie = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -705,13 +704,13 @@ namespace NzbDrone.Common.Test.Http
requestDelete.StoreRequestCookie = false;
requestDelete.StoreResponseCookie = true;
var responseDelete = Subject.Get(requestDelete);
var responseDelete = await Subject.GetAsync(requestDelete);
requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.StoreRequestCookie = false;
requestCookies.StoreResponseCookie = false;
responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().BeEmpty();
@@ -719,14 +718,14 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_delete_temp_response_cookie()
public async Task should_delete_temp_response_cookie()
{
var requestCookies = new HttpRequest($"https://{_httpBinHost}/cookies");
requestCookies.Cookies.Add("my", "cookie");
requestCookies.AllowAutoRedirect = false;
requestCookies.StoreRequestCookie = true;
requestCookies.StoreResponseCookie = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
var responseCookies = await Subject.GetAsync<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
@@ -734,7 +733,7 @@ namespace NzbDrone.Common.Test.Http
requestDelete.AllowAutoRedirect = true;
requestDelete.StoreRequestCookie = false;
requestDelete.StoreResponseCookie = false;
var responseDelete = Subject.Get<HttpCookieResource>(requestDelete);
var responseDelete = await Subject.GetAsync<HttpCookieResource>(requestDelete);
responseDelete.Resource.Cookies.Should().BeEmpty();
@@ -752,13 +751,13 @@ namespace NzbDrone.Common.Test.Http
{
var request = new HttpRequest($"https://{_httpBinHost}/status/429");
Assert.Throws<TooManyRequestsException>(() => Subject.Get(request));
Assert.ThrowsAsync<TooManyRequestsException>(async () => await Subject.GetAsync(request));
ExceptionVerification.IgnoreWarns();
}
[Test]
public void should_call_interceptor()
public async Task should_call_interceptor()
{
Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(new[] { Mocker.GetMock<IHttpRequestInterceptor>().Object });
@@ -772,7 +771,7 @@ namespace NzbDrone.Common.Test.Http
var request = new HttpRequest($"https://{_httpBinHost}/get");
Subject.Get(request);
await Subject.GetAsync(request);
Mocker.GetMock<IHttpRequestInterceptor>()
.Verify(v => v.PreRequest(It.IsAny<HttpRequest>()), Times.Once());
@@ -783,7 +782,7 @@ namespace NzbDrone.Common.Test.Http
[TestCase("en-US")]
[TestCase("es-ES")]
public void should_parse_malformed_cloudflare_cookie(string culture)
public async Task should_parse_malformed_cloudflare_cookie(string culture)
{
var origCulture = Thread.CurrentThread.CurrentCulture;
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture);
@@ -799,11 +798,11 @@ namespace NzbDrone.Common.Test.Http
requestSet.AllowAutoRedirect = false;
requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet);
var responseSet = await Subject.GetAsync(requestSet);
var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("Cookie");
@@ -821,7 +820,7 @@ namespace NzbDrone.Common.Test.Http
}
[TestCase("lang_code=en; expires=Wed, 23-Dec-2026 18:09:14 GMT; Max-Age=31536000; path=/; domain=.abc.com")]
public void should_reject_malformed_domain_cookie(string malformedCookie)
public async Task should_reject_malformed_domain_cookie(string malformedCookie)
{
try
{
@@ -831,11 +830,11 @@ namespace NzbDrone.Common.Test.Http
requestSet.AllowAutoRedirect = false;
requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet);
var responseSet = await Subject.GetAsync(requestSet);
var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Get<HttpBinResource>(request);
var response = await Subject.GetAsync<HttpBinResource>(request);
response.Resource.Headers.Should().NotContainKey("Cookie");
@@ -847,12 +846,12 @@ namespace NzbDrone.Common.Test.Http
}
[Test]
public void should_correctly_use_basic_auth_with_basic_network_credential()
public async Task should_correctly_use_basic_auth()
{
var request = new HttpRequest($"https://{_httpBinHost}/basic-auth/username/password");
request.Credentials = new BasicNetworkCredential("username", "password");
var response = Subject.Execute(request);
var response = await Subject.ExecuteAsync(request);
response.StatusCode.Should().Be(HttpStatusCode.OK);
}

View File

@@ -1,9 +1,10 @@
using System.Net;
using System.Threading.Tasks;
namespace NzbDrone.Common.Http.Dispatchers
{
public interface IHttpDispatcher
{
HttpResponse GetResponse(HttpRequest request, CookieContainer cookies);
Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies);
}
}

View File

@@ -44,9 +44,13 @@ namespace NzbDrone.Common.Http.Dispatchers
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
}
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
{
var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url);
var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url)
{
Version = HttpVersion.Version20,
VersionPolicy = HttpVersionPolicy.RequestVersionOrLower
};
requestMessage.Headers.UserAgent.ParseAdd(_userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent));
requestMessage.Headers.ConnectionClose = !request.ConnectionKeepAlive;
@@ -99,7 +103,7 @@ namespace NzbDrone.Common.Http.Dispatchers
var httpClient = GetClient(request.Url);
using var responseMessage = httpClient.Send(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
{
byte[] data = null;
@@ -107,7 +111,7 @@ namespace NzbDrone.Common.Http.Dispatchers
{
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
{
responseMessage.Content.CopyTo(request.ResponseStream, null, cts.Token);
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
}
else
{
@@ -123,7 +127,7 @@ namespace NzbDrone.Common.Http.Dispatchers
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode);
return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode, responseMessage.Version);
}
}
@@ -160,6 +164,8 @@ namespace NzbDrone.Common.Http.Dispatchers
var client = new System.Net.Http.HttpClient(handler)
{
DefaultRequestVersion = HttpVersion.Version20,
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower,
Timeout = Timeout.InfiniteTimeSpan
};

View File

@@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
@@ -25,6 +26,16 @@ namespace NzbDrone.Common.Http
HttpResponse Post(HttpRequest request);
HttpResponse<T> Post<T>(HttpRequest request)
where T : new();
Task<HttpResponse> ExecuteAsync(HttpRequest request);
Task DownloadFileAsync(string url, string fileName, string userAgent = null);
Task<HttpResponse> GetAsync(HttpRequest request);
Task<HttpResponse<T>> GetAsync<T>(HttpRequest request)
where T : new();
Task<HttpResponse> HeadAsync(HttpRequest request);
Task<HttpResponse> PostAsync(HttpRequest request);
Task<HttpResponse<T>> PostAsync<T>(HttpRequest request)
where T : new();
}
public class HttpClient : IHttpClient
@@ -52,11 +63,11 @@ namespace NzbDrone.Common.Http
_cookieContainerCache = cacheManager.GetCache<CookieContainer>(typeof(HttpClient));
}
public HttpResponse Execute(HttpRequest request)
public virtual async Task<HttpResponse> ExecuteAsync(HttpRequest request)
{
var cookieContainer = InitializeRequestCookies(request);
var response = ExecuteRequest(request, cookieContainer);
var response = await ExecuteRequestAsync(request, cookieContainer);
if (request.AllowAutoRedirect && response.HasHttpRedirect)
{
@@ -82,7 +93,7 @@ namespace NzbDrone.Common.Http
request.ContentSummary = null;
}
response = ExecuteRequest(request, cookieContainer);
response = await ExecuteRequestAsync(request, cookieContainer);
}
while (response.HasHttpRedirect);
}
@@ -112,6 +123,11 @@ namespace NzbDrone.Common.Http
return response;
}
public HttpResponse Execute(HttpRequest request)
{
return ExecuteAsync(request).GetAwaiter().GetResult();
}
private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod)
{
return statusCode switch
@@ -122,7 +138,7 @@ namespace NzbDrone.Common.Http
};
}
private HttpResponse ExecuteRequest(HttpRequest request, CookieContainer cookieContainer)
private async Task<HttpResponse> ExecuteRequestAsync(HttpRequest request, CookieContainer cookieContainer)
{
foreach (var interceptor in _requestInterceptors)
{
@@ -131,14 +147,14 @@ namespace NzbDrone.Common.Http
if (request.RateLimit != TimeSpan.Zero)
{
_rateLimitService.WaitAndPulse(request.Url.Host, request.RateLimitKey, request.RateLimit);
await _rateLimitService.WaitAndPulseAsync(request.Url.Host, request.RateLimitKey, request.RateLimit);
}
_logger.Trace(request);
var stopWatch = Stopwatch.StartNew();
var response = _httpDispatcher.GetResponse(request, cookieContainer);
var response = await _httpDispatcher.GetResponseAsync(request, cookieContainer);
HandleResponseCookies(response, cookieContainer);
@@ -246,7 +262,7 @@ namespace NzbDrone.Common.Http
}
}
public void DownloadFile(string url, string fileName, string userAgent = null)
public async Task DownloadFileAsync(string url, string fileName, string userAgent = null)
{
var fileNamePart = fileName + ".part";
@@ -261,12 +277,13 @@ namespace NzbDrone.Common.Http
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
var stopWatch = Stopwatch.StartNew();
using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite))
await using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite))
{
var request = new HttpRequest(url);
request.AllowAutoRedirect = true;
request.ResponseStream = fileStream;
var response = Get(request);
request.RequestTimeout = TimeSpan.FromSeconds(300);
var response = await GetAsync(request);
if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html"))
{
@@ -293,38 +310,71 @@ namespace NzbDrone.Common.Http
}
}
public HttpResponse Get(HttpRequest request)
public void DownloadFile(string url, string fileName, string userAgent = null)
{
// https://docs.microsoft.com/en-us/archive/msdn-magazine/2015/july/async-programming-brownfield-async-development#the-thread-pool-hack
Task.Run(() => DownloadFileAsync(url, fileName, userAgent)).GetAwaiter().GetResult();
}
public Task<HttpResponse> GetAsync(HttpRequest request)
{
request.Method = HttpMethod.Get;
return Execute(request);
return ExecuteAsync(request);
}
public HttpResponse Get(HttpRequest request)
{
return Task.Run(() => GetAsync(request)).GetAwaiter().GetResult();
}
public async Task<HttpResponse<T>> GetAsync<T>(HttpRequest request)
where T : new()
{
var response = await GetAsync(request);
CheckResponseContentType(response);
return new HttpResponse<T>(response);
}
public HttpResponse<T> Get<T>(HttpRequest request)
where T : new()
{
var response = Get(request);
CheckResponseContentType(response);
return new HttpResponse<T>(response);
return Task.Run(() => GetAsync<T>(request)).GetAwaiter().GetResult();
}
public Task<HttpResponse> HeadAsync(HttpRequest request)
{
request.Method = HttpMethod.Head;
return ExecuteAsync(request);
}
public HttpResponse Head(HttpRequest request)
{
request.Method = HttpMethod.Head;
return Execute(request);
return Task.Run(() => HeadAsync(request)).GetAwaiter().GetResult();
}
public Task<HttpResponse> PostAsync(HttpRequest request)
{
request.Method = HttpMethod.Post;
return ExecuteAsync(request);
}
public HttpResponse Post(HttpRequest request)
{
request.Method = HttpMethod.Post;
return Execute(request);
return Task.Run(() => PostAsync(request)).GetAwaiter().GetResult();
}
public async Task<HttpResponse<T>> PostAsync<T>(HttpRequest request)
where T : new()
{
var response = await PostAsync(request);
CheckResponseContentType(response);
return new HttpResponse<T>(response);
}
public HttpResponse<T> Post<T>(HttpRequest request)
where T : new()
{
var response = Post(request);
CheckResponseContentType(response);
return new HttpResponse<T>(response);
return Task.Run(() => PostAsync<T>(request)).GetAwaiter().GetResult();
}
private void CheckResponseContentType(HttpResponse response)

View File

@@ -16,6 +16,7 @@ namespace NzbDrone.Common.Http
Method = HttpMethod.Get;
Url = new HttpUri(url);
Headers = new HttpHeader();
ConnectionKeepAlive = true;
AllowAutoRedirect = true;
StoreRequestCookie = true;
LogHttpError = true;

View File

@@ -9,28 +9,31 @@ namespace NzbDrone.Common.Http
{
public class HttpResponse
{
private static readonly Regex RegexSetCookie = new Regex("^(.*?)=(.*?)(?:;|$)", RegexOptions.Compiled);
private static readonly Regex RegexSetCookie = new ("^(.*?)=(.*?)(?:;|$)", RegexOptions.Compiled);
public HttpResponse(HttpRequest request, HttpHeader headers, byte[] binaryData, HttpStatusCode statusCode = HttpStatusCode.OK)
public HttpResponse(HttpRequest request, HttpHeader headers, byte[] binaryData, HttpStatusCode statusCode = HttpStatusCode.OK, Version version = null)
{
Request = request;
Headers = headers;
ResponseData = binaryData;
StatusCode = statusCode;
Version = version;
}
public HttpResponse(HttpRequest request, HttpHeader headers, string content, HttpStatusCode statusCode = HttpStatusCode.OK)
public HttpResponse(HttpRequest request, HttpHeader headers, string content, HttpStatusCode statusCode = HttpStatusCode.OK, Version version = null)
{
Request = request;
Headers = headers;
ResponseData = Headers.GetEncodingFromContentType().GetBytes(content);
_content = content;
StatusCode = statusCode;
Version = version;
}
public HttpRequest Request { get; private set; }
public HttpHeader Headers { get; private set; }
public HttpStatusCode StatusCode { get; private set; }
public Version Version { get; private set; }
public byte[] ResponseData { get; private set; }
private string _content;
@@ -84,7 +87,7 @@ namespace NzbDrone.Common.Http
public override string ToString()
{
var result = string.Format("Res: [{0}] {1}: {2}.{3} ({4} bytes)", Request.Method, Request.Url, (int)StatusCode, StatusCode, ResponseData?.Length ?? 0);
var result = $"Res: HTTP/{Version} [{Request.Method}] {Request.Url}: {(int)StatusCode}.{StatusCode} ({ResponseData?.Length ?? 0} bytes)";
if (HasHttpError && Headers.ContentType.IsNotNullOrWhiteSpace() && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase))
{
@@ -99,7 +102,7 @@ namespace NzbDrone.Common.Http
where T : new()
{
public HttpResponse(HttpResponse response)
: base(response.Request, response.Headers, response.ResponseData, response.StatusCode)
: base(response.Request, response.Headers, response.ResponseData, response.StatusCode, response.Version)
{
Resource = Json.Deserialize<T>(response.Content);
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
@@ -10,6 +11,8 @@ namespace NzbDrone.Common.TPL
{
void WaitAndPulse(string key, TimeSpan interval);
void WaitAndPulse(string key, string subKey, TimeSpan interval);
Task WaitAndPulseAsync(string key, TimeSpan interval);
Task WaitAndPulseAsync(string key, string subKey, TimeSpan interval);
}
public class RateLimitService : IRateLimitService
@@ -28,7 +31,34 @@ namespace NzbDrone.Common.TPL
WaitAndPulse(key, null, interval);
}
public async Task WaitAndPulseAsync(string key, TimeSpan interval)
{
await WaitAndPulseAsync(key, null, interval);
}
public void WaitAndPulse(string key, string subKey, TimeSpan interval)
{
var delay = GetDelay(key, subKey, interval);
if (delay.TotalSeconds > 0.0)
{
_logger.Trace("Rate Limit triggered, delaying '{0}' for {1:0.000} sec", key, delay.TotalSeconds);
System.Threading.Thread.Sleep(delay);
}
}
public async Task WaitAndPulseAsync(string key, string subKey, TimeSpan interval)
{
var delay = GetDelay(key, subKey, interval);
if (delay.TotalSeconds > 0.0)
{
_logger.Trace("Rate Limit triggered, delaying '{0}' for {1:0.000} sec", key, delay.TotalSeconds);
await Task.Delay(delay);
}
}
private TimeSpan GetDelay(string key, string subKey, TimeSpan interval)
{
var waitUntil = DateTime.UtcNow.Add(interval);
@@ -59,13 +89,7 @@ namespace NzbDrone.Common.TPL
waitUntil -= interval;
var delay = waitUntil - DateTime.UtcNow;
if (delay.TotalSeconds > 0.0)
{
_logger.Trace("Rate Limit triggered, delaying '{0}' for {1:0.000} sec", key, delay.TotalSeconds);
System.Threading.Thread.Sleep(delay);
}
return waitUntil - DateTime.UtcNow;
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
@@ -58,7 +59,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
}
[Test]
public void should_download_report_if_book_was_not_already_downloaded()
public async Task should_download_report_if_book_was_not_already_downloaded()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -66,12 +67,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteBook));
Subject.ProcessDecisions(decisions);
await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
}
[Test]
public void should_only_download_book_once()
public async Task should_only_download_book_once()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -80,12 +81,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteBook));
decisions.Add(new DownloadDecision(remoteBook));
Subject.ProcessDecisions(decisions);
await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
}
[Test]
public void should_not_download_if_any_book_was_already_downloaded()
public async Task should_not_download_if_any_book_was_already_downloaded()
{
var remoteBook1 = GetRemoteBook(
new List<Book> { GetBook(1) },
@@ -99,12 +100,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteBook1));
decisions.Add(new DownloadDecision(remoteBook2));
Subject.ProcessDecisions(decisions);
await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
}
[Test]
public void should_return_downloaded_reports()
public async Task should_return_downloaded_reports()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -112,11 +113,13 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteBook));
Subject.ProcessDecisions(decisions).Grabbed.Should().HaveCount(1);
var result = await Subject.ProcessDecisions(decisions);
result.Grabbed.Should().HaveCount(1);
}
[Test]
public void should_return_all_downloaded_reports()
public async Task should_return_all_downloaded_reports()
{
var remoteBook1 = GetRemoteBook(
new List<Book> { GetBook(1) },
@@ -130,11 +133,13 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteBook1));
decisions.Add(new DownloadDecision(remoteBook2));
Subject.ProcessDecisions(decisions).Grabbed.Should().HaveCount(2);
var result = await Subject.ProcessDecisions(decisions);
result.Grabbed.Should().HaveCount(2);
}
[Test]
public void should_only_return_downloaded_reports()
public async Task should_only_return_downloaded_reports()
{
var remoteBook1 = GetRemoteBook(
new List<Book> { GetBook(1) },
@@ -153,11 +158,13 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteBook2));
decisions.Add(new DownloadDecision(remoteBook3));
Subject.ProcessDecisions(decisions).Grabbed.Should().HaveCount(2);
var result = await Subject.ProcessDecisions(decisions);
result.Grabbed.Should().HaveCount(2);
}
[Test]
public void should_not_add_to_downloaded_list_when_download_fails()
public async Task should_not_add_to_downloaded_list_when_download_fails()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -166,7 +173,11 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteBook));
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteBook>())).Throws(new Exception());
Subject.ProcessDecisions(decisions).Grabbed.Should().BeEmpty();
var result = await Subject.ProcessDecisions(decisions);
result.Grabbed.Should().BeEmpty();
ExceptionVerification.ExpectedWarns(1);
}
@@ -181,7 +192,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
}
[Test]
public void should_not_grab_if_pending()
public async Task should_not_grab_if_pending()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -189,12 +200,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteBook, new Rejection("Failure!", RejectionType.Temporary)));
Subject.ProcessDecisions(decisions);
await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Never());
}
[Test]
public void should_not_add_to_pending_if_book_was_grabbed()
public async Task should_not_add_to_pending_if_book_was_grabbed()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -203,12 +214,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteBook));
decisions.Add(new DownloadDecision(remoteBook, new Rejection("Failure!", RejectionType.Temporary)));
Subject.ProcessDecisions(decisions);
await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Never());
}
[Test]
public void should_add_to_pending_even_if_already_added_to_pending()
public async Task should_add_to_pending_even_if_already_added_to_pending()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -217,12 +228,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteBook, new Rejection("Failure!", RejectionType.Temporary)));
decisions.Add(new DownloadDecision(remoteBook, new Rejection("Failure!", RejectionType.Temporary)));
Subject.ProcessDecisions(decisions);
await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Once());
}
[Test]
public void should_add_to_failed_if_already_failed_for_that_protocol()
public async Task should_add_to_failed_if_already_failed_for_that_protocol()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -234,12 +245,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteBook>()))
.Throws(new DownloadClientUnavailableException("Download client failed"));
Subject.ProcessDecisions(decisions);
await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
}
[Test]
public void should_not_add_to_failed_if_failed_for_a_different_protocol()
public async Task should_not_add_to_failed_if_failed_for_a_different_protocol()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3), DownloadProtocol.Usenet);
@@ -252,13 +263,13 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)))
.Throws(new DownloadClientUnavailableException("Download client failed"));
Subject.ProcessDecisions(decisions);
await Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)), Times.Once());
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Torrent)), Times.Once());
}
[Test]
public void should_add_to_rejected_if_release_unavailable_on_indexer()
public async Task should_add_to_rejected_if_release_unavailable_on_indexer()
{
var books = new List<Book> { GetBook(1) };
var remoteBook = GetRemoteBook(books, new QualityModel(Quality.MP3));
@@ -270,7 +281,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
.Setup(s => s.DownloadReport(It.IsAny<RemoteBook>()))
.Throws(new ReleaseUnavailableException(remoteBook.Release, "That 404 Error is not just a Quirk"));
var result = Subject.ProcessDecisions(decisions);
var result = await Subject.ProcessDecisions(decisions);
result.Grabbed.Should().BeEmpty();
result.Rejected.Should().NotBeEmpty();

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
@@ -69,7 +70,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
protected void GivenFailedDownload()
{
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Setup(s => s.GetAsync(It.IsAny<HttpRequest>()))
.Throws(new WebException());
}
@@ -147,19 +148,19 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
}
[Test]
public void Download_should_download_file_if_it_doesnt_exist()
public async Task Download_should_download_file_if_it_doesnt_exist()
{
var remoteBook = CreateRemoteBook();
Subject.Download(remoteBook, CreateIndexer());
await Subject.Download(remoteBook, CreateIndexer());
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.GetAsync(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_filePath), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
}
[Test]
public void Download_should_save_magnet_if_enabled()
public async Task Download_should_save_magnet_if_enabled()
{
GivenMagnetFilePath();
Subject.Definition.Settings.As<TorrentBlackholeSettings>().SaveMagnetFiles = true;
@@ -167,16 +168,16 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
var remoteBook = CreateRemoteBook();
remoteBook.Release.DownloadUrl = null;
Subject.Download(remoteBook, CreateIndexer());
await Subject.Download(remoteBook, CreateIndexer());
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.GetAsync(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Never());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_filePath), Times.Never());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_magnetFilePath), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
}
[Test]
public void Download_should_save_magnet_using_specified_extension()
public async Task Download_should_save_magnet_using_specified_extension()
{
var magnetFileExtension = ".url";
GivenMagnetFilePath(magnetFileExtension);
@@ -187,12 +188,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
var remoteBook = CreateRemoteBook();
remoteBook.Release.DownloadUrl = null;
Subject.Download(remoteBook, CreateIndexer());
await Subject.Download(remoteBook, CreateIndexer());
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.GetAsync(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Never());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_filePath), Times.Never());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_magnetFilePath), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
}
[Test]
@@ -202,31 +203,31 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
var remoteBook = CreateRemoteBook();
remoteBook.Release.DownloadUrl = null;
Assert.Throws<ReleaseDownloadException>(() => Subject.Download(remoteBook, CreateIndexer()));
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.Download(remoteBook, CreateIndexer()));
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.GetAsync(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Never());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_filePath), Times.Never());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_magnetFilePath), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
}
[Test]
public void Download_should_prefer_torrent_over_magnet()
public async Task Download_should_prefer_torrent_over_magnet()
{
Subject.Definition.Settings.As<TorrentBlackholeSettings>().SaveMagnetFiles = true;
var remoteBook = CreateRemoteBook();
Subject.Download(remoteBook, CreateIndexer());
await Subject.Download(remoteBook, CreateIndexer());
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.GetAsync(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_filePath), Times.Once());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_magnetFilePath), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
}
[Test]
public void Download_should_replace_illegal_characters_in_title()
public async Task Download_should_replace_illegal_characters_in_title()
{
var illegalTitle = "Radiohead - Scotch Mist [2008/FLAC/Lossless]";
var expectedFilename = Path.Combine(_blackholeFolder, "Radiohead - Scotch Mist [2008+FLAC+Lossless]" + Path.GetExtension(_filePath));
@@ -234,11 +235,11 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
var remoteBook = CreateRemoteBook();
remoteBook.Release.Title = illegalTitle;
Subject.Download(remoteBook, CreateIndexer());
await Subject.Download(remoteBook, CreateIndexer());
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.GetAsync(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(expectedFilename), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
}
[Test]
@@ -247,7 +248,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
var remoteBook = CreateRemoteBook();
remoteBook.Release.DownloadUrl = null;
Assert.Throws<ReleaseDownloadException>(() => Subject.Download(remoteBook, CreateIndexer()));
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.Download(remoteBook, CreateIndexer()));
}
[Test]
@@ -317,11 +318,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
}
[Test]
public void should_return_null_hash()
public async Task should_return_null_hash()
{
var remoteBook = CreateRemoteBook();
Subject.Download(remoteBook, CreateIndexer()).Should().BeNull();
var result = await Subject.Download(remoteBook, CreateIndexer());
result.Should().BeNull();
}
}
}

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
@@ -119,19 +120,19 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
}
[Test]
public void Download_should_download_file_if_it_doesnt_exist()
public async Task Download_should_download_file_if_it_doesnt_exist()
{
var remoteBook = CreateRemoteBook();
Subject.Download(remoteBook, CreateIndexer());
await Subject.Download(remoteBook, CreateIndexer());
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.GetAsync(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_filePath), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
}
[Test]
public void Download_should_replace_illegal_characters_in_title()
public async Task Download_should_replace_illegal_characters_in_title()
{
var illegalTitle = "Radiohead - Scotch Mist [2008/FLAC/Lossless]";
var expectedFilename = Path.Combine(_blackholeFolder, "Radiohead - Scotch Mist [2008+FLAC+Lossless]" + Path.GetExtension(_filePath));
@@ -139,11 +140,11 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
var remoteBook = CreateRemoteBook();
remoteBook.Release.Title = illegalTitle;
Subject.Download(remoteBook, CreateIndexer());
await Subject.Download(remoteBook, CreateIndexer());
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.GetAsync(It.Is<HttpRequest>(v => v.Url.FullUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(expectedFilename), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never());
}
[Test]

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -200,26 +201,26 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
}
[Test]
public void Download_should_return_unique_id()
public async Task Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}
[TestCase("magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp", "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951")]
public void Download_should_get_hash_from_magnet_url(string magnetUrl, string expectedHash)
public async Task Download_should_get_hash_from_magnet_url(string magnetUrl, string expectedHash)
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
remoteBook.Release.DownloadUrl = magnetUrl;
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().Be(expectedHash);
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NLog;
@@ -36,8 +37,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests
.Returns(() => CreateRemoteBook());
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[0]));
.Setup(s => s.GetAsync(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), Array.Empty<byte>())));
Mocker.GetMock<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal(It.IsAny<string>(), It.IsAny<OsPath>()))

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -385,7 +386,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
}
[Test]
public void Download_with_TvDirectory_should_force_directory()
public async Task Download_with_TvDirectory_should_force_directory()
{
GivenSerialNumber();
GivenTvDirectory();
@@ -393,7 +394,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -402,7 +403,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
}
[Test]
public void Download_with_category_should_force_directory()
public async Task Download_with_category_should_force_directory()
{
GivenSerialNumber();
GivenMusicCategory();
@@ -410,7 +411,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -419,14 +420,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
}
[Test]
public void Download_without_TvDirectory_and_Category_should_use_default()
public async Task Download_without_TvDirectory_and_Category_should_use_default()
{
GivenSerialNumber();
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -505,7 +506,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
.Setup(s => s.GetSerialNumber(_settings))
.Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException"));
Assert.Throws(Is.InstanceOf<Exception>(), () => Subject.Download(remoteBook, CreateIndexer()));
Assert.ThrowsAsync(Is.InstanceOf<Exception>(), async () => await Subject.Download(remoteBook, CreateIndexer()));
Mocker.GetMock<IDownloadStationTaskProxy>()
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), null, _settings), Times.Never());

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -262,7 +263,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
}
[Test]
public void Download_with_TvDirectory_should_force_directory()
public async Task Download_with_TvDirectory_should_force_directory()
{
GivenSerialNumber();
GivenTvDirectory();
@@ -270,7 +271,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -279,7 +280,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
}
[Test]
public void Download_with_category_should_force_directory()
public async Task Download_with_category_should_force_directory()
{
GivenSerialNumber();
GivenMusicCategory();
@@ -287,7 +288,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -296,14 +297,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
}
[Test]
public void Download_without_TvDirectory_and_Category_should_use_default()
public async Task Download_without_TvDirectory_and_Category_should_use_default()
{
GivenSerialNumber();
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -382,7 +383,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
.Setup(s => s.GetSerialNumber(_settings))
.Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException"));
Assert.Throws(Is.InstanceOf<Exception>(), () => Subject.Download(remoteBook, CreateIndexer()));
Assert.ThrowsAsync(Is.InstanceOf<Exception>(), async () => await Subject.Download(remoteBook, CreateIndexer()));
Mocker.GetMock<IDownloadStationTaskProxy>()
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), null, _settings), Times.Never());

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -103,8 +104,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests
protected void GivenSuccessfulDownload()
{
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[1000]));
.Setup(s => s.GetAsync(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), new byte[1000])));
Mocker.GetMock<IHadoukenProxy>()
.Setup(s => s.AddTorrentUri(It.IsAny<HadoukenSettings>(), It.IsAny<string>()))
@@ -196,13 +197,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests
}
[Test]
public void Download_should_return_unique_id()
public async Task Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}
@@ -277,7 +278,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests
}
[Test]
public void Download_from_magnet_link_should_return_hash_uppercase()
public async Task Download_from_magnet_link_should_return_hash_uppercase()
{
var remoteBook = CreateRemoteBook();
@@ -286,13 +287,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests
Mocker.GetMock<IHadoukenProxy>()
.Setup(v => v.AddTorrentUri(It.IsAny<HadoukenSettings>(), It.IsAny<string>()));
var result = Subject.Download(remoteBook, CreateIndexer());
var result = await Subject.Download(remoteBook, CreateIndexer());
Assert.IsFalse(result.Any(c => char.IsLower(c)));
}
[Test]
public void Download_from_torrent_file_should_return_hash_uppercase()
public async Task Download_from_torrent_file_should_return_hash_uppercase()
{
var remoteBook = CreateRemoteBook();
@@ -300,7 +301,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests
.Setup(v => v.AddTorrentFile(It.IsAny<HadoukenSettings>(), It.IsAny<byte[]>()))
.Returns("hash");
var result = Subject.Download(remoteBook, CreateIndexer());
var result = await Subject.Download(remoteBook, CreateIndexer());
Assert.IsFalse(result.Any(c => char.IsLower(c)));
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -200,13 +201,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbVortexTests
}
[Test]
public void Download_should_return_unique_id()
public async Task Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}
@@ -218,7 +219,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbVortexTests
var remoteBook = CreateRemoteBook();
Assert.Throws<DownloadClientException>(() => Subject.Download(remoteBook, CreateIndexer()));
Assert.ThrowsAsync<DownloadClientException>(async () => await Subject.Download(remoteBook, CreateIndexer()));
}
[Test]

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
@@ -339,13 +340,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
}
[Test]
public void Download_should_return_unique_id()
public async Task Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}
@@ -357,7 +358,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
var remoteBook = CreateRemoteBook();
Assert.Throws<DownloadClientRejectedReleaseException>(() => Subject.Download(remoteBook, CreateIndexer()));
Assert.ThrowsAsync<DownloadClientRejectedReleaseException>(async () => await Subject.Download(remoteBook, CreateIndexer()));
}
[Test]

View File

@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using FizzWare.NBuilder;
using Moq;
using NLog;
@@ -65,15 +66,15 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests
private void WithFailedDownload()
{
Mocker.GetMock<IHttpClient>().Setup(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Throws(new WebException());
Mocker.GetMock<IHttpClient>().Setup(c => c.DownloadFileAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Throws(new WebException());
}
[Test]
public void should_download_file_if_it_doesnt_exist()
public async Task should_download_file_if_it_doesnt_exist()
{
Subject.Download(_remoteBook, _indexer);
await Subject.Download(_remoteBook, _indexer);
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(_nzbUrl, _nzbPath, null), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(_nzbUrl, _nzbPath, null), Times.Once());
}
[Test]
@@ -81,7 +82,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests
{
WithFailedDownload();
Assert.Throws<WebException>(() => Subject.Download(_remoteBook, _indexer));
Assert.ThrowsAsync<WebException>(async () => await Subject.Download(_remoteBook, _indexer));
}
[Test]
@@ -90,7 +91,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests
_remoteBook.Release.Title = "Alien Ant Farm - Discography";
_remoteBook.ParsedBookInfo.Discography = true;
Assert.Throws<NotSupportedException>(() => Subject.Download(_remoteBook, _indexer));
Assert.ThrowsAsync<NotSupportedException>(async () => await Subject.Download(_remoteBook, _indexer));
}
[Test]
@@ -100,15 +101,15 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests
}
[Test]
public void should_replace_illegal_characters_in_title()
public async Task should_replace_illegal_characters_in_title()
{
var illegalTitle = "Saturday Night Live - S38E08 - Jeremy Renner/Maroon 5 [SDTV]";
var expectedFilename = Path.Combine(_pneumaticFolder, "Saturday Night Live - S38E08 - Jeremy Renner+Maroon 5 [SDTV].nzb");
_remoteBook.Release.Title = illegalTitle;
Subject.Download(_remoteBook, _indexer);
await Subject.Download(_remoteBook, _indexer);
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), expectedFilename, null), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFileAsync(It.IsAny<string>(), expectedFilename, null), Times.Once());
}
}
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -449,26 +450,26 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
}
[Test]
public void Download_should_return_unique_id()
public async Task Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}
[TestCase("magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp", "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951")]
public void Download_should_get_hash_from_magnet_url(string magnetUrl, string expectedHash)
public async Task Download_should_get_hash_from_magnet_url(string magnetUrl, string expectedHash)
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
remoteBook.Release.DownloadUrl = magnetUrl;
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().Be(expectedHash);
}
@@ -483,7 +484,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
var remoteBook = CreateRemoteBook();
remoteBook.Release.DownloadUrl = "magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR";
Assert.Throws<ReleaseDownloadException>(() => Subject.Download(remoteBook, CreateIndexer()));
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.Download(remoteBook, CreateIndexer()));
}
[Test]
@@ -496,28 +497,28 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
var remoteBook = CreateRemoteBook();
remoteBook.Release.DownloadUrl = "magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp://abc";
Assert.DoesNotThrow(() => Subject.Download(remoteBook, CreateIndexer()));
Assert.DoesNotThrowAsync(async () => await Subject.Download(remoteBook, CreateIndexer()));
Mocker.GetMock<IQBittorrentProxy>()
.Verify(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<TorrentSeedConfiguration>(), It.IsAny<QBittorrentSettings>()), Times.Once());
}
[Test]
public void Download_should_set_top_priority()
public async Task Download_should_set_top_priority()
{
GivenHighPriority();
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
Mocker.GetMock<IQBittorrentProxy>()
.Verify(v => v.MoveTorrentToTopInQueue(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once());
}
[Test]
public void Download_should_not_fail_if_top_priority_not_available()
public async Task Download_should_not_fail_if_top_priority_not_available()
{
GivenHighPriority();
GivenSuccessfulDownload();
@@ -528,7 +529,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -555,27 +556,27 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
}
[Test]
public void Download_should_handle_http_redirect_to_magnet()
public async Task Download_should_handle_http_redirect_to_magnet()
{
GivenRedirectToMagnet();
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}
[Test]
public void Download_should_handle_http_redirect_to_torrent()
public async Task Download_should_handle_http_redirect_to_torrent()
{
GivenRedirectToTorrent();
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}
@@ -633,7 +634,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
.Returns(new QBittorrentTorrentProperties
{
Hash = "HASH",
SeedingTime = seedingTime
SeedingTime = seedingTime * 60
});
return torrent;

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -121,13 +122,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.RTorrentTests
}
[Test]
public void Download_should_return_unique_id()
public async Task Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
@@ -300,27 +301,27 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
}
[TestCase("[ TOWN ]-[ http://www.town.ag ]-[ ANIME ]-[Usenet Provider >> http://www.ssl- <<] - [Commie] Aldnoah Zero 18 [234C8FC7]", "[ TOWN ]-[ http-++www.town.ag ]-[ ANIME ]-[Usenet Provider http-++www.ssl- ] - [Commie] Aldnoah Zero 18 [234C8FC7].nzb")]
public void Download_should_use_clean_title(string title, string filename)
public async Task Download_should_use_clean_title(string title, string filename)
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
remoteBook.Release.Title = title;
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
Mocker.GetMock<ISabnzbdProxy>()
.Verify(v => v.DownloadNzb(It.IsAny<byte[]>(), filename, It.IsAny<string>(), It.IsAny<int>(), It.IsAny<SabnzbdSettings>()), Times.Once());
}
[Test]
public void Download_should_return_unique_id()
public async Task Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}
@@ -353,7 +354,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
}
[Test]
public void Download_should_use_sabRecentTvPriority_when_recentEpisode_is_true()
public async Task Download_should_use_sabRecentTvPriority_when_recentEpisode_is_true()
{
Mocker.GetMock<ISabnzbdProxy>()
.Setup(s => s.DownloadNzb(It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<string>(), (int)SabnzbdPriority.High, It.IsAny<SabnzbdSettings>()))
@@ -366,7 +367,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
.Build()
.ToList();
Subject.Download(remoteBook, CreateIndexer());
await Subject.Download(remoteBook, CreateIndexer());
Mocker.GetMock<ISabnzbdProxy>()
.Verify(v => v.DownloadNzb(It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<string>(), (int)SabnzbdPriority.High, It.IsAny<SabnzbdSettings>()), Times.Once());

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -55,26 +56,26 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
}
[Test]
public void Download_should_return_unique_id()
public async Task Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}
[Test]
public void Download_with_TvDirectory_should_force_directory()
public async Task Download_with_TvDirectory_should_force_directory()
{
GivenTvDirectory();
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -83,14 +84,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
}
[Test]
public void Download_with_category_should_force_directory()
public async Task Download_with_category_should_force_directory()
{
GivenMusicCategory();
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -99,7 +100,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
}
[Test]
public void Download_with_category_should_not_have_double_slashes()
public async Task Download_with_category_should_not_have_double_slashes()
{
GivenMusicCategory();
GivenSuccessfulDownload();
@@ -108,7 +109,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -117,13 +118,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
}
[Test]
public void Download_without_TvDirectory_and_Category_should_use_default()
public async Task Download_without_TvDirectory_and_Category_should_use_default()
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -132,14 +133,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
}
[TestCase("magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp", "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951")]
public void Download_should_get_hash_from_magnet_url(string magnetUrl, string expectedHash)
public async Task Download_should_get_hash_from_magnet_url(string magnetUrl, string expectedHash)
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
remoteBook.Release.DownloadUrl = magnetUrl;
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().Be(expectedHash);
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -228,13 +229,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests
}
[Test]
public void Download_should_return_unique_id()
public async Task Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}
@@ -252,14 +253,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests
// Proxy.GetTorrents does not return original url. So item has to be found via magnet url.
[TestCase("magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp", "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951")]
public void Download_should_get_hash_from_magnet_url(string magnetUrl, string expectedHash)
public async Task Download_should_get_hash_from_magnet_url(string magnetUrl, string expectedHash)
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
remoteBook.Release.DownloadUrl = magnetUrl;
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().Be(expectedHash);
}
@@ -350,27 +351,27 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests
}
[Test]
public void Download_should_handle_http_redirect_to_magnet()
public async Task Download_should_handle_http_redirect_to_magnet()
{
GivenRedirectToMagnet();
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}
[Test]
public void Download_should_handle_http_redirect_to_torrent()
public async Task Download_should_handle_http_redirect_to_torrent()
{
GivenRedirectToTorrent();
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -63,26 +64,26 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
}
[Test]
public void Download_should_return_unique_id()
public async Task Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
}
[Test]
public void Download_with_TvDirectory_should_force_directory()
public async Task Download_with_TvDirectory_should_force_directory()
{
GivenTvDirectory();
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -91,14 +92,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
}
[Test]
public void Download_with_category_should_force_directory()
public async Task Download_with_category_should_force_directory()
{
GivenMusicCategory();
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -107,7 +108,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
}
[Test]
public void Download_with_category_should_not_have_double_slashes()
public async Task Download_with_category_should_not_have_double_slashes()
{
GivenMusicCategory();
GivenSuccessfulDownload();
@@ -116,7 +117,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -125,13 +126,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
}
[Test]
public void Download_without_TvDirectory_and_Category_should_use_default()
public async Task Download_without_TvDirectory_and_Category_should_use_default()
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().NotBeNullOrEmpty();
@@ -140,14 +141,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
}
[TestCase("magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp", "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951")]
public void Download_should_get_hash_from_magnet_url(string magnetUrl, string expectedHash)
public async Task Download_should_get_hash_from_magnet_url(string magnetUrl, string expectedHash)
{
GivenSuccessfulDownload();
var remoteBook = CreateRemoteBook();
remoteBook.Release.DownloadUrl = magnetUrl;
var id = Subject.Download(remoteBook, CreateIndexer());
var id = await Subject.Download(remoteBook, CreateIndexer());
id.Should().Be(expectedHash);
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using FizzWare.NBuilder;
using Moq;
using NUnit.Framework;
@@ -77,23 +78,23 @@ namespace NzbDrone.Core.Test.Download
}
[Test]
public void Download_report_should_publish_on_grab_event()
public async Task Download_report_should_publish_on_grab_event()
{
var mock = WithUsenetClient();
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()));
Subject.DownloadReport(_parseResult);
await Subject.DownloadReport(_parseResult);
VerifyEventPublished<BookGrabbedEvent>();
}
[Test]
public void Download_report_should_grab_using_client()
public async Task Download_report_should_grab_using_client()
{
var mock = WithUsenetClient();
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()));
Subject.DownloadReport(_parseResult);
await Subject.DownloadReport(_parseResult);
mock.Verify(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
}
@@ -105,7 +106,7 @@ namespace NzbDrone.Core.Test.Download
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()))
.Throws(new WebException());
Assert.Throws<WebException>(() => Subject.DownloadReport(_parseResult));
Assert.ThrowsAsync<WebException>(async () => await Subject.DownloadReport(_parseResult));
VerifyEventNotPublished<BookGrabbedEvent>();
}
@@ -120,7 +121,7 @@ namespace NzbDrone.Core.Test.Download
throw new ReleaseDownloadException(v.Release, "Error", new WebException());
});
Assert.Throws<ReleaseDownloadException>(() => Subject.DownloadReport(_parseResult));
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult));
Mocker.GetMock<IIndexerStatusService>()
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Once());
@@ -140,7 +141,7 @@ namespace NzbDrone.Core.Test.Download
throw new ReleaseDownloadException(v.Release, "Error", new TooManyRequestsException(request, response));
});
Assert.Throws<ReleaseDownloadException>(() => Subject.DownloadReport(_parseResult));
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult));
Mocker.GetMock<IIndexerStatusService>()
.Verify(v => v.RecordFailure(It.IsAny<int>(), TimeSpan.FromMinutes(5.0)), Times.Once());
@@ -160,7 +161,7 @@ namespace NzbDrone.Core.Test.Download
throw new ReleaseDownloadException(v.Release, "Error", new TooManyRequestsException(request, response));
});
Assert.Throws<ReleaseDownloadException>(() => Subject.DownloadReport(_parseResult));
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult));
Mocker.GetMock<IIndexerStatusService>()
.Verify(v => v.RecordFailure(It.IsAny<int>(),
@@ -174,7 +175,7 @@ namespace NzbDrone.Core.Test.Download
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()))
.Throws(new DownloadClientException("Some Error"));
Assert.Throws<DownloadClientException>(() => Subject.DownloadReport(_parseResult));
Assert.ThrowsAsync<DownloadClientException>(async () => await Subject.DownloadReport(_parseResult));
Mocker.GetMock<IIndexerStatusService>()
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Never());
@@ -190,7 +191,7 @@ namespace NzbDrone.Core.Test.Download
throw new ReleaseUnavailableException(v.Release, "Error", new WebException());
});
Assert.Throws<ReleaseUnavailableException>(() => Subject.DownloadReport(_parseResult));
Assert.ThrowsAsync<ReleaseUnavailableException>(async () => await Subject.DownloadReport(_parseResult));
Mocker.GetMock<IIndexerStatusService>()
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Never());
@@ -199,14 +200,14 @@ namespace NzbDrone.Core.Test.Download
[Test]
public void should_not_attempt_download_if_client_isnt_configured()
{
Assert.Throws<DownloadClientUnavailableException>(() => Subject.DownloadReport(_parseResult));
Assert.ThrowsAsync<DownloadClientUnavailableException>(async () => await Subject.DownloadReport(_parseResult));
Mocker.GetMock<IDownloadClient>().Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
VerifyEventNotPublished<BookGrabbedEvent>();
}
[Test]
public void should_attempt_download_even_if_client_is_disabled()
public async Task should_attempt_download_even_if_client_is_disabled()
{
var mockUsenet = WithUsenetClient();
@@ -221,7 +222,7 @@ namespace NzbDrone.Core.Test.Download
}
});
Subject.DownloadReport(_parseResult);
await Subject.DownloadReport(_parseResult);
Mocker.GetMock<IDownloadClientStatusService>().Verify(c => c.GetBlockedProviders(), Times.Never());
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
@@ -229,26 +230,26 @@ namespace NzbDrone.Core.Test.Download
}
[Test]
public void should_send_download_to_correct_usenet_client()
public async Task should_send_download_to_correct_usenet_client()
{
var mockTorrent = WithTorrentClient();
var mockUsenet = WithUsenetClient();
Subject.DownloadReport(_parseResult);
await Subject.DownloadReport(_parseResult);
mockTorrent.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
}
[Test]
public void should_send_download_to_correct_torrent_client()
public async Task should_send_download_to_correct_torrent_client()
{
var mockTorrent = WithTorrentClient();
var mockUsenet = WithUsenetClient();
_parseResult.Release.DownloadProtocol = DownloadProtocol.Torrent;
Subject.DownloadReport(_parseResult);
await Subject.DownloadReport(_parseResult);
mockTorrent.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Books;
@@ -27,11 +28,11 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
Mocker.GetMock<ISearchForReleases>()
.Setup(s => s.AuthorSearch(_author.Id, false, true, false))
.Returns(new List<DownloadDecision>());
.Returns(Task.FromResult(new List<DownloadDecision>()));
Mocker.GetMock<IProcessDownloadDecisions>()
.Setup(s => s.ProcessDecisions(It.IsAny<List<DownloadDecision>>()))
.Returns(new ProcessedDecisions(new List<DownloadDecision>(), new List<DownloadDecision>(), new List<DownloadDecision>()));
.Returns(Task.FromResult(new ProcessedDecisions(new List<DownloadDecision>(), new List<DownloadDecision>(), new List<DownloadDecision>())));
}
[Test]

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
@@ -60,13 +61,13 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
_mockIndexer.Setup(v => v.Fetch(It.IsAny<BookSearchCriteria>()))
.Callback<BookSearchCriteria>(s => result.Add(s))
.Returns(new List<Parser.Model.ReleaseInfo>());
.Returns(Task.FromResult<IList<Parser.Model.ReleaseInfo>>(new List<Parser.Model.ReleaseInfo>()));
return result;
}
[Test]
public void Tags_IndexerTags_AuthorNoTags_IndexerNotIncluded()
public async Task Tags_IndexerTags_AuthorNoTags_IndexerNotIncluded()
{
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition
{
@@ -76,7 +77,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
var allCriteria = WatchForSearchCriteria();
Subject.BookSearch(_firstBook, false, true, false);
await Subject.BookSearch(_firstBook, false, true, false);
var criteria = allCriteria.OfType<BookSearchCriteria>().ToList();
@@ -84,7 +85,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
}
[Test]
public void Tags_IndexerNoTags_AuthorTags_IndexerIncluded()
public async Task Tags_IndexerNoTags_AuthorTags_IndexerIncluded()
{
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition
{
@@ -102,7 +103,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
var allCriteria = WatchForSearchCriteria();
Subject.BookSearch(_firstBook, false, true, false);
await Subject.BookSearch(_firstBook, false, true, false);
var criteria = allCriteria.OfType<BookSearchCriteria>().ToList();
@@ -110,7 +111,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
}
[Test]
public void Tags_IndexerAndAuthorTagsMatch_IndexerIncluded()
public async Task Tags_IndexerAndAuthorTagsMatch_IndexerIncluded()
{
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition
{
@@ -129,7 +130,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
var allCriteria = WatchForSearchCriteria();
Subject.BookSearch(_firstBook, false, true, false);
await Subject.BookSearch(_firstBook, false, true, false);
var criteria = allCriteria.OfType<BookSearchCriteria>().ToList();
@@ -137,7 +138,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
}
[Test]
public void Tags_IndexerAndAuthorTagsMismatch_IndexerNotIncluded()
public async Task Tags_IndexerAndAuthorTagsMismatch_IndexerNotIncluded()
{
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition
{
@@ -156,7 +157,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
var allCriteria = WatchForSearchCriteria();
Subject.BookSearch(_firstBook, false, true, false);
await Subject.BookSearch(_firstBook, false, true, false);
var criteria = allCriteria.OfType<BookSearchCriteria>().ToList();

View File

@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -26,15 +27,15 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
}
[Test]
public void should_parse_recent_feed_from_FileList()
public async Task should_parse_recent_feed_from_FileList()
{
var recentFeed = ReadAllText(@"Files/Indexers/FileList/RecentFeed.json");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), recentFeed)));
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(2);
releases.First().Should().BeOfType<TorrentInfo>();

View File

@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -30,24 +31,24 @@ namespace NzbDrone.Core.Test.IndexerTests.GazelleTests
}
[Test]
public void should_parse_recent_feed_from_gazelle()
public async Task should_parse_recent_feed_from_gazelle()
{
var recentFeed = ReadAllText(@"Files/Indexers/Gazelle/Gazelle.json");
var indexFeed = ReadAllText(@"Files/Indexers/Gazelle/GazelleIndex.json");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get && v.Url.FullUri.Contains("ajax.php?action=browse"))))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader { ContentType = "application/json" }, recentFeed));
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get && v.Url.FullUri.Contains("ajax.php?action=browse"))))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader { ContentType = "application/json" }, recentFeed)));
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Post && v.Url.FullUri.Contains("ajax.php?action=index"))))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), indexFeed));
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Post && v.Url.FullUri.Contains("ajax.php?action=index"))))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), indexFeed)));
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Post && v.Url.FullUri.Contains("login.php"))))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), indexFeed));
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Post && v.Url.FullUri.Contains("login.php"))))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), indexFeed)));
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(4);

View File

@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -84,15 +85,15 @@ namespace NzbDrone.Core.Test.IndexerTests.IPTorrentsTests
}
[Test]
public void should_parse_recent_feed_from_IPTorrents()
public async Task should_parse_recent_feed_from_IPTorrents()
{
var recentFeed = ReadAllText(@"Files/Indexers/IPTorrents/IPTorrents.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), recentFeed)));
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(5);
releases.First().Should().BeOfType<TorrentInfo>();

View File

@@ -2,6 +2,7 @@ using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -39,15 +40,15 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
}
[Test]
public void should_parse_recent_feed_from_newznab_nzb_su()
public async Task should_parse_recent_feed_from_newznab_nzb_su()
{
var recentFeed = ReadAllText(@"Files/Indexers/Newznab/newznab_nzb_su.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), recentFeed)));
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(100);
@@ -83,7 +84,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
}
[Test]
public void should_record_indexer_failure_if_caps_throw()
public async Task should_record_indexer_failure_if_caps_throw()
{
var request = new HttpRequest("http://my.indexer.com");
var response = new HttpResponse(request, new HttpHeader(), new byte[0], (HttpStatusCode)429);
@@ -96,7 +97,9 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
_caps.MaxPageSize = 30;
_caps.DefaultPageSize = 25;
Subject.FetchRecent().Should().BeEmpty();
var releases = await Subject.FetchRecent();
releases.Should().BeEmpty();
Mocker.GetMock<IIndexerStatusService>()
.Verify(v => v.RecordFailure(It.IsAny<int>(), TimeSpan.FromMinutes(5.0)), Times.Once());

View File

@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -26,15 +27,15 @@ namespace NzbDrone.Core.Test.IndexerTests.NyaaTests
}
[Test]
public void should_parse_recent_feed_from_Nyaa()
public async Task should_parse_recent_feed_from_Nyaa()
{
var recentFeed = ReadAllText(@"Files/Indexers/Nyaa/Nyaa.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), recentFeed)));
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(4);
releases.First().Should().BeOfType<TorrentInfo>();

View File

@@ -22,7 +22,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
{
var result = new List<ValidationFailure>();
SetupNLog(); // Enable this to enable trace logging with nlog for debugging purposes
Test(result);
Test(result).GetAwaiter().GetResult();
return result;
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -34,17 +35,21 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
{
var recentFeed = ReadAllText(@"Files/Indexers/" + rssXmlFile);
Mocker.GetMock<IHttpClient>()
.Setup(o => o.ExecuteAsync(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), recentFeed)));
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
}
[Test]
public void should_parse_recent_feed_from_ImmortalSeed()
public async Task should_parse_recent_feed_from_ImmortalSeed()
{
GivenRecentFeedResponse("TorrentRss/ImmortalSeed.xml");
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(50);
releases.First().Should().BeOfType<TorrentInfo>();
@@ -66,11 +71,11 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
}
[Test]
public void should_parse_recent_feed_from_Ezrss()
public async Task should_parse_recent_feed_from_Ezrss()
{
GivenRecentFeedResponse("TorrentRss/Ezrss.xml");
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(3);
releases.First().Should().BeOfType<TorrentInfo>();
@@ -92,13 +97,13 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
}
[Test]
public void should_parse_recent_feed_from_ShowRSS_info()
public async Task should_parse_recent_feed_from_ShowRSS_info()
{
Subject.Definition.Settings.As<TorrentRssIndexerSettings>().AllowZeroSize = true;
GivenRecentFeedResponse("TorrentRss/ShowRSS.info.xml");
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(5);
releases.First().Should().BeOfType<TorrentInfo>();
@@ -120,13 +125,13 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
}
[Test]
public void should_parse_recent_feed_from_Doki()
public async Task should_parse_recent_feed_from_Doki()
{
Subject.Definition.Settings.As<TorrentRssIndexerSettings>().AllowZeroSize = true;
GivenRecentFeedResponse("TorrentRss/Doki.xml");
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(5);
releases.First().Should().BeOfType<TorrentInfo>();
@@ -148,11 +153,11 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
}
[Test]
public void should_parse_recent_feed_from_ExtraTorrents()
public async Task should_parse_recent_feed_from_ExtraTorrents()
{
GivenRecentFeedResponse("TorrentRss/ExtraTorrents.xml");
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(5);
releases.First().Should().BeOfType<TorrentInfo>();
@@ -174,11 +179,11 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
}
[Test]
public void should_parse_recent_feed_from_LimeTorrents()
public async Task should_parse_recent_feed_from_LimeTorrents()
{
GivenRecentFeedResponse("TorrentRss/LimeTorrents.xml");
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(5);
releases.First().Should().BeOfType<TorrentInfo>();
@@ -200,11 +205,11 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
}
[Test]
public void should_parse_recent_feed_from_AnimeTosho_without_size()
public async Task should_parse_recent_feed_from_AnimeTosho_without_size()
{
GivenRecentFeedResponse("TorrentRss/AnimeTosho_NoSize.xml");
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(2);
releases.First().Should().BeOfType<TorrentInfo>();
@@ -226,11 +231,11 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
}
[Test]
public void should_parse_multi_enclosure_from_AnimeTosho()
public async Task should_parse_multi_enclosure_from_AnimeTosho()
{
GivenRecentFeedResponse("TorrentRss/AnimeTosho_NoSize.xml");
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(2);
releases.Last().Should().BeOfType<TorrentInfo>();
@@ -243,11 +248,11 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
}
[Test]
public void should_parse_recent_feed_from_AlphaRatio()
public async Task should_parse_recent_feed_from_AlphaRatio()
{
GivenRecentFeedResponse("TorrentRss/AlphaRatio.xml");
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(2);
releases.Last().Should().BeOfType<TorrentInfo>();
@@ -260,12 +265,12 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
}
[Test]
public void should_parse_recent_feed_from_EveolutionWorld_without_size()
public async Task should_parse_recent_feed_from_EveolutionWorld_without_size()
{
Subject.Definition.Settings.As<TorrentRssIndexerSettings>().AllowZeroSize = true;
GivenRecentFeedResponse("TorrentRss/EvolutionWorld.xml");
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(2);
releases.First().Should().BeOfType<TorrentInfo>();
@@ -287,11 +292,13 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
}
[Test]
public void should_record_indexer_failure_if_unsupported_feed()
public async Task should_record_indexer_failure_if_unsupported_feed()
{
GivenRecentFeedResponse("TorrentRss/invalid/TorrentDay_NoPubDate.xml");
Subject.FetchRecent().Should().BeEmpty();
var releases = await Subject.FetchRecent();
releases.Should().BeEmpty();
Mocker.GetMock<IIndexerStatusService>()
.Verify(v => v.RecordFailure(It.IsAny<int>(), TimeSpan.Zero), Times.Once());

View File

@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -26,15 +27,15 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentleechTests
}
[Test]
public void should_parse_recent_feed_from_Torrentleech()
public async Task should_parse_recent_feed_from_Torrentleech()
{
var recentFeed = ReadAllText(@"Files/Indexers/Torrentleech/Torrentleech.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), recentFeed)));
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(5);
releases.First().Should().BeOfType<TorrentInfo>();

View File

@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
@@ -44,15 +45,15 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
}
[Test]
public void should_parse_recent_feed_from_torznab_hdaccess_net()
public async Task should_parse_recent_feed_from_torznab_hdaccess_net()
{
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_hdaccess_net.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), recentFeed)));
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(5);
@@ -73,15 +74,15 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
}
[Test]
public void should_parse_recent_feed_from_torznab_tpb()
public async Task should_parse_recent_feed_from_torznab_tpb()
{
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_tpb.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), recentFeed)));
var releases = Subject.FetchRecent();
var releases = await Subject.FetchRecent();
releases.Should().HaveCount(5);
@@ -140,8 +141,8 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
(Subject.Definition.Settings as TorznabSettings).BaseUrl = baseUrl;
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), recentFeed)));
var result = new NzbDroneValidationResult(Subject.Test());
result.IsValid.Should().BeTrue();
@@ -155,8 +156,8 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_tpb.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), recentFeed)));
(Subject.Definition.Settings as TorznabSettings).ApiPath = apiPath;

View File

@@ -122,7 +122,7 @@ namespace NzbDrone.Core.Blocklisting
{
if (release.InfoHash.IsNotNullOrWhiteSpace())
{
return release.InfoHash.Equals(item.TorrentInfoHash);
return release.InfoHash.Equals(item.TorrentInfoHash, StringComparison.InvariantCultureIgnoreCase);
}
return HasSameIndexer(item, release.Indexer);

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
@@ -32,7 +33,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
public override string Download(RemoteBook remoteBook, IIndexer indexer)
public override async Task<string> Download(RemoteBook remoteBook, IIndexer indexer)
{
var url = remoteBook.Release.DownloadUrl;
var title = remoteBook.Release.Title;
@@ -48,7 +49,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
var nzbFile = Path.Combine(Settings.NzbFolder, title + ".nzb");
_logger.Debug("Downloading NZB from: {0} to: {1}", url, nzbFile);
_httpClient.DownloadFile(url, nzbFile);
await _httpClient.DownloadFileAsync(url, nzbFile);
_logger.Debug("NZB Download succeeded, saved to: {0}", nzbFile);

View File

@@ -628,11 +628,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
if (torrent.SeedingTimeLimit >= 0)
{
seedingTimeLimit = torrent.SeedingTimeLimit;
seedingTimeLimit = torrent.SeedingTimeLimit * 60;
}
else if (torrent.SeedingTimeLimit == -2 && config.MaxSeedingTimeEnabled)
{
seedingTimeLimit = config.MaxSeedingTime;
seedingTimeLimit = config.MaxSeedingTime * 60;
}
else
{

View File

@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public float RatioLimit { get; set; } = -2;
[JsonProperty(PropertyName = "seeding_time")]
public long? SeedingTime { get; set; } // Torrent seeding time (not provided by the list api)
public long? SeedingTime { get; set; } // Torrent seeding time (in seconds, not provided by the list api)
[JsonProperty(PropertyName = "seeding_time_limit")] // Per torrent seeding time limit (-2 = use global, -1 = unlimited)
public long SeedingTimeLimit { get; set; } = -2;
@@ -47,7 +47,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public string SavePath { get; set; }
[JsonProperty(PropertyName = "seeding_time")]
public long SeedingTime { get; set; } // Torrent seeding time
public long SeedingTime { get; set; } // Torrent seeding time (in seconds)
}
public class QBittorrentTorrentFile

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
@@ -58,7 +59,7 @@ namespace NzbDrone.Core.Download
get;
}
public abstract string Download(RemoteBook remoteBook, IIndexer indexer);
public abstract Task<string> Download(RemoteBook remoteBook, IIndexer indexer);
public abstract IEnumerable<DownloadClientItem> GetItems();
public virtual DownloadClientItem GetImportItem(DownloadClientItem item, DownloadClientItem previousImportAttempt)

View File

@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions;
@@ -16,7 +17,7 @@ namespace NzbDrone.Core.Download
{
public interface IDownloadService
{
void DownloadReport(RemoteBook remoteBook);
Task DownloadReport(RemoteBook remoteBook);
}
public class DownloadService : IDownloadService
@@ -49,15 +50,23 @@ namespace NzbDrone.Core.Download
_logger = logger;
}
public void DownloadReport(RemoteBook remoteBook)
public async Task DownloadReport(RemoteBook remoteBook)
{
var filterBlockedClients = remoteBook.Release.PendingReleaseReason == PendingReleaseReason.DownloadClientUnavailable;
var tags = remoteBook.Author?.Tags;
var downloadClient = _downloadClientProvider.GetDownloadClient(remoteBook.Release.DownloadProtocol, remoteBook.Release.IndexerId, filterBlockedClients, tags);
await DownloadReport(remoteBook, downloadClient);
}
private async Task DownloadReport(RemoteBook remoteBook, IDownloadClient downloadClient)
{
Ensure.That(remoteBook.Author, () => remoteBook.Author).IsNotNull();
Ensure.That(remoteBook.Books, () => remoteBook.Books).HasItems();
var downloadTitle = remoteBook.Release.Title;
var filterBlockedClients = remoteBook.Release.PendingReleaseReason == PendingReleaseReason.DownloadClientUnavailable;
var tags = remoteBook.Author?.Tags;
var downloadClient = _downloadClientProvider.GetDownloadClient(remoteBook.Release.DownloadProtocol, remoteBook.Release.IndexerId, filterBlockedClients, tags);
if (downloadClient == null)
{
@@ -71,7 +80,7 @@ namespace NzbDrone.Core.Download
if (remoteBook.Release.DownloadUrl.IsNotNullOrWhiteSpace() && !remoteBook.Release.DownloadUrl.StartsWith("magnet:"))
{
var url = new HttpUri(remoteBook.Release.DownloadUrl);
_rateLimitService.WaitAndPulse(url.Host, TimeSpan.FromSeconds(2));
await _rateLimitService.WaitAndPulseAsync(url.Host, TimeSpan.FromSeconds(2));
}
IIndexer indexer = null;
@@ -84,7 +93,7 @@ namespace NzbDrone.Core.Download
string downloadClientId;
try
{
downloadClientId = downloadClient.Download(remoteBook, indexer);
downloadClientId = await downloadClient.Download(remoteBook, indexer);
_downloadClientStatusService.RecordSuccess(downloadClient.Definition.Id);
_indexerStatusService.RecordSuccess(remoteBook.Release.IndexerId);
}
@@ -100,8 +109,7 @@ namespace NzbDrone.Core.Download
}
catch (ReleaseDownloadException ex)
{
var http429 = ex.InnerException as TooManyRequestsException;
if (http429 != null)
if (ex.InnerException is TooManyRequestsException http429)
{
_indexerStatusService.RecordFailure(remoteBook.Release.IndexerId, http429.RetryAfter);
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
@@ -8,7 +9,7 @@ namespace NzbDrone.Core.Download
public interface IDownloadClient : IProvider
{
DownloadProtocol Protocol { get; }
string Download(RemoteBook remoteBook, IIndexer indexer);
Task<string> Download(RemoteBook remoteBook, IIndexer indexer);
IEnumerable<DownloadClientItem> GetItems();
DownloadClientItem GetImportItem(DownloadClientItem item, DownloadClientItem previousImportAttempt);
void RemoveItem(DownloadClientItem item, bool deleteData);

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download.Clients;
@@ -12,7 +13,7 @@ namespace NzbDrone.Core.Download
{
public interface IProcessDownloadDecisions
{
ProcessedDecisions ProcessDecisions(List<DownloadDecision> decisions);
Task<ProcessedDecisions> ProcessDecisions(List<DownloadDecision> decisions);
}
public class ProcessDownloadDecisions : IProcessDownloadDecisions
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Download
_logger = logger;
}
public ProcessedDecisions ProcessDecisions(List<DownloadDecision> decisions)
public async Task<ProcessedDecisions> ProcessDecisions(List<DownloadDecision> decisions)
{
var qualifiedReports = GetQualifiedReports(decisions);
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(qualifiedReports);
@@ -75,7 +76,7 @@ namespace NzbDrone.Core.Download
try
{
_logger.Trace("Grabbing from Indexer {0} at priority {1}.", remoteBook.Release.Indexer, remoteBook.Release.IndexerPriority);
_downloadService.DownloadReport(remoteBook);
await _downloadService.DownloadReport(remoteBook);
grabbed.Add(report);
}
catch (ReleaseUnavailableException)

View File

@@ -1,5 +1,6 @@
using System;
using System.Net;
using System.Threading.Tasks;
using MonoTorrent;
using NLog;
using NzbDrone.Common.Disk;
@@ -41,7 +42,7 @@ namespace NzbDrone.Core.Download
protected abstract string AddFromMagnetLink(RemoteBook remoteBook, string hash, string magnetLink);
protected abstract string AddFromTorrentFile(RemoteBook remoteBook, string hash, string filename, byte[] fileContent);
public override string Download(RemoteBook remoteBook, IIndexer indexer)
public override async Task<string> Download(RemoteBook remoteBook, IIndexer indexer)
{
var torrentInfo = remoteBook.Release as TorrentInfo;
@@ -68,7 +69,7 @@ namespace NzbDrone.Core.Download
{
try
{
return DownloadFromWebUrl(remoteBook, indexer, torrentUrl);
return await DownloadFromWebUrl(remoteBook, indexer, torrentUrl);
}
catch (Exception ex)
{
@@ -114,14 +115,14 @@ namespace NzbDrone.Core.Download
if (torrentUrl.IsNotNullOrWhiteSpace())
{
return DownloadFromWebUrl(remoteBook, indexer, torrentUrl);
return await DownloadFromWebUrl(remoteBook, indexer, torrentUrl);
}
}
return null;
}
private string DownloadFromWebUrl(RemoteBook remoteBook, IIndexer indexer, string torrentUrl)
private async Task<string> DownloadFromWebUrl(RemoteBook remoteBook, IIndexer indexer, string torrentUrl)
{
byte[] torrentFile = null;
@@ -132,7 +133,7 @@ namespace NzbDrone.Core.Download
request.Headers.Accept = "application/x-bittorrent";
request.AllowAutoRedirect = false;
var response = _httpClient.Get(request);
var response = await _httpClient.GetAsync(request);
if (response.StatusCode == HttpStatusCode.MovedPermanently ||
response.StatusCode == HttpStatusCode.Found ||
@@ -151,7 +152,7 @@ namespace NzbDrone.Core.Download
request.Url += new HttpUri(locationHeader);
return DownloadFromWebUrl(remoteBook, indexer, request.Url.ToString());
return await DownloadFromWebUrl(remoteBook, indexer, request.Url.ToString());
}
throw new WebException("Remote website tried to redirect without providing a location.");

View File

@@ -1,4 +1,5 @@
using System.Net;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
@@ -34,7 +35,7 @@ namespace NzbDrone.Core.Download
protected abstract string AddFromNzbFile(RemoteBook remoteBook, string filename, byte[] fileContent);
public override string Download(RemoteBook remoteBook, IIndexer indexer)
public override async Task<string> Download(RemoteBook remoteBook, IIndexer indexer)
{
var url = remoteBook.Release.DownloadUrl;
var filename = FileNameBuilder.CleanFileName(remoteBook.Release.Title) + ".nzb";
@@ -46,7 +47,9 @@ namespace NzbDrone.Core.Download
var request = indexer?.GetDownloadRequest(url) ?? new HttpRequest(url);
request.RateLimitKey = remoteBook?.Release?.IndexerId.ToString();
nzbData = _httpClient.Get(request).ResponseData;
var response = await _httpClient.GetAsync(request);
nzbData = response.ResponseData;
_logger.Debug("Downloaded nzb for release '{0}' finished ({1} bytes from {2})", remoteBook.Release.Title, nzbData.Length, url);
}

View File

@@ -22,8 +22,8 @@ namespace NzbDrone.Core.IndexerSearch
public void Execute(AuthorSearchCommand message)
{
var decisions = _releaseSearchService.AuthorSearch(message.AuthorId, false, message.Trigger == CommandTrigger.Manual, false);
var processed = _processDownloadDecisions.ProcessDecisions(decisions);
var decisions = _releaseSearchService.AuthorSearch(message.AuthorId, false, message.Trigger == CommandTrigger.Manual, false).GetAwaiter().GetResult();
var processed = _processDownloadDecisions.ProcessDecisions(decisions).GetAwaiter().GetResult();
_logger.ProgressInfo("Author search completed. {0} reports downloaded.", processed.Grabbed.Count);
}

View File

@@ -2,11 +2,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Queue;
@@ -39,16 +39,15 @@ namespace NzbDrone.Core.IndexerSearch
_logger = logger;
}
private void SearchForMissingBooks(List<Book> books, bool userInvokedSearch)
private async Task SearchForMissingBooks(List<Book> books, bool userInvokedSearch)
{
_logger.ProgressInfo("Performing missing search for {0} books", books.Count);
var downloadedCount = 0;
foreach (var book in books)
{
List<DownloadDecision> decisions;
decisions = _releaseSearchService.BookSearch(book.Id, false, userInvokedSearch, false);
var processed = _processDownloadDecisions.ProcessDecisions(decisions);
var decisions = await _releaseSearchService.BookSearch(book.Id, false, userInvokedSearch, false);
var processed = await _processDownloadDecisions.ProcessDecisions(decisions);
downloadedCount += processed.Grabbed.Count;
}
@@ -60,9 +59,8 @@ namespace NzbDrone.Core.IndexerSearch
{
foreach (var bookId in message.BookIds)
{
var decisions =
_releaseSearchService.BookSearch(bookId, false, message.Trigger == CommandTrigger.Manual, false);
var processed = _processDownloadDecisions.ProcessDecisions(decisions);
var decisions = _releaseSearchService.BookSearch(bookId, false, message.Trigger == CommandTrigger.Manual, false).GetAwaiter().GetResult();
var processed = _processDownloadDecisions.ProcessDecisions(decisions).GetAwaiter().GetResult();
_logger.ProgressInfo("Book search completed. {0} reports downloaded.", processed.Grabbed.Count);
}
@@ -106,7 +104,7 @@ namespace NzbDrone.Core.IndexerSearch
var queue = _queueService.GetQueue().Where(q => q.Book != null).Select(q => q.Book.Id);
var missing = books.Where(e => !queue.Contains(e.Id)).ToList();
SearchForMissingBooks(missing, message.Trigger == CommandTrigger.Manual);
SearchForMissingBooks(missing, message.Trigger == CommandTrigger.Manual).GetAwaiter().GetResult();
}
public void Execute(CutoffUnmetBookSearchCommand message)
@@ -132,7 +130,7 @@ namespace NzbDrone.Core.IndexerSearch
var queue = _queueService.GetQueue().Where(q => q.Book != null).Select(q => q.Book.Id);
var missing = books.Where(e => !queue.Contains(e.Id)).ToList();
SearchForMissingBooks(missing, message.Trigger == CommandTrigger.Manual);
SearchForMissingBooks(missing, message.Trigger == CommandTrigger.Manual).GetAwaiter().GetResult();
}
}
}

View File

@@ -5,7 +5,6 @@ using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Books;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Indexers;
@@ -16,8 +15,8 @@ namespace NzbDrone.Core.IndexerSearch
{
public interface ISearchForReleases
{
List<DownloadDecision> BookSearch(int bookId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch);
List<DownloadDecision> AuthorSearch(int authorId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch);
Task<List<DownloadDecision>> BookSearch(int bookId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch);
Task<List<DownloadDecision>> AuthorSearch(int authorId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch);
}
public class ReleaseSearchService : ISearchForReleases
@@ -41,31 +40,31 @@ namespace NzbDrone.Core.IndexerSearch
_logger = logger;
}
public List<DownloadDecision> BookSearch(int bookId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch)
public async Task<List<DownloadDecision>> BookSearch(int bookId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch)
{
var downloadDecisions = new List<DownloadDecision>();
var book = _bookService.GetBook(bookId);
var decisions = BookSearch(book, missingOnly, userInvokedSearch, interactiveSearch);
var decisions = await BookSearch(book, missingOnly, userInvokedSearch, interactiveSearch);
downloadDecisions.AddRange(decisions);
return DeDupeDecisions(downloadDecisions);
}
public List<DownloadDecision> AuthorSearch(int authorId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch)
public async Task<List<DownloadDecision>> AuthorSearch(int authorId, bool missingOnly, bool userInvokedSearch, bool interactiveSearch)
{
var downloadDecisions = new List<DownloadDecision>();
var author = _authorService.GetAuthor(authorId);
var decisions = AuthorSearch(author, missingOnly, userInvokedSearch, interactiveSearch);
var decisions = await AuthorSearch(author, missingOnly, userInvokedSearch, interactiveSearch);
downloadDecisions.AddRange(decisions);
return DeDupeDecisions(downloadDecisions);
}
public List<DownloadDecision> AuthorSearch(Author author, bool missingOnly, bool userInvokedSearch, bool interactiveSearch)
public async Task<List<DownloadDecision>> AuthorSearch(Author author, bool missingOnly, bool userInvokedSearch, bool interactiveSearch)
{
var searchSpec = Get<AuthorSearchCriteria>(author, userInvokedSearch, interactiveSearch);
var books = _bookService.GetBooksByAuthor(author.Id);
@@ -74,10 +73,10 @@ namespace NzbDrone.Core.IndexerSearch
searchSpec.Books = books;
return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
return await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
}
public List<DownloadDecision> BookSearch(Book book, bool missingOnly, bool userInvokedSearch, bool interactiveSearch)
public async Task<List<DownloadDecision>> BookSearch(Book book, bool missingOnly, bool userInvokedSearch, bool interactiveSearch)
{
var author = _authorService.GetAuthor(book.AuthorId);
@@ -91,7 +90,7 @@ namespace NzbDrone.Core.IndexerSearch
searchSpec.BookYear = book.ReleaseDate.Value.Year;
}
return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
return await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
}
private TSpec Get<TSpec>(Author author, List<Book> books, bool userInvokedSearch, bool interactiveSearch)
@@ -118,7 +117,7 @@ namespace NzbDrone.Core.IndexerSearch
return spec;
}
private List<DownloadDecision> Dispatch(Func<IIndexer, IEnumerable<ReleaseInfo>> searchAction, SearchCriteriaBase criteriaBase)
private async Task<List<DownloadDecision>> Dispatch(Func<IIndexer, Task<IList<ReleaseInfo>>> searchAction, SearchCriteriaBase criteriaBase)
{
var indexers = criteriaBase.InteractiveSearch ?
_indexerFactory.InteractiveSearchEnabled() :
@@ -127,42 +126,33 @@ namespace NzbDrone.Core.IndexerSearch
// Filter indexers to untagged indexers and indexers with intersecting tags
indexers = indexers.Where(i => i.Definition.Tags.Empty() || i.Definition.Tags.Intersect(criteriaBase.Author.Tags).Any()).ToList();
var reports = new List<ReleaseInfo>();
_logger.ProgressInfo("Searching indexers for {0}. {1} active indexers", criteriaBase, indexers.Count);
var taskList = new List<Task>();
var taskFactory = new TaskFactory(TaskCreationOptions.LongRunning, TaskContinuationOptions.None);
var tasks = indexers.Select(indexer => DispatchIndexer(searchAction, indexer, criteriaBase));
foreach (var indexer in indexers)
{
var indexerLocal = indexer;
var batch = await Task.WhenAll(tasks);
taskList.Add(taskFactory.StartNew(() =>
{
try
{
var indexerReports = searchAction(indexerLocal);
lock (reports)
{
reports.AddRange(indexerReports);
}
}
catch (Exception e)
{
_logger.Error(e, "Error while searching for {0}", criteriaBase);
}
}).LogExceptions());
}
Task.WaitAll(taskList.ToArray());
var reports = batch.SelectMany(x => x).ToList();
_logger.Debug("Total of {0} reports were found for {1} from {2} indexers", reports.Count, criteriaBase, indexers.Count);
return _makeDownloadDecision.GetSearchDecision(reports, criteriaBase).ToList();
}
private async Task<IList<ReleaseInfo>> DispatchIndexer(Func<IIndexer, Task<IList<ReleaseInfo>>> searchAction, IIndexer indexer, SearchCriteriaBase criteriaBase)
{
try
{
return await searchAction(indexer);
}
catch (Exception ex)
{
_logger.Error(ex, "Error while searching for {0}", criteriaBase);
}
return Array.Empty<ReleaseInfo>();
}
private List<DownloadDecision> DeDupeDecisions(List<DownloadDecision> decisions)
{
// De-dupe reports by guid so duplicate results aren't returned. Pick the one with the least rejections and higher indexer priority.

View File

@@ -3,13 +3,12 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers
{
public interface IFetchAndParseRss
{
List<ReleaseInfo> Fetch();
Task<List<ReleaseInfo>> Fetch();
}
public class FetchAndParseRssService : IFetchAndParseRss
@@ -23,54 +22,42 @@ namespace NzbDrone.Core.Indexers
_logger = logger;
}
public List<ReleaseInfo> Fetch()
public async Task<List<ReleaseInfo>> Fetch()
{
var result = new List<ReleaseInfo>();
var indexers = _indexerFactory.RssEnabled();
if (!indexers.Any())
{
_logger.Warn("No available indexers. check your configuration.");
return result;
return new List<ReleaseInfo>();
}
_logger.Debug("Available indexers {0}", indexers.Count);
var taskList = new List<Task>();
var taskFactory = new TaskFactory(TaskCreationOptions.LongRunning, TaskContinuationOptions.None);
var tasks = indexers.Select(FetchIndexer);
foreach (var indexer in indexers)
{
var indexerLocal = indexer;
var batch = await Task.WhenAll(tasks);
var task = taskFactory.StartNew(() =>
{
try
{
var indexerReports = indexerLocal.FetchRecent();
lock (result)
{
_logger.Debug("Found {0} from {1}", indexerReports.Count, indexer.Name);
result.AddRange(indexerReports);
}
}
catch (Exception e)
{
_logger.Error(e, "Error during RSS Sync");
}
}).LogExceptions();
taskList.Add(task);
}
Task.WaitAll(taskList.ToArray());
var result = batch.SelectMany(x => x).ToList();
_logger.Debug("Found {0} reports", result.Count);
return result;
}
private async Task<IList<ReleaseInfo>> FetchIndexer(IIndexer indexer)
{
try
{
return await indexer.FetchRecent();
}
catch (Exception ex)
{
_logger.Error(ex, "Error during RSS Sync");
}
return Array.Empty<ReleaseInfo>();
}
}
}

View File

@@ -23,7 +23,11 @@ namespace NzbDrone.Core.Indexers.FileList
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetRequest("search-torrents", Settings.Categories, string.Format("&type=name&query={0}+{1}", Uri.EscapeDataString(searchCriteria.AuthorQuery.Trim()), Uri.EscapeDataString(searchCriteria.BookQuery.Trim()))));
var authorQuery = searchCriteria.AuthorQuery.Replace("+", " ").Trim();
var bookQuery = searchCriteria.BookQuery.Replace("+", " ").Trim();
pageableRequests.Add(GetRequest("search-torrents", Settings.Categories, string.Format("&type=name&query={0}+{1}", Uri.EscapeDataString(authorQuery), Uri.EscapeDataString(bookQuery))));
pageableRequests.Add(GetRequest("search-torrents", Settings.Categories, string.Format("&type=name&query={0}+{1}", Uri.EscapeDataString(bookQuery), Uri.EscapeDataString(authorQuery))));
return pageableRequests;
}
@@ -32,7 +36,9 @@ namespace NzbDrone.Core.Indexers.FileList
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetRequest("search-torrents", Settings.Categories, string.Format("&type=name&query={0}", Uri.EscapeDataString(searchCriteria.AuthorQuery.Trim()))));
var authorQuery = searchCriteria.AuthorQuery.Replace("+", " ").Trim();
pageableRequests.Add(GetRequest("search-torrents", Settings.Categories, string.Format("&type=name&query={0}", Uri.EscapeDataString(authorQuery))));
return pageableRequests;
}

View File

@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Indexers.FileList
Categories = new int[]
{
(int)FileListCategories.DOCS
(int)FileListCategories.Docs
};
}
@@ -61,7 +61,7 @@ namespace NzbDrone.Core.Indexers.FileList
public enum FileListCategories
{
[FieldOption]
DOCS = 16,
[FieldOption(Label = "Docs")]
Docs = 16,
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Cache;
@@ -68,11 +69,12 @@ namespace NzbDrone.Core.Indexers.Gazelle
return settings;
}
protected override void Test(List<ValidationFailure> failures)
protected override async Task Test(List<ValidationFailure> failures)
{
// Remove previous cookies when testing incase user or pwd change
_authCookieCache.Remove(Settings.BaseUrl.Trim().TrimEnd('/'));
base.Test(failures);
await base.Test(failures);
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
@@ -43,17 +44,14 @@ namespace NzbDrone.Core.Indexers.Gazelle
private IEnumerable<IndexerRequest> GetRequest(string searchParameters)
{
Authenticate();
Authenticate().GetAwaiter().GetResult();
var filter = "";
if (searchParameters == null)
{
}
var request =
new IndexerRequest(
$"{Settings.BaseUrl.Trim().TrimEnd('/')}/ajax.php?action=browse&searchstr={searchParameters}{filter}",
HttpAccept.Json);
var request = new IndexerRequest($"{Settings.BaseUrl.Trim().TrimEnd('/')}/ajax.php?action=browse&searchstr={searchParameters}{filter}", HttpAccept.Json);
var cookies = AuthCookieCache.Find(Settings.BaseUrl.Trim().TrimEnd('/'));
foreach (var cookie in cookies)
@@ -64,36 +62,36 @@ namespace NzbDrone.Core.Indexers.Gazelle
yield return request;
}
private GazelleAuthResponse GetIndex(Dictionary<string, string> cookies)
private async Task<GazelleAuthResponse> GetIndex(Dictionary<string, string> cookies)
{
var indexRequestBuilder = new HttpRequestBuilder($"{Settings.BaseUrl.Trim().TrimEnd('/')}")
{
LogResponseContent = true
LogResponseContent = true,
Method = HttpMethod.Post
};
indexRequestBuilder.SetCookies(cookies);
indexRequestBuilder.Method = HttpMethod.Post;
indexRequestBuilder.Resource("ajax.php?action=index");
var authIndexRequest = indexRequestBuilder
.Accept(HttpAccept.Json)
.Build();
var indexResponse = HttpClient.Execute(authIndexRequest);
var indexResponse = await HttpClient.ExecuteAsync(authIndexRequest);
var result = Json.Deserialize<GazelleAuthResponse>(indexResponse.Content);
return result;
}
private void Authenticate()
private async Task Authenticate()
{
var requestBuilder = new HttpRequestBuilder($"{Settings.BaseUrl.Trim().TrimEnd('/')}")
{
LogResponseContent = true
LogResponseContent = true,
Method = HttpMethod.Post
};
requestBuilder.Method = HttpMethod.Post;
requestBuilder.Resource("login.php");
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
@@ -111,19 +109,20 @@ namespace NzbDrone.Core.Indexers.Gazelle
.Accept(HttpAccept.Json)
.Build();
var response = HttpClient.Execute(authLoginRequest);
var response = await HttpClient.ExecuteAsync(authLoginRequest);
cookies = response.GetCookies();
AuthCookieCache.Set(authKey, cookies);
}
var index = GetIndex(cookies);
var index = await GetIndex(cookies);
if (index == null || index.Status.IsNullOrWhiteSpace() || index.Status != "success")
{
Logger.Debug("Gazelle authentication failed.");
AuthCookieCache.Remove(authKey);
throw new Exception("Failed to authenticate with Gazelle.");
}

View File

@@ -40,31 +40,31 @@ namespace NzbDrone.Core.Indexers
_httpClient = httpClient;
}
public override IList<ReleaseInfo> FetchRecent()
public override Task<IList<ReleaseInfo>> FetchRecent()
{
if (!SupportsRss)
{
return new List<ReleaseInfo>();
return Task.FromResult<IList<ReleaseInfo>>(Array.Empty<ReleaseInfo>());
}
return FetchReleases(g => g.GetRecentRequests(), true);
}
public override IList<ReleaseInfo> Fetch(BookSearchCriteria searchCriteria)
public override Task<IList<ReleaseInfo>> Fetch(BookSearchCriteria searchCriteria)
{
if (!SupportsSearch)
{
return new List<ReleaseInfo>();
return Task.FromResult<IList<ReleaseInfo>>(Array.Empty<ReleaseInfo>());
}
return FetchReleases(g => g.GetSearchRequests(searchCriteria));
}
public override IList<ReleaseInfo> Fetch(AuthorSearchCriteria searchCriteria)
public override Task<IList<ReleaseInfo>> Fetch(AuthorSearchCriteria searchCriteria)
{
if (!SupportsSearch)
{
return new List<ReleaseInfo>();
return Task.FromResult<IList<ReleaseInfo>>(Array.Empty<ReleaseInfo>());
}
return FetchReleases(g => g.GetSearchRequests(searchCriteria));
@@ -75,7 +75,7 @@ namespace NzbDrone.Core.Indexers
return new HttpRequest(link);
}
protected virtual IList<ReleaseInfo> FetchReleases(Func<IIndexerRequestGenerator, IndexerPageableRequestChain> pageableRequestChainSelector, bool isRecent = false)
protected virtual async Task<IList<ReleaseInfo>> FetchReleases(Func<IIndexerRequestGenerator, IndexerPageableRequestChain> pageableRequestChainSelector, bool isRecent = false)
{
var releases = new List<ReleaseInfo>();
var url = string.Empty;
@@ -107,7 +107,7 @@ namespace NzbDrone.Core.Indexers
{
url = request.Url.FullUri;
var page = FetchPage(request, parser);
var page = await FetchPage(request, parser);
pagedReleases.AddRange(page);
@@ -271,9 +271,9 @@ namespace NzbDrone.Core.Indexers
return PageSize != 0 && page.Count >= PageSize;
}
protected virtual IList<ReleaseInfo> FetchPage(IndexerRequest request, IParseIndexerResponse parser)
protected virtual async Task<IList<ReleaseInfo>> FetchPage(IndexerRequest request, IParseIndexerResponse parser)
{
var response = FetchIndexerResponse(request);
var response = await FetchIndexerResponse(request);
try
{
@@ -287,7 +287,7 @@ namespace NzbDrone.Core.Indexers
}
}
protected virtual IndexerResponse FetchIndexerResponse(IndexerRequest request)
protected virtual async Task<IndexerResponse> FetchIndexerResponse(IndexerRequest request)
{
_logger.Debug("Downloading Feed " + request.HttpRequest.ToString(false));
@@ -298,15 +298,17 @@ namespace NzbDrone.Core.Indexers
request.HttpRequest.RateLimitKey = Definition.Id.ToString();
return new IndexerResponse(request, _httpClient.Execute(request.HttpRequest));
var response = await _httpClient.ExecuteAsync(request.HttpRequest);
return new IndexerResponse(request, response);
}
protected override void Test(List<ValidationFailure> failures)
protected override async Task Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
failures.AddIfNotNull(await TestConnection());
}
protected virtual ValidationFailure TestConnection()
protected virtual async Task<ValidationFailure> TestConnection()
{
try
{
@@ -319,7 +321,7 @@ namespace NzbDrone.Core.Indexers
return new ValidationFailure(string.Empty, "No rss feed query available. This may be an issue with the indexer or your indexer category settings.");
}
var releases = FetchPage(firstRequest, parser);
var releases = await FetchPage(firstRequest, parser);
if (releases.Empty())
{

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using NzbDrone.Common.Http;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
@@ -12,9 +13,9 @@ namespace NzbDrone.Core.Indexers
bool SupportsSearch { get; }
DownloadProtocol Protocol { get; }
IList<ReleaseInfo> FetchRecent();
IList<ReleaseInfo> Fetch(BookSearchCriteria searchCriteria);
IList<ReleaseInfo> Fetch(AuthorSearchCriteria searchCriteria);
Task<IList<ReleaseInfo>> FetchRecent();
Task<IList<ReleaseInfo>> Fetch(BookSearchCriteria searchCriteria);
Task<IList<ReleaseInfo>> Fetch(AuthorSearchCriteria searchCriteria);
HttpRequest GetDownloadRequest(string link);
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Http;
@@ -66,16 +67,14 @@ namespace NzbDrone.Core.Indexers
protected TSettings Settings => (TSettings)Definition.Settings;
public abstract IList<ReleaseInfo> FetchRecent();
public abstract IList<ReleaseInfo> Fetch(BookSearchCriteria searchCriteria);
public abstract IList<ReleaseInfo> Fetch(AuthorSearchCriteria searchCriteria);
public abstract Task<IList<ReleaseInfo>> FetchRecent();
public abstract Task<IList<ReleaseInfo>> Fetch(BookSearchCriteria searchCriteria);
public abstract Task<IList<ReleaseInfo>> Fetch(AuthorSearchCriteria searchCriteria);
public abstract HttpRequest GetDownloadRequest(string link);
protected virtual IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases)
{
var result = releases.DistinctBy(v => v.Guid).ToList();
var settings = Definition.Settings as IIndexerSettings;
result.ForEach(c =>
{
@@ -94,7 +93,7 @@ namespace NzbDrone.Core.Indexers
try
{
Test(failures);
Test(failures).GetAwaiter().GetResult();
}
catch (Exception ex)
{
@@ -105,7 +104,7 @@ namespace NzbDrone.Core.Indexers
return new ValidationResult(failures);
}
protected abstract void Test(List<ValidationFailure> failures);
protected abstract Task Test(List<ValidationFailure> failures);
public override string ToString()
{

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
@@ -92,9 +93,10 @@ namespace NzbDrone.Core.Indexers.Newznab
return settings;
}
protected override void Test(List<ValidationFailure> failures)
protected override async Task Test(List<ValidationFailure> failures)
{
base.Test(failures);
await base.Test(failures);
if (failures.HasErrors())
{
return;

View File

@@ -1,4 +1,5 @@
using System.Linq;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.DecisionEngine;
@@ -39,16 +40,16 @@ namespace NzbDrone.Core.Indexers
_logger = logger;
}
private ProcessedDecisions Sync()
private async Task<ProcessedDecisions> Sync()
{
_logger.ProgressInfo("Starting RSS Sync");
var rssReleases = _rssFetcherAndParser.Fetch();
var rssReleases = await _rssFetcherAndParser.Fetch();
var pendingReleases = _pendingReleaseService.GetPending();
var reports = rssReleases.Concat(pendingReleases).ToList();
var decisions = _downloadDecisionMaker.GetRssDecision(reports);
var processed = _processDownloadDecisions.ProcessDecisions(decisions);
var processed = await _processDownloadDecisions.ProcessDecisions(decisions);
var message = string.Format("RSS Sync Completed. Reports found: {0}, Reports grabbed: {1}", reports.Count, processed.Grabbed.Count);
@@ -64,7 +65,7 @@ namespace NzbDrone.Core.Indexers
public void Execute(RssSyncCommand message)
{
var processed = Sync();
var processed = Sync().GetAwaiter().GetResult();
var grabbedOrPending = processed.Grabbed.Concat(processed.Pending).ToList();
_eventAggregator.PublishEvent(new RssSyncCompleteEvent(processed));

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
@@ -69,9 +70,10 @@ namespace NzbDrone.Core.Indexers.Torznab
return settings;
}
protected override void Test(List<ValidationFailure> failures)
protected override async Task Test(List<ValidationFailure> failures)
{
base.Test(failures);
await base.Test(failures);
if (failures.HasErrors())
{
return;

View File

@@ -13,36 +13,36 @@
"45MinutesFourtyFive": "90 minut: {0}",
"60MinutesSixty": "60 minut: {0}",
"APIKey": "Klíč API",
"About": "O",
"About": "O aplikaci",
"AddListExclusion": "Přidat vyloučení seznamu",
"AddingTag": "Přidání značky",
"AgeWhenGrabbed": "Stáří (při zachycení)",
"AlreadyInYourLibrary": "Již ve vaší knihovně",
"AddingTag": "Přidání značky",
"AgeWhenGrabbed": "Stáří (kdy bylo získáno)",
"AlreadyInYourLibrary": "Již máte ve své knihovně",
"AlternateTitles": "Alternativní název",
"Analytics": "Analytics",
"Analytics": "Analýzy",
"AnalyticsEnabledHelpText": "Odesílejte anonymní informace o použití a chybách na servery Radarru. To zahrnuje informace o vašem prohlížeči, které stránky Radarr WebUI používáte, hlášení chyb a také verzi operačního systému a běhového prostředí. Tyto informace použijeme k upřednostnění funkcí a oprav chyb.",
"AppDataDirectory": "Adresář AppData",
"ApplyTags": "Použít značky",
"Authentication": "Ověření",
"Authentication": "Ověřování",
"AuthenticationMethodHelpText": "Vyžadovat uživatelské jméno a heslo pro přístup k Radarr",
"AuthorClickToChangeBook": "Kliknutím změníte film",
"AutoRedownloadFailedHelpText": "Automaticky vyhledejte a pokuste se stáhnout jiné vydání",
"AutoRedownloadFailedHelpText": "Automatické vyhledání a pokus o stažení jiného vydání",
"AutoUnmonitorPreviouslyDownloadedBooksHelpText": "Filmy odstraněné z disku jsou automaticky sledovány v Radarru",
"Automatic": "Automatický",
"BackupFolderHelpText": "Relativní cesty budou v adresáři AppData společnosti Radarr",
"BackupNow": "Zálohovat hned",
"BackupRetentionHelpText": "Automatické zálohy starší než doba uchování budou automaticky vyčištěny",
"BackupNow": "Ihned zálohovat",
"BackupRetentionHelpText": "Automatické zálohy starší než doba uchování budou automaticky vyčištěny",
"Backups": "Zálohy",
"BindAddress": "Vazba adresy",
"BindAddressHelpText": "Platná adresa IP4 nebo '*' pro všechna rozhraní",
"BindAddress": "Vázat adresu",
"BindAddressHelpText": "Platná IP adresa, localhost nebo '*' pro všechna rozhraní",
"BindAddressHelpTextWarning": "Vyžaduje restart, aby se projevilo",
"BookIsDownloading": "Film se stahuje",
"BookIsDownloadingInterp": "Film se stahuje - {0}% {1}",
"Branch": "Větev",
"BypassProxyForLocalAddresses": "Obejít proxy pro místní adresy",
"BypassProxyForLocalAddresses": "Obcházení proxy serveru pro místní adresy",
"Calendar": "Kalendář",
"CalendarWeekColumnHeaderHelpText": "Zobrazuje se nad každým sloupcem, když je aktivní zobrazení týden",
"Cancel": "zrušení",
"Cancel": "Zrušit",
"CancelMessageText": "Opravdu chcete zrušit tento nevyřízený úkol?",
"CertificateValidation": "Ověření certifikátu",
"CertificateValidationHelpText": "Změňte přísnost ověřování certifikátů HTTPS. Neměňte, pokud nerozumíte rizikům.",
@@ -53,33 +53,33 @@
"ChmodFolderHelpTextWarning": "Funguje to pouze v případě, že je uživatel souboru Radarr vlastníkem souboru. Je lepší zajistit, aby stahovací klient správně nastavil oprávnění.",
"ChownGroupHelpText": "Název skupiny nebo gid. Použijte gid pro vzdálené systémy souborů.",
"ChownGroupHelpTextWarning": "Funguje to pouze v případě, že je uživatel souboru Radarr vlastníkem souboru. Je lepší zajistit, aby stahovací klient používal stejnou skupinu jako Radarr.",
"Clear": "Průhledná",
"ClickToChangeQuality": "Klepnutím změníte kvalitu",
"Clear": "Vyčistit",
"ClickToChangeQuality": "Kliknutím změníte kvalitu",
"ClientPriority": "Priorita klienta",
"CloneIndexer": "Klonovat indexátor",
"CloneProfile": "Klonovat profil",
"Close": "Zavřít",
"Columns": "Sloupce",
"CompletedDownloadHandling": "Zpracování stahování bylo dokončeno",
"ConnectSettings": "Připojit nastavení",
"ConnectSettings": "Nastavení připojení",
"Connections": "Připojení",
"CopyUsingHardlinksHelpText": "Hardlinks použijte, když se pokoušíte kopírovat soubory z torrentů, které se stále používají",
"CopyUsingHardlinksHelpTextWarning": "Zámky souborů mohou občas zabránit přejmenování souborů, které se právě vysazují. Výsev můžete dočasně deaktivovat a použít funkci Radarr pro přejmenování.",
"CreateEmptyAuthorFoldersHelpText": "Během skenování disku vytvářejte chybějící složky filmů",
"CreateGroup": "Vytvořit skupinu",
"CutoffHelpText": "Jakmile je této kvality dosaženo, Radarr již nebude stahovat filmy",
"CutoffUnmet": "Cut-off Unmet",
"CutoffUnmet": "Vynechat nevyhovující",
"DBMigration": "Migrace databáze",
"Dates": "Termíny",
"DelayProfile": "Zpožděný profil",
"DelayProfile": "Profil zpoždění",
"DelayProfiles": "Profily zpoždění",
"DelayingDownloadUntilInterp": "Zpoždění stahování do {0} o {1}",
"Delete": "Vymazat",
"DeleteBackup": "Odstranit zálohu",
"DeleteBackupMessageText": "Opravdu chcete smazat zálohu „{0}“?",
"DeleteDelayProfile": "Smazat profil zpoždění",
"Delete": "Smazat",
"DeleteBackup": "Odstranění zálohy",
"DeleteBackupMessageText": "Opravdu chcete odstranit zálohu '{name}'?",
"DeleteDelayProfile": "Odstranění profilu zpoždění",
"DeleteDelayProfileMessageText": "Opravdu chcete smazat tento profil zpoždění?",
"DeleteDownloadClient": "Odstranit staženého klienta",
"DeleteDownloadClient": "Odstranění klienta pro stahování",
"DeleteDownloadClientMessageText": "Opravdu chcete odstranit klienta pro stahování „{0}“?",
"DeleteEmptyFolders": "Odstraňte prázdné složky",
"DeleteEmptyFoldersHelpText": "Během skenování disku a při mazání filmových souborů odstraňte prázdné složky s filmy",
@@ -455,12 +455,12 @@
"ShowTitle": "Ukázat nadpis",
"RemoveFromBlocklist": "Odebrat z černé listiny",
"UnableToLoadBlocklist": "Nelze načíst černou listinu",
"Component": "Součástka",
"Component": "Komponenta",
"Level": "Úroveň",
"ReleaseBranchCheckOfficialBranchMessage": "Pobočka {0} není platná větev vydání Radarr, nebudete dostávat aktualizace",
"Time": "Čas",
"Blocklist": "Černá listina",
"BlocklistRelease": "Vydání černé listiny",
"Blocklist": "Blocklist",
"BlocklistRelease": "Blocklist pro vydání",
"ShowUnknownAuthorItems": "Zobrazit neznámé položky filmu",
"ThisCannotBeCancelled": "Toto nelze zrušit po spuštění bez restartování Radarru.",
"UnselectAll": "Odznačit vše",
@@ -485,7 +485,7 @@
"SettingsRemotePathMappingRemotePath": "Vzdálená cesta",
"UISettingsSummary": "Možnosti kalendáře, data a barev",
"DownloadClientsSettingsSummary": "Stahování klientů, zpracování stahování a mapování vzdálených cest",
"AppDataLocationHealthCheckMessage": "Aktualizace nebude možné zabránit smazání AppData při aktualizaci",
"AppDataLocationHealthCheckMessage": "Aktualizace nebude možná, aby se zabránilo odstranění AppData při aktualizaci",
"FileWasDeletedByViaUI": "Soubor byl odstraněn prostřednictvím uživatelského rozhraní",
"IndexersSettingsSummary": "Indexery a omezení vydání",
"IndexerStatusCheckSingleClientMessage": "Indexery nedostupné z důvodu selhání: {0}",
@@ -556,7 +556,7 @@
"Conditions": "Podmínky",
"CopyToClipboard": "Zkopírovat do schránky",
"CustomFormat": "Vlastní formát",
"DeleteCustomFormat": "Odstranit vlastní formát",
"DeleteCustomFormat": "Odstranění vlastního formátu",
"DeleteCustomFormatMessageText": "Opravdu chcete odstranit indexer „{0}“?",
"ExportCustomFormat": "Exportovat vlastní formát",
"Formats": "Formáty",
@@ -573,14 +573,14 @@
"HideAdvanced": "Skrýt pokročilé",
"ShowAdvanced": "Zobrazit pokročilé",
"ShownClickToHide": "Zobrazeno, kliknutím se skryjete",
"ColonReplacement": "Výměna tlustého střeva",
"ColonReplacement": "Nahrazení dvojtečky",
"ReplaceWithDash": "Nahraďte Dash",
"ReplaceWithSpaceDashSpace": "Nahraďte Space Dash Space",
"ReplaceWithSpaceDash": "Nahraďte Space Dash",
"DeleteRemotePathMapping": "Upravit vzdálené mapování cesty",
"DeleteRemotePathMappingMessageText": "Opravdu chcete toto vzdálené mapování cesty odstranit?",
"Negated": "Negovaný",
"BlocklistReleases": "Vydání černé listiny",
"BlocklistReleases": "Blocklist pro vydání",
"DeleteConditionMessageText": "Opravdu chcete smazat značku „{0}“?",
"RemoveSelectedItemBlocklistMessageText": "Opravdu chcete odebrat vybrané položky z černé listiny?",
"RemoveSelectedItemQueueMessageText": "Opravdu chcete odebrat {0} položku {1} z fronty?",
@@ -597,12 +597,12 @@
"NoChange": "Žádná změna",
"RemovingTag": "Odebírání značky",
"SetTags": "Nastavit značky",
"ApplyTagsHelpTextAdd": "Přidat: Přidejte značky do existujícího seznamu značek",
"ApplyTagsHelpTextHowToApplyDownloadClients": "Jak použít značky na vybrané filmy",
"ApplyTagsHelpTextHowToApplyImportLists": "Jak použít značky na vybrané filmy",
"ApplyTagsHelpTextHowToApplyIndexers": "Jak použít značky na vybrané filmy",
"ApplyTagsHelpTextRemove": "Odebrat: Odebere zadané značky",
"ApplyTagsHelpTextReplace": "Nahradit: Nahradit tagy zadanými tagy (pro vymazání všech tagů zadejte žádné tagy)",
"ApplyTagsHelpTextAdd": "Přidat: Přidá značky k již existujícímu seznamu",
"ApplyTagsHelpTextHowToApplyDownloadClients": "Jak použít značky na vybrané klienty pro stahování",
"ApplyTagsHelpTextHowToApplyImportLists": "Jak použít značky na vybrané importní seznamy",
"ApplyTagsHelpTextHowToApplyIndexers": "Jak použít značky na vybrané indexátory",
"ApplyTagsHelpTextRemove": "Odebrat: Odebrat zadané značky",
"ApplyTagsHelpTextReplace": "Nahradit: Nahradit značky zadanými značkami (zadáním žádné značky vymažete všechny značky)",
"DeleteSelectedDownloadClients": "Odstranit staženého klienta",
"DeleteSelectedIndexersMessageText": "Opravdu chcete odstranit indexer „{0}“?",
"Yes": "Ano",
@@ -617,19 +617,50 @@
"Medium": "Střední",
"NoResultsFound": "Nebyly nalezeny žádné výsledky",
"SomeResultsAreHiddenByTheAppliedFilter": "Některé výsledky jsou použitým filtrem skryty",
"AllResultsAreHiddenByTheAppliedFilter": "Všechny výsledky jsou skryty použitým filtrem",
"AllResultsAreHiddenByTheAppliedFilter": "Všechny výsledky jsou schovány použitým filtrem",
"Events": "Události",
"FreeSpace": "Volný prostor",
"System": "Systém",
"TotalSpace": "Celkový prostor",
"ConnectionLost": "Spojení ztraceno",
"ConnectionLostReconnect": "Radarr se pokusí připojit automaticky, nebo můžete kliknout na znovu načíst níže.",
"ConnectionLostToBackend": "Radarr ztratil spojení s back-endem a pro obnovení funkčnosti bude nutné jej znovu načíst.",
"ConnectionLostReconnect": "{appName} se pokusí připojit automaticky, nebo můžete kliknout na tlačítko znovunačtení níže.",
"ConnectionLostToBackend": "{appName} ztratila spojení s backendem a pro obnovení funkčnosti bude třeba ji znovu načíst.",
"Large": "Velký",
"LastDuration": "lastDuration",
"Ui": "UI",
"WhatsNew": "Co je nového?",
"Activity": "Aktivita",
"AddNew": "Přidat nový",
"NextExecution": "Další spuštění"
"AddNew": "Přidat nové",
"NextExecution": "Další spuštění",
"ClickToChangeReleaseGroup": "Kliknutím změníte skupinu vydání",
"ApplicationURL": "URL aplikace",
"ApplicationUrlHelpText": "Externí adresa URL této aplikace včetně http(s)://, portu a základní adresy URL",
"Continuing": "Pokračující",
"AutomaticUpdatesDisabledDocker": "Automatické aktualizace nejsou při použití aktualizačního mechanismu Docker přímo podporovány. Obraz kontejneru je nutné aktualizovat mimo {appName} nebo použít skript",
"AppUpdated": "{appName} aktualizován",
"ApplyChanges": "Použít změny",
"AutoAdd": "Přidat automaticky",
"AutomaticAdd": "Přidat automaticky",
"ChownGroup": "Skupina chown",
"CloneCondition": "Klonovat podmínku",
"Clone": "Klonovat",
"ApiKeyValidationHealthCheckMessage": "Aktualizujte svůj klíč API tak, aby měl alespoň {0} znaků. Můžete to provést prostřednictvím nastavení nebo konfiguračního souboru",
"ChooseImportMethod": "Vyberte mód importu",
"CatalogNumber": "katalogové číslo",
"Publisher": "Vydavatel",
"StatusEndedContinuing": "Pokračující",
"MetadataProfiles": "profil metadat",
"ReleaseProfiles": "profil vydání",
"MetadataProfile": "profil metadat",
"Label": "Etiketa",
"Library": "Knihovna",
"BypassIfAboveCustomFormatScore": "Obejít, pokud je vyšší než skóre vlastního formátu",
"AppUpdatedVersion": "{appName} byla aktualizována na verzi `{version}`, abyste získali nejnovější změny, musíte znovu načíst {appName}.",
"BypassIfAboveCustomFormatScoreHelpText": "Povolit obcházení, pokud má vydání vyšší skóre, než je nakonfigurované minimální skóre vlastního formátu",
"BypassIfHighestQuality": "Obejít v případě nejvyšší kvality",
"Theme": "Motiv",
"MinimumCustomFormatScoreHelpText": "Minimální skóre vlastního formátu požadované pro obejití zpoždění preferovaného protokolu",
"Series": "Seriál",
"DeleteCondition": "Odstranit podmínku",
"Database": "Databáze"
}

View File

@@ -306,6 +306,7 @@
"Ended": "Ended",
"EndedAllBooksDownloaded": "Ended (All books downloaded)",
"EntityName": "Entity Name",
"ErrorLoadingContent": "There was an error loading this content",
"ErrorLoadingContents": "Error loading contents",
"ErrorLoadingPreviews": "Error loading previews",
"Events": "Events",

View File

@@ -478,7 +478,7 @@
"AllowAuthorChangeClickToChangeAuthor": "Click para cambiar el autor",
"AddedAuthorSettings": "Añadidas las opciones de Autor",
"AddImportListExclusionHelpText": "Evitar que el libro se añada a Readarr mediante listas de importación o la actualización del autor",
"AddMissing": "Añadir los que faltan",
"AddMissing": "Añadir faltantes",
"AllBooks": "Todos los libros",
"AddNewItem": "Añadir nuevo item",
"AllExpandedExpandAll": "Expandir todo",
@@ -539,7 +539,7 @@
"IndexerLongTermStatusCheckAllClientMessage": "Ningún indexer está disponible por errores durando más de 6 horas",
"IndexerLongTermStatusCheckSingleClientMessage": "Indexers no disponible por errores durando más de 6 horas: {0}",
"IndexerRssHealthCheckNoAvailableIndexers": "Todos los indexers capaces de RSS están temporalmente desactivados debido a errores recientes con el indexer",
"IndexerSearchCheckNoInteractiveMessage": "No hay Indexers con Búsqueda Interactiva activada, Radarr no obtendrá ningún resultado en las búsquedas",
"IndexerSearchCheckNoInteractiveMessage": "No hay Indexers con Búsqueda Interactiva activada, Readarr no obtendrá ningún resultado en las búsquedas",
"IndexerStatusCheckSingleClientMessage": "Indexers no disponibles debido a errores: {0}",
"OnBookFileDeleteForUpgrade": "En archivo de película Eliminar para actualizar",
"OnBookFileDeleteForUpgradeHelpText": "En archivo de película Eliminar para actualizar",
@@ -728,5 +728,31 @@
"Events": "Eventos",
"Medium": "Medio",
"AllResultsAreHiddenByTheAppliedFilter": "Todos los resultados están ocultos por el filtro aplicado",
"CatalogNumber": "número de catálogo"
"CatalogNumber": "número de catálogo",
"Authors": "Autores",
"AuthorEditor": "Editor de Autor",
"Author": "Autor",
"BackupIntervalHelpText": "Intervalo para respaldar la base de datos y configuraciones de Readarr",
"BookFileCounttotalBookCountBooksDownloadedInterp": "{0}/{1} libros descargados",
"BookList": "Lista de libros",
"BookFilesCountMessage": "No hay archivos de libros",
"AllowFingerprintingHelpText": "Utilizar la huella digital para mejorar la precisión de la coincidencia de libros",
"BookStudio": "Estudio de libros",
"AutomaticUpdatesDisabledDocker": "Las actualizaciones automáticas no son compatibles directamente al usar el mecanismo de actualización de Docker. Deberás actualizar la imagen del contenedor fuera de {appName} o utilizar un script",
"AuthorIndex": "Índice de autor",
"AudioFileMetadata": "Escribir metadatos en archivos de audio",
"BookIndex": "Índice de libro",
"BookEditor": "Editor de Libro",
"ASIN": "ASIN",
"AnyEditionOkHelpText": "Readarr cambiará automáticamente a la edición que mejor coincida con los archivos descargados",
"AppUpdated": "{appName} Actualizado",
"AppUpdatedVersion": "{appName} ha sido actualizado a la versión `{version}`, para obtener los cambios más recientes, necesitaras recargar {appName}",
"AuthorFolderFormat": "Formato de Carpeta de Autor",
"AuthorNameHelpText": "El nombre del autor/libro a excluir (puede ser cualquier cosa significativa)",
"BookMonitoring": "Monitoreo de libros",
"AllowFingerprintingHelpTextWarning": "Esto requiere que Readarr lea partes del archivo, lo que ralentizará los escaneos y puede provocar una alta actividad en el disco o en la red.",
"BookFileCountBookCountTotalTotalBookCountInterp": "{0} / {1} (Total: {2})",
"Book": "Libro",
"AutomaticallySwitchEdition": "Cambiar edición automáticamente",
"BookNaming": "Nombrado de libros"
}

View File

@@ -646,5 +646,7 @@
"ApplyTagsHelpTextHowToApplyDownloadClients": "Hoe tags toepassen op de geselecteerde download clients",
"ApplyTagsHelpTextRemove": "Verwijderen: Verwijder de ingevoerde tags",
"ApplyTagsHelpTextReplace": "Vervangen: Vervang de tags met de ingevoerde tags (vul geen tags in om alle tags te wissen)",
"AutoAdd": "Automatisch Toevoegen"
"AutoAdd": "Automatisch Toevoegen",
"Activity": "Activiteit",
"AddNew": "Voeg Nieuwe Toe"
}

View File

@@ -22,10 +22,10 @@
"ProxyPasswordHelpText": "Você só precisa digitar um nome de usuário e senha se for necessário. Caso contrário, deixe-os em branco.",
"SslCertPathHelpTextWarning": "Requer reinício para ter efeito",
"UnableToLoadMetadataProfiles": "Não foi possível carregar os perfis de metadados",
"AddListExclusion": "Adicionar exclusão à lista",
"AddListExclusion": "Adicionar à Lista de Exclusão",
"AddingTag": "Adicionar tag",
"AlreadyInYourLibrary": "Já está na sua biblioteca",
"AlternateTitles": "Títulos alternativos",
"AlternateTitles": "Títulos Alternativos",
"Analytics": "Analítica",
"AnalyticsEnabledHelpText": "Envie informações anônimas de uso e erro para os servidores do Readarr. Isso inclui informações sobre seu navegador, quais páginas da interface Web do Readarr você usa, relatórios de erros, e a versão do sistema operacional e do tempo de execução. Usaremos essas informações para priorizar recursos e correções de bugs.",
"AppDataDirectory": "Diretório AppData",
@@ -57,7 +57,7 @@
"Edit": "Editar",
"Edition": "Edição",
"Enable": "Habilitar",
"GrabRelease": "Capturar Versão",
"GrabRelease": "Obter Lançamento",
"GrabReleaseMessageText": "O Readarr não conseguiu determinar a qual autor e livro esse lançamento está relacionado. O Readarr pode não conseguir importar automaticamente este lançamento. Quer obter \"{0}\"?",
"GrabSelected": "Obter Selecionado",
"Group": "Grupo",
@@ -122,7 +122,7 @@
"DeleteIndexerMessageText": "Tem certeza de que deseja excluir o indexador '{name}'?",
"DeleteMetadataProfileMessageText": "Tem certeza de que deseja excluir o perfil de metadados '{name}'?",
"DeleteNotification": "Excluir Notificação",
"DeleteNotificationMessageText": "Tem certeza de que deseja excluir o perfil de metadados '{name}'?",
"DeleteNotificationMessageText": "Tem certeza de que deseja excluir a notificação '{name}'?",
"DeleteQualityProfile": "Excluir Perfil de Qualidade",
"DeleteQualityProfileMessageText": "Tem certeza de que deseja excluir o perfil de qualidade '{name}'?",
"DeleteReleaseProfile": "Excluir Perfil de Lançamento",
@@ -133,7 +133,7 @@
"DeleteTag": "Excluir tag",
"DeleteTagMessageText": "Tem certeza de que deseja excluir a tag \"{0}\"?",
"DestinationPath": "Caminho de Destino",
"DetailedProgressBarHelpText": "Mostrar texto em barra de progresso",
"DetailedProgressBarHelpText": "Mostrar texto na barra de progresso",
"EnableAutomaticAdd": "Habilitar Adição Automática",
"EnableAutomaticSearch": "Ativar a pesquisa automática",
"EnableColorImpairedMode": "Habilitar Modo para Deficientes Visuais",
@@ -145,7 +145,7 @@
"EnableSSL": "Habilitar SSL",
"EnableSslHelpText": " Requer a reinicialização com a execução como administrador para fazer efeito",
"Ended": "Terminou",
"ErrorLoadingContents": "Erro ao carregar conteúdo",
"ErrorLoadingContents": "Erro ao carregar o conteúdo",
"ErrorLoadingPreviews": "Erro ao carregar as visualizações",
"Exception": "Exceção",
"ExtraFileExtensionsHelpTexts1": "Lista separada por vírgulas de arquivos adicionais a importar (.nfo será importado como .nfo-orig)",
@@ -202,7 +202,7 @@
"Logging": "Registrando",
"LongDateFormat": "Formato de Data Longa",
"MIA": "Desaparecidos",
"ManualImport": "Importação manual",
"ManualImport": "Importação Manual",
"MarkAsFailed": "Marcar como Falha",
"MarkAsFailedMessageText": "Tem certeza que deseja marcar \"{0}\" como falhado?",
"MaximumLimits": "Limites Máximos",
@@ -239,7 +239,7 @@
"OpenBrowserOnStart": "Abrir navegador ao iniciar",
"Options": "Opções",
"Original": "Original",
"Overview": "Visão geral",
"Overview": "Visão Geral",
"PackageVersion": "Versão do pacote",
"PageSize": "Tamanho da página",
"PageSizeHelpText": "Quantidade de itens a exibir em cada página",
@@ -249,8 +249,8 @@
"Port": "Porta",
"PortHelpTextWarning": "Requer reinício para ter efeito",
"PortNumber": "Número da Porta",
"PosterSize": "Tamanho do pôster",
"PreviewRename": "Visualizar renomeação",
"PosterSize": "Tamanho do Pôster",
"PreviewRename": "Prévia da Renomeação",
"Profiles": "Perfis",
"Proper": "Proper",
"PropersAndRepacks": "Propers e repacks",
@@ -284,7 +284,7 @@
"RefreshInformationAndScanDisk": "Atualizar as informações e verificar o disco",
"ReleaseDate": "Data de lançamento",
"ReleaseGroup": "Grupo de lançamento",
"ReleaseRejected": "Versão rejeitada",
"ReleaseRejected": "Lançamento Rejeitado",
"ReleaseWillBeProcessedInterp": "A versão será processada {0}",
"Reload": "Recarregar",
"RemotePathMappings": "Mapeamentos de Caminho Remoto",
@@ -327,9 +327,9 @@
"Scheduled": "Programado",
"ScriptPath": "Caminho do Script",
"Search": "Pesquisar",
"SearchAll": "Pesquisar tudo",
"SearchAll": "Pesquisar Todos",
"SearchForMissing": "Pesquisar por Ausentes",
"SearchSelected": "Pesquisar selecionado(s)",
"SearchSelected": "Pesquisar Selecionado",
"Security": "Segurança",
"SendAnonymousUsageData": "Enviar dados de uso anônimos",
"SetPermissions": "Definir Permissões",
@@ -338,17 +338,17 @@
"Settings": "Configurações",
"ShortDateFormat": "Formato de Data Curta",
"ShowCutoffUnmetIconHelpText": "Mostrar ícone para arquivos quando o limite não foi atingindo",
"ShowDateAdded": "Mostrar data de adição",
"ShowMonitored": "Mostrar monitorado(s)",
"ShowMonitoredHelpText": "Mostrar status de monitoramento sob o pôster",
"ShowPath": "Mostrar caminho",
"ShowQualityProfile": "Mostrar perfil de qualidade",
"ShowQualityProfileHelpText": "Mostrar perfil de qualidade sob o pôster",
"ShowDateAdded": "Mostrar Data de Adição",
"ShowMonitored": "Mostrar Monitorados",
"ShowMonitoredHelpText": "Mostrar status monitorado sob o pôster",
"ShowPath": "Mostrar Caminho",
"ShowQualityProfile": "Mostrar Perfil de Qualidade",
"ShowQualityProfileHelpText": "Mostrar perfil de qualidade abaixo do pôster",
"ShowRelativeDates": "Mostrar Datas Relativas",
"ShowRelativeDatesHelpText": "Mostrar datas relativas (Hoje/Ontem/etc) ou absolutas",
"ShowSearch": "Mostrar pesquisa",
"ShowSearch": "Mostrar Pesquisa",
"ShowSearchActionHelpText": "Mostrar botão de pesquisa ao passar o mouse",
"ShowSizeOnDisk": "Mostrar tamanho no disco",
"ShowSizeOnDisk": "Mostrar Tamanho no Disco",
"ShownAboveEachColumnWhenWeekIsTheActiveView": "Mostrado acima de cada coluna quando a semana é a exibição ativa",
"Size": " Tamanho",
"SkipFreeSpaceCheck": "Ignorar verificação de espaço livre",
@@ -385,7 +385,7 @@
"TorrentDelay": "Atraso do Torrent",
"TorrentDelayHelpText": "Demora em minutos para esperar antes de pegar um torrent",
"Torrents": "Torrents",
"TotalFileSize": "Tamanho total do arquivo",
"TotalFileSize": "Tamanho Total do Arquivo",
"UILanguage": "Idioma da interface",
"UILanguageHelpText": "Idioma que o Readarr usará para a interface",
"UILanguageHelpTextWarning": "É necessário recarregar o navegador",
@@ -470,14 +470,14 @@
"ShowLastBook": "Mostrar o último livro",
"ShowBookCount": "Mostrar contagem de livros",
"ShowBannersHelpText": "Mostrar banners em vez de nomes",
"ShowBanners": "Mostrar banners",
"ShowBanners": "Mostrar Banners",
"ShouldSearchHelpText": "Indexadores de pesquisa para novos itens adicionados. Tenha cuidado ao usar com listas grandes.",
"ShouldMonitorHelpText": "Monitorar novos autores e livros adicionados por esta lista",
"SeriesNumber": "Número da série",
"Series": "Séries",
"SendMetadataToCalibre": "Enviar metadados para o Calibre",
"SelectedCountAuthorsSelectedInterp": "{0} autor(es) selecionado(s)",
"SearchMonitored": "Pesquisar monitorados",
"SearchMonitored": "Pesquisa Monitorada",
"SearchForNewItems": "Pesquisar novos itens",
"SearchForMonitoredBooks": "Pesquisar livros monitorados",
"SearchForAllMissingBooks": "Pesquisar todos os livros ausentes",
@@ -604,7 +604,7 @@
"ContinuingNoAdditionalBooksAreExpected": "Não espera-se mais livros",
"ContinuingMoreBooksAreExpected": "Espera-se mais livros",
"ContinuingAllBooksDownloaded": "Continuação (todos os livros baixados)",
"Continuing": "Continuação",
"Continuing": "Continuando",
"ConsoleLogLevel": "Nível de log do console",
"CollapseMultipleBooksHelpText": "Recolher vários livros lançados no mesmo dia",
"CollapseMultipleBooks": "Recolher vários livros",
@@ -675,7 +675,7 @@
"ShowBookTitleHelpText": "Mostrar título do filme abaixo do pôster",
"ShowReleaseDate": "Mostrar data de lançamento",
"NotAvailable": "Indisponível",
"ShowTitle": "Mostrar título",
"ShowTitle": "Mostrar Título",
"TheAuthorFolderAndAllOfItsContentWillBeDeleted": "A pasta do filme \"{0}\" e todo o seu conteúdo serão excluídos.",
"BookMonitoring": "Monitoramento de Livro",
"ExistingItems": "Itens Existentes",
@@ -702,10 +702,10 @@
"BookIndex": "Índice de Livro",
"MassBookSearch": "Busca em Massa por Livro",
"MassBookSearchWarning": "Tem certeza de que deseja executar pesquisa em massa para {0} livros?",
"SelectAll": "Selecionar Todos",
"SelectAll": "Selecionar Tudo",
"SelectedCountBooksSelectedInterp": "{0} Livro(s) Selecionado(s)",
"TheFollowingFilesWillBeDeleted": "Os seguintes arquivos irão ser excluídos:",
"UnselectAll": "Deselecionar Todos",
"UnselectAll": "Desmarcar Todos",
"BookEditor": "Editor de Livro",
"BookList": "Lista de Livros",
"ThisCannotBeCancelled": "Isso não pode ser cancelado uma vez iniciado sem desabilitar todos os seus indexadores.",
@@ -839,7 +839,7 @@
"EditList": "Editar Lista",
"MonitorExistingBooks": "Monitorar Livros Existentes",
"RenameFiles": "Renomear Arquivos",
"Test": "Testar",
"Test": "Teste",
"WriteMetadataTags": "Salvar tags de metadados",
"RestartRequiredHelpTextWarning": "Requer reinicialização para entrar em vigor",
"InstanceName": "Nome da Instância",
@@ -862,12 +862,12 @@
"Theme": "Tema",
"ThemeHelpText": "Alterar o tema da interface do usuário do aplicativo, o tema 'Auto' usará o tema do sistema operacional para definir o modo Claro ou Escuro. Inspirado por Theme.Park",
"EnableRssHelpText": "Será usado quando o Readarr procurar periodicamente lançamentos via RSS Sync",
"HardlinkCopyFiles": "Vínculo real/Copiar arquivos",
"MoveFiles": "Mover arquivos",
"HardlinkCopyFiles": "Hardlink/Copiar Arquivos",
"MoveFiles": "Mover Arquivos",
"OnApplicationUpdate": "Na Atualização do Aplicativo",
"OnApplicationUpdateHelpText": "Ao atualizar o aplicativo",
"ChooseImportMethod": "Escolha o método de importação",
"ClickToChangeReleaseGroup": "Clique para alterar o grupo do lançamento",
"ClickToChangeReleaseGroup": "Clique para alterar o grupo de lançamento",
"BypassIfAboveCustomFormatScore": "Ignorar se estiver acima da pontuação do formato personalizado",
"BypassIfAboveCustomFormatScoreHelpText": "Ativar ignorar quando a versão tiver uma pontuação maior que a pontuação mínima configurada do formato personalizado",
"BypassIfHighestQuality": "Ignorar se a qualidade mais alta",
@@ -921,7 +921,7 @@
"ReplaceWithSpaceDashSpace": "Substituir com Espaço, Traço e Espaço",
"DeleteRemotePathMapping": "Excluir Mapeamento de Caminho Remoto",
"BlocklistReleases": "Lançamentos na lista de bloqueio",
"CloneCondition": "Condição de clone",
"CloneCondition": "Clonar Condição",
"DeleteConditionMessageText": "Tem certeza de que deseja excluir a condição '{name}'?",
"DeleteRemotePathMappingMessageText": "Tem certeza de que deseja excluir este mapeamento de caminho remoto?",
"DeleteCondition": "Excluir condição",
@@ -1016,5 +1016,7 @@
"ConnectionLostToBackend": "{appName} perdeu sua conexão com o backend e precisará ser recarregado para restaurar a funcionalidade.",
"CountAuthorsSelected": "{selectedCount} autor(es) selecionado(s)",
"RecentChanges": "Mudanças Recentes",
"WhatsNew": "O quê há de novo?"
"WhatsNew": "O que há de novo?",
"RemotePathMappingsInfo": "Raramente são necessários mapeamentos de caminho remoto, se {app} e seu cliente de download estiverem no mesmo sistema, é melhor combinar seus caminhos. Para obter mais informações, consulte o [wiki]({wikiLink}).",
"ErrorLoadingContent": "Ocorreu um erro ao carregar este conteúdo"
}

View File

@@ -114,7 +114,7 @@
"Cancel": "Anulează",
"CancelMessageText": "Sigur doriți să anulați această sarcină în așteptare?",
"CertificateValidation": "Validarea certificatului",
"CertificateValidationHelpText": "Modificați cât de strictă este validarea certificării HTTPS",
"CertificateValidationHelpText": "Modificați cât de strictă este validarea certificării HTTPS. Nu schimbați dacă nu înțelegeți riscurile.",
"ChangeHasNotBeenSavedYet": "Modificarea nu a fost încă salvată",
"ChmodFolder": "chmod Folder",
"ChmodFolderHelpText": "Octal, aplicat în timpul importului / redenumirii în dosare și fișiere media (fără biți de executare)",
@@ -149,11 +149,11 @@
"DeleteImportListMessageText": "Sigur doriți să ștergeți lista „{0}”?",
"DeleteIndexer": "Ștergeți Indexer",
"DeleteIndexerMessageText": "Sigur doriți să ștergeți indexatorul „{0}”?",
"DeleteMetadataProfileMessageText": "Sigur doriți să ștergeți profilul de calitate {0}",
"DeleteMetadataProfileMessageText": "Sigur doriți să ștergeți profilul de metadate '{name}'?",
"DeleteNotification": "Ștergeți notificarea",
"DeleteNotificationMessageText": "Sigur doriți să ștergeți notificarea „{0}”?",
"DeleteQualityProfile": "Ștergeți profilul de calitate",
"DeleteQualityProfileMessageText": "Sigur doriți să ștergeți profilul de calitate {0}",
"DeleteQualityProfileMessageText": "Sigur doriți să ștergeți profilul de calitate '{name}'?",
"DeleteReleaseProfile": "Ștergeți profilul de întârziere",
"DeleteReleaseProfileMessageText": "Sigur doriți să ștergeți acest profil de întârziere?",
"DeleteSelectedBookFiles": "Ștergeți fișierele film selectate",
@@ -617,7 +617,7 @@
"Authentication": "Autentificare",
"ResetQualityDefinitions": "Resetare definitii de calitate",
"ChooseImportMethod": "Alege modul de import",
"NotificationStatusSingleClientHealthCheckMessage": "Aplicații indisponibile datorită erorilor: {0}",
"NotificationStatusSingleClientHealthCheckMessage": "Notificări indisponibile datorită erorilor: {0}",
"Small": "Mic",
"Ui": "Interfață Grafica",
"Events": "Evenimente",

View File

@@ -80,23 +80,23 @@
"DelayingDownloadUntilInterp": "Приостановить скачивание до {0} в {1}",
"Delete": "Удалить",
"DeleteBackup": "Удалить резервную копию",
"DeleteBackupMessageText": "Вы уверены, что хотите удалить резервную копию '{0}'?",
"DeleteBackupMessageText": "Вы уверены, что хотите удалить резервную копию '{name}'?",
"DeleteDelayProfile": "Удалить профиль задержки",
"DeleteDelayProfileMessageText": "Вы уверены, что хотите удалить этот профиль задержки?",
"DeleteDownloadClient": "Удалить программу для скачивания",
"DeleteDownloadClientMessageText": "Вы уверены, что хотите удалить программу для скачивания '{0}'?",
"DeleteDownloadClientMessageText": "Вы уверены, что хотите удалить клиент загрузки '{name}'?",
"DeleteEmptyFolders": "Удалить пустые папки",
"DeleteEmptyFoldersHelpText": "Удалять пустые папки во время сканирования диска, а так же после удаления файлов фильмов",
"DeleteImportListExclusion": "Удалить лист исключения для импорта",
"DeleteImportListExclusionMessageText": "Вы уверены, что хотите удалить этот лист исключений?",
"DeleteImportListMessageText": "Вы уверены, что хотите удалить список '{0}'?",
"DeleteImportListExclusionMessageText": "Вы уверены, что хотите удалить это исключение из списка импорта?",
"DeleteImportListMessageText": "Вы уверены, что хотите удалить список '{name}'?",
"DeleteIndexer": "Удалить индексер",
"DeleteIndexerMessageText": "Вы уверены что хотите удалить индексер '{0}'?",
"DeleteIndexerMessageText": "Вы уверены что хотите удалить индексатор '{name}'?",
"DeleteMetadataProfileMessageText": "Вы действительно хотите удалить профиль качества {0}",
"DeleteNotification": "Удалить уведомление",
"DeleteNotificationMessageText": "Вы уверены, что хотите удалить уведомление '{0}'?",
"DeleteNotificationMessageText": "Вы уверены, что хотите удалить уведомление '{name}'?",
"DeleteQualityProfile": "Удалить качественный профиль",
"DeleteQualityProfileMessageText": "Вы действительно хотите удалить профиль качества {0}",
"DeleteQualityProfileMessageText": "Вы уверены, что хотите удалить профиль качества '{name}'?",
"DeleteReleaseProfile": "Удалить профиль задержки",
"DeleteReleaseProfileMessageText": "Вы уверены, что хотите удалить этот профиль задержки?",
"DeleteSelectedBookFiles": "Удалить выбранные файлы фильма",
@@ -613,10 +613,10 @@
"CustomFormats": "Настраиваемое форматирование",
"CutoffFormatScoreHelpText": "Radarr перестанет скачивать фильмы после достижения указанного количества очков",
"DeleteCustomFormat": "Удалить пользовательский формат",
"DeleteCustomFormatMessageText": "Вы уверены что хотите удалить индексер '{0}'?",
"DeleteCustomFormatMessageText": "Вы уверены, что хотите удалить пользовательский формат '{name}'?",
"MinFormatScoreHelpText": "Минимальная оценка пользовательского формата для скачивания",
"NegateHelpText": "Если отмечено, то настроенный формат не будет применён при условии {0}.",
"ResetDefinitionTitlesHelpText": "Сбросить заголовки определений, а также значения",
"ResetDefinitionTitlesHelpText": "Сбросить названия определений, а также значения",
"ResetDefinitions": "Сбросить определения",
"ResetTitles": "Сбросить заголовки",
"ImportListMissingRoot": "Отсутствует корневая папка для импортирования списка(ов): {0}",
@@ -634,7 +634,7 @@
"ApiKeyValidationHealthCheckMessage": "Пожалуйста, обновите свой ключ API, чтобы он был длиной не менее {0} символов. Вы можете сделать это через настройки или файл конфигурации",
"BlocklistReleaseHelpText": "Запрещает Lidarr автоматически получать этот релиз повторно",
"NoEventsFound": "Событий не найдено",
"RemoveSelectedItemQueueMessageText": "Вы действительно хотите удалить {0} из очереди?",
"RemoveSelectedItemQueueMessageText": "Вы уверены, что хотите удалить 1 элемент из очереди?",
"RemoveSelectedItems": "Удалить выбранные элементы",
"RemoveSelectedItemsQueueMessageText": "Вы действительно хотите удалить {0} из очереди?",
"ResetQualityDefinitions": "Сбросить определения качества",
@@ -647,26 +647,26 @@
"SetTags": "Установить теги",
"Yes": "Да",
"ApplyTagsHelpTextHowToApplyAuthors": "Как добавить ярлыки к выбранным фильмам",
"DeleteSelectedIndexersMessageText": "Вы уверены что хотите удалить индексер '{0}'?",
"DeleteSelectedIndexersMessageText": "Вы уверены, что хотите удалить {count} выбранных индексатора?",
"BlocklistReleases": "Релиз из черного списка",
"DeleteRemotePathMapping": "Редактировать расположение подключенной папки",
"DeleteRemotePathMapping": "Удалить сопоставление удаленного пути",
"Negated": "Отрицательный",
"RedownloadFailed": "Неудачное скачивание",
"RemoveCompleted": "Удаление завершено",
"RemoveDownloadsAlert": "Настройки удаления были перенесены в отдельные настройки клиента загрузки выше.",
"RemoveFailed": "Удаление не удалось",
"RemoveSelectedItem": "Удалить выбранный элемент",
"ApplyChanges": "Применять изменения",
"ApplyTagsHelpTextAdd": "Добавить: добавить ярлыки к существующему списку",
"ApplyTagsHelpTextHowToApplyImportLists": "Как добавить ярлыки к выбранным фильмам",
"ApplyTagsHelpTextHowToApplyIndexers": "Как добавить ярлыки к выбранным фильмам",
"ApplyTagsHelpTextRemove": "Убрать: Убрать введенные тэги",
"ApplyTagsHelpTextReplace": "Заменить: Изменить существующие тэги на введенные тэги (оставьте пустым чтобы очистить все тэги)",
"ApplyTagsHelpTextHowToApplyDownloadClients": "Как добавить ярлыки к выбранным фильмам",
"DeleteSelectedDownloadClients": "Удалить программу для скачивания",
"DeleteSelectedDownloadClientsMessageText": "Вы уверены что хотите удалить индексер '{0}'?",
"DeleteSelectedImportListsMessageText": "Вы уверены что хотите удалить индексер '{0}'?",
"DeleteSelectedIndexers": "Удалить индексер",
"ApplyChanges": "Применить изменения",
"ApplyTagsHelpTextAdd": "Добавить: Добавьте теги в существующий список тегов",
"ApplyTagsHelpTextHowToApplyImportLists": "Как применить теги к выбранным спискам импорта",
"ApplyTagsHelpTextHowToApplyIndexers": "Как применить теги к выбранным индексаторам",
"ApplyTagsHelpTextRemove": "Удалить: удалить введенные теги",
"ApplyTagsHelpTextReplace": "Заменить: заменить теги введенными тегами (оставьте поле пустым, чтобы удалить все теги)",
"ApplyTagsHelpTextHowToApplyDownloadClients": "Как применить теги к выбранным клиентам загрузки",
"DeleteSelectedDownloadClients": "Удалить клиент(ы) загрузки",
"DeleteSelectedDownloadClientsMessageText": "Вы уверены, что хотите удалить {count} выбранных клиента загрузки?",
"DeleteSelectedImportListsMessageText": "Вы уверены, что хотите удалить {count} выбранных списков импорта?",
"DeleteSelectedIndexers": "Удалить индексатор(ы)",
"DownloadClientTagHelpText": "Используйте этот индексатор только для фильмов с хотя бы одним совпадающим тегом. Оставьте пустым, чтобы использовать для всех фильмов.",
"EditSelectedImportLists": "Редактировать выбранные списки импорта",
"ExistingTag": "Существующий тэг",
@@ -678,7 +678,7 @@
"Activity": "Активность",
"AddNew": "Добавить",
"Medium": "Средний",
"NotificationStatusAllClientHealthCheckMessage": "Все листы недоступны из-за ошибок",
"NotificationStatusAllClientHealthCheckMessage": "Все уведомления недоступны из-за сбоев",
"TotalSpace": "Общее сводное место",
"Ui": "Пользовательский интерфейс",
"Backup": "Резервное копирование",
@@ -687,7 +687,7 @@
"LastWriteTime": "Последнее время записи",
"Library": "Библиотека",
"NoResultsFound": "Нет результатов",
"NotificationStatusSingleClientHealthCheckMessage": "Листы недоступны из-за ошибок: {0}",
"NotificationStatusSingleClientHealthCheckMessage": "Уведомления недоступны из-за сбоев: {0}",
"System": "Система",
"AllResultsAreHiddenByTheAppliedFilter": "Все результаты скрыты фильтром",
"ConnectionLostToBackend": "Radarr потерял связь с сервером и его необходимо перезагрузить, чтобы восстановить работоспособность.",
@@ -696,5 +696,36 @@
"SomeResultsAreHiddenByTheAppliedFilter": "Некоторые результаты скрыты примененным фильтром",
"WhatsNew": "Что нового?",
"NextExecution": "Следующее выполнение",
"Small": "Маленький"
"Small": "Маленький",
"DeleteRootFolder": "Удалить корневую папку",
"ListWillRefreshEveryInterp": "Список будет обновляться каждые {0}",
"ListRefreshInterval": "Интервал обновления списка",
"RemoveCompletedDownloads": "Удалить завершенные загрузки",
"RemoveFailedDownloads": "Удаление неудачных загрузок",
"RemoveSelectedItemBlocklistMessageText": "Вы уверены, что хотите удалить выбранные элементы из списка заблокированных?",
"ManageClients": "Управление клиентами",
"ManageDownloadClients": "Менеджер клиентов загрузки",
"ManageImportLists": "Управление списками импорта",
"ManageIndexers": "Управление индексаторами",
"DeleteSelectedImportLists": "Удалить списки импорта",
"ManageLists": "Управление листами",
"NoDownloadClientsFound": "Клиенты для загрузки не найдены",
"NoImportListsFound": "Списки импорта не найдены",
"NoHistoryBlocklist": "Нет истории блокировок",
"ThereWasAnErrorLoadingThisItem": "Произошла ошибка при загрузке этого элемента",
"ThereWasAnErrorLoadingThisPage": "Произошла ошибка при загрузке этой страницы",
"SkipRedownload": "Пропустить повторное скачивание",
"DeleteImportList": "Удалить список импорта",
"AutomaticUpdatesDisabledDocker": "Автоматические обновления напрямую не поддерживаются при использовании механизма обновления Docker. Вам нужно будет обновить образ контейнера за пределами {AppName} или использовать скрипт",
"EditSelectedIndexers": "Редактировать выбранный индексатор",
"CloneCondition": "Условие клонирования",
"DeleteCondition": "Удалить условие",
"Implementation": "Реализация",
"NoIndexersFound": "Индексаторы не найдены",
"IndexerDownloadClientHealthCheckMessage": "Индексаторы с недопустимыми клиентами загрузки: {0}.",
"AutomaticAdd": "Автоматическое добавление",
"ResetQualityDefinitionsMessageText": "Вы уверены, что хотите сбросить определения качества?",
"DeleteRemotePathMappingMessageText": "Вы уверены, что хотите удалить это сопоставление удаленного пути?",
"RemotePathMappingsInfo": "Сопоставление удаленных путей требуется крайне редко, если {app} и клиент загрузки находятся в одной системе, то лучше согласовать пути. Более подробную информацию можно найти в [wiki]({wikiLink}).",
"WouldYouLikeToRestoreBackup": "Желаете восстановить резервную копию {name} ?"
}

View File

@@ -1,6 +1,6 @@
{
"ChmodFolder": "修改文件夹权限",
"ChmodFolderHelpText": "八进制,当导入和重命名媒体文件夹和文件时应用",
"ChmodFolderHelpText": "八进制,当导入和重命名媒体文件夹和文件时应用(不带执行位)",
"History": "历史记录",
"Host": "主机",
"PortNumber": "端口号",
@@ -8,7 +8,7 @@
"AddingTag": "添加标签",
"AgeWhenGrabbed": "发布时长",
"AlreadyInYourLibrary": "已经在你的库中",
"AlternateTitles": "替代名称",
"AlternateTitles": "备选标题",
"Analytics": "分析",
"AnalyticsEnabledHelpText": "发送匿名使用信息和错误信息给我们这些信息包括您的浏览器信息使用的Web界面错误报告操作系统及运行版本。我们会根据这些信息来调整功能和修复问题的优先级。",
"AnalyticsEnabledHelpTextWarning": "需要重启才能生效",
@@ -44,7 +44,7 @@
"Clear": "清除",
"ClickToChangeQuality": "点击修改质量",
"ClientPriority": "客户端优先级",
"CloneIndexer": "复制索引",
"CloneIndexer": "复制索引",
"CloneProfile": "复制配置",
"Close": "关闭",
"Columns": "列",
@@ -66,22 +66,22 @@
"DeleteBackup": "删除备份",
"DeleteBackupMessageText": "您确定要删除备份 '{0}' 吗?",
"DeleteDelayProfile": "删除延迟配置",
"DeleteDelayProfileMessageText": "是否确实要删除此延迟配置文件",
"DeleteDelayProfileMessageText": "你确定要删除此延迟配置",
"DeleteDownloadClient": "删除下载客户端",
"DeleteDownloadClientMessageText": "确定要删除下载客户端 '{0}' 吗?",
"DeleteDownloadClientMessageText": "确定要删除下载客户端 “{name}” 吗?",
"DeleteEmptyFolders": "删除空目录",
"DeleteEmptyFoldersHelpText": "磁盘扫描过程中删除被移除的电影空目录",
"DeleteImportListExclusion": "删除导入排除列表",
"DeleteImportListExclusionMessageText": "你确定要删除这个导入排除列表吗?",
"DeleteImportListMessageText": "您确定要删除列表 '{0}'",
"DeleteIndexer": "删除索引",
"DeleteIndexerMessageText": "您确定要删除索引 '{0}'吗?",
"DeleteImportListMessageText": "您确定要删除列表 “{name}” 吗",
"DeleteIndexer": "删除索引",
"DeleteIndexerMessageText": "您确定要删除索引器 “{name}” 吗?",
"DeleteMetadataProfileMessageText": "确定要删除元数据配置吗‘{0}",
"DeleteNotification": "删除消息推送",
"DeleteNotificationMessageText": "您确定要删除推送 '{0}' 吗?",
"DeleteQualityProfile": "删除电影质量配置",
"DeleteQualityProfileMessageText": "确定要删除歌曲质量配置“{0}”",
"DeleteReleaseProfile": "删除延迟配置",
"DeleteNotificationMessageText": "您确定要删除消息推送 “{name}” 吗?",
"DeleteQualityProfile": "删除质量配置",
"DeleteQualityProfileMessageText": "确定要删除质量配置 “{name}” 吗?",
"DeleteReleaseProfile": "删除发布组配置",
"DeleteReleaseProfileMessageText": "您确定要删除这个发行配置?",
"DeleteSelectedBookFiles": "删除选择的电影文件",
"DeleteSelectedBookFilesMessageText": "您确定要删除选择的电影文件吗?",
@@ -783,7 +783,7 @@
"IfYouDontAddAnImportListExclusionAndTheAuthorHasAMetadataProfileOtherThanNoneThenThisBookMayBeReaddedDuringTheNextAuthorRefresh": "如果您不添加导入列表例外,并且作者元数据配置不是“无”,那这本书可能会在下次作者刷新是重新添加",
"IndexerIdHelpTextWarning": "使用带有首字母的特定索引器可能会导致复制版本被抓取",
"MassBookSearchWarning": "您确定要对{0}本书进行批量书籍搜索吗?",
"MediaManagementSettingsSummary": "命名文件管理设置、根目录文件夹",
"MediaManagementSettingsSummary": "命名文件管理和根文件夹设置",
"MetadataConsumers": "用户元数据",
"MetadataProfileIdHelpText": "元数据配置文件列表项应添加",
"MetadataSettingsSummary": "在导入书籍或刷新作者时创建元数据文件",
@@ -817,7 +817,7 @@
"HideBooks": "隐藏书籍",
"HostHelpText": "Calibre内容服务器主机",
"IgnoredMetaHelpText": "如果书籍包含一个/多个术语(不区分大小写)将被忽略",
"ImportListExclusions": "导入列表例外",
"ImportListExclusions": "导入排除列表",
"ImportLists": "导入列表",
"ISBN": "ISBN",
"IsCalibreLibraryHelpText": "使用Calibre内容服务器操作库",
@@ -856,8 +856,8 @@
"CollapseMultipleBooksHelpText": "折叠在同日发行的多本书籍",
"ASIN": "亚马逊标准识别码",
"NoTagsHaveBeenAddedYet": "尚未添加标签,添加标签以链接具有延迟配置文件、限制或通知的作者。单击{0}以了解有关Readarr中标签的更多信息。",
"BindAddressHelpText": "有效的 IPv4 地址、localhost、或以'*'代表所有接口",
"ApplicationURL": "程序URL",
"BindAddressHelpText": "有效的 IP 地址、localhost、或以'*'代表所有接口",
"ApplicationURL": "应用程序 URL",
"ApplicationUrlHelpText": "此应用的外部URL包含 http(s)://、端口和基本URL",
"ClickToChangeReleaseGroup": "点击修改发布组",
"MoveFiles": "移动文件",
@@ -901,7 +901,7 @@
"DataFutureBooks": "监控尚未发布的书籍",
"ExportCustomFormat": "已有自定义格式",
"ResetDefinitionTitlesHelpText": "重置定义标题和值",
"DeleteRemotePathMappingMessageText": "是否确实要删除此远程路径映射?",
"DeleteRemotePathMappingMessageText": "你确定要删除此远程路径映射",
"ListRefreshInterval": "列表刷新间隔",
"CloneCondition": "克隆条件",
"DeleteCondition": "删除条件",
@@ -942,5 +942,17 @@
"NoChange": "无修改",
"Negated": "无效的",
"No": "否",
"ResetQualityDefinitions": "重置质量定义"
"ResetQualityDefinitions": "重置质量定义",
"AppUpdated": "{appName} 升级",
"ConnectionLost": "连接丢失",
"ConnectionLostReconnect": "{appName} 将会尝试自动连接,您也可以点击下方的重新加载。",
"ConnectionLostToBackend": "{appName} 与后端的链接已断开,需要重新加载恢复功能。",
"DeleteRemotePathMapping": "删除远程映射路径",
"AutomaticUpdatesDisabledDocker": "不支持在使用 Docker 容器时直接升级。你需要升级 {appName} 容器镜像或使用脚本(script)",
"ColonReplacement": "替换冒号",
"Clone": "复制",
"ErrorLoadingContent": "加载此内容时出错",
"ReplaceWithSpaceDash": "替换为空格破折号",
"ReplaceWithDash": "替换为破折号",
"ReplaceWithSpaceDashSpace": "替换为空格破折号空格"
}

View File

@@ -2,7 +2,6 @@ using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Processing;
namespace NzbDrone.Core.MediaCover
@@ -23,9 +22,6 @@ namespace NzbDrone.Core.MediaCover
_enabled = true;
// More conservative memory allocation
SixLabors.ImageSharp.Configuration.Default.MemoryAllocator = new SimpleGcMemoryAllocator();
// Thumbnails don't need super high quality
SixLabors.ImageSharp.Configuration.Default.ImageFormatsManager.SetEncoder(JpegFormat.Instance, new JpegEncoder
{

View File

@@ -8,6 +8,7 @@ using System.Threading;
using LazyCache;
using LazyCache.Providers;
using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
@@ -18,6 +19,7 @@ using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Http;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MetadataSource.Goodreads;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace NzbDrone.Core.MetadataSource.BookInfo
{
@@ -78,7 +80,7 @@ namespace NzbDrone.Core.MetadataSource.BookInfo
var httpResponse = _httpClient.Get<RecentUpdatesResource>(httpRequest);
if (httpResponse.Resource.Limited)
if (httpResponse.Resource == null || httpResponse.Resource.Limited)
{
return null;
}
@@ -469,17 +471,17 @@ namespace NzbDrone.Core.MetadataSource.BookInfo
private List<Book> MapSearchResult(List<int> ids)
{
HttpRequest httpRequest;
HttpResponse<BulkBookResource> httpResponse;
while (true)
{
httpRequest = _requestBuilder.GetRequestBuilder().Create()
.SetSegment("route", $"book/bulk")
var httpRequest = _requestBuilder.GetRequestBuilder().Create()
.SetSegment("route", "book/bulk")
.SetHeader("Content-Type", "application/json")
.Build();
httpRequest.SetContent(ids.ToJson());
httpRequest.ContentSummary = ids.ToJson(Formatting.None);
httpRequest.AllowAutoRedirect = true;
httpRequest.SuppressHttpError = true;

View File

@@ -36,6 +36,11 @@ namespace NzbDrone.Core.Notifications
_logger = logger;
}
protected override List<NotificationDefinition> Active()
{
return base.Active().Where(c => c.Enable).ToList();
}
public List<INotification> OnGrabEnabled(bool filterBlockedNotifications = true)
{
if (filterBlockedNotifications)

View File

@@ -0,0 +1,15 @@
namespace NzbDrone.Core.Notifications.Plex
{
public class PlexAuthenticationException : PlexException
{
public PlexAuthenticationException(string message)
: base(message)
{
}
public PlexAuthenticationException(string message, params object[] args)
: base(message, args)
{
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Notifications.Plex
{
public class PlexException : NzbDroneException
{
public PlexException(string message)
: base(message)
{
}
public PlexException(string message, params object[] args)
: base(message, args)
{
}
public PlexException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@@ -0,0 +1,9 @@
namespace NzbDrone.Core.Notifications.Plex.PlexTv
{
public class PlexTvPinResponse
{
public int Id { get; set; }
public string Code { get; set; }
public string AuthToken { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Notifications.Plex.PlexTv
{
public class PlexTvPinUrlResponse
{
public string Url { get; set; }
public string Method => "POST";
public Dictionary<string, string> Headers { get; set; }
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.Net;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Exceptions;
namespace NzbDrone.Core.Notifications.Plex.PlexTv
{
public interface IPlexTvProxy
{
string GetAuthToken(string clientIdentifier, int pinId);
bool Ping(string clientIdentifier, string authToken);
}
public class PlexTvProxy : IPlexTvProxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public PlexTvProxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public string GetAuthToken(string clientIdentifier, int pinId)
{
var request = BuildRequest(clientIdentifier);
request.ResourceUrl = $"/api/v2/pins/{pinId}";
if (!Json.TryDeserialize<PlexTvPinResponse>(ProcessRequest(request), out var response))
{
response = new PlexTvPinResponse();
}
return response.AuthToken;
}
public bool Ping(string clientIdentifier, string authToken)
{
try
{
// Allows us to tell plex.tv that we're still active and tokens should not be expired.
var request = BuildRequest(clientIdentifier);
request.ResourceUrl = "/api/v2/ping";
request.AddQueryParam("X-Plex-Token", authToken);
ProcessRequest(request);
return true;
}
catch (Exception e)
{
// Catch all exceptions and log at trace, this information could be interesting in debugging, but expired tokens will be handled elsewhere.
_logger.Trace(e, "Unable to ping plex.tv");
}
return false;
}
private HttpRequestBuilder BuildRequest(string clientIdentifier)
{
var requestBuilder = new HttpRequestBuilder("https://plex.tv")
.Accept(HttpAccept.Json)
.AddQueryParam("X-Plex-Client-Identifier", clientIdentifier)
.AddQueryParam("X-Plex-Product", BuildInfo.AppName)
.AddQueryParam("X-Plex-Platform", "Windows")
.AddQueryParam("X-Plex-Platform-Version", "7")
.AddQueryParam("X-Plex-Device-Name", BuildInfo.AppName)
.AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString());
return requestBuilder;
}
private string ProcessRequest(HttpRequestBuilder requestBuilder)
{
var httpRequest = requestBuilder.Build();
HttpResponse response;
_logger.Debug("Url: {0}", httpRequest.Url);
try
{
response = _httpClient.Execute(httpRequest);
}
catch (HttpException ex)
{
throw new NzbDroneClientException(ex.Response.StatusCode, "Unable to connect to plex.tv");
}
catch (WebException)
{
throw new NzbDroneClientException(HttpStatusCode.BadRequest, "Unable to connect to plex.tv");
}
return response.Content;
}
}
}

View File

@@ -0,0 +1,95 @@
using System;
using System.Linq;
using System.Net.Http;
using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.Notifications.Plex.PlexTv
{
public interface IPlexTvService
{
PlexTvPinUrlResponse GetPinUrl();
PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl, int pinId, string pinCode);
string GetAuthToken(int pinId);
void Ping(string authToken);
}
public class PlexTvService : IPlexTvService
{
private readonly IPlexTvProxy _proxy;
private readonly IConfigService _configService;
private readonly ICached<bool> _cache;
public PlexTvService(IPlexTvProxy proxy, IConfigService configService, ICacheManager cacheManager)
{
_proxy = proxy;
_configService = configService;
_cache = cacheManager.GetCache<bool>(GetType());
}
public PlexTvPinUrlResponse GetPinUrl()
{
var clientIdentifier = _configService.PlexClientIdentifier;
var requestBuilder = new HttpRequestBuilder("https://plex.tv/api/v2/pins")
.Accept(HttpAccept.Json)
.AddQueryParam("X-Plex-Client-Identifier", clientIdentifier)
.AddQueryParam("X-Plex-Product", BuildInfo.AppName)
.AddQueryParam("X-Plex-Platform", "Windows")
.AddQueryParam("X-Plex-Platform-Version", "7")
.AddQueryParam("X-Plex-Device-Name", BuildInfo.AppName)
.AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString())
.AddQueryParam("strong", true);
requestBuilder.Method = HttpMethod.Post;
var request = requestBuilder.Build();
return new PlexTvPinUrlResponse
{
Url = request.Url.ToString(),
Headers = request.Headers.ToDictionary(h => h.Key, h => h.Value)
};
}
public PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl, int pinId, string pinCode)
{
var clientIdentifier = _configService.PlexClientIdentifier;
var requestBuilder = new HttpRequestBuilder("https://app.plex.tv/auth/hashBang")
.AddQueryParam("clientID", clientIdentifier)
.AddQueryParam("forwardUrl", callbackUrl)
.AddQueryParam("code", pinCode)
.AddQueryParam("context[device][product]", BuildInfo.AppName)
.AddQueryParam("context[device][platform]", "Windows")
.AddQueryParam("context[device][platformVersion]", "7")
.AddQueryParam("context[device][version]", BuildInfo.Version.ToString());
// #! is stripped out of the URL when building, this works around it.
requestBuilder.Segments.Add("hashBang", "#!");
var request = requestBuilder.Build();
return new PlexTvSignInUrlResponse
{
OauthUrl = request.Url.ToString(),
PinId = pinId
};
}
public string GetAuthToken(int pinId)
{
var authToken = _proxy.GetAuthToken(_configService.PlexClientIdentifier, pinId);
return authToken;
}
public void Ping(string authToken)
{
// Ping plex.tv if we haven't done so in the last 24 hours for this auth token.
_cache.Get(authToken, () => _proxy.Ping(_configService.PlexClientIdentifier, authToken), TimeSpan.FromHours(24));
}
}
}

View File

@@ -0,0 +1,8 @@
namespace NzbDrone.Core.Notifications.Plex.PlexTv
{
public class PlexTvSignInUrlResponse
{
public string OauthUrl { get; set; }
public int PinId { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Notifications.Plex
{
public class PlexVersionException : NzbDroneException
{
public PlexVersionException(string message)
: base(message)
{
}
public PlexVersionException(string message, params object[] args)
: base(message, args)
{
}
}
}

View File

@@ -0,0 +1,7 @@
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexError
{
public string Error { get; set; }
}
}

View File

@@ -0,0 +1,8 @@
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexIdentity
{
public string MachineIdentifier { get; set; }
public string Version { get; set; }
}
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexPreferences
{
[JsonProperty("Setting")]
public List<PlexPreference> Preferences { get; set; }
}
public class PlexPreference
{
public string Id { get; set; }
public string Type { get; set; }
public string Value { get; set; }
}
public class PlexPreferencesLegacy
{
[JsonProperty("_children")]
public List<PlexPreference> Preferences { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexResponse<T>
{
public T MediaContainer { get; set; }
}
}

View File

@@ -0,0 +1,57 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexSectionLocation
{
public int Id { get; set; }
public string Path { get; set; }
}
public class PlexSection
{
public PlexSection()
{
Locations = new List<PlexSectionLocation>();
}
[JsonProperty("key")]
public int Id { get; set; }
public string Type { get; set; }
public string Language { get; set; }
[JsonProperty("Location")]
public List<PlexSectionLocation> Locations { get; set; }
}
public class PlexSectionsContainer
{
public PlexSectionsContainer()
{
Sections = new List<PlexSection>();
}
[JsonProperty("Directory")]
public List<PlexSection> Sections { get; set; }
}
public class PlexSectionLegacy
{
[JsonProperty("key")]
public int Id { get; set; }
public string Type { get; set; }
public string Language { get; set; }
[JsonProperty("_children")]
public List<PlexSectionLocation> Locations { get; set; }
}
public class PlexMediaContainerLegacy
{
[JsonProperty("_children")]
public List<PlexSectionLegacy> Sections { get; set; }
}
}

View File

@@ -0,0 +1,37 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexSectionItem
{
[JsonProperty("ratingKey")]
public string Id { get; set; }
public string Title { get; set; }
public int Year { get; set; }
public string Guid { get; set; }
}
public class PlexSectionResponse
{
[JsonProperty("Metadata")]
public List<PlexSectionItem> Items { get; set; }
public PlexSectionResponse()
{
Items = new List<PlexSectionItem>();
}
}
public class PlexSectionResponseLegacy
{
[JsonProperty("_children")]
public List<PlexSectionItem> Items { get; set; }
public PlexSectionResponseLegacy()
{
Items = new List<PlexSectionItem>();
}
}
}

View File

@@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Books;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Notifications.Plex.PlexTv;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexServer : NotificationBase<PlexServerSettings>
{
private readonly IPlexServerService _plexServerService;
private readonly IPlexTvService _plexTvService;
private readonly Logger _logger;
private class PlexUpdateQueue
{
public Dictionary<int, Author> Pending { get; } = new ();
public bool Refreshing { get; set; }
}
private readonly ICached<PlexUpdateQueue> _pendingAuthorsCache;
public PlexServer(IPlexServerService plexServerService, IPlexTvService plexTvService, ICacheManager cacheManager, Logger logger)
{
_plexServerService = plexServerService;
_plexTvService = plexTvService;
_logger = logger;
_pendingAuthorsCache = cacheManager.GetRollingCache<PlexUpdateQueue>(GetType(), "pendingAuthors", TimeSpan.FromDays(1));
}
public override string Link => "https://www.plex.tv/";
public override string Name => "Plex Media Server";
public override void OnReleaseImport(BookDownloadMessage message)
{
UpdateIfEnabled(message.Author);
}
public override void OnRename(Author author, List<RenamedBookFile> renamedFiles)
{
UpdateIfEnabled(author);
}
public override void OnBookRetag(BookRetagMessage message)
{
UpdateIfEnabled(message.Author);
}
public override void OnBookDelete(BookDeleteMessage deleteMessage)
{
if (deleteMessage.DeletedFiles)
{
UpdateIfEnabled(deleteMessage.Book.Author);
}
}
public override void OnAuthorDelete(AuthorDeleteMessage deleteMessage)
{
if (deleteMessage.DeletedFiles)
{
UpdateIfEnabled(deleteMessage.Author);
}
}
private void UpdateIfEnabled(Author author)
{
_plexTvService.Ping(Settings.AuthToken);
if (Settings.UpdateLibrary)
{
_logger.Debug("Scheduling library update for author {0} {1}", author.Id, author.Name);
var queue = _pendingAuthorsCache.Get(Settings.Host, () => new PlexUpdateQueue());
lock (queue)
{
queue.Pending[author.Id] = author;
}
}
}
public override void ProcessQueue()
{
var queue = _pendingAuthorsCache.Find(Settings.Host);
if (queue == null)
{
return;
}
lock (queue)
{
if (queue.Refreshing)
{
return;
}
queue.Refreshing = true;
}
try
{
while (true)
{
List<Author> refreshingAuthors;
lock (queue)
{
if (queue.Pending.Empty())
{
queue.Refreshing = false;
return;
}
refreshingAuthors = queue.Pending.Values.ToList();
queue.Pending.Clear();
}
if (Settings.UpdateLibrary)
{
_logger.Debug("Performing library update for {0} authors", refreshingAuthors.Count);
_plexServerService.UpdateLibrary(refreshingAuthors, Settings);
}
}
}
catch
{
lock (queue)
{
queue.Refreshing = false;
}
throw;
}
}
public override ValidationResult Test()
{
_plexTvService.Ping(Settings.AuthToken);
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_plexServerService.Test(Settings));
return new ValidationResult(failures);
}
public override object RequestAction(string action, IDictionary<string, string> query)
{
if (action == "startOAuth")
{
Settings.Validate().Filter("ConsumerKey", "ConsumerSecret").ThrowOnError();
return _plexTvService.GetPinUrl();
}
else if (action == "continueOAuth")
{
Settings.Validate().Filter("ConsumerKey", "ConsumerSecret").ThrowOnError();
if (query["callbackUrl"].IsNullOrWhiteSpace())
{
throw new BadRequestException("QueryParam callbackUrl invalid.");
}
if (query["id"].IsNullOrWhiteSpace())
{
throw new BadRequestException("QueryParam id invalid.");
}
if (query["code"].IsNullOrWhiteSpace())
{
throw new BadRequestException("QueryParam code invalid.");
}
return _plexTvService.GetSignInUrl(query["callbackUrl"], Convert.ToInt32(query["id"]), query["code"]);
}
else if (action == "getOAuthToken")
{
Settings.Validate().Filter("ConsumerKey", "ConsumerSecret").ThrowOnError();
if (query["pinId"].IsNullOrWhiteSpace())
{
throw new BadRequestException("QueryParam pinId invalid.");
}
var authToken = _plexTvService.GetAuthToken(Convert.ToInt32(query["pinId"]));
return new
{
authToken
};
}
return new { };
}
}
}

View File

@@ -0,0 +1,173 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public interface IPlexServerProxy
{
List<PlexSection> GetTvSections(PlexServerSettings settings);
string Version(PlexServerSettings settings);
void Update(int sectionId, string path, PlexServerSettings settings);
}
public class PlexServerProxy : IPlexServerProxy
{
private readonly IHttpClient _httpClient;
private readonly IConfigService _configService;
private readonly Logger _logger;
public PlexServerProxy(IHttpClient httpClient, IConfigService configService, Logger logger)
{
_httpClient = httpClient;
_configService = configService;
_logger = logger;
}
public List<PlexSection> GetTvSections(PlexServerSettings settings)
{
var request = BuildRequest("library/sections", HttpMethod.Get, settings);
var response = ProcessRequest(request);
CheckForError(response);
if (response.Contains("_children"))
{
return Json.Deserialize<PlexMediaContainerLegacy>(response)
.Sections
.Where(d => d.Type == "artist")
.Select(s => new PlexSection
{
Id = s.Id,
Language = s.Language,
Locations = s.Locations,
Type = s.Type
})
.ToList();
}
return Json.Deserialize<PlexResponse<PlexSectionsContainer>>(response)
.MediaContainer
.Sections
.Where(d => d.Type == "artist")
.ToList();
}
public void Update(int sectionId, string path, PlexServerSettings settings)
{
var resource = $"library/sections/{sectionId}/refresh";
var request = BuildRequest(resource, HttpMethod.Get, settings);
request.AddQueryParam("path", path);
var response = ProcessRequest(request);
CheckForError(response);
}
public string Version(PlexServerSettings settings)
{
var request = BuildRequest("identity", HttpMethod.Get, settings);
var response = ProcessRequest(request);
CheckForError(response);
if (response.Contains("_children"))
{
return Json.Deserialize<PlexIdentity>(response)
.Version;
}
return Json.Deserialize<PlexResponse<PlexIdentity>>(response)
.MediaContainer
.Version;
}
private HttpRequestBuilder BuildRequest(string resource, HttpMethod method, PlexServerSettings settings)
{
var scheme = settings.UseSsl ? "https" : "http";
var requestBuilder = new HttpRequestBuilder($"{scheme}://{settings.Host.ToUrlHost()}:{settings.Port}")
.Accept(HttpAccept.Json)
.AddQueryParam("X-Plex-Client-Identifier", _configService.PlexClientIdentifier)
.AddQueryParam("X-Plex-Product", BuildInfo.AppName)
.AddQueryParam("X-Plex-Platform", "Windows")
.AddQueryParam("X-Plex-Platform-Version", "7")
.AddQueryParam("X-Plex-Device-Name", BuildInfo.AppName)
.AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString());
if (settings.AuthToken.IsNotNullOrWhiteSpace())
{
requestBuilder.AddQueryParam("X-Plex-Token", settings.AuthToken);
}
requestBuilder.ResourceUrl = resource;
requestBuilder.Method = method;
return requestBuilder;
}
private string ProcessRequest(HttpRequestBuilder requestBuilder)
{
var httpRequest = requestBuilder.Build();
HttpResponse response;
_logger.Debug("Url: {0}", httpRequest.Url);
try
{
response = _httpClient.Execute(httpRequest);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new PlexAuthenticationException("Unauthorized - AuthToken is invalid");
}
throw new PlexException("Unable to connect to Plex Media Server. Status Code: {0}", ex.Response.StatusCode);
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.TrustFailure)
{
throw new PlexException("Unable to connect to Plex Media Server, certificate validation failed.", ex);
}
throw new PlexException($"Unable to connect to Plex Media Server, {ex.Message}", ex);
}
return response.Content;
}
private void CheckForError(string response)
{
_logger.Trace("Checking for error");
if (response.IsNullOrWhiteSpace())
{
_logger.Trace("No response body returned, no error detected");
return;
}
var error = response.Contains("_children") ?
Json.Deserialize<PlexError>(response) :
Json.Deserialize<PlexResponse<PlexError>>(response).MediaContainer;
if (error != null && !error.Error.IsNullOrWhiteSpace())
{
throw new PlexException(error.Error);
}
_logger.Trace("No error detected");
}
}
}

View File

@@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Books;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public interface IPlexServerService
{
void UpdateLibrary(Author author, PlexServerSettings settings);
void UpdateLibrary(IEnumerable<Author> authors, PlexServerSettings settings);
ValidationFailure Test(PlexServerSettings settings);
}
public class PlexServerService : IPlexServerService
{
private readonly ICached<Version> _versionCache;
private readonly IPlexServerProxy _plexServerProxy;
private readonly IRootFolderService _rootFolderService;
private readonly Logger _logger;
public PlexServerService(ICacheManager cacheManager, IPlexServerProxy plexServerProxy, IRootFolderService rootFolderService, Logger logger)
{
_versionCache = cacheManager.GetCache<Version>(GetType(), "versionCache");
_plexServerProxy = plexServerProxy;
_rootFolderService = rootFolderService;
_logger = logger;
}
public void UpdateLibrary(Author author, PlexServerSettings settings)
{
UpdateLibrary(new[] { author }, settings);
}
public void UpdateLibrary(IEnumerable<Author> authors, PlexServerSettings settings)
{
try
{
_logger.Debug("Sending Update Request to Plex Server");
var watch = Stopwatch.StartNew();
var version = _versionCache.Get(settings.Host, () => GetVersion(settings), TimeSpan.FromHours(2));
ValidateVersion(version);
var sections = GetSections(settings);
foreach (var author in authors)
{
UpdateSections(author, sections, settings);
}
_logger.Debug("Finished sending Update Request to Plex Server (took {0} ms)", watch.ElapsedMilliseconds);
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to Update Plex host: " + settings.Host);
throw;
}
}
private List<PlexSection> GetSections(PlexServerSettings settings)
{
_logger.Debug("Getting sections from Plex host: {0}", settings.Host);
return _plexServerProxy.GetTvSections(settings).ToList();
}
private void ValidateVersion(Version version)
{
if (version >= new Version(1, 3, 0) && version < new Version(1, 3, 1))
{
throw new PlexVersionException("Found version {0}, upgrade to PMS 1.3.1 to fix library updating and then restart Readarr", version);
}
}
private Version GetVersion(PlexServerSettings settings)
{
_logger.Debug("Getting version from Plex host: {0}", settings.Host);
var rawVersion = _plexServerProxy.Version(settings);
var version = new Version(Regex.Match(rawVersion, @"^(\d+[.-]){4}").Value.Trim('.', '-'));
return version;
}
private void UpdateSections(Author author, List<PlexSection> sections, PlexServerSettings settings)
{
var rootFolderPath = _rootFolderService.GetBestRootFolderPath(author.Path);
var authorRelativePath = rootFolderPath.GetRelativePath(author.Path);
// Try to update a matching section location before falling back to updating all section locations.
foreach (var section in sections)
{
foreach (var location in section.Locations)
{
var rootFolder = new OsPath(rootFolderPath);
var mappedPath = rootFolder;
if (settings.MapTo.IsNotNullOrWhiteSpace())
{
mappedPath = new OsPath(settings.MapTo) + (rootFolder - new OsPath(settings.MapFrom));
_logger.Trace("Mapping Path from {0} to {1} for partial scan", rootFolder, mappedPath);
}
if (location.Path.PathEquals(mappedPath.FullPath))
{
_logger.Debug("Updating matching section location, {0}", location.Path);
UpdateSectionPath(authorRelativePath, section, location, settings);
return;
}
}
}
_logger.Debug("Unable to find matching section location, updating all Music sections");
foreach (var section in sections)
{
foreach (var location in section.Locations)
{
UpdateSectionPath(authorRelativePath, section, location, settings);
}
}
}
private void UpdateSectionPath(string authorRelativePath, PlexSection section, PlexSectionLocation location, PlexServerSettings settings)
{
var separator = location.Path.Contains('\\') ? "\\" : "/";
var locationRelativePath = authorRelativePath.Replace("\\", separator).Replace("/", separator);
// Plex location paths trim trailing extraneous separator characters, so it doesn't need to be trimmed
var pathToUpdate = $"{location.Path}{separator}{locationRelativePath}";
_logger.Debug("Updating section location, {0}", location.Path);
_plexServerProxy.Update(section.Id, pathToUpdate, settings);
}
public ValidationFailure Test(PlexServerSettings settings)
{
try
{
_versionCache.Remove(settings.Host);
var sections = GetSections(settings);
if (sections.Empty())
{
return new ValidationFailure("Host", "At least one Music library is required");
}
}
catch (PlexAuthenticationException ex)
{
_logger.Error(ex, "Unable to connect to Plex Media Server");
return new ValidationFailure("AuthToken", "Invalid authentication token");
}
catch (PlexException ex)
{
return new NzbDroneValidationFailure("Host", ex.Message);
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to connect to Plex Media Server");
return new NzbDroneValidationFailure("Host", "Unable to connect to Plex Media Server")
{
DetailedDescription = ex.Message
};
}
return null;
}
}
}

View File

@@ -0,0 +1,62 @@
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexServerSettingsValidator : AbstractValidator<PlexServerSettings>
{
public PlexServerSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.MapFrom).NotEmpty().Unless(c => c.MapTo.IsNullOrWhiteSpace());
RuleFor(c => c.MapTo).NotEmpty().Unless(c => c.MapFrom.IsNullOrWhiteSpace());
}
}
public class PlexServerSettings : IProviderConfig
{
private static readonly PlexServerSettingsValidator Validator = new PlexServerSettingsValidator();
public PlexServerSettings()
{
Port = 32400;
UpdateLibrary = true;
SignIn = "startOAuth";
}
[FieldDefinition(0, Label = "Host")]
public string Host { get; set; }
[FieldDefinition(1, Label = "Port")]
public int Port { get; set; }
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Connect to Plex over HTTPS instead of HTTP")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "Auth Token", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey, Advanced = true)]
public string AuthToken { get; set; }
[FieldDefinition(4, Label = "Authenticate with Plex.tv", Type = FieldType.OAuth)]
public string SignIn { get; set; }
[FieldDefinition(5, Label = "Update Library", Type = FieldType.Checkbox)]
public bool UpdateLibrary { get; set; }
[FieldDefinition(6, Label = "Map Paths From", Type = FieldType.Textbox, Advanced = true, HelpText = "Readarr path, used to modify author paths when Plex sees library path location differently from Readarr")]
public string MapFrom { get; set; }
[FieldDefinition(7, Label = "Map Paths To", Type = FieldType.Textbox, Advanced = true, HelpText = "Plex path, used to modify author paths when Plex sees library path location differently from Readarr")]
public string MapTo { get; set; }
public bool IsValid => !string.IsNullOrWhiteSpace(Host);
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

Some files were not shown because too many files have changed in this diff Show More