Compare commits

..

19 Commits

Author SHA1 Message Date
Qstick
aa52f9fc6d Simplify X-Forwarded-For handling
This happens in asp.net middleware now

Co-Authored-By: ta264 <ta264@users.noreply.github.com>

(cherry picked from commit 16e2d130e6a2e7239bcfe92187a7f990f93eff00)
2022-12-08 05:09:36 +00:00
Qstick
da5e35fc25 Unbork UI Build 2022-11-28 22:11:13 -06:00
Mark McDowall
a6a2219bc4 Fixed: Handle Flood reporting errors for completed and stopped downloads
(cherry picked from commit f2b2eb69a3e8b271535bd92dc2f5cbfd45664daf)
2022-11-28 21:25:03 -06:00
Qstick
aa2855a62b Re-saving edited providers will forcibly save them
Fixes #533

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2022-11-27 19:32:14 -06:00
Qstick
599f52e72f Fixed: Validation when testing indexers, import lists, connections and download clients
Fixes #1612

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2022-11-27 19:10:12 -06:00
Qstick
c5c2b94b9a Fixup Repack Tests 2022-11-25 22:16:34 -06:00
Qstick
b016b36435 Fixed: Validate if equals or child for startup folder
Fixes #1712
Close #1713

(cherry picked from commit 0991cfe27efd6ddb533227b25754661e18d7e9ad)
2022-11-25 22:07:42 -06:00
Qstick
d93a0c27e9 Sentry logging exceptions
Fixes #855

Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
2022-11-25 21:58:58 -06:00
Qstick
eecf08e063 Updated NLog Version
Co-Authored-By: Robin Dadswell <19610103+RobinDadswell@users.noreply.github.com>
2022-11-25 21:52:52 -06:00
Qstick
fb1643f630 Lint Fixes 2022-11-25 21:39:44 -06:00
bakerboy448
a061179f6b Fixed: Repack Preference Ignored
Fixes #1867

(cherry picked from commit 04447d9d4db8cc3d54baf0a721f4430cf77908c4)
2022-11-25 21:39:44 -06:00
Stevie Robinson
b441f6c05b Fixed: updated rTorrent download client note
Fixes #1882

(cherry picked from commit 743d28b93a55553ee25381570d0daa04ed2117af)
2022-11-25 21:33:35 -06:00
Chromo-residuum-opec
0904eac300 Update help text for rTorrent download client options
Fixes #1868

(cherry picked from commit d2a23f7bcdf71800f019644d7b6b5d712e311d7f)
2022-11-25 21:31:57 -06:00
Qstick
29e3d8f477 Fixed: Fall back to sorting by release title if artist is not matched
Fixes #1870
2022-11-25 21:29:38 -06:00
Qstick
71bd88e531 New: Add Release group to history for all events
Fixes #1499

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2022-11-25 21:28:05 -06:00
Stevie Robinson
0689bec779 New: Add optional Source Title column to history
(cherry picked from commit 581fb2cb3d47d62fe16b840081647056ec77043d)
2022-11-25 21:16:37 -06:00
Devin Buhl
117a5c8010 New: Add application URL to host configuration settings
Fixes #1816
Closes #1817

(cherry picked from commit 762042ba97c2ae689cee32d8e66a458f6d7a8adc)
2022-11-25 21:16:37 -06:00
Qstick
d10d91439f New: Add indexer name to the download report log
Fixes #1904
2022-11-25 21:16:37 -06:00
Mark McDowall
ed0722bae4 New: Validate that naming formats don't contain illegal characters
Fixes #620

(cherry picked from commit 145c644c9d8f1636027da8a782a7e74f3182c678)
2022-11-25 21:16:37 -06:00
39 changed files with 343 additions and 194 deletions

View File

