Compare commits

..

1 Commits

Author SHA1 Message Date
Bakerboy448
09d423848d New: Use Notifiarr API
New: Notifiarr Add Instance Name Support

Fixed: Notifiarr - Better HTTP Error Handling

also quiet sentry

(cherry picked from commit 1db690ad39ec103c0f4dc89ac4545801ef95bec7)

Fixed: Improve Notifiarr Exception Handling and Validation Errors

(cherry picked from commit 6aaa024d71b939030950460ae986ada5bbae5ad7)

also move notifiarr to header auth from url auth
2022-11-25 20:34:22 -06:00
39 changed files with 194 additions and 343 deletions

View File

@@ -169,16 +169,6 @@ 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,7 +21,6 @@ function HostSettings(props) {
port, port,
urlBase, urlBase,
instanceName, instanceName,
applicationUrl,
enableSsl, enableSsl,
sslPort, sslPort,
sslCertPath, sslCertPath,
@@ -97,21 +96,6 @@ 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,7 +6,6 @@ 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) {
@@ -28,33 +27,27 @@ 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: `${requestUrl}?${$.param(params, true)}`, url: `${url}?${$.param(queryParams, true)}`,
method: id ? 'PUT' : 'POST', method: '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,11 +71,6 @@ 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="5.1.0" /> <PackageVersion Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageVersion Include="NLog" Version="5.0.5" /> <PackageVersion Include="NLog" Version="4.7.14" />
<PackageVersion Include="NLog.Targets.Syslog" Version="7.0.0" /> <PackageVersion Include="NLog.Targets.Syslog" Version="6.0.2" />
<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,8 +24,7 @@ namespace NzbDrone.Common.Disk
"/boot", "/boot",
"/lib", "/lib",
"/sbin", "/sbin",
"/proc", "/proc"
"/usr/bin"
}; };
} }
} }

View File

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

View File

@@ -1,15 +1,13 @@
using System.Text; using NLog;
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 void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target) protected override string GetFormattedMessage(LogEventInfo logEvent)
{ {
var result = CleanseLogMessage.Cleanse(Layout.Render(logEvent)); return CleanseLogMessage.Cleanse(Layout.Render(logEvent));
target.Append(result);
} }
} }
} }

View File

@@ -34,8 +34,6 @@ namespace NzbDrone.Common.Instrumentation
var appFolderInfo = new AppFolderInfo(startupContext); var appFolderInfo = new AppFolderInfo(startupContext);
RegisterGlobalFilters();
if (Debugger.IsAttached) if (Debugger.IsAttached)
{ {
RegisterDebugger(); RegisterDebugger();
@@ -103,16 +101,6 @@ 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,11 +206,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
if (ex != null) if (ex != null)
{ {
fingerPrint.Add(ex.GetType().FullName); fingerPrint.Add(ex.GetType().FullName);
if (ex.TargetSite != null) fingerPrint.Add(ex.TargetSite.ToString());
{
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,4 +1,3 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using FizzWare.NBuilder; using FizzWare.NBuilder;
@@ -6,7 +5,6 @@ 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;
@@ -246,89 +244,5 @@ 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,8 +410,6 @@ 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,6 +97,5 @@ namespace NzbDrone.Core.Configuration
int BackupRetention { get; } int BackupRetention { get; }
CertificateValidationType CertificateValidation { get; } CertificateValidationType CertificateValidation { get; }
string ApplicationUrl { get; }
} }
} }

View File

@@ -1,11 +1,9 @@
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
{ {
@@ -13,14 +11,12 @@ 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, IConfigService configService, Logger logger) public RepackSpecification(IMediaFileService mediaFileService, UpgradableSpecification upgradableSpecification, Logger logger)
{ {
_mediaFileService = mediaFileService; _mediaFileService = mediaFileService;
_upgradableSpecification = upgradableSpecification; _upgradableSpecification = upgradableSpecification;
_configService = configService;
_logger = logger; _logger = logger;
} }
@@ -34,14 +30,6 @@ 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;
@@ -51,12 +39,6 @@ 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("seeding") || properties.Status.Contains("complete")) if (properties.Status.Contains("error"))
{
item.Status = DownloadItemStatus.Completed;
}
else if (properties.Status.Contains("stopped"))
{
item.Status = DownloadItemStatus.Paused;
}
else if (properties.Status.Contains("error"))
{ {
item.Status = DownloadItemStatus.Warning; item.Status = DownloadItemStatus.Warning;
} }
else if (properties.Status.Contains("seeding") || properties.Status.Contains("complete"))
{
item.Status = DownloadItemStatus.Completed;
}
else if (properties.Status.Contains("downloading")) else if (properties.Status.Contains("downloading"))
{ {
item.Status = DownloadItemStatus.Downloading; item.Status = DownloadItemStatus.Downloading;
} }
else if (properties.Status.Contains("stopped"))
{
item.Status = DownloadItemStatus.Paused;
}
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($"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 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 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 rTorrent")] [FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to ruTorrent")]
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]. This is usually RPC2 or [path to ruTorrent]/plugins/rpc/rpc.php when using ruTorrent.")] [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")]
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 rTorrent in a stopped state. This may break magnet files.")] [FieldDefinition(11, Label = "Add Stopped", Type = FieldType.Checkbox, HelpText = "Enabling will add torrents and magnets to ruTorrent in a stopped state")]
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.ForDebugEvent() _logger.Debug()
.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")
.Log(); .Write();
} }
trackedDownload.State = TrackedDownloadState.Imported; trackedDownload.State = TrackedDownloadState.Imported;

