1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-15 15:54:47 -04:00

Compare commits

...

39 Commits

Author SHA1 Message Date
ta264
d96dfd228e Ipv6 logging 2022-01-30 04:09:03 +00:00
ta264
e7a8f6332c Fixed: Handle missing category when getting Qbittorrent download path
Fixes RADARR-7HC
Fixes RADARR-V49

(cherry picked from commit 6f97ca9a55471386454457ca52b93733e18e85e4)
Closes #6993
2022-01-25 20:10:52 -06:00
Qstick
b8c92d23f4 Fixed: Ignore case for DIVX VideoCodecID 2022-01-25 19:01:01 -06:00
bakerboy448
093e076db0 Fixed: Clarify Indexer Priority Helptext 2022-01-24 19:35:38 -06:00
Volodymyr Medvid
f6f949415c Fix Ukrainian language mappings 2022-01-24 19:23:12 -06:00
Qstick
ea2576a56c Bump to 4.0.4 2022-01-23 19:03:07 -06:00
Qstick
595acb696d Fixed: Bump FFMpegCore to avoid DivideByZero error 2022-01-23 18:24:28 -06:00
Qstick
38c9534eac Fixed: Webhook fails due to Frames/Analysis property serialization on MediaInfo 2022-01-23 18:24:28 -06:00
bakerboy448
9377ef7942 Fixed: Improved Indexer test failure message when no results are returned
(cherry picked from commit 05b1581b7dfa5bc8a2b38a60179e3472c8134f74)
2022-01-23 16:18:37 -06:00
bakerboy448
c2e5686bcf fixed sonarr ref in UTorrentSettings 2022-01-23 14:44:31 -06:00
Qstick
f08807daf6 Update donation links [common] 2022-01-23 12:12:16 -06:00
bakerboy448
72b3caa72d Fixed: Various Translations 2022-01-21 18:52:46 -06:00
Qstick
589368781b Fixed Incorrect placeholder width on search page
Co-Authored-By: nitsua <8321115+austinwbest@users.noreply.github.com>
2022-01-20 21:12:30 -06:00
Qstick
8fd6101121 New: Add AppName to system status response
Fixes #6952
2022-01-18 23:23:41 -06:00
Mark McDowall
ac9d6cbf0a Fixed: Jump bar on series page not showing when window is made wider
(cherry picked from commit 0cb8d93069d6310abd39ee2fe73219e17aa83fe6)
2022-01-17 11:30:47 -06:00
Qstick
6e0ed36e9f Fixed: Correct queue color for status bars 2022-01-17 08:58:07 -06:00
Qstick
fcb65055ef Bump to 4.0.3 2022-01-15 16:24:50 -06:00
bakerboy448
90456bbfed fixup 2022-01-15 14:51:11 -06:00
bakerboy448
2a74b7b2e1 Fixed: Parse HD.DVD as BluRay
Fixes #6925
2022-01-15 14:51:11 -06:00
Qstick
fc08c39fb8 Fixed: Revert manual import augmentation for unknown items 2022-01-15 14:24:51 -06:00
PearsonFlyer
76d65bf990 Fixed: Translation warning for search all 2022-01-15 07:27:58 -06:00
ta264
de243991dd Support for digest auth with HttpRequests
(cherry picked from commit 1e2d931f9a)
2022-01-13 19:23:30 -06:00
Qstick
4d1f251c1f Bump version 4.0.2 2022-01-10 19:59:16 -06:00
Qstick
ebb1e3131a Fixed: Use general settings cert validation for email 2022-01-10 09:17:54 -05:00
Qstick
6e502d63c2 Fixed: Handle wmapro and wmv3 codecs in formatter
Fixes RADARR-1G5R
Fixes RADARR-1G7V
2022-01-08 13:50:03 -06:00
Qstick
57e05b70da Remove unused variable from MovieHistoryRow 2022-01-08 01:09:02 -06:00
Qstick
59186adbfc Fixed: Mark as Failed from Movie Details page
Fixes #6483
2022-01-08 00:54:27 -06:00
Qstick
bc20e159ba Bump dotnet to 6.0.1
Security patch release
2022-01-07 21:33:48 -06:00
ta264
39b99341cd Fixed: Use our own HttpClient for Aria2 requests
[common]
2022-01-07 21:22:55 -06:00
ta264
b626c5bbf0 Fixed: Use our own HttpClient for rTorrent RPC requests
[common]
2022-01-07 21:22:55 -06:00
Robin Dadswell
a33b861cec Fixed: Download Client not sending on Import or Upgrade notifications (#6908)
* Fixed: Download client and ID for custom scripts

Based on Sonarr Commit eea3419849

* fixup! test

Co-authored-by: Qstick <qstick@gmail.com>
2022-01-06 22:04:10 -06:00
Qstick
3a48f07702 Fixed: Twitter connect not sending messages after http rework (#6901) [common] 2022-01-06 21:42:32 -06:00
Weblate
1aec0b7ee5 Translated using Weblate (Portuguese (Brazil)) [skip ci]
Currently translated at 99.9% (1106 of 1107 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 99.0% (1096 of 1107 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 99.5% (1102 of 1107 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 99.8% (1105 of 1107 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 99.9% (1106 of 1107 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 97.2% (1077 of 1107 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 99.4% (1097 of 1103 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 100.0% (1103 of 1103 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 98.6% (1088 of 1103 strings)

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Gian Klug <gian.klug@ict-scouts.ch>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Math <thimath62@live.fr>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: diemade <spamkill@posteo.ch>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translation: Servarr/Radarr
2022-01-06 21:32:57 -06:00
nitsua
3e32161791 Fix broken headers on List Exclusion table 2022-01-05 20:03:50 -06:00
Qstick
fda1ad237b Fixed: Map DV Blu-ray to HDR10 compatibility 2022-01-04 23:46:36 -06:00
bakerboy448
52b6f39026 Fixed: Update indexer flag help link 2022-01-04 21:53:40 -06:00
Qstick
100fd95dd9 Fixed: Fix bad ratings objects in Migration 206 2022-01-03 20:20:16 -06:00
Mehul Vaghani
d571c7b75a Fixed: Updated link to Indexer Flags (#6893)
* Fixed: Updated link to Indexer Flags

* Fix: Updated link to Indexer Flags documentation

* Fix: Updated link to reflect right section
2022-01-03 18:15:32 -06:00
Qstick
8d7f48739b Bump version to 4.0.1 2022-01-03 14:20:47 -06:00
80 changed files with 951 additions and 816 deletions

View File

@@ -7,13 +7,13 @@ variables:
outputFolder: './_output'
artifactsFolder: './_artifacts'
testsFolder: './_tests'
majorVersion: '4.0.0'
majorVersion: '4.0.4'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.100'
dotnetVersion: '6.0.101'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
trigger:

View File

@@ -86,6 +86,13 @@ class AddNewMovieSearchResult extends Component {
} = this.state;
const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress };
const posterWidth = 167;
const posterHeight = 250;
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`
};
return (
<div className={styles.searchResult}>
@@ -102,6 +109,7 @@ class AddNewMovieSearchResult extends Component {
<div className={styles.posterContainer}>
<MoviePoster
className={styles.poster}
style={elementStyle}
images={images}
size={250}
overflow={true}
@@ -114,7 +122,7 @@ class AddNewMovieSearchResult extends Component {
monitored={monitored}
hasFile={hasFile}
status={status}
posterWidth={167}
posterWidth={posterWidth}
detailedProgressBar={true}
queueStatus={queueStatus}
queueState={queueState}

View File

@@ -81,7 +81,7 @@ class PageHeader extends Component {
<IconButton
className={styles.donate}
name={icons.HEART}
to="https://opencollective.com/radarr"
to="https://radarr.video/donate"
size={14}
/>
<IconButton

View File

@@ -100,7 +100,9 @@ class PageJumpBar extends Component {
// Listeners
onMeasure = ({ height }) => {
this.setState({ height });
if (height > 0) {
this.setState({ height });
}
}
//

View File

@@ -67,8 +67,7 @@ class MovieHistoryRow extends Component {
data,
isMarkingAsFailed,
shortDateFormat,
timeFormat,
onMarkAsFailedPress
timeFormat
} = this.props;
const {
@@ -143,7 +142,7 @@ class MovieHistoryRow extends Component {
isMarkingAsFailed={isMarkingAsFailed}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
onMarkAsFailedPress={onMarkAsFailedPress}
onMarkAsFailedPress={this.onMarkAsFailedPress}
onModalClose={this.onDetailsModalClose}
/>
</TableRow>

View File

@@ -50,7 +50,7 @@ class ImportListExclusions extends Component {
errorMessage={translate('UnableToLoadListExclusions')}
{...otherProps}
>
<div className={styles.importExclusionsHeader}>
<div className={styles.importListExclusionsHeader}>
<div className={styles.tmdbId}>
TMDb Id
</div>

View File

@@ -85,7 +85,7 @@ function IndexerOptions(props) {
type={inputTypes.CHECK}
name="preferIndexerFlags"
helpText={translate('PreferIndexerFlagsHelpText')}
helpLink="https://wiki.servarr.com/Definitions#Indexer_Flags"
helpLink="https://wiki.servarr.com/radarr/settings#indexer-flags"
onChange={onInputChange}
{...settings.preferIndexerFlags}
/>

View File

@@ -13,7 +13,7 @@ class Donations extends Component {
return (
<FieldSet legend={translate('Donations')}>
<div className={styles.logoContainer} title="Radarr">
<Link to="https://opencollective.com/radarr">
<Link to="https://radarr.video/donate">
<img
className={styles.logo}
src={`${window.Radarr.urlBase}/Content/Images/Icons/logo-radarr.png`}
@@ -21,7 +21,7 @@ class Donations extends Component {
</Link>
</div>
<div className={styles.logoContainer} title="Lidarr">
<Link to="https://opencollective.com/lidarr">
<Link to="https://lidarr.audio/donate">
<img
className={styles.logo}
src={`${window.Radarr.urlBase}/Content/Images/Icons/logo-lidarr.png`}
@@ -29,7 +29,7 @@ class Donations extends Component {
</Link>
</div>
<div className={styles.logoContainer} title="Readarr">
<Link to="https://opencollective.com/readarr">
<Link to="https://readarr.com/donate">
<img
className={styles.logo}
src={`${window.Radarr.urlBase}/Content/Images/Icons/logo-readarr.png`}
@@ -37,7 +37,7 @@ class Donations extends Component {
</Link>
</div>
<div className={styles.logoContainer} title="Prowlarr">
<Link to="https://opencollective.com/prowlarr">
<Link to="https://prowlarr.com/donate">
<img
className={styles.logo}
src={`${window.Radarr.urlBase}/Content/Images/Icons/logo-prowlarr.png`}
@@ -45,7 +45,7 @@ class Donations extends Component {
</Link>
</div>
<div className={styles.logoContainer} title="Sonarr">
<Link to="https://opencollective.com/sonarr">
<Link to="https://sonarr.tv/donate">
<img
className={styles.logo}
src={`${window.Radarr.urlBase}/Content/Images/Icons/logo-sonarr.png`}

View File

@@ -200,7 +200,7 @@ class QueuedTaskRow extends Component {
{
clientUserAgent ?
<span className={styles.userAgent} title={translate('TaskUserAgentTooltip')}>
{translate('from')}: {clientUserAgent}
{translate('From')}: {clientUserAgent}
</span> :
null
}

View File

@@ -2,7 +2,7 @@ import { kinds } from 'Helpers/Props';
function getStatusStyle(status, monitored, hasFile, isAvailable, returnType, queue = false) {
if (queue) {
return returnType === 'kinds' ? kinds.SUCCESS : 'queue';
return returnType === 'kinds' ? kinds.QUEUE : 'queue';
}
if (hasFile && monitored) {

View File

@@ -30,7 +30,7 @@
"@fortawesome/free-regular-svg-icons": "5.15.3",
"@fortawesome/free-solid-svg-icons": "5.15.3",
"@fortawesome/react-fontawesome": "0.1.14",
"@microsoft/signalr": "6.0.0",
"@microsoft/signalr": "6.0.1",
"@sentry/browser": "6.13.2",
"@sentry/integrations": "6.13.2",
"classnames": "2.3.1",

View File

@@ -171,5 +171,26 @@ namespace NzbDrone.Common.Extensions
{
return source.Contains(value, StringComparer.InvariantCultureIgnoreCase);
}
public static string EncodeRFC3986(this string value)
{
// From Twitterizer http://www.twitterizer.net/
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
var encoded = Uri.EscapeDataString(value);
return Regex
.Replace(encoded, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper())
.Replace("(", "%28")
.Replace(")", "%29")
.Replace("$", "%24")
.Replace("!", "%21")
.Replace("*", "%2A")
.Replace("'", "%27")
.Replace("%7E", "~");
}
}
}

View File

@@ -0,0 +1,12 @@
using System.Net;
namespace NzbDrone.Common.Http
{
public class BasicNetworkCredential : NetworkCredential
{
public BasicNetworkCredential(string user, string pass)
: base(user, pass)
{
}
}
}

View File

@@ -4,20 +4,24 @@ using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Common.Http.Dispatchers
{
public class ManagedHttpDispatcher : IHttpDispatcher
{
private const string NO_PROXY_KEY = "no-proxy";
private const int connection_establish_timeout = 2000;
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(ManagedHttpDispatcher));
private static bool useIPv6 = Socket.OSSupportsIPv6;
private static bool hasResolvedIPv6Availability;
@@ -26,12 +30,13 @@ namespace NzbDrone.Common.Http.Dispatchers
private readonly ICertificateValidationService _certificateValidationService;
private readonly IUserAgentBuilder _userAgentBuilder;
private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
private readonly ICached<CredentialCache> _credentialCache;
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
ICreateManagedWebProxy createManagedWebProxy,
ICertificateValidationService certificateValidationService,
IUserAgentBuilder userAgentBuilder,
ICacheManager cacheManager)
ICreateManagedWebProxy createManagedWebProxy,
ICertificateValidationService certificateValidationService,
IUserAgentBuilder userAgentBuilder,
ICacheManager cacheManager)
{
_proxySettingsProvider = proxySettingsProvider;
_createManagedWebProxy = createManagedWebProxy;
@@ -39,6 +44,7 @@ namespace NzbDrone.Common.Http.Dispatchers
_userAgentBuilder = userAgentBuilder;
_httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
}
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
@@ -64,6 +70,26 @@ namespace NzbDrone.Common.Http.Dispatchers
cts.CancelAfter(TimeSpan.FromSeconds(100));
}
if (request.Credentials != null)
{
if (request.Credentials is BasicNetworkCredential bc)
{
// Manually set header to avoid initial challenge response
var authInfo = bc.UserName + ":" + bc.Password;
authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo));
requestMessage.Headers.Add("Authorization", "Basic " + authInfo);
}
else if (request.Credentials is NetworkCredential nc)
{
var creds = GetCredentialCache();
foreach (var authtype in new[] { "Basic", "Digest" })
{
creds.Remove((Uri)request.Url, authtype);
creds.Add((Uri)request.Url, authtype, nc);
}
}
}
if (request.ContentData != null)
{
requestMessage.Content = new ByteArrayContent(request.ContentData);
@@ -120,6 +146,8 @@ namespace NzbDrone.Common.Http.Dispatchers
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Brotli,
UseCookies = false, // sic - we don't want to use a shared cookie container
AllowAutoRedirect = false,
Credentials = GetCredentialCache(),
PreAuthenticate = true,
MaxConnectionsPerServer = 12,
ConnectCallback = onConnect,
SslOptions = new SslClientAuthenticationOptions
@@ -204,18 +232,28 @@ namespace NzbDrone.Common.Http.Dispatchers
headers.Add(header, value);
}
private CredentialCache GetCredentialCache()
{
return _credentialCache.Get("credentialCache", () => new CredentialCache());
}
private static async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
Logger.Trace($"useIPv6: {useIPv6} hasResolvedipv6availability: {hasResolvedIPv6Availability}");
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
// This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6.
if (useIPv6)
{
Logger.Trace("Trying Ipv6");
try
{
var localToken = cancellationToken;
if (!hasResolvedIPv6Availability)
{
Logger.Trace($"Using fast timeout {connection_establish_timeout}");
// to make things move fast, use a very low timeout for the initial ipv6 attempt.
var quickFailCts = new CancellationTokenSource(connection_establish_timeout);
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, quickFailCts.Token);
@@ -225,8 +263,10 @@ namespace NzbDrone.Common.Http.Dispatchers
return await attemptConnection(AddressFamily.InterNetworkV6, context, localToken);
}
catch
catch (Exception e)
{
Logger.Trace(e, "Error in ipv6 attempt");
// very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt.
// note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance)
// but in the interest of keeping this implementation simple, this is acceptable.
@@ -238,6 +278,8 @@ namespace NzbDrone.Common.Http.Dispatchers
}
}
Logger.Trace("Falling back to ipv4");
// fallback to IPv4.
return await attemptConnection(AddressFamily.InterNetwork, context, cancellationToken);
}

View File

@@ -38,6 +38,7 @@ namespace NzbDrone.Common.Http
public HttpHeader Headers { get; set; }
public byte[] ContentData { get; set; }
public string ContentSummary { get; set; }
public ICredentials Credentials { get; set; }
public bool SuppressHttpError { get; set; }
public IEnumerable<HttpStatusCode> SuppressHttpErrorStatusCodes { get; set; }
public bool UseSimplifiedUserAgent { get; set; }
@@ -89,12 +90,5 @@ namespace NzbDrone.Common.Http
var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType);
ContentData = encoding.GetBytes(data);
}
public void AddBasicAuthentication(string username, string password)
{
var authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes($"{username}:{password}"));
Headers.Set("Authorization", "Basic " + authInfo);
}
}
}

View File

@@ -26,10 +26,9 @@ namespace NzbDrone.Common.Http
public bool ConnectionKeepAlive { get; set; }
public TimeSpan RateLimit { get; set; }
public bool LogResponseContent { get; set; }
public NetworkCredential NetworkCredential { get; set; }
public ICredentials NetworkCredential { get; set; }
public Dictionary<string, string> Cookies { get; private set; }
public List<HttpFormData> FormData { get; private set; }
public Action<HttpRequest> PostProcess { get; set; }
public HttpRequestBuilder(string baseUrl)
@@ -109,13 +108,7 @@ namespace NzbDrone.Common.Http
request.ConnectionKeepAlive = ConnectionKeepAlive;
request.RateLimit = RateLimit;
request.LogResponseContent = LogResponseContent;
if (NetworkCredential != null)
{
var authInfo = NetworkCredential.UserName + ":" + NetworkCredential.Password;
authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo));
request.Headers.Set("Authorization", "Basic " + authInfo);
}
request.Credentials = NetworkCredential;
foreach (var header in Headers)
{

View File

@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Xml.Linq;
using NLog;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Common.Http
{
public class XmlRpcRequestBuilder : HttpRequestBuilder
{
public static string XmlRpcContentType = "text/xml";
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(XmlRpcRequestBuilder));
public string XmlMethod { get; private set; }
public List<object> XmlParameters { get; private set; }
public XmlRpcRequestBuilder(string baseUrl)
: base(baseUrl)
{
Method = HttpMethod.Post;
XmlParameters = new List<object>();
}
public XmlRpcRequestBuilder(bool useHttps, string host, int port, string urlBase = null)
: this(BuildBaseUrl(useHttps, host, port, urlBase))
{
}
public override HttpRequestBuilder Clone()
{
var clone = base.Clone() as XmlRpcRequestBuilder;
clone.XmlParameters = new List<object>(XmlParameters);
return clone;
}
public XmlRpcRequestBuilder Call(string method, params object[] parameters)
{
var clone = Clone() as XmlRpcRequestBuilder;
clone.XmlMethod = method;
clone.XmlParameters = parameters.ToList();
return clone;
}
protected override void Apply(HttpRequest request)
{
base.Apply(request);
request.Headers.ContentType = XmlRpcContentType;
var methodCallElements = new List<XElement> { new XElement("methodName", XmlMethod) };
if (XmlParameters.Any())
{
var argElements = XmlParameters.Select(x => new XElement("param", ConvertParameter(x))).ToList();
var paramsElement = new XElement("params", argElements);
methodCallElements.Add(paramsElement);
}
var message = new XDocument(
new XDeclaration("1.0", "utf-8", "yes"),
new XElement("methodCall", methodCallElements));
var body = message.ToString();
Logger.Debug($"Executing remote method: {XmlMethod}");
Logger.Trace($"methodCall {XmlMethod} body:\n{body}");
request.SetContent(body);
}
private static XElement ConvertParameter(object value)
{
XElement data;
if (value is string s)
{
data = new XElement("string", s);
}
else if (value is List<string> l)
{
data = new XElement("array", new XElement("data", l.Select(x => new XElement("value", new XElement("string", x)))));
}
else if (value is int i)
{
data = new XElement("int", i);
}
else if (value is byte[] bytes)
{
data = new XElement("base64", Convert.ToBase64String(bytes));
}
else
{
throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}");
}
return new XElement("value", data);
}
}
}

View File

@@ -12,7 +12,7 @@
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageReference Include="Sentry" Version="3.11.1" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.Text.Json" Version="6.0.0" />
<PackageReference Include="System.Text.Json" Version="6.0.1" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />

View File

@@ -176,7 +176,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
public void should_return_ok_on_movie_imported_event()
{
GivenFolderExists(_downloadRootPath);
var importEvent = new MovieImportedEvent(new LocalMovie(), new MovieFile(), true, new DownloadClientItem(), _downloadItem.DownloadId);
var importEvent = new MovieImportedEvent(new LocalMovie(), new MovieFile(), new List<MovieFile>(), true, new DownloadClientItem());
Subject.Check(importEvent).ShouldBeOk();
}

View File

@@ -91,7 +91,7 @@ namespace NzbDrone.Core.Test.HistoryTests
DownloadId = "abcd"
};
Subject.Handle(new MovieImportedEvent(localMovie, movieFile, true, downloadClientItem, "abcd"));
Subject.Handle(new MovieImportedEvent(localMovie, movieFile, new List<MovieFile>(), true, downloadClientItem));
Mocker.GetMock<IHistoryRepository>()
.Verify(v => v.Insert(It.Is<MovieHistory>(h => h.SourceTitle == Path.GetFileNameWithoutExtension(localMovie.Path))));

View File

@@ -19,6 +19,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
[TestCase("wmv1, WMV1", "Droned.wmv", "WMV")]
[TestCase("wmv2, WMV2", "Droned.wmv", "WMV")]
[TestCase("mpeg4, XVID", "", "XviD")]
[TestCase("mpeg4, DIVX", "", "DivX")]
[TestCase("mpeg4, divx", "", "DivX")]
[TestCase("mpeg4, DIV3", "spsm.dvdrip.divx.avi'.", "DivX")]
[TestCase("msmpeg4, DIV3", "Exit the Dragon, Enter the Tiger (1976) 360p MPEG Audio.avi", "DivX")]
[TestCase("msmpeg4v2, DIV3", "Exit the Dragon, Enter the Tiger (1976) 360p MPEG Audio.avi", "DivX")]

View File

@@ -259,6 +259,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Movie.Name.2011.1080p.UHD.BluRay.DD5.1.HDR.x265-CtrlHD.mkv", false)]
[TestCase("Movie.Name.2016.German.DTS.DL.1080p.UHDBD.x265-TDO.mkv", false)]
[TestCase("Movie.Name.2021.1080p.BDLight.x265-AVCDVD", false)]
[TestCase("Random.Title.2010.1080p.HD.DVD.AVC.DDP.5.1-GRouP", false)]
public void should_parse_bluray1080p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Source.BLURAY, proper, Resolution.R1080p);

View File

@@ -18,7 +18,7 @@ namespace NzbDrone.Core.Datastore.Migration
_serializerSettings = new JsonSerializerOptions
{
AllowTrailingCommas = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
@@ -43,18 +43,24 @@ namespace NzbDrone.Core.Datastore.Migration
foreach (var row in rows)
{
var oldRatings = JsonSerializer.Deserialize<Ratings205>(row.Ratings, _serializerSettings);
var newRatings = new Ratings206
{
Tmdb = new RatingChild206
{
Votes = oldRatings.Votes,
Value = oldRatings.Value,
Votes = 0,
Value = 0,
Type = RatingType206.User
}
};
if (row.Ratings != null)
{
var oldRatings = JsonSerializer.Deserialize<Ratings205>(row.Ratings, _serializerSettings);
newRatings.Tmdb.Votes = oldRatings.Votes;
newRatings.Tmdb.Value = oldRatings.Value;
}
corrected.Add(new Movie206
{
Id = row.Id,

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using CookComputing.XmlRpc;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
@@ -91,12 +90,7 @@ namespace NzbDrone.Core.Download.Clients.Aria2
var downloadSpeed = long.Parse(torrent.DownloadSpeed);
var status = DownloadItemStatus.Failed;
var title = "";
if (torrent.Bittorrent?.ContainsKey("info") == true && ((XmlRpcStruct)torrent.Bittorrent["info"]).ContainsKey("name"))
{
title = ((XmlRpcStruct)torrent.Bittorrent["info"])["name"].ToString();
}
var title = torrent.Bittorrent?.Name ?? "";
switch (torrent.Status)
{

View File

@@ -1,111 +1,161 @@
using CookComputing.XmlRpc;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.Aria2
{
public class Aria2Version
public class Aria2Fault
{
[XmlRpcMember("version")]
public string Version;
public Aria2Fault(XElement element)
{
foreach (var e in element.XPathSelectElements("./value/struct/member"))
{
var name = e.ElementAsString("name");
if (name == "faultCode")
{
FaultCode = e.Element("value").ElementAsInt("int");
}
else if (name == "faultString")
{
FaultString = e.Element("value").GetStringValue();
}
}
}
[XmlRpcMember("enabledFeatures")]
public string[] EnabledFeatures;
public int FaultCode { get; set; }
public string FaultString { get; set; }
}
public class Aria2Uri
public class Aria2Version
{
[XmlRpcMember("status")]
public string Status;
public Aria2Version(XElement element)
{
foreach (var e in element.XPathSelectElements("./struct/member"))
{
if (e.ElementAsString("name") == "version")
{
Version = e.Element("value").GetStringValue();
}
}
}
[XmlRpcMember("uri")]
public string Uri;
public string Version { get; set; }
}
public class Aria2File
{
[XmlRpcMember("index")]
public string Index;
public Aria2File(XElement element)
{
foreach (var e in element.XPathSelectElements("./struct/member"))
{
var name = e.ElementAsString("name");
[XmlRpcMember("length")]
public string Length;
if (name == "path")
{
Path = e.Element("value").GetStringValue();
}
}
}
[XmlRpcMember("completedLength")]
public string CompletedLength;
public string Path { get; set; }
}
[XmlRpcMember("path")]
public string Path;
public class Aria2Dict
{
public Aria2Dict(XElement element)
{
Dict = new Dictionary<string, string>();
[XmlRpcMember("selected")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string Selected;
foreach (var e in element.XPathSelectElements("./struct/member"))
{
Dict.Add(e.ElementAsString("name"), e.Element("value").GetStringValue());
}
}
[XmlRpcMember("uris")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public Aria2Uri[] Uris;
public Dictionary<string, string> Dict { get; set; }
}
public class Aria2Bittorrent
{
public Aria2Bittorrent(XElement element)
{
foreach (var e in element.Descendants("member"))
{
if (e.ElementAsString("name") == "name")
{
Name = e.Element("value").GetStringValue();
}
}
}
public string Name;
}
public class Aria2Status
{
[XmlRpcMember("bittorrent")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public XmlRpcStruct Bittorrent;
public Aria2Status(XElement element)
{
foreach (var e in element.XPathSelectElements("./struct/member"))
{
var name = e.ElementAsString("name");
[XmlRpcMember("bitfield")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string Bitfield;
if (name == "bittorrent")
{
Bittorrent = new Aria2Bittorrent(e.Element("value"));
}
else if (name == "infoHash")
{
InfoHash = e.Element("value").GetStringValue();
}
else if (name == "completedLength")
{
CompletedLength = e.Element("value").GetStringValue();
}
else if (name == "downloadSpeed")
{
DownloadSpeed = e.Element("value").GetStringValue();
}
else if (name == "files")
{
Files = e.XPathSelectElement("./value/array/data")
.Elements()
.Select(x => new Aria2File(x))
.ToArray();
}
else if (name == "gid")
{
Gid = e.Element("value").GetStringValue();
}
else if (name == "status")
{
Status = e.Element("value").GetStringValue();
}
else if (name == "totalLength")
{
TotalLength = e.Element("value").GetStringValue();
}
else if (name == "uploadLength")
{
UploadLength = e.Element("value").GetStringValue();
}
else if (name == "errorMessage")
{
ErrorMessage = e.Element("value").GetStringValue();
}
}
}
[XmlRpcMember("infoHash")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string InfoHash;
[XmlRpcMember("completedLength")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string CompletedLength;
[XmlRpcMember("connections")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string Connections;
[XmlRpcMember("dir")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string Dir;
[XmlRpcMember("downloadSpeed")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string DownloadSpeed;
[XmlRpcMember("files")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public Aria2File[] Files;
[XmlRpcMember("gid")]
public string Gid;
[XmlRpcMember("numPieces")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string NumPieces;
[XmlRpcMember("pieceLength")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string PieceLength;
[XmlRpcMember("status")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string Status;
[XmlRpcMember("totalLength")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string TotalLength;
[XmlRpcMember("uploadLength")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string UploadLength;
[XmlRpcMember("uploadSpeed")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string UploadSpeed;
[XmlRpcMember("errorMessage")]
[XmlRpcMissingMapping(MappingAction.Ignore)]
public string ErrorMessage;
public Aria2Bittorrent Bittorrent { get; set; }
public string InfoHash { get; set; }
public string CompletedLength { get; set; }
public string DownloadSpeed { get; set; }
public Aria2File[] Files { get; set; }
public string Gid { get; set; }
public string Status { get; set; }
public string TotalLength { get; set; }
public string UploadLength { get; set; }
public string ErrorMessage { get; set; }
}
}

View File

@@ -1,9 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using CookComputing.XmlRpc;
using NLog;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.Aria2
{
@@ -19,103 +19,61 @@ namespace NzbDrone.Core.Download.Clients.Aria2
Aria2Status GetFromGID(Aria2Settings settings, string gid);
}
public interface IAria2 : IXmlRpcProxy
{
[XmlRpcMethod("aria2.getVersion")]
Aria2Version GetVersion(string token);
[XmlRpcMethod("aria2.addUri")]
string AddUri(string token, string[] uri);
[XmlRpcMethod("aria2.addTorrent")]
string AddTorrent(string token, byte[] torrent);
[XmlRpcMethod("aria2.forceRemove")]
string Remove(string token, string gid);
[XmlRpcMethod("aria2.removeDownloadResult")]
string RemoveResult(string token, string gid);
[XmlRpcMethod("aria2.tellStatus")]
Aria2Status GetFromGid(string token, string gid);
[XmlRpcMethod("aria2.getGlobalOption")]
XmlRpcStruct GetGlobalOption(string token);
[XmlRpcMethod("aria2.tellActive")]
Aria2Status[] GetActive(string token);
[XmlRpcMethod("aria2.tellWaiting")]
Aria2Status[] GetWaiting(string token, int offset, int num);
[XmlRpcMethod("aria2.tellStopped")]
Aria2Status[] GetStopped(string token, int offset, int num);
}
public class Aria2Proxy : IAria2Proxy
{
private readonly Logger _logger;
private readonly IHttpClient _httpClient;
public Aria2Proxy(Logger logger)
public Aria2Proxy(IHttpClient httpClient)
{
_logger = logger;
}
private string GetToken(Aria2Settings settings)
{
return $"token:{settings?.SecretToken}";
}
private string GetURL(Aria2Settings settings)
{
return $"http{(settings.UseSsl ? "s" : "")}://{settings.Host}:{settings.Port}{settings.RpcPath}";
_httpClient = httpClient;
}
public string GetVersion(Aria2Settings settings)
{
_logger.Trace("> aria2.getVersion");
var response = ExecuteRequest(settings, "aria2.getVersion", GetToken(settings));
var client = BuildClient(settings);
var version = ExecuteRequest(() => client.GetVersion(GetToken(settings)));
var element = response.XPathSelectElement("./methodResponse/params/param/value");
_logger.Trace("< aria2.getVersion");
var version = new Aria2Version(element);
return version.Version;
}
public Aria2Status GetFromGID(Aria2Settings settings, string gid)
{
_logger.Trace("> aria2.tellStatus");
var response = ExecuteRequest(settings, "aria2.tellStatus", GetToken(settings), gid);
var client = BuildClient(settings);
var found = ExecuteRequest(() => client.GetFromGid(GetToken(settings), gid));
var element = response.XPathSelectElement("./methodResponse/params/param/value");
_logger.Trace("< aria2.tellStatus");
return new Aria2Status(element);
}
return found;
private List<Aria2Status> GetTorrentsMethod(Aria2Settings settings, string method, params object[] args)
{
var allArgs = new List<object> { GetToken(settings) };
if (args.Any())
{
allArgs.AddRange(args);
}
var response = ExecuteRequest(settings, method, allArgs.ToArray());
var element = response.XPathSelectElement("./methodResponse/params/param/value/array/data");
var torrents = element?.Elements()
.Select(x => new Aria2Status(x))
.ToList()
?? new List<Aria2Status>();
return torrents;
}
public List<Aria2Status> GetTorrents(Aria2Settings settings)
{
_logger.Trace("> aria2.tellActive");
var active = GetTorrentsMethod(settings, "aria2.tellActive");
var client = BuildClient(settings);
var waiting = GetTorrentsMethod(settings, "aria2.tellWaiting", 0, 10 * 1024);
var active = ExecuteRequest(() => client.GetActive(GetToken(settings)));
_logger.Trace("< aria2.tellActive");
_logger.Trace("> aria2.tellWaiting");
var waiting = ExecuteRequest(() => client.GetWaiting(GetToken(settings), 0, 10 * 1024));
_logger.Trace("< aria2.tellWaiting");
_logger.Trace("> aria2.tellStopped");
var stopped = ExecuteRequest(() => client.GetStopped(GetToken(settings), 0, 10 * 1024));
_logger.Trace("< aria2.tellStopped");
var stopped = GetTorrentsMethod(settings, "aria2.tellStopped", 0, 10 * 1024);
var items = new List<Aria2Status>();
@@ -128,98 +86,79 @@ namespace NzbDrone.Core.Download.Clients.Aria2
public Dictionary<string, string> GetGlobals(Aria2Settings settings)
{
_logger.Trace("> aria2.getGlobalOption");
var response = ExecuteRequest(settings, "aria2.getGlobalOption", GetToken(settings));
var client = BuildClient(settings);
var options = ExecuteRequest(() => client.GetGlobalOption(GetToken(settings)));
var element = response.XPathSelectElement("./methodResponse/params/param/value");
_logger.Trace("< aria2.getGlobalOption");
var result = new Aria2Dict(element);
var ret = new Dictionary<string, string>();
foreach (DictionaryEntry option in options)
{
ret.Add(option.Key.ToString(), option.Value?.ToString());
}
return ret;
return result.Dict;
}
public string AddMagnet(Aria2Settings settings, string magnet)
{
_logger.Trace("> aria2.addUri");
var response = ExecuteRequest(settings, "aria2.addUri", GetToken(settings), new List<string> { magnet });
var client = BuildClient(settings);
var gid = ExecuteRequest(() => client.AddUri(GetToken(settings), new[] { magnet }));
_logger.Trace("< aria2.addUri");
var gid = response.GetStringResponse();
return gid;
}
public string AddTorrent(Aria2Settings settings, byte[] torrent)
{
_logger.Trace("> aria2.addTorrent");
var response = ExecuteRequest(settings, "aria2.addTorrent", GetToken(settings), torrent);
var client = BuildClient(settings);
var gid = ExecuteRequest(() => client.AddTorrent(GetToken(settings), torrent));
_logger.Trace("< aria2.addTorrent");
var gid = response.GetStringResponse();
return gid;
}
public bool RemoveTorrent(Aria2Settings settings, string gid)
{
_logger.Trace("> aria2.forceRemove");
var response = ExecuteRequest(settings, "aria2.forceRemove", GetToken(settings), gid);
var client = BuildClient(settings);
var gidres = ExecuteRequest(() => client.Remove(GetToken(settings), gid));
_logger.Trace("< aria2.forceRemove");
var gidres = response.GetStringResponse();
return gid == gidres;
}
public bool RemoveCompletedTorrent(Aria2Settings settings, string gid)
{
_logger.Trace("> aria2.removeDownloadResult");
var response = ExecuteRequest(settings, "aria2.removeDownloadResult", GetToken(settings), gid);
var client = BuildClient(settings);
var result = ExecuteRequest(() => client.RemoveResult(GetToken(settings), gid));
_logger.Trace("< aria2.removeDownloadResult");
var result = response.GetStringResponse();
return result == "OK";
}
private IAria2 BuildClient(Aria2Settings settings)
private string GetToken(Aria2Settings settings)
{
var client = XmlRpcProxyGen.Create<IAria2>();
client.Url = GetURL(settings);
return client;
return $"token:{settings?.SecretToken}";
}
private T ExecuteRequest<T>(Func<T> task)
private XDocument ExecuteRequest(Aria2Settings settings, string methodName, params object[] args)
{
try
var requestBuilder = new XmlRpcRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.RpcPath)
{
return task();
}
catch (XmlRpcServerException ex)
{
throw new DownloadClientException("Unable to connect to aria2, please check your settings", ex);
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.TrustFailure)
{
throw new DownloadClientUnavailableException("Unable to connect to aria2, certificate validation failed.", ex);
}
LogResponseContent = true,
};
throw new DownloadClientUnavailableException("Unable to connect to aria2, please check your settings", ex);
var request = requestBuilder.Call(methodName, args).Build();
var response = _httpClient.Execute(request);
var doc = XDocument.Parse(response.Content);
var faultElement = doc.XPathSelectElement("./methodResponse/fault");
if (faultElement != null)
{
var fault = new Aria2Fault(faultElement);
throw new DownloadClientException($"Aria2 returned error code {fault.FaultCode}: {fault.FaultString}");
}
return doc;
}
}
}

View File

@@ -73,7 +73,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
baseUrl = HttpUri.CombinePath(baseUrl, "api");
var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, parameters);
requestBuilder.LogResponseContent = true;
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password);
requestBuilder.Headers.Add("Accept-Encoding", "gzip,deflate");
var httpRequest = requestBuilder.Build();

View File

@@ -229,7 +229,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, parameters);
requestBuilder.LogResponseContent = true;
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password);
var httpRequest = requestBuilder.Build();

View File

@@ -368,9 +368,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
if (Settings.MovieCategory.IsNotNullOrWhiteSpace() && version >= Version.Parse("2.0"))
{
var label = Proxy.GetLabels(Settings)[Settings.MovieCategory];
if (label.SavePath.IsNotNullOrWhiteSpace())
if (Proxy.GetLabels(Settings).TryGetValue(Settings.MovieCategory, out var label) && label.SavePath.IsNotNullOrWhiteSpace())
{
var labelDir = new OsPath(label.SavePath);

View File

@@ -293,7 +293,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
{
LogResponseContent = true,
NetworkCredential = new NetworkCredential(settings.Username, settings.Password)
NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password)
};
return requestBuilder;
}

View File

@@ -335,7 +335,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
{
LogResponseContent = true,
NetworkCredential = new NetworkCredential(settings.Username, settings.Password)
NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password)
};
return requestBuilder;
}

View File

@@ -200,7 +200,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
.Accept(HttpAccept.Json);
requestBuilder.LogResponseContent = true;
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password);
requestBuilder.AllowAutoRedirect = false;
return requestBuilder;

View File

@@ -127,6 +127,12 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
continue;
}
// Ignore torrents with an empty path
if (torrent.Path.IsNullOrWhiteSpace())
{
continue;
}
if (torrent.Path.StartsWith("."))
{
throw new DownloadClientException("Download paths must be absolute. Please specify variable \"directory\" in rTorrent.");

View File

@@ -0,0 +1,28 @@
using System.Xml.Linq;
using System.Xml.XPath;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.RTorrent
{
public class RTorrentFault
{
public RTorrentFault(XElement element)
{
foreach (var e in element.XPathSelectElements("./value/struct/member"))
{
var name = e.ElementAsString("name");
if (name == "faultCode")
{
FaultCode = e.Element("value").GetIntValue();
}
else if (name == "faultString")
{
FaultString = e.Element("value").GetStringValue();
}
}
}
public int FaultCode { get; set; }
public string FaultString { get; set; }
}
}

View File

@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using CookComputing.XmlRpc;
using NLog;
using System.Xml.Linq;
using System.Xml.XPath;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.RTorrent
{
@@ -21,125 +23,67 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings);
}
public interface IRTorrent : IXmlRpcProxy
{
[XmlRpcMethod("d.multicall2")]
object[] TorrentMulticall(params string[] parameters);
[XmlRpcMethod("load.normal")]
int LoadNormal(string target, string data, params string[] commands);
[XmlRpcMethod("load.start")]
int LoadStart(string target, string data, params string[] commands);
[XmlRpcMethod("load.raw")]
int LoadRaw(string target, byte[] data, params string[] commands);
[XmlRpcMethod("load.raw_start")]
int LoadRawStart(string target, byte[] data, params string[] commands);
[XmlRpcMethod("d.erase")]
int Remove(string hash);
[XmlRpcMethod("d.name")]
string GetName(string hash);
[XmlRpcMethod("d.custom1.set")]
string SetLabel(string hash, string label);
[XmlRpcMethod("d.views.push_back_unique")]
int PushUniqueView(string hash, string view);
[XmlRpcMethod("system.client_version")]
string GetVersion();
}
public class RTorrentProxy : IRTorrentProxy
{
private readonly Logger _logger;
private readonly IHttpClient _httpClient;
public RTorrentProxy(Logger logger)
public RTorrentProxy(IHttpClient httpClient)
{
_logger = logger;
_httpClient = httpClient;
}
public string GetVersion(RTorrentSettings settings)
{
_logger.Debug("Executing remote method: system.client_version");
var document = ExecuteRequest(settings, "system.client_version");
var client = BuildClient(settings);
var version = ExecuteRequest(() => client.GetVersion());
return version;
return document.Descendants("string").FirstOrDefault()?.Value ?? "0.0.0";
}
public List<RTorrentTorrent> GetTorrents(RTorrentSettings settings)
{
_logger.Debug("Executing remote method: d.multicall2");
var document = ExecuteRequest(settings,
"d.multicall2",
"",
"",
"d.name=", // string
"d.hash=", // string
"d.base_path=", // string
"d.custom1=", // string (label)
"d.size_bytes=", // long
"d.left_bytes=", // long
"d.down.rate=", // long (in bytes / s)
"d.ratio=", // long
"d.is_open=", // long
"d.is_active=", // long
"d.complete=", //long
"d.timestamp.finished="); // long (unix timestamp)
var client = BuildClient(settings);
var ret = ExecuteRequest(() => client.TorrentMulticall(
"",
"",
"d.name=", // string
"d.hash=", // string
"d.base_path=", // string
"d.custom1=", // string (label)
"d.size_bytes=", // long
"d.left_bytes=", // long
"d.down.rate=", // long (in bytes / s)
"d.ratio=", // long
"d.is_open=", // long
"d.is_active=", // long
"d.complete=", //long
"d.timestamp.finished=")); // long (unix timestamp)
var torrents = document.XPathSelectElement("./methodResponse/params/param/value/array/data")
?.Elements()
.Select(x => new RTorrentTorrent(x))
.ToList()
?? new List<RTorrentTorrent>();
_logger.Trace(ret.ToJson());
var items = new List<RTorrentTorrent>();
foreach (object[] torrent in ret)
{
var labelDecoded = System.Web.HttpUtility.UrlDecode((string)torrent[3]);
var item = new RTorrentTorrent();
item.Name = (string)torrent[0];
item.Hash = (string)torrent[1];
item.Path = (string)torrent[2];
item.Category = labelDecoded;
item.TotalSize = (long)torrent[4];
item.RemainingSize = (long)torrent[5];
item.DownRate = (long)torrent[6];
item.Ratio = (long)torrent[7];
item.IsOpen = Convert.ToBoolean((long)torrent[8]);
item.IsActive = Convert.ToBoolean((long)torrent[9]);
item.IsFinished = Convert.ToBoolean((long)torrent[10]);
item.FinishedTime = (long)torrent[11];
items.Add(item);
}
return items;
return torrents;
}
public void AddTorrentFromUrl(string torrentUrl, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
{
var client = BuildClient(settings);
var response = ExecuteRequest(() =>
{
if (settings.AddStopped)
{
_logger.Debug("Executing remote method: load.normal");
return client.LoadNormal("", torrentUrl, GetCommands(label, priority, directory));
}
else
{
_logger.Debug("Executing remote method: load.start");
return client.LoadStart("", torrentUrl, GetCommands(label, priority, directory));
}
});
var args = new List<object> { "", torrentUrl };
args.AddRange(GetCommands(label, priority, directory));
if (response != 0)
XDocument response;
if (settings.AddStopped)
{
response = ExecuteRequest(settings, "load.normal", args.ToArray());
}
else
{
response = ExecuteRequest(settings, "load.start", args.ToArray());
}
if (response.GetIntResponse() != 0)
{
throw new DownloadClientException("Could not add torrent: {0}.", torrentUrl);
}
@@ -147,22 +91,21 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public void AddTorrentFromFile(string fileName, byte[] fileContent, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
{
var client = BuildClient(settings);
var response = ExecuteRequest(() =>
{
if (settings.AddStopped)
{
_logger.Debug("Executing remote method: load.raw");
return client.LoadRaw("", fileContent, GetCommands(label, priority, directory));
}
else
{
_logger.Debug("Executing remote method: load.raw_start");
return client.LoadRawStart("", fileContent, GetCommands(label, priority, directory));
}
});
var args = new List<object> { "", fileContent };
args.AddRange(GetCommands(label, priority, directory));
if (response != 0)
XDocument response;
if (settings.AddStopped)
{
response = ExecuteRequest(settings, "load.raw", args.ToArray());
}
else
{
response = ExecuteRequest(settings, "load.raw_start", args.ToArray());
}
if (response.GetIntResponse() != 0)
{
throw new DownloadClientException("Could not add torrent: {0}.", fileName);
}
@@ -170,12 +113,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public void SetTorrentLabel(string hash, string label, RTorrentSettings settings)
{
_logger.Debug("Executing remote method: d.custom1.set");
var response = ExecuteRequest(settings, "d.custom1.set", hash, label);
var client = BuildClient(settings);
var response = ExecuteRequest(() => client.SetLabel(hash, label));
if (response != label)
if (response.GetStringResponse() != label)
{
throw new DownloadClientException("Could not set label to {1} for torrent: {0}.", hash, label);
}
@@ -183,11 +123,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings)
{
_logger.Debug("Executing remote method: d.views.push_back_unique");
var response = ExecuteRequest(settings, "d.views.push_back_unique", hash, view);
var client = BuildClient(settings);
var response = ExecuteRequest(() => client.PushUniqueView(hash, view));
if (response != 0)
if (response.GetIntResponse() != 0)
{
throw new DownloadClientException("Could not push unique view {0} for torrent: {1}.", view, hash);
}
@@ -195,12 +133,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public void RemoveTorrent(string hash, RTorrentSettings settings)
{
_logger.Debug("Executing remote method: d.erase");
var response = ExecuteRequest(settings, "d.erase", hash);
var client = BuildClient(settings);
var response = ExecuteRequest(() => client.Remove(hash));
if (response != 0)
if (response.GetIntResponse() != 0)
{
throw new DownloadClientException("Could not remove torrent: {0}.", hash);
}
@@ -208,13 +143,10 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public bool HasHashTorrent(string hash, RTorrentSettings settings)
{
_logger.Debug("Executing remote method: d.name");
var client = BuildClient(settings);
try
{
var name = ExecuteRequest(() => client.GetName(hash));
var response = ExecuteRequest(settings, "d.name", hash);
var name = response.GetStringResponse();
if (name.IsNullOrWhiteSpace())
{
@@ -253,45 +185,34 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
return result.ToArray();
}
private IRTorrent BuildClient(RTorrentSettings settings)
private XDocument ExecuteRequest(RTorrentSettings settings, string methodName, params object[] args)
{
var client = XmlRpcProxyGen.Create<IRTorrent>();
client.Url = string.Format(@"{0}://{1}:{2}/{3}",
settings.UseSsl ? "https" : "http",
settings.Host,
settings.Port,
settings.UrlBase);
client.EnableCompression = true;
var requestBuilder = new XmlRpcRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
{
LogResponseContent = true,
};
if (!settings.Username.IsNullOrWhiteSpace())
{
client.Credentials = new NetworkCredential(settings.Username, settings.Password);
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
}
return client;
}
var request = requestBuilder.Call(methodName, args).Build();
private T ExecuteRequest<T>(Func<T> task)
{
try
{
return task();
}
catch (XmlRpcServerException ex)
{
throw new DownloadClientException("Unable to connect to rTorrent, please check your settings", ex);
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.TrustFailure)
{
throw new DownloadClientUnavailableException("Unable to connect to rTorrent, certificate validation failed.", ex);
}
var response = _httpClient.Execute(request);
throw new DownloadClientUnavailableException("Unable to connect to rTorrent, please check your settings", ex);
var doc = XDocument.Parse(response.Content);
var faultElement = doc.XPathSelectElement("./methodResponse/fault");
if (faultElement != null)
{
var fault = new RTorrentFault(faultElement);
throw new DownloadClientException($"rTorrent returned error code {fault.FaultCode}: {fault.FaultString}");
}
return doc;
}
}
}

View File

@@ -1,7 +1,35 @@
namespace NzbDrone.Core.Download.Clients.RTorrent
using System;
using System.Linq;
using System.Web;
using System.Xml.Linq;
using NzbDrone.Core.Download.Extensions;
namespace NzbDrone.Core.Download.Clients.RTorrent
{
public class RTorrentTorrent
{
public RTorrentTorrent()
{
}
public RTorrentTorrent(XElement element)
{
var data = element.Descendants("value").ToList();
Name = data[0].GetStringValue();
Hash = data[1].GetStringValue();
Path = data[2].GetStringValue();
Category = HttpUtility.UrlDecode(data[3].GetStringValue());
TotalSize = data[4].GetLongValue();
RemainingSize = data[5].GetLongValue();
DownRate = data[6].GetLongValue();
Ratio = data[7].GetLongValue();
IsOpen = Convert.ToBoolean(data[8].GetLongValue());
IsActive = Convert.ToBoolean(data[9].GetLongValue());
IsFinished = Convert.ToBoolean(data[10].GetLongValue());
FinishedTime = data[11].GetLongValue();
}
public string Name { get; set; }
public string Hash { get; set; }
public string Path { get; set; }

View File

@@ -196,7 +196,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
.Accept(HttpAccept.Json);
requestBuilder.LogResponseContent = true;
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.Username, settings.Password);
return requestBuilder;
}

View File

@@ -49,7 +49,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")]
public string MovieCategory { get; set; }
[FieldDefinition(7, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Radarr to set after it has imported the download. Sonarr will not remove the torrent if seeding has finished. Leave blank to keep same category.")]
[FieldDefinition(7, Label = "Post-Import Category", Type = FieldType.Textbox, Advanced = true, HelpText = "Category for Radarr to set after it has imported the download. Radarr will not remove the torrent if seeding has finished. Leave blank to keep same category.")]
public string MovieImportedCategory { get; set; }
[FieldDefinition(8, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing movies that aired within the last 21 days")]

View File

@@ -0,0 +1,55 @@
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
namespace NzbDrone.Core.Download.Extensions
{
internal static class XmlExtensions
{
public static string GetStringValue(this XElement element)
{
return element.ElementAsString("string");
}
public static long GetLongValue(this XElement element)
{
return element.ElementAsLong("i8");
}
public static int GetIntValue(this XElement element)
{
return element.ElementAsInt("i4");
}
public static string ElementAsString(this XElement element, XName name, bool trim = false)
{
var el = element.Element(name);
return string.IsNullOrWhiteSpace(el?.Value)
? null
: (trim ? el.Value.Trim() : el.Value);
}
public static long ElementAsLong(this XElement element, XName name)
{
var el = element.Element(name);
return long.TryParse(el?.Value, out long value) ? value : default;
}
public static int ElementAsInt(this XElement element, XName name)
{
var el = element.Element(name);
return int.TryParse(el?.Value, out int value) ? value : default(int);
}
public static int GetIntResponse(this XDocument document)
{
return document.XPathSelectElement("./methodResponse/params/param/value").GetIntValue();
}
public static string GetStringResponse(this XDocument document)
{
return document.XPathSelectElement("./methodResponse/params/param/value").GetStringValue();
}
}
}

View File

@@ -57,7 +57,7 @@ namespace NzbDrone.Core.Indexers.FileList
[FieldDefinition(5, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
public int MinimumSeeders { get; set; }
[FieldDefinition(6, Type = FieldType.TagSelect, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#torrent-tracker-configuration", Advanced = true)]
[FieldDefinition(6, Type = FieldType.TagSelect, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
public IEnumerable<int> RequiredFlags { get; set; }
[FieldDefinition(7)]

View File

@@ -58,7 +58,7 @@ namespace NzbDrone.Core.Indexers.HDBits
[FieldDefinition(7, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
public int MinimumSeeders { get; set; }
[FieldDefinition(8, Type = FieldType.TagSelect, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#torrent-tracker-configuration", Advanced = true)]
[FieldDefinition(8, Type = FieldType.TagSelect, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
public IEnumerable<int> RequiredFlags { get; set; }
[FieldDefinition(9)]

View File

@@ -335,7 +335,7 @@ namespace NzbDrone.Core.Indexers
if (releases.Empty())
{
return new ValidationFailure(string.Empty, "Query successful, but no results were returned from your indexer. This may be an issue with the indexer or your indexer category settings.");
return new ValidationFailure(string.Empty, "Query successful, but no results in the configured categories were returned from your indexer. This may be an issue with the indexer or your indexer category settings.");
}
}
catch (ApiKeyException ex)

View File

@@ -46,7 +46,7 @@ namespace NzbDrone.Core.Indexers.IPTorrents
[FieldDefinition(2, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
public int MinimumSeeders { get; set; }
[FieldDefinition(3, Type = FieldType.TagSelect, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#torrent-tracker-configuration", Advanced = true)]
[FieldDefinition(3, Type = FieldType.TagSelect, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
public IEnumerable<int> RequiredFlags { get; set; }
[FieldDefinition(4)]

View File

@@ -44,7 +44,7 @@ namespace NzbDrone.Core.Indexers.Nyaa
[FieldDefinition(3, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
public int MinimumSeeders { get; set; }
[FieldDefinition(4, Type = FieldType.TagSelect, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#torrent-tracker-configuration", Advanced = true)]
[FieldDefinition(4, Type = FieldType.TagSelect, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
public IEnumerable<int> RequiredFlags { get; set; }
[FieldDefinition(5)]

View File

@@ -49,7 +49,7 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
[FieldDefinition(5)]
public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings();
[FieldDefinition(6, Type = FieldType.TagSelect, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#torrent-tracker-configuration", Advanced = true)]
[FieldDefinition(6, Type = FieldType.TagSelect, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
public IEnumerable<int> RequiredFlags { get; set; }
public NzbDroneValidationResult Validate()

View File

@@ -60,7 +60,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
[FieldDefinition(4, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
public int MinimumSeeders { get; set; }
[FieldDefinition(5, Type = FieldType.TagSelect, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#torrent-tracker-configuration", Advanced = true)]
[FieldDefinition(5, Type = FieldType.TagSelect, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
public IEnumerable<int> RequiredFlags { get; set; }
[FieldDefinition(6, Type = FieldType.Select, Label = "Categories", SelectOptions = typeof(RarbgCategories), HelpText = "Categories for use in search and feeds. If unspecified, all options are used.")]

View File

@@ -48,7 +48,7 @@ namespace NzbDrone.Core.Indexers.TorrentRss
[FieldDefinition(5)]
public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings();
[FieldDefinition(6, Type = FieldType.TagSelect, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#torrent-tracker-configuration", Advanced = true)]
[FieldDefinition(6, Type = FieldType.TagSelect, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
public IEnumerable<int> RequiredFlags { get; set; }
public NzbDroneValidationResult Validate()

View File

@@ -65,7 +65,7 @@ namespace NzbDrone.Core.Indexers.Torznab
[FieldDefinition(9)]
public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings();
[FieldDefinition(10, Type = FieldType.TagSelect, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#torrent-tracker-configuration", Advanced = true)]
[FieldDefinition(10, Type = FieldType.TagSelect, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)]
public IEnumerable<int> RequiredFlags { get; set; }
public override NzbDroneValidationResult Validate()

View File

@@ -102,7 +102,7 @@ namespace NzbDrone.Core.Languages
public static Language Bulgarian => new Language(29, "Bulgarian");
public static Language PortugueseBR => new Language(30, "Portuguese (Brazil)");
public static Language Arabic => new Language(31, "Arabic");
public static Language Ukrainian => new Language(32, "Unkrainian");
public static Language Ukrainian => new Language(32, "Ukrainian");
public static Language Persian => new Language(33, "Persian");
public static Language Bengali => new Language(34, "Bengali");
public static Language Any => new Language(-1, "Any");

View File

@@ -19,7 +19,7 @@
"CompletedDownloadHandling": "Verarbeitung abgeschlossener Downloads",
"Connect": "Verbindungen",
"Connections": "Verbindungen",
"Crew": "Besetzung",
"Crew": "Crew",
"CustomFilters": "Filter anpassen",
"CustomFormats": "Eigene Formate",
"Date": "Datum",
@@ -516,7 +516,7 @@
"ShowTitleHelpText": "Filmtitel unter dem Plakat anzeigen",
"ShowUnknownMovieItems": "Unzugeordente Filmeinträge anzeigen",
"SkipFreeSpaceCheck": "Pürfung des freien Speichers überspringen",
"SkipFreeSpaceCheckWhenImportingHelpText": "Aktiviere dies, wenn es nicht möglich ist, den freien Speicherplatz des Stammverzeichnisses zu ermitteln",
"SkipFreeSpaceCheckWhenImportingHelpText": "Aktiviere diese Option, wenn es nicht möglich ist, den freien Speicherplatz des Stammverzeichnisses für Filme zu erkennen",
"SorryThatMovieCannotBeFound": "Schade, dieser Film kann nicht gefunden werden.",
"SourcePath": "Quellpfad",
"SourceRelativePath": "Relativer Quellpfad",
@@ -936,7 +936,7 @@
"Months": "Monate",
"MonitoredStatus": "Beobachtet/Status",
"Monday": "Montag",
"MissingFromDisk": "Radarr konnte die Datei nicht auf der Festplatte finden, daher wurde die Datei aus der Datenbank entfernt",
"MissingFromDisk": "Radarr konnte die Datei nicht auf der Festplatte finden, daher wurde die Verknüpfung auf die Datei aus der Datenbank entfernt",
"Minutes": "Minuten",
"MinimumCustomFormatScore": "Minimum der eigenen Formate Bewertungspunkte",
"Min": "Min.",
@@ -1090,5 +1090,16 @@
"LocalPath": "Lokaler Pfad",
"AnnouncedMsg": "Film ist angekündigt",
"ClickToChangeReleaseGroup": "Releasegruppe ändern",
"Filters": "Filter"
"Filters": "Filter",
"SelectLanguages": "Sprachen auswählen",
"IndexerJackettAll": "Indexer, welche den nicht unterstützten 'all'-Endpoint von Jackett verwenden: {0}",
"ManualImportSetReleaseGroup": "Manueller Import - Releasegruppe setzen",
"OnApplicationUpdate": "Bei Programm-Update",
"OnApplicationUpdateHelpText": "Bei Programm-Update",
"RemotePath": "Entfernter Pfad",
"SelectReleaseGroup": "Releasgruppe auswählen",
"SizeLimit": "Grössenlimit",
"SetReleaseGroup": "Releasgruppe setzen",
"IndexerDownloadClientHelpText": "Wähle aus, welcher Download-Client für diesen Indexer verwendet wird",
"DiscordUrlInSlackNotification": "Du hast eine Discord-Benachrichtigung als Slack-Benachrichtigung eingerichtet. Richte diese für bessere Funktionalität als Discord-Benachrichtigung ein. Folgende Benachrichtigen sind betroffen: {}"
}

View File

@@ -71,6 +71,7 @@
"Authentication": "Authentication",
"AuthenticationMethodHelpText": "Require Username and Password to access Radarr",
"AuthForm": "Forms (Login Page)",
"Auto": "Auto",
"Automatic": "Automatic",
"AutomaticSearch": "Automatic Search",
"AutoRedownloadFailedHelpText": "Automatically search for and attempt to download a different release",
@@ -226,10 +227,6 @@
"DetailedProgressBar": "Detailed Progress Bar",
"DetailedProgressBarHelpText": "Show text on progress bar",
"Details": "Details",
"TmdbRating": "TMDb Rating",
"TmdbVotes": "TMDb Votes",
"ImdbRating": "IMDb Rating",
"ImdbVotes": "IMDb Votes",
"DigitalRelease": "Digital Release",
"Disabled": "Disabled",
"Discord": "Discord",
@@ -266,6 +263,7 @@
"DownloadPropersAndRepacksHelpTextWarning": "Use custom formats for automatic upgrades to Propers/Repacks",
"DownloadWarning": "Download warning: {0}",
"DownloadWarningCheckDownloadClientForMoreDetails": "Download warning: check download client for more details",
"Duration": "Duration",
"Edit": "Edit",
"EditCustomFormat": "Edit Custom Format",
"EditDelayProfile": "Edit Delay Profile",
@@ -347,7 +345,7 @@
"ForMoreInformationOnTheIndividualImportListsClinkOnTheInfoButtons": "For more information on the individual import lists, click on the info buttons.",
"ForMoreInformationOnTheIndividualIndexers": "For more information on the individual indexers, click on the info buttons.",
"FreeSpace": "Free Space",
"From": "From",
"From": "from",
"General": "General",
"GeneralSettings": "General Settings",
"GeneralSettingsSummary": "Port, SSL, username/password, proxy, analytics and updates",
@@ -386,6 +384,8 @@
"IllRestartLater": "I'll restart later",
"Images": "Images",
"IMDb": "IMDb",
"ImdbRating": "IMDb Rating",
"ImdbVotes": "IMDb Votes",
"Import": "Import",
"ImportCustomFormat": "Import Custom Format",
"Imported": "Imported",
@@ -423,10 +423,11 @@
"Indexer": "Indexer",
"IndexerDownloadClientHelpText": "Specify which download client is used for grabs from this indexer",
"IndexerFlags": "Indexer Flags",
"IndexerJackettAll": "Indexers using the unsupported Jackett 'all' endpoint: {0}",
"IndexerLongTermStatusCheckAllClientMessage": "All indexers are unavailable due to failures for more than 6 hours",
"IndexerLongTermStatusCheckSingleClientMessage": "Indexers unavailable due to failures for more than 6 hours: {0}",
"IndexerPriority": "Indexer Priority",
"IndexerPriorityHelpText": "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25.",
"IndexerPriorityHelpText": "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25. Used when grabbing releases as a tiebreaker for otherwise equal releases, Radarr will still use all enabled indexers for RSS Sync and Searching",
"IndexerRssHealthCheckNoAvailableIndexers": "All rss-capable indexers are temporarily unavailable due to recent indexer errors",
"IndexerRssHealthCheckNoIndexers": "No indexers available with RSS sync enabled, Radarr will not grab new releases automatically",
"Indexers": "Indexers",
@@ -438,7 +439,6 @@
"IndexerStatusCheckAllClientMessage": "All indexers are unavailable due to failures",
"IndexerStatusCheckSingleClientMessage": "Indexers unavailable due to failures: {0}",
"IndexerTagHelpText": "Only use this indexer for movies with at least one matching tag. Leave blank to use with all movies.",
"IndexerJackettAll": "Indexers using the unsupported Jackett 'all' endpoint: {0}",
"Info": "Info",
"InstallLatest": "Install Latest",
"InteractiveImport": "Interactive Import",
@@ -463,6 +463,7 @@
"Level": "Level",
"LinkHere": "here",
"Links": "Links",
"List": "List",
"ListExclusions": "List Exclusions",
"Lists": "Lists",
"ListSettings": "List Settings",
@@ -589,6 +590,7 @@
"Negated": "Negated",
"NegateHelpText": "If checked, the custom format will not apply if this {0} condition matches.",
"NetCore": ".NET",
"Never": "Never",
"New": "New",
"NextExecution": "Next Execution",
"No": "No",
@@ -727,6 +729,7 @@
"RadarrSupportsCustomConditionsAgainstTheReleasePropertiesBelow": "Radarr supports custom conditions against the release properties below.",
"RadarrTags": "Radarr Tags",
"RadarrUpdated": "Radarr Updated",
"Rating": "Rating",
"Ratings": "Ratings",
"ReadTheWikiForMoreInformation": "Read the Wiki for more information",
"Real": "Real",
@@ -943,6 +946,7 @@
"SSLCertPathHelpText": "Path to pfx file",
"SSLPort": "SSL Port",
"StandardMovieFormat": "Standard Movie Format",
"Started": "Started",
"StartImport": "Start Import",
"StartProcessing": "Start Processing",
"StartSearchForMissingMovie": "Start search for missing movie",
@@ -973,7 +977,7 @@
"TestAllIndexers": "Test All Indexers",
"TestAllLists": "Test All Lists",
"TheLogLevelDefault": "The log level defaults to 'Info' and can be changed in",
"ThisCannotBeCancelled": "This cannot be cancelled once started without restarting Radarr.",
"ThisCannotBeCancelled": "This cannot be cancelled once started without disabling all of your indexers.",
"ThisConditionMatchesUsingRegularExpressions": "This condition matches using Regular Expressions. Note that the characters {0} have special meanings and need escaping with a {1}",
"Time": "Time",
"TimeFormat": "Time Format",
@@ -983,6 +987,8 @@
"TMDb": "TMDb",
"TMDBId": "TMDb Id",
"TmdbIdHelpText": "The TMDb Id of the movie to exclude",
"TmdbRating": "TMDb Rating",
"TmdbVotes": "TMDb Votes",
"Today": "Today",
"Tomorrow": "Tomorrow",
"TorrentDelay": "Torrent Delay",
@@ -1088,6 +1094,7 @@
"VideoCodec": "Video Codec",
"View": "View",
"VisitGithubCustomFormatsAphrodite": "Visit the wiki for more details: ",
"Waiting": "Waiting",
"WaitingToImport": "Waiting to Import",
"WaitingToProcess": "Waiting to Process",
"Wanted": "Wanted",
@@ -1106,4 +1113,4 @@
"YesMoveFiles": "Yes, Move the Files",
"Yesterday": "Yesterday",
"YouCanAlsoSearch": "You can also search using TMDb ID or IMDb ID of a movie. e.g. `tmdb:71663`"
}
}

View File

@@ -1089,5 +1089,21 @@
"RemoveDownloadsAlert": "Poistoasetukset on siirretty yllä olevassa taulukossa yksittäisten lataustyökalujen alle.",
"OnApplicationUpdate": "Kun sovellus päivittyy",
"OnApplicationUpdateHelpText": "Kun sovellus päivittyy",
"DiscordUrlInSlackNotification": "Olet määrittänyt Discord-ilmoituksen Slack-ilmoitukseksi. Määritä se Discord-ilmoitukseksi parempaa toiminnallisuutta varten. Koskee seuraavia ilmoituksia: {0}"
"DiscordUrlInSlackNotification": "Olet määrittänyt Discord-ilmoituksen Slack-ilmoitukseksi. Määritä se Discord-ilmoitukseksi parempaa toiminnallisuutta varten. Koskee seuraavia ilmoituksia: {0}",
"LocalPath": "Paikallinen sijainti",
"ManualImportSetReleaseGroup": "Manuaalinen tuonti - Määritä julkaisuryhmä",
"AnnouncedMsg": "Elokuva on julkistettu",
"IndexerDownloadClientHelpText": "Määritä tämän tietolähteen kanssa käytettävä lataustyökalu",
"RemotePath": "Etäsijainti",
"SelectLanguages": "Valitse kielet",
"SelectReleaseGroup": "Valitse julkaisuryhmä",
"SetReleaseGroup": "Määritä julkaisuryhmä",
"SizeLimit": "Kokorajoitus",
"ClickToChangeReleaseGroup": "Paina vaihtaaksesi julkaisuryhmää",
"Filters": "Suodattimet",
"TmdbRating": "TMDb-arvio",
"IndexerJackettAll": "Jackettin ei-tuettua 'all'-päätettä käyttävät tietolähteet: {0}",
"TmdbVotes": "TMDb-äänet",
"ImdbRating": "IMDb-arvio",
"ImdbVotes": "IMDb-äänet"
}

View File

@@ -59,7 +59,7 @@
"Activity": "Activité",
"About": "À propos",
"CustomFormatsSettingsSummary": "Paramètres et Formats personnalisés",
"IndexerStatusCheckSingleClientMessage": "Indexeurs indisponibles en raison d'échecs: {0}",
"IndexerStatusCheckSingleClientMessage": "Indexeurs indisponibles en raison d'échecs: {0}",
"DownloadClientStatusCheckSingleClientMessage": "Clients de Téléchargement indisponibles en raison d'échecs: {0}",
"SetTags": "Définir Tags",
"ReleaseTitle": "Titre de la version",
@@ -1077,5 +1077,14 @@
"ClickToChangeReleaseGroup": "Cliquez pour changer de release group",
"AnnouncedMsg": "Le film est annoncé",
"Filters": "Filtres",
"IndexerDownloadClientHelpText": "Précisez quel client de téléchargement est utilisé pour cet indexer"
"IndexerDownloadClientHelpText": "Précisez quel client de téléchargement est utilisé pour cet indexer",
"TmdbRating": "Note TMDb",
"IndexerTagHelpText": "Utiliser seulement cet indexeur pour les films avec au moins un tag correspondant. Laissez vide pour l'utiliser avec tous les films.",
"IndexerJackettAll": "Les indexeurs utilisant le endpoint 'all' de Jackett: {0}",
"ManualImportSetReleaseGroup": "Import manuel - Spécifier le groupe de Release",
"TmdbVotes": "Votes TMDb",
"ImdbRating": "Note IMDb",
"ImdbVotes": "Votes IMDb",
"LocalPath": "Chemin local",
"DiscordUrlInSlackNotification": "Vous avez une configuration de notification Discord en tant que notification Slack. Configurez cela comme une notification Discord pour une meilleure fonctionnalité. Les notifications affectées sont: {0}"
}

View File

@@ -1086,5 +1086,14 @@
"RemoveSelectedItems": "A kijelölt elemek eltávolítása",
"RemoveFailed": "Eltávolítás Sikertelen",
"RemoveCompleted": "Eltávolítás Kész",
"RemoveDownloadsAlert": "Az eltávolításhoz szükséges beállítások átkerültek a fenti táblázatban található egyéni letöltő beállítások közé."
"RemoveDownloadsAlert": "Az eltávolításhoz szükséges beállítások átkerültek a fenti táblázatban található egyéni letöltő beállítások közé.",
"ImdbVotes": "IMDb Szavazatok",
"DiscordUrlInSlackNotification": "A Discord-értesítést Slack-értesítésként állította be. Állítsa be ezt Discord-értesítésként a jobb működés érdekében. A végrehajtott értesítések a következők: {0}",
"Filters": "Szűrők",
"IndexerDownloadClientHelpText": "Adja meg, hogy melyik letöltési kliens használja az indexelőből történő megfogásokat",
"AnnouncedMsg": "A filmet bejelentették",
"ClickToChangeReleaseGroup": "Kiadási csoport módosítása",
"TmdbRating": "TMDb Értékelés",
"TmdbVotes": "TMDb Szavazatok",
"ImdbRating": "IMDb Értékelés"
}

View File

@@ -1100,5 +1100,10 @@
"ClickToChangeReleaseGroup": "Clique para mudar o grupo do lançamento",
"Filters": "Filtros",
"RemotePath": "Caminho Remoto",
"SizeLimit": "Limite de Tamanho"
"SizeLimit": "Limite de Tamanho",
"ImdbRating": "Avaliação no IMDb",
"TmdbRating": "Avaliação no TMDb",
"TmdbVotes": "Votos no TMDb",
"ImdbVotes": "Votos no IMDb",
"IndexerJackettAll": "Indexadores que usam o não suportado Jackett 'all' endpoint: {0}"
}

View File

@@ -385,7 +385,7 @@
"ReleaseTitle": "Название релиза",
"ReleaseStatus": "Статус релиза",
"ReleaseRejected": "Релиз отклонен",
"ReleaseGroup": "Группа выпуска",
"ReleaseGroup": "Релиз группа",
"ReleasedMsg": "Фильм выпущен",
"ReleaseDates": "Дата выпуска",
"Released": "Выпущен",
@@ -1046,7 +1046,7 @@
"More": "Более",
"Download": "Скачать",
"DownloadClientCheckDownloadingToRoot": "Клиент загрузки {0} помещает загрузки в корневую папку {1}. Вы не должны загружать в корневую папку.",
"DeleteFileLabel": "Удалить {0} фалйлов фильма",
"DeleteFileLabel": "Удалить {0} файл фильма",
"RemotePathMappingCheckWrongOSPath": "Удалённый клиент загрузки {0} загружает файлы в {1}, но это не действительный путь {2}. Проверьте соответствие удаленных путей и настройки клиента загрузки.",
"RemotePathMappingCheckRemoteDownloadClient": "Удалённый клиент загрузки {0} сообщил о файлах в {1}, но эта директория, похоже, не существует. Вероятно, отсутствует сопоставление удаленных путей.",
"RemotePathMappingCheckLocalWrongOSPath": "Локальный клиент загрузки {0} загружает файлы в {1}, но это не правильный путь {2}. Проверьте настройки клиента загрузки.",
@@ -1099,5 +1099,10 @@
"AnnouncedMsg": "Фильм анонсирован",
"Filters": "Фильтры",
"RemotePath": "Удалённый путь",
"SetReleaseGroup": "Установить релиз-группу"
"SetReleaseGroup": "Установить релиз-группу",
"IndexerJackettAll": "Используется не поддерживаемый в Jackett конечный параметр 'all' в индексаторе: {0}",
"TmdbRating": "TMDb рейтинг",
"ImdbRating": "IMDb рейтинг",
"TmdbVotes": "TMDb оценок",
"ImdbVotes": "IMDb оценок"
}

View File

@@ -1,26 +0,0 @@
using System.Collections.Generic;
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Download;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.Events
{
public class MovieDownloadedEvent : IEvent
{
public LocalMovie Movie { get; private set; }
public MovieFile MovieFile { get; private set; }
public List<MovieFile> OldFiles { get; private set; }
public string DownloadId { get; private set; }
public MovieDownloadedEvent(LocalMovie movie, MovieFile movieFile, List<MovieFile> oldFiles, DownloadClientItem downloadClientItem)
{
Movie = movie;
MovieFile = movieFile;
OldFiles = oldFiles;
if (downloadClientItem != null)
{
DownloadId = downloadClientItem.DownloadId;
}
}
}
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Download;
using NzbDrone.Core.Parser.Model;
@@ -8,24 +9,22 @@ namespace NzbDrone.Core.MediaFiles.Events
{
public LocalMovie MovieInfo { get; private set; }
public MovieFile ImportedMovie { get; private set; }
public List<MovieFile> OldFiles { get; private set; }
public bool NewDownload { get; private set; }
public DownloadClientItemClientInfo DownloadClientInfo { get; set; }
public string DownloadId { get; private set; }
public MovieImportedEvent(LocalMovie movieInfo, MovieFile importedMovie, bool newDownload)
public MovieImportedEvent(LocalMovie movieInfo, MovieFile importedMovie, List<MovieFile> oldFiles, bool newDownload, DownloadClientItem downloadClientItem)
{
MovieInfo = movieInfo;
ImportedMovie = importedMovie;
OldFiles = oldFiles;
NewDownload = newDownload;
}
public MovieImportedEvent(LocalMovie movieInfo, MovieFile importedMovie, bool newDownload, DownloadClientItem downloadClientItem, string downloadId)
{
MovieInfo = movieInfo;
ImportedMovie = importedMovie;
NewDownload = newDownload;
DownloadClientInfo = downloadClientItem.DownloadClientInfo;
DownloadId = downloadId;
if (downloadClientItem != null)
{
DownloadClientInfo = downloadClientItem.DownloadClientInfo;
DownloadId = downloadClientItem.DownloadId;
}
}
}
}

View File

@@ -147,7 +147,8 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
}
if (audioFormat == "wmav1" ||
audioFormat == "wmav2")
audioFormat == "wmav2" ||
audioFormat == "wmapro")
{
return "WMA";
}
@@ -216,8 +217,8 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
}
if (videoCodecID == "DIV3" ||
videoCodecID == "DIVX" ||
videoCodecID == "DX50")
videoCodecID == "DX50" ||
videoCodecID.ToUpperInvariant() == "DIVX")
{
return "DivX";
}
@@ -244,7 +245,8 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
}
if (videoFormat == "wmv1" ||
videoFormat == "wmv2")
videoFormat == "wmv2" ||
videoFormat == "wmv3")
{
return "WMV";
}
@@ -254,7 +256,8 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
videoFormat == "rv10" ||
videoFormat == "rv20" ||
videoFormat == "rv30" ||
videoFormat == "rv40")
videoFormat == "rv40" ||
videoFormat == "cinepak")
{
return "";
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using FFMpegCore;
using NzbDrone.Core.Datastore;
@@ -12,12 +11,6 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
public string RawFrameData { get; set; }
public int SchemaRevision { get; set; }
[JsonIgnore]
public IMediaAnalysis Analysis => FFProbe.Analyse(RawStreamData);
[JsonIgnore]
public IMediaAnalysis Frames => FFProbe.Analyse(RawFrameData);
public string ContainerFormat { get; set; }
public string VideoFormat { get; set; }

View File

@@ -22,7 +22,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
private readonly List<FFProbePixelFormat> _pixelFormats;
public const int MINIMUM_MEDIA_INFO_SCHEMA_REVISION = 8;
public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 9;
public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 10;
private static readonly string[] ValidHdrColourPrimaries = { "bt2020" };
private static readonly string[] HlgTransferFunctions = { "bt2020-10", "arib-std-b67" };
@@ -169,6 +169,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
1 => HdrFormat.DolbyVisionHdr10,
2 => HdrFormat.DolbyVisionSdr,
4 => HdrFormat.DolbyVisionHlg,
6 => HdrFormat.DolbyVisionHdr10,
_ => HdrFormat.DolbyVision
};
}

View File

@@ -144,19 +144,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport
_extraService.ImportMovie(localMovie, movieFile, copyOnly);
}
if (downloadClientItem != null)
{
_eventAggregator.PublishEvent(new MovieImportedEvent(localMovie, movieFile, newDownload, downloadClientItem, downloadClientItem.DownloadId));
}
else
{
_eventAggregator.PublishEvent(new MovieImportedEvent(localMovie, movieFile, newDownload));
}
if (newDownload)
{
_eventAggregator.PublishEvent(new MovieDownloadedEvent(localMovie, movieFile, oldFiles, downloadClientItem));
}
_eventAggregator.PublishEvent(new MovieImportedEvent(localMovie, movieFile, oldFiles, newDownload, downloadClientItem));
}
catch (RootFolderNotFoundException e)
{

View File

@@ -108,7 +108,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
_logger.Debug("Language couldn't be parsed from release, fallback to movie original language: {0}", movie.OriginalLanguage.Name);
}
var localEpisode = new LocalMovie
var localMovie = new LocalMovie
{
Movie = movie,
FileMovieInfo = Parser.Parser.ParseMoviePath(path),
@@ -122,7 +122,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup,
};
return MapItem(_importDecisionMaker.GetDecision(localEpisode, downloadClientItem), rootFolder, downloadId, null);
return MapItem(_importDecisionMaker.GetDecision(localMovie, downloadClientItem), rootFolder, downloadId, null);
}
private List<ManualImportItem> ProcessFolder(string rootFolder, string baseFolder, string downloadId, int? movieId, bool filterExistingFiles)
@@ -210,10 +210,10 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
{
var localMovie = new LocalMovie();
localMovie.Path = file;
localMovie.FileMovieInfo = Parser.Parser.ParseMoviePath(file);
localMovie.DownloadClientMovieInfo = trackedDownload?.RemoteMovie?.ParsedMovieInfo;
localMovie = _aggregationService.Augment(localMovie, null, false);
localMovie.ReleaseGroup = Parser.Parser.ParseReleaseGroup(file);
localMovie.Quality = QualityParser.ParseQuality(file);
localMovie.Languages = LanguageParser.ParseLanguages(file);
localMovie.Size = _diskProvider.GetFileSize(file);
return MapItem(new ImportDecision(localMovie, new Rejection("Unknown Movie")), rootFolder, downloadId, null);
}
@@ -246,14 +246,14 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
foreach (var file in videoFiles)
{
var localEpisode = new LocalMovie();
localEpisode.Path = file;
localEpisode.Quality = new QualityModel(Quality.Unknown);
localEpisode.Languages = new List<Language> { Language.Unknown };
localEpisode.ReleaseGroup = Parser.Parser.ParseReleaseGroup(file);
localEpisode.Size = _diskProvider.GetFileSize(file);
var localMovie = new LocalMovie();
localMovie.Path = file;
localMovie.Quality = new QualityModel(Quality.Unknown);
localMovie.Languages = new List<Language> { Language.Unknown };
localMovie.ReleaseGroup = Parser.Parser.ParseReleaseGroup(file);
localMovie.Size = _diskProvider.GetFileSize(file);
items.Add(MapItem(new ImportDecision(localEpisode), rootFolder, null, null));
items.Add(MapItem(new ImportDecision(localMovie), rootFolder, null, null));
}
return items;

View File

@@ -7,17 +7,21 @@ using MailKit.Security;
using MimeKit;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Dispatchers;
using NzbDrone.Core.Security;
namespace NzbDrone.Core.Notifications.Email
{
public class Email : NotificationBase<EmailSettings>
{
private readonly ICertificateValidationService _certificateValidationService;
private readonly Logger _logger;
public override string Name => "Email";
public Email(Logger logger)
public Email(ICertificateValidationService certificateValidationService, Logger logger)
{
_certificateValidationService = certificateValidationService;
_logger = logger;
}
@@ -141,6 +145,8 @@ namespace NzbDrone.Core.Notifications.Email
}
}
client.ServerCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError;
_logger.Debug("Connecting to mail server");
client.Connect(settings.Server, settings.Port, serverOption);

View File

@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System;
using System.Collections.Specialized;
using System.Net;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Notifications.Notifiarr

View File

@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Notifications
public class NotificationService
: IHandle<MovieRenamedEvent>,
IHandle<MovieGrabbedEvent>,
IHandle<MovieDownloadedEvent>,
IHandle<MovieImportedEvent>,
IHandle<MoviesDeletedEvent>,
IHandle<MovieFileDeletedEvent>,
IHandle<HealthCheckFailedEvent>,
@@ -117,21 +117,29 @@ namespace NzbDrone.Core.Notifications
}
}
public void Handle(MovieDownloadedEvent message)
public void Handle(MovieImportedEvent message)
{
var downloadMessage = new DownloadMessage();
downloadMessage.Message = GetMessage(message.Movie.Movie, message.Movie.Quality);
downloadMessage.MovieFile = message.MovieFile;
downloadMessage.Movie = message.Movie.Movie;
downloadMessage.OldMovieFiles = message.OldFiles;
downloadMessage.SourcePath = message.Movie.Path;
downloadMessage.DownloadId = message.DownloadId;
if (!message.NewDownload)
{
return;
}
var downloadMessage = new DownloadMessage
{
Message = GetMessage(message.MovieInfo.Movie, message.MovieInfo.Quality),
MovieFile = message.ImportedMovie,
Movie = message.MovieInfo.Movie,
OldMovieFiles = message.OldFiles,
SourcePath = message.MovieInfo.Path,
DownloadClient = message.DownloadClientInfo?.Name,
DownloadId = message.DownloadId
};
foreach (var notification in _notificationFactory.OnDownloadEnabled())
{
try
{
if (ShouldHandleMovie(notification.Definition, message.Movie.Movie))
if (ShouldHandleMovie(notification.Definition, message.MovieInfo.Movie))
{
if (downloadMessage.OldMovieFiles.Empty() || ((NotificationDefinition)notification.Definition).OnUpgrade)
{

View File

@@ -102,7 +102,7 @@ namespace NzbDrone.Core.Notifications.PushBullet
var request = requestBuilder.Build();
request.Method = HttpMethod.Get;
request.AddBasicAuthentication(settings.ApiKey, string.Empty);
request.Credentials = new BasicNetworkCredential(settings.ApiKey, string.Empty);
var response = _httpClient.Execute(request);
@@ -198,7 +198,7 @@ namespace NzbDrone.Core.Notifications.PushBullet
var request = requestBuilder.Build();
request.AddBasicAuthentication(settings.ApiKey, string.Empty);
request.Credentials = new BasicNetworkCredential(settings.ApiKey, string.Empty);
_httpClient.Execute(request);
}

View File

@@ -0,0 +1,110 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Web;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.OAuth;
namespace NzbDrone.Core.Notifications.Twitter
{
public interface ITwitterProxy
{
NameValueCollection GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier);
string GetOAuthRedirect(string consumerKey, string consumerSecret, string callbackUrl);
void UpdateStatus(string message, TwitterSettings settings);
void DirectMessage(string message, TwitterSettings settings);
}
public class TwitterProxy : ITwitterProxy
{
private readonly IHttpClient _httpClient;
public TwitterProxy(IHttpClient httpClient)
{
_httpClient = httpClient;
}
public string GetOAuthRedirect(string consumerKey, string consumerSecret, string callbackUrl)
{
// Creating a new instance with a helper method
var oAuthRequest = OAuthRequest.ForRequestToken(consumerKey, consumerSecret, callbackUrl);
oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/request_token";
var qscoll = HttpUtility.ParseQueryString(ExecuteRequest(GetRequest(oAuthRequest, new Dictionary<string, string>())).Content);
return string.Format("https://api.twitter.com/oauth/authorize?oauth_token={0}", qscoll["oauth_token"]);
}
public NameValueCollection GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier)
{
// Creating a new instance with a helper method
var oAuthRequest = OAuthRequest.ForAccessToken(consumerKey, consumerSecret, oauthToken, "", oauthVerifier);
oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/access_token";
return HttpUtility.ParseQueryString(ExecuteRequest(GetRequest(oAuthRequest, new Dictionary<string, string>())).Content);
}
public void UpdateStatus(string message, TwitterSettings settings)
{
var oAuthRequest = OAuthRequest.ForProtectedResource("POST", settings.ConsumerKey, settings.ConsumerSecret, settings.AccessToken, settings.AccessTokenSecret);
oAuthRequest.RequestUrl = "https://api.twitter.com/1.1/statuses/update.json";
var customParams = new Dictionary<string, string>
{
{ "status", message.EncodeRFC3986() }
};
var request = GetRequest(oAuthRequest, customParams);
request.Headers.ContentType = "application/x-www-form-urlencoded";
request.SetContent(Encoding.ASCII.GetBytes(GetCustomParametersString(customParams)));
ExecuteRequest(request);
}
public void DirectMessage(string message, TwitterSettings settings)
{
var oAuthRequest = OAuthRequest.ForProtectedResource("POST", settings.ConsumerKey, settings.ConsumerSecret, settings.AccessToken, settings.AccessTokenSecret);
oAuthRequest.RequestUrl = "https://api.twitter.com/1.1/direct_messages/new.json";
var customParams = new Dictionary<string, string>
{
{ "text", message.EncodeRFC3986() },
{ "screenname", settings.Mention.EncodeRFC3986() }
};
var request = GetRequest(oAuthRequest, customParams);
request.Headers.ContentType = "application/x-www-form-urlencoded";
request.SetContent(Encoding.ASCII.GetBytes(GetCustomParametersString(customParams)));
ExecuteRequest(request);
}
private string GetCustomParametersString(Dictionary<string, string> customParams)
{
return customParams.Select(x => string.Format("{0}={1}", x.Key, x.Value)).Join("&");
}
private HttpRequest GetRequest(OAuthRequest oAuthRequest, Dictionary<string, string> customParams)
{
var auth = oAuthRequest.GetAuthorizationHeader(customParams);
var request = new HttpRequest(oAuthRequest.RequestUrl);
request.Headers.Add("Authorization", auth);
request.Method = oAuthRequest.Method == "POST" ? HttpMethod.Post : HttpMethod.Get;
return request;
}
private HttpResponse ExecuteRequest(HttpRequest request)
{
return _httpClient.Execute(request);
}
}
}

View File

@@ -1,13 +1,9 @@
using System;
using System.Collections.Specialized;
using System;
using System.IO;
using System.Net;
using System.Web;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.OAuth;
namespace NzbDrone.Core.Notifications.Twitter
{
@@ -21,31 +17,18 @@ namespace NzbDrone.Core.Notifications.Twitter
public class TwitterService : ITwitterService
{
private readonly IHttpClient _httpClient;
private readonly ITwitterProxy _twitterProxy;
private readonly Logger _logger;
public TwitterService(IHttpClient httpClient, Logger logger)
public TwitterService(ITwitterProxy twitterProxy, Logger logger)
{
_httpClient = httpClient;
_twitterProxy = twitterProxy;
_logger = logger;
}
private NameValueCollection OAuthQuery(OAuthRequest oAuthRequest)
{
var auth = oAuthRequest.GetAuthorizationHeader();
var request = new Common.Http.HttpRequest(oAuthRequest.RequestUrl);
request.Headers.Add("Authorization", auth);
var response = _httpClient.Get(request);
return HttpUtility.ParseQueryString(response.Content);
}
public OAuthToken GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier)
{
// Creating a new instance with a helper method
var oAuthRequest = OAuthRequest.ForAccessToken(consumerKey, consumerSecret, oauthToken, "", oauthVerifier);
oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/access_token";
var qscoll = OAuthQuery(oAuthRequest);
var qscoll = _twitterProxy.GetOAuthToken(consumerKey, consumerSecret, oauthToken, oauthVerifier);
return new OAuthToken
{
@@ -56,31 +39,16 @@ namespace NzbDrone.Core.Notifications.Twitter
public string GetOAuthRedirect(string consumerKey, string consumerSecret, string callbackUrl)
{
// Creating a new instance with a helper method
var oAuthRequest = OAuthRequest.ForRequestToken(consumerKey, consumerSecret, callbackUrl);
oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/request_token";
var qscoll = OAuthQuery(oAuthRequest);
return string.Format("https://api.twitter.com/oauth/authorize?oauth_token={0}", qscoll["oauth_token"]);
return _twitterProxy.GetOAuthRedirect(consumerKey, consumerSecret, callbackUrl);
}
public void SendNotification(string message, TwitterSettings settings)
{
try
{
var oAuth = new TinyTwitter.OAuthInfo
{
ConsumerKey = settings.ConsumerKey,
ConsumerSecret = settings.ConsumerSecret,
AccessToken = settings.AccessToken,
AccessSecret = settings.AccessTokenSecret
};
var twitter = new TinyTwitter.TinyTwitter(oAuth);
if (settings.DirectMessage)
{
twitter.DirectMessage(message, settings.Mention);
_twitterProxy.DirectMessage(message, settings);
}
else
{
@@ -89,7 +57,7 @@ namespace NzbDrone.Core.Notifications.Twitter
message += string.Format(" @{0}", settings.Mention);
}
twitter.UpdateStatus(message);
_twitterProxy.UpdateStatus(message, settings);
}
}
catch (WebException ex)

View File

@@ -40,7 +40,7 @@ namespace NzbDrone.Core.Notifications.Webhook
if (settings.Username.IsNotNullOrWhiteSpace() || settings.Password.IsNotNullOrWhiteSpace())
{
request.AddBasicAuthentication(settings.Username, settings.Password);
request.Credentials = new BasicNetworkCredential(settings.Username, settings.Password);
}
_httpClient.Execute(request);

View File

@@ -84,7 +84,7 @@ namespace NzbDrone.Core.Notifications.Xbmc
if (!settings.Username.IsNullOrWhiteSpace())
{
request.AddBasicAuthentication(settings.Username, settings.Password);
request.Credentials = new BasicNetworkCredential(settings.Username, settings.Password);
}
var response = _httpClient.Execute(request);

View File

@@ -355,7 +355,7 @@ namespace NzbDrone.Core.Organizer
new Dictionary<string, int>(FileNameBuilderTokenEqualityComparer.Instance)
{
{ MediaInfoVideoDynamicRangeToken, 5 },
{ MediaInfoVideoDynamicRangeTypeToken, 9 }
{ MediaInfoVideoDynamicRangeTypeToken, 10 }
};
private void AddMediaInfoTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, MovieFile movieFile)

View File

@@ -40,7 +40,7 @@ namespace NzbDrone.Core.Parser
new IsoLanguage("ro", "", "ron", "Romanian", Language.Romanian),
new IsoLanguage("pt", "br", "", "Portuguese (Brazil)", Language.PortugueseBR),
new IsoLanguage("ar", "", "ara", "Arabic", Language.Arabic),
new IsoLanguage("uk", "", "uar", "Ukrainian", Language.Ukrainian),
new IsoLanguage("uk", "", "ukr", "Ukrainian", Language.Ukrainian),
new IsoLanguage("fa", "", "fas", "Persian", Language.Persian),
new IsoLanguage("be", "", "ben", "Bengali", Language.Bengali)
};

View File

@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Parser
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(QualityParser));
private static readonly Regex SourceRegex = new Regex(@"\b(?:
(?<bluray>M?BluRay|Blu-Ray|HDDVD|BD(?!$)|UHDBD|BDISO|BDMux|BD25|BD50|BR.?DISK)|
(?<bluray>M?BluRay|Blu-Ray|HD.?DVD|BD(?!$)|UHDBD|BDISO|BDMux|BD25|BD50|BR.?DISK)|
(?<webdl>WEB[-_. ]DL(?:mux)?|WEBDL|AmazonHD|iTunesHD|MaxdomeHD|NetflixU?HD|WebHD|[. ]WEB[. ](?:[xh]26[45]|DDP?5[. ]1)|[. ](?-i:WEB)$|(?:\d{3,4}0p)[-. ]WEB[-. ]|[-. ]WEB[-. ]\d{3,4}0p|\b\s\/\sWEB\s\/\s\b|AMZN[. -]WEB[. -]|NF[. ]WEB[. ])|
(?<webrip>WebRip|Web-Rip|WEBMux)|
(?<hdtv>HDTV)|

View File

@@ -5,6 +5,7 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="MailKit" Version="2.15.0" />
<PackageReference Include="Servarr.FFMpegCore" Version="4.5.0-25" />
<PackageReference Include="Servarr.FFprobe" Version="4.4.1.63" />
<PackageReference Include="System.Memory" Version="4.5.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
@@ -16,11 +17,9 @@
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="4.7.12" />
<PackageReference Include="Kveer.XmlRPC" Version="1.2.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Text.Json" Version="6.0.0" />
<PackageReference Include="System.Text.Json" Version="6.0.1" />
<PackageReference Include="MonoTorrent" Version="2.0.1" />
<PackageReference Include="FFMpegCore" Version="4.6.16" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Common\Radarr.Common.csproj" />

View File

@@ -22,14 +22,27 @@ namespace NzbDrone.Core.Security
public bool ShouldByPassValidationError(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
if (sender is not SslStream request)
var targetHostName = string.Empty;
if (sender is not SslStream && sender is not string)
{
return true;
}
if (sender is SslStream request)
{
targetHostName = request.TargetHostName;
}
// Mailkit passes host in sender as string
if (sender is string stringHost)
{
targetHostName = stringHost;
}
if (certificate is X509Certificate2 cert2 && cert2.SignatureAlgorithm.FriendlyName == "md5RSA")
{
_logger.Error("https://{0} uses the obsolete md5 hash in it's https certificate, if that is your certificate, please (re)create certificate with better algorithm as soon as possible.", request.TargetHostName);
_logger.Error("https://{0} uses the obsolete md5 hash in it's https certificate, if that is your certificate, please (re)create certificate with better algorithm as soon as possible.", targetHostName);
}
if (sslPolicyErrors == SslPolicyErrors.None)
@@ -37,12 +50,12 @@ namespace NzbDrone.Core.Security
return true;
}
if (request.TargetHostName == "localhost" || request.TargetHostName == "127.0.0.1")
if (targetHostName == "localhost" || targetHostName == "127.0.0.1")
{
return true;
}
var ipAddresses = GetIPAddresses(request.TargetHostName);
var ipAddresses = GetIPAddresses(targetHostName);
var certificateValidation = _configService.CertificateValidation;
if (certificateValidation == CertificateValidationType.Disabled)
@@ -56,7 +69,7 @@ namespace NzbDrone.Core.Security
return true;
}
_logger.Error("Certificate validation for {0} failed. {1}", request.TargetHostName, sslPolicyErrors);
_logger.Error("Certificate validation for {0} failed. {1}", targetHostName, sslPolicyErrors);
return false;
}

View File

@@ -1,190 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
namespace TinyTwitter
{
public class OAuthInfo
{
public string ConsumerKey { get; set; }
public string ConsumerSecret { get; set; }
public string AccessToken { get; set; }
public string AccessSecret { get; set; }
}
public class Tweet
{
public long Id { get; set; }
public DateTime CreatedAt { get; set; }
public string UserName { get; set; }
public string ScreenName { get; set; }
public string Text { get; set; }
}
public class TinyTwitter
{
private readonly OAuthInfo _oauth;
public TinyTwitter(OAuthInfo oauth)
{
_oauth = oauth;
}
public void UpdateStatus(string message)
{
new RequestBuilder(_oauth, HttpMethod.Post, "https://api.twitter.com/1.1/statuses/update.json")
.AddParameter("status", message)
.Execute();
}
/**
*
* As of June 26th 2015 Direct Messaging is not part of TinyTwitter.
* I have added it to Sonarr's copy to make our implementation easier
* and added this banner so it's not blindly updated.
*
**/
public void DirectMessage(string message, string screenName)
{
new RequestBuilder(_oauth, HttpMethod.Post, "https://api.twitter.com/1.1/direct_messages/new.json")
.AddParameter("text", message)
.AddParameter("screen_name", screenName)
.Execute();
}
public class RequestBuilder
{
private const string VERSION = "1.0";
private const string SIGNATURE_METHOD = "HMAC-SHA1";
private readonly OAuthInfo _oauth;
private readonly HttpMethod _method;
private readonly IDictionary<string, string> _customParameters;
private readonly string _url;
private readonly HttpClient _httpClient;
public RequestBuilder(OAuthInfo oauth, HttpMethod method, string url)
{
_oauth = oauth;
_method = method;
_url = url;
_customParameters = new Dictionary<string, string>();
_httpClient = new ();
}
public RequestBuilder AddParameter(string name, string value)
{
_customParameters.Add(name, value.EncodeRFC3986());
return this;
}
public string Execute()
{
var timespan = GetTimestamp();
var nonce = CreateNonce();
var parameters = new Dictionary<string, string>(_customParameters);
AddOAuthParameters(parameters, timespan, nonce);
var signature = GenerateSignature(parameters);
var headerValue = GenerateAuthorizationHeaderValue(parameters, signature);
var request = new HttpRequestMessage(_method, _url);
request.Content = new FormUrlEncodedContent(_customParameters);
request.Headers.Add("Authorization", headerValue);
var response = _httpClient.Send(request);
return response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
}
private string GenerateAuthorizationHeaderValue(IEnumerable<KeyValuePair<string, string>> parameters, string signature)
{
return new StringBuilder("OAuth ")
.Append(parameters.Concat(new KeyValuePair<string, string>("oauth_signature", signature))
.Where(x => x.Key.StartsWith("oauth_"))
.Select(x => string.Format("{0}=\"{1}\"", x.Key, x.Value.EncodeRFC3986()))
.Join(","))
.ToString();
}
private string GenerateSignature(IEnumerable<KeyValuePair<string, string>> parameters)
{
var dataToSign = new StringBuilder()
.Append(_method).Append('&')
.Append(_url.EncodeRFC3986()).Append('&')
.Append(parameters
.OrderBy(x => x.Key)
.Select(x => string.Format("{0}={1}", x.Key, x.Value))
.Join("&")
.EncodeRFC3986());
var signatureKey = string.Format("{0}&{1}", _oauth.ConsumerSecret.EncodeRFC3986(), _oauth.AccessSecret.EncodeRFC3986());
var sha1 = new HMACSHA1(Encoding.ASCII.GetBytes(signatureKey));
var signatureBytes = sha1.ComputeHash(Encoding.ASCII.GetBytes(dataToSign.ToString()));
return Convert.ToBase64String(signatureBytes);
}
private void AddOAuthParameters(IDictionary<string, string> parameters, string timestamp, string nonce)
{
parameters.Add("oauth_version", VERSION);
parameters.Add("oauth_consumer_key", _oauth.ConsumerKey);
parameters.Add("oauth_nonce", nonce);
parameters.Add("oauth_signature_method", SIGNATURE_METHOD);
parameters.Add("oauth_timestamp", timestamp);
parameters.Add("oauth_token", _oauth.AccessToken);
}
private static string GetTimestamp()
{
return ((int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds).ToString();
}
private static string CreateNonce()
{
return new Random().Next(0x0000000, 0x7fffffff).ToString("X8");
}
}
}
public static class TinyTwitterHelperExtensions
{
public static string Join<T>(this IEnumerable<T> items, string separator)
{
return string.Join(separator, items.ToArray());
}
public static IEnumerable<T> Concat<T>(this IEnumerable<T> items, T value)
{
return items.Concat(new[] { value });
}
public static string EncodeRFC3986(this string value)
{
// From Twitterizer http://www.twitterizer.net/
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
var encoded = Uri.EscapeDataString(value);
return Regex
.Replace(encoded, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper())
.Replace("(", "%28")
.Replace(")", "%29")
.Replace("$", "%24")
.Replace("!", "%21")
.Replace("*", "%2A")
.Replace("'", "%27")
.Replace("%7E", "~");
}
}
}

View File

@@ -4,7 +4,7 @@
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Radarr.Test.Common.csproj" />

View File

@@ -58,6 +58,7 @@ namespace Radarr.Api.V3.System
{
return new
{
AppName = BuildInfo.AppName,
Version = BuildInfo.Version.ToString(),
BuildTime = BuildInfo.BuildDateTime,
IsDebug = BuildInfo.IsDebug,

View File

@@ -1081,10 +1081,10 @@
dependencies:
prop-types "^15.7.2"
"@microsoft/signalr@6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@microsoft/signalr/-/signalr-6.0.0.tgz#cb9cb166d8ee0522547e13c100a0992a1f027ce1"
integrity sha512-Y38bG/i9V1fOfOLcfTl2+OqPH7+0A7DgojJ7YILgfQ0vGGmnZwdCtrzCvSsRIzKTIrB/ZSQ3n4BA5VOO+MFkrA==
"@microsoft/signalr@6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/@microsoft/signalr/-/signalr-6.0.1.tgz#89eeacabb558cfc90c546f8bf165ea34a1f91c28"
integrity sha512-witYtScUxPxl1QA69AsuVIsn54S4Rcfym3c/ON2bsA0ZWHcvskA0dUnOX9JCxOduGMEGwkMprKFfVTxUO/inzQ==
dependencies:
abort-controller "^3.0.0"
eventsource "^1.0.7"