@@ -169,6 +169,16 @@ class HistoryRow extends Component {
); );
} }
if (name === 'sourceTitle') {
return (
<TableRowCell
key={name}
>
{sourceTitle}
</TableRowCell>
);
}
if (name === 'details') { if (name === 'details') {
return ( return (
<TableRowCell <TableRowCell

View File

@@ -21,6 +21,7 @@ function HostSettings(props) {
port, port,
urlBase, urlBase,
instanceName, instanceName,
applicationUrl,
enableSsl, enableSsl,
sslPort, sslPort,
sslCertPath, sslCertPath,
@@ -96,6 +97,21 @@ function HostSettings(props) {
/> />
</FormGroup> </FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ApplicationURL')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="applicationUrl"
helpText={translate('ApplicationUrlHelpText')}
onChange={onInputChange}
{...applicationUrl}
/>
</FormGroup>
<FormGroup <FormGroup
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}

View File

@@ -6,6 +6,7 @@ import getProviderState from 'Utilities/State/getProviderState';
import { removeItem, set, updateItem } from '../baseActions'; import { removeItem, set, updateItem } from '../baseActions';
const abortCurrentRequests = {}; const abortCurrentRequests = {};
let lastSaveData = null;
export function createCancelSaveProviderHandler(section) { export function createCancelSaveProviderHandler(section) {
return function(getState, payload, dispatch) { return function(getState, payload, dispatch) {
@@ -27,27 +28,33 @@ function createSaveProviderHandler(section, url, options = {}, removeStale = fal
} = payload; } = payload;
const saveData = Array.isArray(id) ? id.map((x) => getProviderState({ id: x, ...otherPayload }, getState, section)) : getProviderState({ id, ...otherPayload }, getState, section); const saveData = Array.isArray(id) ? id.map((x) => getProviderState({ id: x, ...otherPayload }, getState, section)) : getProviderState({ id, ...otherPayload }, getState, section);
const requestUrl = id ? `${url}/${id}` : url;
const params = { ...queryParams };
// If the user is re-saving the same provider without changes
// force it to be saved. Only applies to editing existing providers.
if (id && _.isEqual(saveData, lastSaveData)) {
params.forceSave = true;
}
lastSaveData = saveData;
const ajaxOptions = { const ajaxOptions = {
url: `${url}?${$.param(queryParams, true)}`, url: `${requestUrl}?${$.param(params, true)}`,
method: 'POST', method: id ? 'PUT' : 'POST',
contentType: 'application/json', contentType: 'application/json',
dataType: 'json', dataType: 'json',
data: JSON.stringify(saveData) data: JSON.stringify(saveData)
}; };
if (id) {
ajaxOptions.method = 'PUT';
if (!Array.isArray(id)) {
ajaxOptions.url = `${url}/${id}?${$.param(queryParams, true)}`;
}
}
const { request, abortRequest } = createAjaxRequest(ajaxOptions); const { request, abortRequest } = createAjaxRequest(ajaxOptions);
abortCurrentRequests[section] = abortRequest; abortCurrentRequests[section] = abortRequest;
request.done((data) => { request.done((data) => {
lastSaveData = null;
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
data = [data]; data = [data];
} }

View File

@@ -71,6 +71,11 @@ export const defaultState = {
label: 'Release Group', label: 'Release Group',
isVisible: false isVisible: false
}, },
{
name: 'sourceTitle',
label: 'Source Title',
isVisible: false
},
{ {
name: 'details', name: 'details',
columnLabel: 'Details', columnLabel: 'Details',

View File

@@ -29,9 +29,9 @@
<PackageVersion Include="MonoTorrent" Version="2.0.6" /> <PackageVersion Include="MonoTorrent" Version="2.0.6" />
<PackageVersion Include="NBuilder" Version="6.1.0" /> <PackageVersion Include="NBuilder" Version="6.1.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.1" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
<PackageVersion Include="NLog.Extensions.Logging" Version="1.7.4" /> <PackageVersion Include="NLog.Extensions.Logging" Version="5.1.0" />
<PackageVersion Include="NLog" Version="4.7.14" /> <PackageVersion Include="NLog" Version="5.0.5" />
<PackageVersion Include="NLog.Targets.Syslog" Version="6.0.2" /> <PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageVersion Include="Npgsql" Version="6.0.3" /> <PackageVersion Include="Npgsql" Version="6.0.3" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" /> <PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageVersion Include="NUnit" Version="3.13.3" /> <PackageVersion Include="NUnit" Version="3.13.3" />

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
@@ -24,7 +24,8 @@ namespace NzbDrone.Common.Disk
"/boot", "/boot",
"/lib", "/lib",
"/sbin", "/sbin",
"/proc" "/proc",
"/usr/bin"
}; };
} }
} }

View File

@@ -1,6 +1,6 @@
using System.Linq; using System.Collections.Generic;
using System.Linq;
using NLog; using NLog;
using NLog.Fluent;
namespace NzbDrone.Common.Instrumentation.Extensions namespace NzbDrone.Common.Instrumentation.Extensions
{ {
@@ -8,47 +8,46 @@ namespace NzbDrone.Common.Instrumentation.Extensions
{ {
public static readonly Logger SentryLogger = LogManager.GetLogger("Sentry"); public static readonly Logger SentryLogger = LogManager.GetLogger("Sentry");
public static LogBuilder SentryFingerprint(this LogBuilder logBuilder, params string[] fingerprint) public static LogEventBuilder SentryFingerprint(this LogEventBuilder logBuilder, params string[] fingerprint)
{ {
return logBuilder.Property("Sentry", fingerprint); return logBuilder.Property("Sentry", fingerprint);
} }
public static LogBuilder WriteSentryDebug(this LogBuilder logBuilder, params string[] fingerprint) public static LogEventBuilder WriteSentryDebug(this LogEventBuilder logBuilder, params string[] fingerprint)
{ {
return LogSentryMessage(logBuilder, LogLevel.Debug, fingerprint); return LogSentryMessage(logBuilder, LogLevel.Debug, fingerprint);
} }
public static LogBuilder WriteSentryInfo(this LogBuilder logBuilder, params string[] fingerprint) public static LogEventBuilder WriteSentryInfo(this LogEventBuilder logBuilder, params string[] fingerprint)
{ {
return LogSentryMessage(logBuilder, LogLevel.Info, fingerprint); return LogSentryMessage(logBuilder, LogLevel.Info, fingerprint);
} }
public static LogBuilder WriteSentryWarn(this LogBuilder logBuilder, params string[] fingerprint) public static LogEventBuilder WriteSentryWarn(this LogEventBuilder logBuilder, params string[] fingerprint)
{ {
return LogSentryMessage(logBuilder, LogLevel.Warn, fingerprint); return LogSentryMessage(logBuilder, LogLevel.Warn, fingerprint);
} }
public static LogBuilder WriteSentryError(this LogBuilder logBuilder, params string[] fingerprint) public static LogEventBuilder WriteSentryError(this LogEventBuilder logBuilder, params string[] fingerprint)
{ {
return LogSentryMessage(logBuilder, LogLevel.Error, fingerprint); return LogSentryMessage(logBuilder, LogLevel.Error, fingerprint);
} }
private static LogBuilder LogSentryMessage(LogBuilder logBuilder, LogLevel level, string[] fingerprint) private static LogEventBuilder LogSentryMessage(LogEventBuilder logBuilder, LogLevel level, string[] fingerprint)
{ {
SentryLogger.Log(level) SentryLogger.ForLogEvent(level)
.CopyLogEvent(logBuilder.LogEventInfo) .CopyLogEvent(logBuilder.LogEvent)
.SentryFingerprint(fingerprint) .SentryFingerprint(fingerprint)
.Write(); .Log();
return logBuilder.Property("Sentry", null); return logBuilder.Property<string>("Sentry", null);
} }
private static LogBuilder CopyLogEvent(this LogBuilder logBuilder, LogEventInfo logEvent) private static LogEventBuilder CopyLogEvent(this LogEventBuilder logBuilder, LogEventInfo logEvent)
{ {
return logBuilder.LoggerName(logEvent.LoggerName) return logBuilder.TimeStamp(logEvent.TimeStamp)
.TimeStamp(logEvent.TimeStamp)
.Message(logEvent.Message, logEvent.Parameters) .Message(logEvent.Message, logEvent.Parameters)
.Properties(logEvent.Properties.ToDictionary(v => v.Key, v => v.Value)) .Properties(logEvent.Properties.Select(p => new KeyValuePair<string, object>(p.Key.ToString(), p.Value)))
.Exception(logEvent.Exception); .Exception(logEvent.Exception);
} }
} }

View File

@@ -1,13 +1,15 @@
using NLog; using System.Text;
using NLog;
using NLog.Targets; using NLog.Targets;
namespace NzbDrone.Common.Instrumentation namespace NzbDrone.Common.Instrumentation
{ {
public class NzbDroneFileTarget : FileTarget public class NzbDroneFileTarget : FileTarget
{ {
protected override string GetFormattedMessage(LogEventInfo logEvent) protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
{ {
return CleanseLogMessage.Cleanse(Layout.Render(logEvent)); var result = CleanseLogMessage.Cleanse(Layout.Render(logEvent));
target.Append(result);
} }
} }
} }

View File

@@ -34,6 +34,8 @@ namespace NzbDrone.Common.Instrumentation
var appFolderInfo = new AppFolderInfo(startupContext); var appFolderInfo = new AppFolderInfo(startupContext);
RegisterGlobalFilters();
if (Debugger.IsAttached) if (Debugger.IsAttached)
{ {
RegisterDebugger(); RegisterDebugger();
@@ -101,6 +103,16 @@ namespace NzbDrone.Common.Instrumentation
LogManager.Configuration.LoggingRules.Add(loggingRule); LogManager.Configuration.LoggingRules.Add(loggingRule);
} }
private static void RegisterGlobalFilters()
{
LogManager.Setup().LoadConfiguration(c =>
{
c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info);
c.ForLogger("System*").WriteToNil(LogLevel.Warn);
c.ForLogger("Microsoft*").WriteToNil(LogLevel.Warn);
});
}
private static void RegisterConsole() private static void RegisterConsole()
{ {
var level = LogLevel.Trace; var level = LogLevel.Trace;

View File

@@ -206,7 +206,11 @@ namespace NzbDrone.Common.Instrumentation.Sentry
if (ex != null) if (ex != null)
{ {
fingerPrint.Add(ex.GetType().FullName); fingerPrint.Add(ex.GetType().FullName);
fingerPrint.Add(ex.TargetSite.ToString()); if (ex.TargetSite != null)
{
fingerPrint.Add(ex.TargetSite.ToString());
}
if (ex.InnerException != null) if (ex.InnerException != null)
{ {
fingerPrint.Add(ex.InnerException.GetType().FullName); fingerPrint.Add(ex.InnerException.GetType().FullName);

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using FizzWare.NBuilder; using FizzWare.NBuilder;
@@ -5,6 +6,7 @@ using FluentAssertions;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Books; using NzbDrone.Core.Books;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
@@ -244,5 +246,89 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
.Should() .Should()
.BeFalse(); .BeFalse();
} }
[Test]
public void should_return_true_when_repacks_are_not_preferred()
{
Mocker.GetMock<IConfigService>()
.Setup(s => s.DownloadPropersAndRepacks)
.Returns(ProperDownloadTypes.DoNotPrefer);
_trackFiles.Select(c =>
{
c.ReleaseGroup = "";
return c;
}).ToList();
_trackFiles.Select(c =>
{
c.Quality = new QualityModel(Quality.FLAC);
return c;
}).ToList();
var remoteAlbum = Builder<RemoteBook>.CreateNew()
.With(e => e.ParsedBookInfo = _parsedBookInfo)
.With(e => e.Books = _books)
.Build();
Subject.IsSatisfiedBy(remoteAlbum, null).Accepted.Should().BeTrue();
}
[Test]
public void should_return_true_when_repack_but_auto_download_repacks_is_true()
{
Mocker.GetMock<IConfigService>()
.Setup(s => s.DownloadPropersAndRepacks)
.Returns(ProperDownloadTypes.PreferAndUpgrade);
_parsedBookInfo.Quality.Revision.IsRepack = true;
_trackFiles.Select(c =>
{
c.ReleaseGroup = "Readarr";
return c;
}).ToList();
_trackFiles.Select(c =>
{
c.Quality = new QualityModel(Quality.FLAC);
return c;
}).ToList();
var remoteAlbum = Builder<RemoteBook>.CreateNew()
.With(e => e.ParsedBookInfo = _parsedBookInfo)
.With(e => e.Books = _books)
.Build();
Subject.IsSatisfiedBy(remoteAlbum, null).Accepted.Should().BeTrue();
}
[Test]
public void should_return_false_when_repack_but_auto_download_repacks_is_false()
{
Mocker.GetMock<IConfigService>()
.Setup(s => s.DownloadPropersAndRepacks)
.Returns(ProperDownloadTypes.DoNotUpgrade);
_parsedBookInfo.Quality.Revision.IsRepack = true;
_trackFiles.Select(c =>
{
c.ReleaseGroup = "Readarr";
return c;
}).ToList();
_trackFiles.Select(c =>
{
c.Quality = new QualityModel(Quality.FLAC);
return c;
}).ToList();
var remoteAlbum = Builder<RemoteBook>.CreateNew()
.With(e => e.ParsedBookInfo = _parsedBookInfo)
.With(e => e.Books = _books)
.Build();
Subject.IsSatisfiedBy(remoteAlbum, null).Accepted.Should().BeFalse();
}
} }
} }

View File

@@ -410,6 +410,8 @@ namespace NzbDrone.Core.Configuration
public CertificateValidationType CertificateValidation => public CertificateValidationType CertificateValidation =>
GetValueEnum("CertificateValidation", CertificateValidationType.Enabled); GetValueEnum("CertificateValidation", CertificateValidationType.Enabled);
public string ApplicationUrl => GetValue("ApplicationUrl", string.Empty);
private string GetValue(string key) private string GetValue(string key)
{ {
return GetValue(key, string.Empty); return GetValue(key, string.Empty);

View File

@@ -97,5 +97,6 @@ namespace NzbDrone.Core.Configuration
int BackupRetention { get; } int BackupRetention { get; }
CertificateValidationType CertificateValidation { get; } CertificateValidationType CertificateValidation { get; }
string ApplicationUrl { get; }
} }
} }

View File

@@ -1,9 +1,11 @@
using System; using System;
using NLog; using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.DecisionEngine.Specifications namespace NzbDrone.Core.DecisionEngine.Specifications
{ {
@@ -11,12 +13,14 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
{ {
private readonly IMediaFileService _mediaFileService; private readonly IMediaFileService _mediaFileService;
private readonly UpgradableSpecification _upgradableSpecification; private readonly UpgradableSpecification _upgradableSpecification;
private readonly IConfigService _configService;
private readonly Logger _logger; private readonly Logger _logger;
public RepackSpecification(IMediaFileService mediaFileService, UpgradableSpecification upgradableSpecification, Logger logger) public RepackSpecification(IMediaFileService mediaFileService, UpgradableSpecification upgradableSpecification, IConfigService configService, Logger logger)
{ {
_mediaFileService = mediaFileService; _mediaFileService = mediaFileService;
_upgradableSpecification = upgradableSpecification; _upgradableSpecification = upgradableSpecification;
_configService = configService;
_logger = logger; _logger = logger;
} }
@@ -30,6 +34,14 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
return Decision.Accept(); return Decision.Accept();
} }
var downloadPropersAndRepacks = _configService.DownloadPropersAndRepacks;
if (downloadPropersAndRepacks == ProperDownloadTypes.DoNotPrefer)
{
_logger.Debug("Repacks are not preferred, skipping check");
return Decision.Accept();
}
foreach (var book in subject.Books) foreach (var book in subject.Books)
{ {
var releaseGroup = subject.ParsedBookInfo.ReleaseGroup; var releaseGroup = subject.ParsedBookInfo.ReleaseGroup;
@@ -39,6 +51,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
{ {
if (_upgradableSpecification.IsRevisionUpgrade(file.Quality, subject.ParsedBookInfo.Quality)) if (_upgradableSpecification.IsRevisionUpgrade(file.Quality, subject.ParsedBookInfo.Quality))
{ {
if (downloadPropersAndRepacks == ProperDownloadTypes.DoNotUpgrade)
{
_logger.Debug("Auto downloading of repacks is disabled");
return Decision.Reject("Repack downloading is disabled");
}
var fileReleaseGroup = file.ReleaseGroup; var fileReleaseGroup = file.ReleaseGroup;
if (fileReleaseGroup.IsNullOrWhiteSpace()) if (fileReleaseGroup.IsNullOrWhiteSpace())

View File

@@ -125,22 +125,22 @@ namespace NzbDrone.Core.Download.Clients.Flood
item.RemainingTime = TimeSpan.FromSeconds(properties.Eta); item.RemainingTime = TimeSpan.FromSeconds(properties.Eta);
} }
if (properties.Status.Contains("error")) if (properties.Status.Contains("seeding") || properties.Status.Contains("complete"))
{
item.Status = DownloadItemStatus.Warning;
}
else if (properties.Status.Contains("seeding") || properties.Status.Contains("complete"))
{ {
item.Status = DownloadItemStatus.Completed; item.Status = DownloadItemStatus.Completed;
} }
else if (properties.Status.Contains("downloading"))
{
item.Status = DownloadItemStatus.Downloading;
}
else if (properties.Status.Contains("stopped")) else if (properties.Status.Contains("stopped"))
{ {
item.Status = DownloadItemStatus.Paused; item.Status = DownloadItemStatus.Paused;
} }
else if (properties.Status.Contains("error"))
{
item.Status = DownloadItemStatus.Warning;
}
else if (properties.Status.Contains("downloading"))
{
item.Status = DownloadItemStatus.Downloading;
}
if (item.Status == DownloadItemStatus.Completed) if (item.Status == DownloadItemStatus.Completed)
{ {

View File

@@ -115,7 +115,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public override string Name => "rTorrent"; public override string Name => "rTorrent";
public override ProviderMessage Message => new ProviderMessage($"Readarr will handle automatic removal of torrents based on the current seed criteria in Settings->Indexers. After importing it will also set \"{_imported_view}\" as an rTorrent view, which can be used in rTorrent scripts to customize behavior.", ProviderMessageType.Info); public override ProviderMessage Message => new ProviderMessage($"rTorrent will not pause torrents when they meet the seed criteria. Readarr will handle automatic removal of torrents based on the current seed criteria in Settings->Indexers only when Remove Completed is enabled. After importing it will also set \"{_imported_view}\" as an rTorrent view, which can be used in rTorrent scripts to customize behavior.", ProviderMessageType.Info);
public override IEnumerable<DownloadClientItem> GetItems() public override IEnumerable<DownloadClientItem> GetItems()
{ {

View File

@@ -37,10 +37,10 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; } public int Port { get; set; }
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to ruTorrent")] [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to rTorrent")]
public bool UseSsl { get; set; } public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "Url Path", Type = FieldType.Textbox, HelpText = "Path to the XMLRPC endpoint, see http(s)://[host]:[port]/[urlPath]. When using ruTorrent this usually is RPC2 or (path to ruTorrent)/plugins/rpc/rpc.php")] [FieldDefinition(3, Label = "Url Path", Type = FieldType.Textbox, HelpText = "Path to the XMLRPC endpoint, see http(s)://[host]:[port]/[urlPath]. This is usually RPC2 or [path to ruTorrent]/plugins/rpc/rpc.php when using ruTorrent.")]
public string UrlBase { get; set; } public string UrlBase { get; set; }
[FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
@@ -64,7 +64,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
[FieldDefinition(10, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing books released over 14 days ago")] [FieldDefinition(10, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing books released over 14 days ago")]
public int OlderTvPriority { get; set; } public int OlderTvPriority { get; set; }
[FieldDefinition(11, Label = "Add Stopped", Type = FieldType.Checkbox, HelpText = "Enabling will add torrents and magnets to ruTorrent in a stopped state")] [FieldDefinition(11, Label = "Add Stopped", Type = FieldType.Checkbox, HelpText = "Enabling will add torrents and magnets to rTorrent in a stopped state. This may break magnet files.")]
public bool AddStopped { get; set; } public bool AddStopped { get; set; }
public NzbDroneValidationResult Validate() public NzbDroneValidationResult Validate()

View File

@@ -164,14 +164,14 @@ namespace NzbDrone.Core.Download
} }
else else
{ {
_logger.Debug() _logger.ForDebugEvent()
.Message("No books were just imported, but all books were previously imported, possible issue with download history.") .Message("No books were just imported, but all books were previously imported, possible issue with download history.")
.Property("AuthorId", trackedDownload.RemoteBook.Author.Id) .Property("AuthorId", trackedDownload.RemoteBook.Author.Id)
.Property("DownloadId", trackedDownload.DownloadItem.DownloadId) .Property("DownloadId", trackedDownload.DownloadItem.DownloadId)
.Property("Title", trackedDownload.DownloadItem.Title) .Property("Title", trackedDownload.DownloadItem.Title)
.Property("Path", trackedDownload.DownloadItem.OutputPath.ToString()) .Property("Path", trackedDownload.DownloadItem.OutputPath.ToString())
.WriteSentryWarn("DownloadHistoryIncomplete") .WriteSentryWarn("DownloadHistoryIncomplete")
.Write(); .Log();
} }
trackedDownload.State = TrackedDownloadState.Imported; trackedDownload.State = TrackedDownloadState.Imported;

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.Download namespace NzbDrone.Core.Download
@@ -13,5 +14,6 @@ namespace NzbDrone.Core.Download
public DownloadClientItemClientInfo DownloadClientInfo { get; set; } public DownloadClientItemClientInfo DownloadClientInfo { get; set; }
public string DownloadId { get; set; } public string DownloadId { get; set; }
public string Message { get; set; } public string Message { get; set; }
public TrackedDownload TrackedDownload { get; set; }
} }
} }

View File

@@ -110,7 +110,7 @@ namespace NzbDrone.Core.Download
bookGrabbedEvent.DownloadId = downloadClientId; bookGrabbedEvent.DownloadId = downloadClientId;
} }
_logger.ProgressInfo("Report sent to {0}. {1}", downloadClient.Definition.Name, downloadTitle); _logger.ProgressInfo("Report sent to {0} from indexer {1}. {2}", downloadClient.Definition.Name, remoteBook.Release.Indexer, downloadTitle);
_eventAggregator.PublishEvent(bookGrabbedEvent); _eventAggregator.PublishEvent(bookGrabbedEvent);
} }
} }