View File

@@ -1,6 +1,5 @@
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
@@ -14,6 +13,5 @@ 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} from indexer {1}. {2}", downloadClient.Definition.Name, remoteBook.Release.Indexer, downloadTitle); _logger.ProgressInfo("Report sent to {0}. {1}", downloadClient.Definition.Name, downloadTitle);
_eventAggregator.PublishEvent(bookGrabbedEvent); _eventAggregator.PublishEvent(bookGrabbedEvent);
} }
} }

View File

@@ -42,7 +42,6 @@ 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,7 +200,6 @@ 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);
} }
} }
@@ -236,7 +235,6 @@ 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);
} }
@@ -288,7 +286,6 @@ 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);
} }
@@ -310,7 +307,6 @@ 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);
} }
@@ -362,7 +358,6 @@ 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,6 +117,7 @@ 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,8 +34,6 @@
"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.ForWarnEvent() Logger.Warn()
.Exception(ex) .Exception(ex)
.Message($"Tag writing failed for {path}") .Message($"Tag writing failed for {path}")
.WriteSentryWarn("Tag writing failed") .WriteSentryWarn("Tag writing failed")
.Log(); .Write();
} }
finally finally
{ {

View File

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

View File

@@ -65,10 +65,10 @@ namespace NzbDrone.Core.MediaFiles
} }
else else
{ {
Logger.ForDebugEvent() Logger.Debug()
.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)
.Log(); .Write();
return "Unknown"; return "Unknown";
} }

View File