View File

@@ -42,6 +42,7 @@ namespace NzbDrone.Core.Download
SourceTitle = trackedDownload.DownloadItem.Title, SourceTitle = trackedDownload.DownloadItem.Title,
DownloadClientInfo = trackedDownload.DownloadItem.DownloadClientInfo, DownloadClientInfo = trackedDownload.DownloadItem.DownloadClientInfo,
DownloadId = trackedDownload.DownloadItem.DownloadId, DownloadId = trackedDownload.DownloadItem.DownloadId,
TrackedDownload = trackedDownload,
Message = "Manually ignored" Message = "Manually ignored"
}; };

View File

@@ -200,6 +200,7 @@ namespace NzbDrone.Core.History
}; };
history.Data.Add("StatusMessages", message.TrackedDownload.StatusMessages.ToJson()); history.Data.Add("StatusMessages", message.TrackedDownload.StatusMessages.ToJson());
history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteBook?.ParsedBookInfo?.ReleaseGroup);
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }
} }
@@ -235,6 +236,7 @@ namespace NzbDrone.Core.History
history.Data.Add("ImportedPath", message.ImportedBook.Path); history.Data.Add("ImportedPath", message.ImportedBook.Path);
history.Data.Add("DownloadClient", message.DownloadClientInfo?.Type); history.Data.Add("DownloadClient", message.DownloadClientInfo?.Type);
history.Data.Add("DownloadClientName", message.DownloadClientInfo?.Name); history.Data.Add("DownloadClientName", message.DownloadClientInfo?.Name);
history.Data.Add("ReleaseGroup", message.BookInfo.ReleaseGroup);
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }
@@ -286,6 +288,7 @@ namespace NzbDrone.Core.History
}; };
history.Data.Add("Reason", message.Reason.ToString()); history.Data.Add("Reason", message.Reason.ToString());
history.Data.Add("ReleaseGroup", message.BookFile.ReleaseGroup);
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }
@@ -307,6 +310,7 @@ namespace NzbDrone.Core.History
history.Data.Add("SourcePath", sourcePath); history.Data.Add("SourcePath", sourcePath);
history.Data.Add("Path", path); history.Data.Add("Path", path);
history.Data.Add("ReleaseGroup", message.BookFile.ReleaseGroup);
_historyRepository.Insert(history); _historyRepository.Insert(history);
} }
@@ -358,6 +362,7 @@ namespace NzbDrone.Core.History
}; };
history.Data.Add("DownloadClient", message.DownloadClientInfo.Name); history.Data.Add("DownloadClient", message.DownloadClientInfo.Name);
history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteBook?.ParsedBookInfo?.ReleaseGroup);
history.Data.Add("Message", message.Message); history.Data.Add("Message", message.Message);
historyToAdd.Add(history); historyToAdd.Add(history);

View File

@@ -117,7 +117,6 @@ namespace NzbDrone.Core.Instrumentation
syslogTarget.MessageSend.Protocol = ProtocolType.Udp; syslogTarget.MessageSend.Protocol = ProtocolType.Udp;
syslogTarget.MessageSend.Udp.Port = syslogPort; syslogTarget.MessageSend.Udp.Port = syslogPort;
syslogTarget.MessageSend.Udp.Server = syslogServer; syslogTarget.MessageSend.Udp.Server = syslogServer;
syslogTarget.MessageSend.Udp.ReconnectInterval = 500;
syslogTarget.MessageCreation.Rfc = RfcNumber.Rfc5424; syslogTarget.MessageCreation.Rfc = RfcNumber.Rfc5424;
syslogTarget.MessageCreation.Rfc5424.AppName = _configFileProvider.InstanceName; syslogTarget.MessageCreation.Rfc5424.AppName = _configFileProvider.InstanceName;

View File

@@ -34,6 +34,8 @@
"ApiKeyHelpTextWarning": "Requires restart to take effect", "ApiKeyHelpTextWarning": "Requires restart to take effect",
"AppDataDirectory": "AppData directory", "AppDataDirectory": "AppData directory",
"AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update", "AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update",
"ApplicationURL": "Application URL",
"ApplicationUrlHelpText": "This application's external URL including http(s)://, port and URL base",
"ApplyTags": "Apply Tags", "ApplyTags": "Apply Tags",
"ApplyTagsHelpTexts1": "How to apply tags to the selected author", "ApplyTagsHelpTexts1": "How to apply tags to the selected author",
"ApplyTagsHelpTexts2": "Add: Add the tags the existing list of tags", "ApplyTagsHelpTexts2": "Add: Add the tags the existing list of tags",