@@ -1,11 +1,10 @@
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.EnvironmentInfo; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.Notifications.Notifiarr namespace NzbDrone.Core.Notifications.Notifiarr
{ {
@@ -17,27 +16,21 @@ namespace NzbDrone.Core.Notifications.Notifiarr
public class NotifiarrProxy : INotifiarrProxy public class NotifiarrProxy : INotifiarrProxy
{ {
private const string URL = "https://notifiarr.com/notifier.php"; private const string URL = "https://notifiarr.com";
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IConfigFileProvider _configFileProvider;
private readonly Logger _logger; private readonly Logger _logger;
public NotifiarrProxy(IHttpClient httpClient, Logger logger) public NotifiarrProxy(IHttpClient httpClient, IConfigFileProvider configFileProvider, 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)
@@ -50,21 +43,14 @@ namespace NzbDrone.Core.Notifications.Notifiarr
SendNotification(variables, settings); SendNotification(variables, settings);
return null; return null;
} }
catch (HttpException ex) catch (NotifiarrException ex)
{ {
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) return new ValidationFailure("APIKey", ex.Message);
{
_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, "Unable to send test notification: " + ex.Message); _logger.Error(ex, ex.Message);
return new ValidationFailure("", "Unable to send test notification"); return new ValidationFailure("", "Unable to send test notification. Check the log for more details.");
} }
} }
@@ -72,8 +58,10 @@ namespace NzbDrone.Core.Notifications.Notifiarr
{ {
try try
{ {
var requestBuilder = new HttpRequestBuilder(URL).Post(); var instanceName = _configFileProvider.InstanceName;
requestBuilder.AddFormParameter("api", settings.APIKey).Build(); var requestBuilder = new HttpRequestBuilder(URL + "/api/v1/notification/readarr").Post();
requestBuilder.AddFormParameter("instanceName", instanceName).Build();
requestBuilder.SetHeader("X-API-Key", settings.APIKey);
foreach (string key in message.Keys) foreach (string key in message.Keys)
{ {
@@ -86,13 +74,31 @@ namespace NzbDrone.Core.Notifications.Notifiarr
} }
catch (HttpException ex) catch (HttpException ex)
{ {
if (ex.Response.StatusCode == HttpStatusCode.BadRequest) var responseCode = ex.Response.StatusCode;
switch ((int)responseCode)
{ {
_logger.Error(ex, "API key is invalid"); case 401:
throw; _logger.Error("Unauthorized", "HTTP 401 - API key is invalid");
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,10 +1,6 @@
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
{ {
@@ -16,16 +12,12 @@ 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");
} }
} }
@@ -50,41 +42,4 @@ 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 {relationship} the start up folder") : base("Path cannot be an ancestor of the start up folder")
{ {
_appFolderInfo = appFolderInfo; _appFolderInfo = appFolderInfo;
} }
@@ -21,24 +21,7 @@ namespace NzbDrone.Core.Validation.Paths
return true; return true;
} }
var startupFolder = _appFolderInfo.StartUpFolder; return !_appFolderInfo.StartUpFolder.IsParentPath(context.PropertyValue.ToString());
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,7 +26,6 @@ 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; }
@@ -83,8 +82,7 @@ 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,5 +12,15 @@ 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,5 +22,15 @@ 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,5 +12,15 @@ 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,5 +12,15 @@ 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,5 +12,15 @@ 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,7 +7,6 @@ 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
@@ -61,7 +60,7 @@ namespace Readarr.Api.V1
[RestPostById] [RestPostById]
public ActionResult<TProviderResource> CreateProvider(TProviderResource providerResource) public ActionResult<TProviderResource> CreateProvider(TProviderResource providerResource)
{ {
var providerDefinition = GetDefinition(providerResource, true, false, false); var providerDefinition = GetDefinition(providerResource, false);
if (providerDefinition.Enable) if (providerDefinition.Enable)
{ {
@@ -76,11 +75,11 @@ namespace Readarr.Api.V1
[RestPutById] [RestPutById]
public ActionResult<TProviderResource> UpdateProvider(TProviderResource providerResource) public ActionResult<TProviderResource> UpdateProvider(TProviderResource providerResource)
{ {
var providerDefinition = GetDefinition(providerResource, true, false, false); var providerDefinition = GetDefinition(providerResource, false);
var forceSave = Request.GetBooleanQueryParameter("forceSave"); var existingDefinition = _providerFactory.Get(providerDefinition.Id);
// Only test existing definitions if it is enabled and forceSave isn't set. // Only test existing definitions if it was previously disabled
if (providerDefinition.Enable && !forceSave) if (providerDefinition.Enable && !existingDefinition.Enable)
{ {
Test(providerDefinition, false); Test(providerDefinition, false);
} }
@@ -90,11 +89,11 @@ namespace Readarr.Api.V1
return Accepted(providerResource.Id); return Accepted(providerResource.Id);
} }
private TProviderDefinition GetDefinition(TProviderResource providerResource, bool validate, bool includeWarnings, bool forceValidate) private TProviderDefinition GetDefinition(TProviderResource providerResource, bool includeWarnings = false, bool validate = true)
{ {
var definition = _resourceMapper.ToModel(providerResource); var definition = _resourceMapper.ToModel(providerResource);
if (validate && (definition.Enable || forceValidate)) if (validate)
{ {
Validate(definition, includeWarnings); Validate(definition, includeWarnings);
} }
@@ -114,7 +113,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)
{ {
@@ -135,7 +134,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, true, true); var providerDefinition = GetDefinition(providerResource, true);
Test(providerDefinition, true); Test(providerDefinition, true);
@@ -168,7 +167,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, false, false, false); var providerDefinition = GetDefinition(resource, true, 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());
@@ -177,7 +176,7 @@ namespace Readarr.Api.V1
return Content(data.ToJson(), "application/json"); return Content(data.ToJson(), "application/json");
} }
private void Validate(TProviderDefinition definition, bool includeWarnings) protected virtual 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 ?? q.Title; return q => q.Author?.Metadata.Value.SortName ?? string.Empty;
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,7 +162,39 @@ namespace Readarr.Http.Extensions
remoteIP = remoteIP.MapToIPv4(); remoteIP = remoteIP.MapToIPv4();
} }
return remoteIP.ToString(); var remoteAddress = 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)