View File

@@ -400,11 +400,11 @@ namespace NzbDrone.Core.MediaFiles
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.Warn() Logger.ForWarnEvent()
.Exception(ex) .Exception(ex)
.Message($"Tag writing failed for {path}") .Message($"Tag writing failed for {path}")
.WriteSentryWarn("Tag writing failed") .WriteSentryWarn("Tag writing failed")
.Write(); .Log();
} }
finally finally
{ {

View File

@@ -147,11 +147,11 @@ namespace NzbDrone.Core.MediaFiles
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Warn() _logger.ForWarnEvent()
.Exception(ex) .Exception(ex)
.Message($"Tag removal failed for {path}") .Message($"Tag removal failed for {path}")
.WriteSentryWarn("Tag removal failed") .WriteSentryWarn("Tag removal failed")
.Write(); .Log();
} }
finally finally
{ {

View File

@@ -65,10 +65,10 @@ namespace NzbDrone.Core.MediaFiles
} }
else else
{ {
Logger.Debug() Logger.ForDebugEvent()
.Message("Unknown audio format: '{0}'.", string.Join(", ", mediaInfo.AudioFormat)) .Message("Unknown audio format: '{0}'.", string.Join(", ", mediaInfo.AudioFormat))
.WriteSentryWarn("UnknownAudioFormat", mediaInfo.AudioFormat) .WriteSentryWarn("UnknownAudioFormat", mediaInfo.AudioFormat)
.Write(); .Log();
return "Unknown"; return "Unknown";
} }

View File

@@ -1,10 +1,11 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Net;
using FluentValidation.Results; using FluentValidation.Results;
using NLog; using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.Notifications.Notifiarr namespace NzbDrone.Core.Notifications.Notifiarr
{ {
@@ -16,21 +17,27 @@ namespace NzbDrone.Core.Notifications.Notifiarr
public class NotifiarrProxy : INotifiarrProxy public class NotifiarrProxy : INotifiarrProxy
{ {
private const string URL = "https://notifiarr.com"; private const string URL = "https://notifiarr.com/notifier.php";
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IConfigFileProvider _configFileProvider;
private readonly Logger _logger; private readonly Logger _logger;
public NotifiarrProxy(IHttpClient httpClient, IConfigFileProvider configFileProvider, Logger logger) public NotifiarrProxy(IHttpClient httpClient, Logger logger)
{ {
_httpClient = httpClient; _httpClient = httpClient;
_configFileProvider = configFileProvider;
_logger = logger; _logger = logger;
} }
public void SendNotification(StringDictionary message, NotifiarrSettings settings) public void SendNotification(StringDictionary message, NotifiarrSettings settings)
{ {
try
{
ProcessNotification(message, settings); ProcessNotification(message, settings);
}
catch (NotifiarrException ex)
{
_logger.Error(ex, "Unable to send notification");
throw new NotifiarrException("Unable to send notification");
}
} }
public ValidationFailure Test(NotifiarrSettings settings) public ValidationFailure Test(NotifiarrSettings settings)
@@ -43,14 +50,21 @@ namespace NzbDrone.Core.Notifications.Notifiarr
SendNotification(variables, settings); SendNotification(variables, settings);
return null; return null;
} }
catch (NotifiarrException ex) catch (HttpException ex)
{ {
return new ValidationFailure("APIKey", ex.Message); if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
{
_logger.Error(ex, "API key is invalid: " + ex.Message);
return new ValidationFailure("APIKey", "API key is invalid");
}
_logger.Error(ex, "Unable to send test message: " + ex.Message);
return new ValidationFailure("APIKey", "Unable to send test notification");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, ex.Message); _logger.Error(ex, "Unable to send test notification: " + ex.Message);
return new ValidationFailure("", "Unable to send test notification. Check the log for more details."); return new ValidationFailure("", "Unable to send test notification");
} }
} }
@@ -58,10 +72,8 @@ namespace NzbDrone.Core.Notifications.Notifiarr
{ {
try try
{ {
var instanceName = _configFileProvider.InstanceName; var requestBuilder = new HttpRequestBuilder(URL).Post();
var requestBuilder = new HttpRequestBuilder(URL + "/api/v1/notification/readarr").Post(); requestBuilder.AddFormParameter("api", settings.APIKey).Build();
requestBuilder.AddFormParameter("instanceName", instanceName).Build();
requestBuilder.SetHeader("X-API-Key", settings.APIKey);
foreach (string key in message.Keys) foreach (string key in message.Keys)
{ {
@@ -74,31 +86,13 @@ namespace NzbDrone.Core.Notifications.Notifiarr
} }
catch (HttpException ex) catch (HttpException ex)
{ {
var responseCode = ex.Response.StatusCode; if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
switch ((int)responseCode)
{ {
case 401: _logger.Error(ex, "API key is invalid");
_logger.Error("Unauthorized", "HTTP 401 - API key is invalid"); throw;
throw new NotifiarrException("API key is invalid");
case 400:
_logger.Error("Invalid Request", "HTTP 400 - Unable to send notification. Ensure Readarr Integration is enabled & assigned a channel on Notifiarr");
throw new NotifiarrException("Unable to send notification. Ensure Readarr Integration is enabled & assigned a channel on Notifiarr");
case 502:
case 503:
case 504:
_logger.Error("Service Unavailable", "Unable to send notification. Service Unavailable");
throw new NotifiarrException("Unable to send notification. Service Unavailable", ex);
case 520:
case 521:
case 522:
case 523:
case 524:
_logger.Error(ex, "Cloudflare Related HTTP Error - Unable to send notification");
throw new NotifiarrException("Cloudflare Related HTTP Error - Unable to send notification", ex);
default:
_logger.Error(ex, "Unknown HTTP Error - Unable to send notification");
throw new NotifiarrException("Unknown HTTP Error - Unable to send notification", ex);
} }
throw new NotifiarrException("Unable to send notification", ex);
} }
} }
} }

View File

@@ -1,6 +1,10 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using FluentValidation; using FluentValidation;
using FluentValidation.Validators; using FluentValidation.Validators;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.Organizer namespace NzbDrone.Core.Organizer
{ {
@@ -12,12 +16,16 @@ namespace NzbDrone.Core.Organizer
public static IRuleBuilderOptions<T, string> ValidBookFormat<T>(this IRuleBuilder<T, string> ruleBuilder) public static IRuleBuilderOptions<T, string> ValidBookFormat<T>(this IRuleBuilder<T, string> ruleBuilder)
{ {
ruleBuilder.SetValidator(new NotEmptyValidator(null)); ruleBuilder.SetValidator(new NotEmptyValidator(null));
ruleBuilder.SetValidator(new IllegalCharactersValidator());
return ruleBuilder.SetValidator(new ValidStandardTrackFormatValidator()); return ruleBuilder.SetValidator(new ValidStandardTrackFormatValidator());
} }
public static IRuleBuilderOptions<T, string> ValidAuthorFolderFormat<T>(this IRuleBuilder<T, string> ruleBuilder) public static IRuleBuilderOptions<T, string> ValidAuthorFolderFormat<T>(this IRuleBuilder<T, string> ruleBuilder)
{ {
ruleBuilder.SetValidator(new NotEmptyValidator(null)); ruleBuilder.SetValidator(new NotEmptyValidator(null));
ruleBuilder.SetValidator(new IllegalCharactersValidator());
return ruleBuilder.SetValidator(new RegularExpressionValidator(FileNameBuilder.AuthorNameRegex)).WithMessage("Must contain Author name"); return ruleBuilder.SetValidator(new RegularExpressionValidator(FileNameBuilder.AuthorNameRegex)).WithMessage("Must contain Author name");
} }
} }
@@ -42,4 +50,41 @@ namespace NzbDrone.Core.Organizer
return true; return true;
} }
} }
public class IllegalCharactersValidator : PropertyValidator
{
private readonly char[] _invalidPathChars = Path.GetInvalidPathChars();
public IllegalCharactersValidator()
: base("Contains illegal characters: {InvalidCharacters}")
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
var value = context.PropertyValue as string;
var invalidCharacters = new List<char>();
if (value.IsNullOrWhiteSpace())
{
return true;
}
foreach (var i in _invalidPathChars)
{
if (value.IndexOf(i) >= 0)
{
invalidCharacters.Add(i);
}
}
if (invalidCharacters.Any())
{
context.MessageFormatter.AppendArgument("InvalidCharacters", string.Join("", invalidCharacters));
return false;
}
return true;
}
}
} }

View File

@@ -9,7 +9,7 @@ namespace NzbDrone.Core.Validation.Paths
private readonly IAppFolderInfo _appFolderInfo; private readonly IAppFolderInfo _appFolderInfo;
public StartupFolderValidator(IAppFolderInfo appFolderInfo) public StartupFolderValidator(IAppFolderInfo appFolderInfo)
: base("Path cannot be an ancestor of the start up folder") : base("Path cannot be {relationship} the start up folder")
{ {
_appFolderInfo = appFolderInfo; _appFolderInfo = appFolderInfo;
} }
@@ -21,7 +21,24 @@ namespace NzbDrone.Core.Validation.Paths
return true; return true;
} }
return !_appFolderInfo.StartUpFolder.IsParentPath(context.PropertyValue.ToString()); var startupFolder = _appFolderInfo.StartUpFolder;
var folder = context.PropertyValue.ToString();
if (startupFolder.PathEquals(folder))
{
context.MessageFormatter.AppendArgument("relationship", "set to");
return false;
}
if (startupFolder.IsParentPath(folder))
{
context.MessageFormatter.AppendArgument("relationship", "child of");
return false;
}
return true;
} }
} }
} }

View File

@@ -26,6 +26,7 @@ namespace Readarr.Api.V1.Config
public string SslCertPassword { get; set; } public string SslCertPassword { get; set; }
public string UrlBase { get; set; } public string UrlBase { get; set; }
public string InstanceName { get; set; } public string InstanceName { get; set; }
public string ApplicationUrl { get; set; }
public bool UpdateAutomatically { get; set; } public bool UpdateAutomatically { get; set; }
public UpdateMechanism UpdateMechanism { get; set; } public UpdateMechanism UpdateMechanism { get; set; }
public string UpdateScriptPath { get; set; } public string UpdateScriptPath { get; set; }
@@ -82,7 +83,8 @@ namespace Readarr.Api.V1.Config
CertificateValidation = configService.CertificateValidation, CertificateValidation = configService.CertificateValidation,
BackupFolder = configService.BackupFolder, BackupFolder = configService.BackupFolder,
BackupInterval = configService.BackupInterval, BackupInterval = configService.BackupInterval,
BackupRetention = configService.BackupRetention BackupRetention = configService.BackupRetention,
ApplicationUrl = configService.ApplicationUrl
}; };
} }
} }

View File

@@ -12,15 +12,5 @@ namespace Readarr.Api.V1.DownloadClient
: base(downloadClientFactory, "downloadclient", ResourceMapper) : base(downloadClientFactory, "downloadclient", ResourceMapper)
{ {
} }
protected override void Validate(DownloadClientDefinition definition, bool includeWarnings)
{
if (!definition.Enable)
{
return;
}
base.Validate(definition, includeWarnings);
}
} }
} }

View File

@@ -22,15 +22,5 @@ namespace Readarr.Api.V1.ImportLists
SharedValidator.RuleFor(c => c.QualityProfileId).SetValidator(qualityProfileExistsValidator); SharedValidator.RuleFor(c => c.QualityProfileId).SetValidator(qualityProfileExistsValidator);
SharedValidator.RuleFor(c => c.MetadataProfileId).SetValidator(metadataProfileExistsValidator); SharedValidator.RuleFor(c => c.MetadataProfileId).SetValidator(metadataProfileExistsValidator);
} }
protected override void Validate(ImportListDefinition definition, bool includeWarnings)
{
if (!definition.Enable)
{
return;
}
base.Validate(definition, includeWarnings);
}
} }
} }

View File

@@ -12,15 +12,5 @@ namespace Readarr.Api.V1.Indexers
: base(indexerFactory, "indexer", ResourceMapper) : base(indexerFactory, "indexer", ResourceMapper)
{ {
} }
protected override void Validate(IndexerDefinition definition, bool includeWarnings)
{
if (!definition.Enable)
{
return;
}
base.Validate(definition, includeWarnings);
}
} }
} }

View File

@@ -12,15 +12,5 @@ namespace Readarr.Api.V1.Metadata
: base(metadataFactory, "metadata", ResourceMapper) : base(metadataFactory, "metadata", ResourceMapper)
{ {
} }
protected override void Validate(MetadataDefinition definition, bool includeWarnings)
{
if (!definition.Enable)
{
return;
}
base.Validate(definition, includeWarnings);
}
} }
} }

View File

@@ -12,15 +12,5 @@ namespace Readarr.Api.V1.Notifications
: base(notificationFactory, "notification", ResourceMapper) : base(notificationFactory, "notification", ResourceMapper)
{ {
} }
protected override void Validate(NotificationDefinition definition, bool includeWarnings)
{
if (!definition.Enable)
{
return;
}
base.Validate(definition, includeWarnings);
}
} }
} }

View File

@@ -7,6 +7,7 @@ using NzbDrone.Common.Serializer;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
using NzbDrone.Http.REST.Attributes; using NzbDrone.Http.REST.Attributes;
using Readarr.Http.Extensions;
using Readarr.Http.REST; using Readarr.Http.REST;
namespace Readarr.Api.V1 namespace Readarr.Api.V1
@@ -60,7 +61,7 @@ namespace Readarr.Api.V1
[RestPostById] [RestPostById]
public ActionResult<TProviderResource> CreateProvider(TProviderResource providerResource) public ActionResult<TProviderResource> CreateProvider(TProviderResource providerResource)
{ {
var providerDefinition = GetDefinition(providerResource, false); var providerDefinition = GetDefinition(providerResource, true, false, false);
if (providerDefinition.Enable) if (providerDefinition.Enable)
{ {
@@ -75,11 +76,11 @@ namespace Readarr.Api.V1
[RestPutById] [RestPutById]
public ActionResult<TProviderResource> UpdateProvider(TProviderResource providerResource) public ActionResult<TProviderResource> UpdateProvider(TProviderResource providerResource)
{ {
var providerDefinition = GetDefinition(providerResource, false); var providerDefinition = GetDefinition(providerResource, true, false, false);
var existingDefinition = _providerFactory.Get(providerDefinition.Id); var forceSave = Request.GetBooleanQueryParameter("forceSave");
// Only test existing definitions if it was previously disabled // Only test existing definitions if it is enabled and forceSave isn't set.
if (providerDefinition.Enable && !existingDefinition.Enable) if (providerDefinition.Enable && !forceSave)
{ {
Test(providerDefinition, false); Test(providerDefinition, false);
} }
@@ -89,11 +90,11 @@ namespace Readarr.Api.V1
return Accepted(providerResource.Id); return Accepted(providerResource.Id);
} }
private TProviderDefinition GetDefinition(TProviderResource providerResource, bool includeWarnings = false, bool validate = true) private TProviderDefinition GetDefinition(TProviderResource providerResource, bool validate, bool includeWarnings, bool forceValidate)
{ {
var definition = _resourceMapper.ToModel(providerResource); var definition = _resourceMapper.ToModel(providerResource);
if (validate) if (validate && (definition.Enable || forceValidate))
{ {
Validate(definition, includeWarnings); Validate(definition, includeWarnings);
} }
@@ -113,7 +114,7 @@ namespace Readarr.Api.V1
{ {
var defaultDefinitions = _providerFactory.GetDefaultDefinitions().OrderBy(p => p.ImplementationName).ToList(); var defaultDefinitions = _providerFactory.GetDefaultDefinitions().OrderBy(p => p.ImplementationName).ToList();
var result = new List<TProviderResource>(defaultDefinitions.Count()); var result = new List<TProviderResource>(defaultDefinitions.Count);
foreach (var providerDefinition in defaultDefinitions) foreach (var providerDefinition in defaultDefinitions)
{ {
@@ -134,7 +135,7 @@ namespace Readarr.Api.V1
[HttpPost("test")] [HttpPost("test")]
public object Test([FromBody] TProviderResource providerResource) public object Test([FromBody] TProviderResource providerResource)
{ {
var providerDefinition = GetDefinition(providerResource, true); var providerDefinition = GetDefinition(providerResource, true, true, true);
Test(providerDefinition, true); Test(providerDefinition, true);
@@ -167,7 +168,7 @@ namespace Readarr.Api.V1
[HttpPost("action/{name}")] [HttpPost("action/{name}")]
public IActionResult RequestAction(string name, [FromBody] TProviderResource resource) public IActionResult RequestAction(string name, [FromBody] TProviderResource resource)
{ {
var providerDefinition = GetDefinition(resource, true, false); var providerDefinition = GetDefinition(resource, false, false, false);
var query = Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString()); var query = Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString());
@@ -176,7 +177,7 @@ namespace Readarr.Api.V1
return Content(data.ToJson(), "application/json"); return Content(data.ToJson(), "application/json");
} }
protected virtual void Validate(TProviderDefinition definition, bool includeWarnings) private void Validate(TProviderDefinition definition, bool includeWarnings)
{ {
var validationResult = definition.Settings.Validate(); var validationResult = definition.Settings.Validate();

View File

@@ -176,7 +176,7 @@ namespace Readarr.Api.V1.Queue
case "status": case "status":
return q => q.Status; return q => q.Status;
case "authors.sortName": case "authors.sortName":
return q => q.Author?.Metadata.Value.SortName ?? string.Empty; return q => q.Author?.Metadata.Value.SortName ?? q.Title;
case "authors.sortNameLastFirst": case "authors.sortNameLastFirst":
return q => q.Author?.Metadata.Value.SortNameLastFirst ?? string.Empty; return q => q.Author?.Metadata.Value.SortNameLastFirst ?? string.Empty;
case "title": case "title":

View File

@@ -162,39 +162,7 @@ namespace Readarr.Http.Extensions
remoteIP = remoteIP.MapToIPv4(); remoteIP = remoteIP.MapToIPv4();
} }
var remoteAddress = remoteIP.ToString(); return remoteIP.ToString();
// Only check if forwarded by a local network reverse proxy
if (remoteIP.IsLocalAddress())
{
var realIPHeader = request.Headers["X-Real-IP"];
if (realIPHeader.Any())
{
return realIPHeader.First().ToString();
}
var forwardedForHeader = request.Headers["X-Forwarded-For"];
if (forwardedForHeader.Any())
{
// Get the first address that was forwarded by a local IP to prevent remote clients faking another proxy
foreach (var forwardedForAddress in forwardedForHeader.SelectMany(v => v.Split(',')).Select(v => v.Trim()).Reverse())
{
if (!IPAddress.TryParse(forwardedForAddress, out remoteIP))
{
return remoteAddress;
}
if (!remoteIP.IsLocalAddress())
{
return forwardedForAddress;
}
remoteAddress = forwardedForAddress;
}
}
}
return remoteAddress;
} }
public static void DisableCache(this IHeaderDictionary headers) public static void DisableCache(this IHeaderDictionary headers)