New: OnGrab Notifications

pull/1451/head
Qstick 2 years ago
parent 99bc56efb6
commit 1640980e2b

@ -71,6 +71,19 @@ class SearchIndexRow extends Component {
});
};
onSavePress = () => {
const {
downloadUrl,
fileName,
onSavePress
} = this.props;
onSavePress({
downloadUrl,
fileName
});
};
//
// Render
@ -85,7 +98,6 @@ class SearchIndexRow extends Component {
publishDate,
title,
infoUrl,
downloadUrl,
indexer,
size,
files,
@ -300,7 +312,7 @@ class SearchIndexRow extends Component {
className={styles.downloadLink}
name={icons.SAVE}
title={translate('Save')}
to={downloadUrl}
onPress={this.onSavePress}
/>
</VirtualTableRowCell>
);
@ -323,6 +335,7 @@ SearchIndexRow.propTypes = {
ageMinutes: PropTypes.number.isRequired,
publishDate: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
fileName: PropTypes.string.isRequired,
infoUrl: PropTypes.string.isRequired,
downloadUrl: PropTypes.string.isRequired,
indexerId: PropTypes.number.isRequired,
@ -335,6 +348,7 @@ SearchIndexRow.propTypes = {
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onGrabPress: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
isGrabbing: PropTypes.bool.isRequired,
isGrabbed: PropTypes.bool.isRequired,
grabError: PropTypes.string,

@ -51,7 +51,8 @@ class SearchIndexTable extends Component {
timeFormat,
selectedState,
onSelectedChange,
onGrabPress
onGrabPress,
onSavePress
} = this.props;
const release = items[rowIndex];
@ -71,6 +72,7 @@ class SearchIndexTable extends Component {
longDateFormat={longDateFormat}
timeFormat={timeFormat}
onGrabPress={onGrabPress}
onSavePress={onSavePress}
/>
</VirtualTableRow>
);
@ -134,6 +136,7 @@ SearchIndexTable.propTypes = {
timeFormat: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
selectedState: PropTypes.object.isRequired,

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { grabRelease, setReleasesSort } from 'Store/Actions/releaseActions';
import { grabRelease, saveRelease, setReleasesSort } from 'Store/Actions/releaseActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import SearchIndexTable from './SearchIndexTable';
@ -25,6 +25,9 @@ function createMapDispatchToProps(dispatch, props) {
},
onGrabPress(payload) {
dispatch(grabRelease(payload));
},
onSavePress(payload) {
dispatch(saveRelease(payload));
}
};
}

@ -56,17 +56,9 @@ class Notification extends Component {
id,
name,
onGrab,
onDownload,
onUpgrade,
onRename,
onDelete,
onHealthIssue,
onApplicationUpdate,
supportsOnGrab,
supportsOnDownload,
supportsOnUpgrade,
supportsOnRename,
supportsOnDelete,
supportsOnHealthIssue,
supportsOnApplicationUpdate
} = this.props;
@ -88,34 +80,6 @@ class Notification extends Component {
</Label>
}
{
supportsOnDelete && onDelete &&
<Label kind={kinds.SUCCESS}>
{translate('OnDelete')}
</Label>
}
{
supportsOnDownload && onDownload &&
<Label kind={kinds.SUCCESS}>
{translate('OnImport')}
</Label>
}
{
supportsOnUpgrade && onDownload && onUpgrade &&
<Label kind={kinds.SUCCESS}>
{translate('OnUpgrade')}
</Label>
}
{
supportsOnRename && onRename &&
<Label kind={kinds.SUCCESS}>
{translate('OnRename')}
</Label>
}
{
supportsOnHealthIssue && onHealthIssue &&
<Label kind={kinds.SUCCESS}>
@ -132,7 +96,7 @@ class Notification extends Component {
}
{
!onGrab && !onDownload && !onRename && !onHealthIssue && !onDelete && !onApplicationUpdate ?
!onGrab && !onHealthIssue && !onApplicationUpdate ?
<Label
kind={kinds.DISABLED}
outline={true}
@ -167,17 +131,9 @@ Notification.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
onGrab: PropTypes.bool.isRequired,
onDownload: PropTypes.bool.isRequired,
onUpgrade: PropTypes.bool.isRequired,
onRename: PropTypes.bool.isRequired,
onDelete: PropTypes.bool.isRequired,
onHealthIssue: PropTypes.bool.isRequired,
onApplicationUpdate: PropTypes.bool.isRequired,
supportsOnGrab: PropTypes.bool.isRequired,
supportsOnDownload: PropTypes.bool.isRequired,
supportsOnDelete: PropTypes.bool.isRequired,
supportsOnUpgrade: PropTypes.bool.isRequired,
supportsOnRename: PropTypes.bool.isRequired,
supportsOnHealthIssue: PropTypes.bool.isRequired,
supportsOnApplicationUpdate: PropTypes.bool.isRequired,
onConfirmDeleteNotification: PropTypes.func.isRequired

@ -15,8 +15,11 @@ function NotificationEventItems(props) {
} = props;
const {
onGrab,
onHealthIssue,
onApplicationUpdate,
supportsOnGrab,
includeManualGrabs,
supportsOnHealthIssue,
includeHealthWarnings,
supportsOnApplicationUpdate
@ -31,6 +34,31 @@ function NotificationEventItems(props) {
link="https://wiki.servarr.com/prowlarr/settings#connections"
/>
<div className={styles.events}>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onGrab"
helpText={translate('OnGrabHelpText')}
isDisabled={!supportsOnGrab.value}
{...onGrab}
onChange={onInputChange}
/>
</div>
{
onGrab.value &&
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="includeManualGrabs"
helpText={translate('IncludeManualGrabsHelpText')}
isDisabled={!supportsOnGrab.value}
{...includeManualGrabs}
onChange={onInputChange}
/>
</div>
}
<div>
<FormInputGroup
type={inputTypes.CHECK}

@ -103,9 +103,6 @@ export default {
[SELECT_NOTIFICATION_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
selectedSchema.onGrab = selectedSchema.supportsOnGrab;
selectedSchema.onDownload = selectedSchema.supportsOnDownload;
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
selectedSchema.onRename = selectedSchema.supportsOnRename;
selectedSchema.onApplicationUpdate = selectedSchema.supportsOnApplicationUpdate;
return selectedSchema;

@ -1,3 +1,4 @@
import $ from 'jquery';
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
@ -229,6 +230,7 @@ export const CANCEL_FETCH_RELEASES = 'releases/cancelFetchReleases';
export const SET_RELEASES_SORT = 'releases/setReleasesSort';
export const CLEAR_RELEASES = 'releases/clearReleases';
export const GRAB_RELEASE = 'releases/grabRelease';
export const SAVE_RELEASE = 'releases/saveRelease';
export const BULK_GRAB_RELEASES = 'release/bulkGrabReleases';
export const UPDATE_RELEASE = 'releases/updateRelease';
export const SET_RELEASES_FILTER = 'releases/setReleasesFilter';
@ -243,6 +245,7 @@ export const cancelFetchReleases = createThunk(CANCEL_FETCH_RELEASES);
export const setReleasesSort = createAction(SET_RELEASES_SORT);
export const clearReleases = createAction(CLEAR_RELEASES);
export const grabRelease = createThunk(GRAB_RELEASE);
export const saveRelease = createThunk(SAVE_RELEASE);
export const bulkGrabReleases = createThunk(BULK_GRAB_RELEASES);
export const updateRelease = createAction(UPDATE_RELEASE);
export const setReleasesFilter = createAction(SET_RELEASES_FILTER);
@ -304,6 +307,32 @@ export const actionHandlers = handleThunks({
});
},
[SAVE_RELEASE]: function(getState, payload, dispatch) {
const link = payload.downloadUrl;
const file = payload.fileName;
$.ajax({
url: link,
method: 'GET',
headers: {
'X-Prowlarr-Client': true
},
xhrFields: {
responseType: 'blob'
},
success: function(data) {
const a = document.createElement('a');
const url = window.URL.createObjectURL(data);
a.href = url;
a.download = file;
document.body.append(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
}
});
},
[BULK_GRAB_RELEASES]: function(getState, payload, dispatch) {
dispatch(set({
section,

@ -16,6 +16,11 @@ function addApiKey(ajaxOptions) {
ajaxOptions.headers['X-Api-Key'] = window.Prowlarr.apiKey;
}
function addUIHeader(ajaxOptions) {
ajaxOptions.headers = ajaxOptions.headers || {};
ajaxOptions.headers['X-Prowlarr-Client'] = true;
}
function addContentType(ajaxOptions) {
if (
ajaxOptions.contentType == null &&
@ -42,6 +47,7 @@ export default function createAjaxRequest(originalAjaxOptions) {
if (isRelative(ajaxOptions)) {
addRootUrl(ajaxOptions);
addApiKey(ajaxOptions);
addUIHeader(ajaxOptions);
addContentType(ajaxOptions);
}

@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace NzbDrone.Common.Http
{

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using FluentAssertions;
using FluentValidation.Results;
using NUnit.Framework;
@ -56,6 +55,11 @@ namespace NzbDrone.Core.Test.NotificationTests
{
TestLogger.Info("OnApplicationUpdate was called");
}
public override void OnGrab(GrabMessage message)
{
TestLogger.Info("OnGrab was called");
}
}
private class TestNotificationWithNoEvents : NotificationBase<TestSetting>
@ -76,6 +80,7 @@ namespace NzbDrone.Core.Test.NotificationTests
notification.SupportsOnHealthIssue.Should().BeTrue();
notification.SupportsOnApplicationUpdate.Should().BeTrue();
notification.SupportsOnGrab.Should().BeTrue();
}
[Test]
@ -85,6 +90,7 @@ namespace NzbDrone.Core.Test.NotificationTests
notification.SupportsOnHealthIssue.Should().BeFalse();
notification.SupportsOnApplicationUpdate.Should().BeFalse();
notification.SupportsOnGrab.Should().BeFalse();
}
}
}

@ -0,0 +1,15 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(029)]
public class add_on_grab_to_notifications : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Notifications").AddColumn("OnGrab").AsBoolean().WithDefaultValue(false);
Alter.Table("Notifications").AddColumn("IncludeManualGrabs").AsBoolean().WithDefaultValue(false).NotNullable();
}
}
}

@ -66,6 +66,7 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<NotificationDefinition>("Notifications").RegisterModel()
.Ignore(x => x.ImplementationName)
.Ignore(i => i.SupportsOnGrab)
.Ignore(i => i.SupportsOnHealthIssue)
.Ignore(i => i.SupportsOnApplicationUpdate);

@ -62,6 +62,15 @@ namespace NzbDrone.Core.Download
// remoteMovie.SeedConfiguration = _seedConfigProvider.GetSeedConfiguration(remoteMovie);
var indexer = _indexerFactory.GetInstance(_indexerFactory.Get(release.IndexerId));
var grabEvent = new IndexerDownloadEvent(release, true, source, host, release.Title, release.DownloadUrl)
{
DownloadClient = downloadClient.Name,
DownloadClientId = downloadClient.Definition.Id,
DownloadClientName = downloadClient.Definition.Name,
Redirect = redirect,
GrabTrigger = source == "Prowlarr" ? GrabTrigger.Manual : GrabTrigger.Api
};
string downloadClientId;
try
{
@ -72,13 +81,15 @@ namespace NzbDrone.Core.Download
catch (ReleaseUnavailableException)
{
_logger.Trace("Release {0} no longer available on indexer.", release);
_eventAggregator.PublishEvent(new IndexerDownloadEvent(release.IndexerId, false, source, host, release.Title, release.DownloadUrl, redirect));
grabEvent.Successful = false;
_eventAggregator.PublishEvent(grabEvent);
throw;
}
catch (DownloadClientRejectedReleaseException)
{
_logger.Trace("Release {0} rejected by download client, possible duplicate.", release);
_eventAggregator.PublishEvent(new IndexerDownloadEvent(release.IndexerId, false, source, host, release.Title, release.DownloadUrl, redirect));
grabEvent.Successful = false;
_eventAggregator.PublishEvent(grabEvent);
throw;
}
catch (ReleaseDownloadException ex)
@ -92,14 +103,21 @@ namespace NzbDrone.Core.Download
_indexerStatusService.RecordFailure(release.IndexerId);
}
_eventAggregator.PublishEvent(new IndexerDownloadEvent(release.IndexerId, false, source, host, release.Title, release.DownloadUrl, redirect));
grabEvent.Successful = false;
_eventAggregator.PublishEvent(grabEvent);
throw;
}
_logger.ProgressInfo("Report sent to {0}. {1}", downloadClient.Definition.Name, downloadTitle);
_eventAggregator.PublishEvent(new IndexerDownloadEvent(release.IndexerId, true, source, host, release.Title, release.DownloadUrl, redirect));
if (!string.IsNullOrWhiteSpace(downloadClientId))
{
grabEvent.DownloadId = downloadClientId;
}
_eventAggregator.PublishEvent(grabEvent);
}
public async Task<byte[]> DownloadReport(string link, int indexerId, string source, string host, string title)
@ -117,16 +135,30 @@ namespace NzbDrone.Core.Download
var success = false;
var downloadedBytes = Array.Empty<byte>();
var release = new ReleaseInfo
{
Title = title,
DownloadUrl = link,
IndexerId = indexerId,
Indexer = indexer.Definition.Name,
DownloadProtocol = indexer.Protocol
};
var grabEvent = new IndexerDownloadEvent(release, success, source, host, release.Title, release.DownloadUrl)
{
GrabTrigger = source == "Prowlarr" ? GrabTrigger.Manual : GrabTrigger.Api
};
try
{
downloadedBytes = await indexer.Download(url);
_indexerStatusService.RecordSuccess(indexerId);
success = true;
grabEvent.Successful = true;
}
catch (ReleaseUnavailableException)
{
_logger.Trace("Release {0} no longer available on indexer.", link);
_eventAggregator.PublishEvent(new IndexerDownloadEvent(indexerId, success, source, host, title, url.AbsoluteUri));
_eventAggregator.PublishEvent(grabEvent);
throw;
}
catch (ReleaseDownloadException ex)
@ -140,19 +172,36 @@ namespace NzbDrone.Core.Download
_indexerStatusService.RecordFailure(indexerId);
}
_eventAggregator.PublishEvent(new IndexerDownloadEvent(indexerId, success, source, host, title, url.AbsoluteUri));
_eventAggregator.PublishEvent(grabEvent);
throw;
}
_logger.Trace("Downloaded {0} bytes from {1}", downloadedBytes.Length, link);
_eventAggregator.PublishEvent(new IndexerDownloadEvent(indexerId, success, source, host, title, url.AbsoluteUri));
_eventAggregator.PublishEvent(grabEvent);
return downloadedBytes;
}
public void RecordRedirect(string link, int indexerId, string source, string host, string title)
{
_eventAggregator.PublishEvent(new IndexerDownloadEvent(indexerId, true, source, host, title, link, true));
var indexer = _indexerFactory.GetInstance(_indexerFactory.Get(indexerId));
var release = new ReleaseInfo
{
Title = title,
DownloadUrl = link,
IndexerId = indexerId,
Indexer = indexer.Definition.Name,
DownloadProtocol = indexer.Protocol
};
var grabEvent = new IndexerDownloadEvent(release, true, source, host, release.Title, release.DownloadUrl)
{
Redirect = true,
GrabTrigger = source == "Prowlarr" ? GrabTrigger.Manual : GrabTrigger.Api
};
_eventAggregator.PublishEvent(grabEvent);
}
}
}

@ -185,7 +185,7 @@ namespace NzbDrone.Core.History
var history = new History
{
Date = DateTime.UtcNow,
IndexerId = message.IndexerId,
IndexerId = message.Release.IndexerId,
EventType = HistoryEventType.ReleaseGrabbed,
Successful = message.Successful
};

@ -1,26 +1,37 @@
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Events
{
public class IndexerDownloadEvent : IEvent
{
public int IndexerId { get; set; }
public ReleaseInfo Release { get; set; }
public bool Successful { get; set; }
public string Source { get; set; }
public string Host { get; set; }
public string Title { get; set; }
public bool Redirect { get; set; }
public string Url { get; set; }
public int DownloadClientId { get; set; }
public string DownloadClient { get; set; }
public string DownloadClientName { get; set; }
public string DownloadId { get; set; }
public GrabTrigger GrabTrigger { get; set; }
public IndexerDownloadEvent(int indexerId, bool successful, string source, string host, string title, string url, bool redirect = false)
public IndexerDownloadEvent(ReleaseInfo release, bool successful, string source, string host, string title, string url)
{
IndexerId = indexerId;
Release = release;
Successful = successful;
Source = source;
Host = host;
Title = title;
Redirect = redirect;
Url = url;
}
}
public enum GrabTrigger
{
Api,
Manual
}
}

@ -185,6 +185,7 @@
"IgnoredAddresses": "Ignored Addresses",
"IllRestartLater": "I'll restart later",
"IncludeHealthWarningsHelpText": "Include Health Warnings",
"IncludeManualGrabsHelpText": "Include Manual Grabs made within Prowlarr",
"Indexer": "Indexer",
"IndexerAlreadySetup": "At least one instance of indexer is already setup",
"IndexerAuth": "Indexer Auth",
@ -272,7 +273,8 @@
"Ok": "Ok",
"OnApplicationUpdate": "On Application Update",
"OnApplicationUpdateHelpText": "On Application Update",
"OnGrab": "On Grab",
"OnGrab": "On Release Grab",
"OnGrabHelpText": "On Release Grab",
"OnHealthIssue": "On Health Issue",
"OnHealthIssueHelpText": "On Health Issue",
"OpenBrowserOnStart": "Open browser on start",

@ -17,6 +17,11 @@ namespace NzbDrone.Core.Notifications.Apprise
_proxy = proxy;
}
public override void OnGrab(GrabMessage message)
{
_proxy.SendNotification(Settings, RELEASE_GRABBED_TITLE_BRANDED, $"{message.Message}");
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
_proxy.SendNotification(Settings, HEALTH_ISSUE_TITLE_BRANDED, $"{healthCheck.Message}");

@ -15,6 +15,12 @@ namespace NzbDrone.Core.Notifications.Boxcar
public override string Link => "https://boxcar.io/client";
public override string Name => "Boxcar";
public override void OnGrab(GrabMessage message)
{
_proxy.SendNotification(RELEASE_GRABBED_TITLE, message.Message, Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck message)
{
_proxy.SendNotification(HEALTH_ISSUE_TITLE, message.Message, Settings);

@ -19,6 +19,65 @@ namespace NzbDrone.Core.Notifications.Discord
public override string Name => "Discord";
public override string Link => "https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks";
public override void OnGrab(GrabMessage message)
{
var embed = new Embed
{
Author = new DiscordAuthor
{
Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author,
IconUrl = "https://raw.githubusercontent.com/Prowlarr/Prowlarr/develop/Logo/256.png"
},
Title = RELEASE_GRABBED_TITLE,
Description = message.Message,
Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"),
Color = message.Successful ? (int)DiscordColors.Success : (int)DiscordColors.Danger,
Fields = new List<DiscordField>()
};
foreach (var field in Settings.GrabFields)
{
var discordField = new DiscordField();
switch ((DiscordGrabFieldType)field)
{
case DiscordGrabFieldType.Release:
discordField.Name = "Release";
discordField.Value = string.Format("```{0}```", message.Release.Title);
break;
case DiscordGrabFieldType.Indexer:
discordField.Name = "Indexer";
discordField.Value = message.Release.Indexer ?? string.Empty;
break;
case DiscordGrabFieldType.DownloadClient:
discordField.Name = "Download Client";
discordField.Value = message.DownloadClientName ?? string.Empty;
break;
case DiscordGrabFieldType.GrabTrigger:
discordField.Name = "Grab Trigger";
discordField.Value = message.GrabTrigger.ToString() ?? string.Empty;
break;
case DiscordGrabFieldType.Source:
discordField.Name = "Source";
discordField.Value = message.Source ?? string.Empty;
break;
case DiscordGrabFieldType.Host:
discordField.Name = "Host";
discordField.Value = message.Host ?? string.Empty;
break;
}
if (discordField.Name.IsNotNullOrWhiteSpace() && discordField.Value.IsNotNullOrWhiteSpace())
{
embed.Fields.Add(discordField);
}
}
var payload = CreatePayload(null, new List<Embed> { embed });
_proxy.SendPayload(payload, Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
var attachments = new List<Embed>

@ -2,32 +2,11 @@ namespace NzbDrone.Core.Notifications.Discord
{
public enum DiscordGrabFieldType
{
Overview,
Rating,
Genres,
Quality,
Group,
Size,
Links,
Release,
Poster,
Fanart
}
public enum DiscordImportFieldType
{
Overview,
Rating,
Genres,
Quality,
Codecs,
Group,
Size,
Languages,
Subtitles,
Links,
Release,
Poster,
Fanart
Indexer,
DownloadClient,
GrabTrigger,
Source,
Host
}
}

@ -20,7 +20,6 @@ namespace NzbDrone.Core.Notifications.Discord
{
//Set Default Fields
GrabFields = new List<int> { 0, 1, 2, 3, 5, 6, 7, 8, 9 };
ImportFields = new List<int> { 0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12 };
}
private static readonly DiscordSettingsValidator Validator = new DiscordSettingsValidator();
@ -40,9 +39,6 @@ namespace NzbDrone.Core.Notifications.Discord
[FieldDefinition(4, Label = "On Grab Fields", Advanced = true, SelectOptions = typeof(DiscordGrabFieldType), HelpText = "Change the fields that are passed in for this 'on grab' notification", Type = FieldType.TagSelect)]
public IEnumerable<int> GrabFields { get; set; }
[FieldDefinition(5, Label = "On Import Fields", Advanced = true, SelectOptions = typeof(DiscordImportFieldType), HelpText = "Change the fields that are passed for this 'on import' notification", Type = FieldType.TagSelect)]
public IEnumerable<int> ImportFields { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

@ -26,6 +26,11 @@ namespace NzbDrone.Core.Notifications.Email
public override string Link => null;
public override void OnGrab(GrabMessage message)
{
SendEmail(Settings, RELEASE_GRABBED_TITLE_BRANDED, message.Message);
}
public override void OnHealthIssue(HealthCheck.HealthCheck message)
{
SendEmail(Settings, HEALTH_ISSUE_TITLE_BRANDED, message.Message);

@ -19,6 +19,11 @@ namespace NzbDrone.Core.Notifications.Gotify
public override string Name => "Gotify";
public override string Link => "https://gotify.net/";
public override void OnGrab(GrabMessage message)
{
_proxy.SendNotification(RELEASE_GRABBED_TITLE, message.Message, Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
_proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);

@ -0,0 +1,25 @@
using NzbDrone.Core.Indexers.Events;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Notifications
{
public class GrabMessage
{
public ReleaseInfo Release { get; set; }
public bool Successful { get; set; }
public string Host { get; set; }
public string Source { get; set; }
public GrabTrigger GrabTrigger { get; set; }
public bool Redirect { get; set; }
public string Message { get; set; }
public string DownloadClientType { get; set; }
public string DownloadClientName { get; set; }
public string DownloadId { get; set; }
public override string ToString()
{
return Message;
}
}
}

@ -6,9 +6,11 @@ namespace NzbDrone.Core.Notifications
{
string Link { get; }
void OnGrab(GrabMessage grabMessage);
void OnHealthIssue(HealthCheck.HealthCheck healthCheck);
void OnApplicationUpdate(ApplicationUpdateMessage updateMessage);
void ProcessQueue();
bool SupportsOnGrab { get; }
bool SupportsOnHealthIssue { get; }
bool SupportsOnApplicationUpdate { get; }
}

@ -17,6 +17,11 @@ namespace NzbDrone.Core.Notifications.Join
public override string Link => "https://joaoapps.com/join/";
public override void OnGrab(GrabMessage message)
{
_proxy.SendNotification(RELEASE_GRABBED_TITLE_BRANDED, message.Message, Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck message)
{
_proxy.SendNotification(HEALTH_ISSUE_TITLE_BRANDED, message.Message, Settings);

@ -19,6 +19,12 @@ namespace NzbDrone.Core.Notifications.Notifiarr
public override string Link => "https://notifiarr.com";
public override string Name => "Notifiarr";
public override void OnGrab(GrabMessage grabMessage)
{
_proxy.SendNotification(BuildGrabPayload(grabMessage), Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
_proxy.SendNotification(BuildHealthPayload(healthCheck), Settings);

@ -8,9 +8,11 @@ namespace NzbDrone.Core.Notifications
public abstract class NotificationBase<TSettings> : INotification
where TSettings : IProviderConfig, new()
{
protected const string RELEASE_GRABBED_TITLE = "Release Grabbed";
protected const string HEALTH_ISSUE_TITLE = "Health Check Failure";
protected const string APPLICATION_UPDATE_TITLE = "Application Updated";
protected const string RELEASE_GRABBED_TITLE_BRANDED = "Prowlarr - " + RELEASE_GRABBED_TITLE;
protected const string HEALTH_ISSUE_TITLE_BRANDED = "Prowlarr - " + HEALTH_ISSUE_TITLE;
protected const string APPLICATION_UPDATE_TITLE_BRANDED = "Prowlarr - " + APPLICATION_UPDATE_TITLE;
@ -27,6 +29,10 @@ namespace NzbDrone.Core.Notifications
public abstract string Link { get; }
public virtual void OnGrab(GrabMessage grabMessage)
{
}
public virtual void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
}
@ -39,6 +45,7 @@ namespace NzbDrone.Core.Notifications
{
}
public bool SupportsOnGrab => HasConcreteImplementation("OnGrab");
public bool SupportsOnHealthIssue => HasConcreteImplementation("OnHealthIssue");
public bool SupportsOnApplicationUpdate => HasConcreteImplementation("OnApplicationUpdate");

@ -6,10 +6,13 @@ namespace NzbDrone.Core.Notifications
{
public bool OnHealthIssue { get; set; }
public bool OnApplicationUpdate { get; set; }
public bool OnGrab { get; set; }
public bool SupportsOnGrab { get; set; }
public bool IncludeManualGrabs { get; set; }
public bool SupportsOnHealthIssue { get; set; }
public bool IncludeHealthWarnings { get; set; }
public bool SupportsOnApplicationUpdate { get; set; }
public override bool Enable => OnHealthIssue || OnApplicationUpdate;
public override bool Enable => OnHealthIssue || OnApplicationUpdate || OnGrab;
}
}

@ -9,6 +9,7 @@ namespace NzbDrone.Core.Notifications
{
public interface INotificationFactory : IProviderFactory<INotification, NotificationDefinition>
{
List<INotification> OnGrabEnabled();
List<INotification> OnHealthIssueEnabled();
List<INotification> OnApplicationUpdateEnabled();
}
@ -20,6 +21,11 @@ namespace NzbDrone.Core.Notifications
{
}
public List<INotification> OnGrabEnabled()
{
return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnGrab).ToList();
}
public List<INotification> OnHealthIssueEnabled()
{
return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnHealthIssue).ToList();
@ -34,6 +40,7 @@ namespace NzbDrone.Core.Notifications
{
base.SetProviderCharacteristics(provider, definition);
definition.SupportsOnGrab = provider.SupportsOnGrab;
definition.SupportsOnHealthIssue = provider.SupportsOnHealthIssue;
definition.SupportsOnApplicationUpdate = provider.SupportsOnApplicationUpdate;
}

@ -1,7 +1,12 @@
using System;
using System.Drawing.Drawing2D;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.HealthCheck;
using NzbDrone.Core.Indexers.Events;
using NzbDrone.Core.Indexers.PassThePopcorn;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Update.History.Events;
namespace NzbDrone.Core.Notifications
@ -9,7 +14,8 @@ namespace NzbDrone.Core.Notifications
public class NotificationService
: IHandle<HealthCheckFailedEvent>,
IHandleAsync<HealthCheckCompleteEvent>,
IHandle<UpdateInstalledEvent>
IHandle<UpdateInstalledEvent>,
IHandle<IndexerDownloadEvent>
{
private readonly INotificationFactory _notificationFactory;
private readonly Logger _logger;
@ -35,6 +41,43 @@ namespace NzbDrone.Core.Notifications
return false;
}
private bool ShouldHandleOnGrab(GrabMessage message, bool includeManual)
{
if (message.GrabTrigger == GrabTrigger.Api)
{
return true;
}
if (message.GrabTrigger == GrabTrigger.Manual && includeManual)
{
return true;
}
return false;
}
private string GetMessage(ReleaseInfo release, GrabTrigger grabTrigger, string source, string downloadClient)
{
var message = string.Format("{0} grabbed by {1} from {2}",
release.Title,
source,
release.Indexer);
if (grabTrigger == GrabTrigger.Manual)
{
message = string.Format("{0} manually grabbed in Prowlarr from {1}",
release.Title,
release.Indexer);
}
if (downloadClient.IsNotNullOrWhiteSpace())
{
message += $" and sent to {downloadClient}";
}
return message;
}
public void Handle(HealthCheckFailedEvent message)
{
// Don't send health check notifications during the start up grace period,
@ -99,5 +142,37 @@ namespace NzbDrone.Core.Notifications
}
}
}
public void Handle(IndexerDownloadEvent message)
{
var grabMessage = new GrabMessage
{
Release = message.Release,
Source = message.Source,
Host = message.Host,
Successful = message.Successful,
DownloadClientName = message.DownloadClientName,
DownloadClientType = message.DownloadClient,
DownloadId = message.DownloadId,
Redirect = message.Redirect,
GrabTrigger = message.GrabTrigger,
Message = GetMessage(message.Release, message.GrabTrigger, message.Source, message.DownloadClientName)
};
foreach (var notification in _notificationFactory.OnGrabEnabled())
{
try
{
if (ShouldHandleOnGrab(grabMessage, ((NotificationDefinition)notification.Definition).IncludeManualGrabs))
{
notification.OnGrab(grabMessage);
}
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to send OnGrab notification to {0}", notification.Definition.Name);
}
}
}
}
}

@ -17,6 +17,11 @@ namespace NzbDrone.Core.Notifications.Ntfy
public override string Link => "https://ntfy.sh/";
public override void OnGrab(GrabMessage message)
{
_proxy.SendNotification(RELEASE_GRABBED_TITLE_BRANDED, message.Message, Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck message)
{
_proxy.SendNotification(HEALTH_ISSUE_TITLE_BRANDED, message.Message, Settings);

@ -16,6 +16,11 @@ namespace NzbDrone.Core.Notifications.Prowl
public override string Link => "https://www.prowlapp.com/";
public override string Name => "Prowl";
public override void OnGrab(GrabMessage message)
{
_prowlProxy.SendNotification(RELEASE_GRABBED_TITLE, message.Message, Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
_prowlProxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);

@ -19,6 +19,11 @@ namespace NzbDrone.Core.Notifications.PushBullet
public override string Name => "Pushbullet";
public override string Link => "https://www.pushbullet.com/";
public override void OnGrab(GrabMessage message)
{
_proxy.SendNotification(RELEASE_GRABBED_TITLE_BRANDED, message.Message, Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
_proxy.SendNotification(HEALTH_ISSUE_TITLE_BRANDED, healthCheck.Message, Settings);

@ -16,6 +16,11 @@ namespace NzbDrone.Core.Notifications.Pushover
public override string Name => "Pushover";
public override string Link => "https://pushover.net/";
public override void OnGrab(GrabMessage message)
{
_proxy.SendNotification(RELEASE_GRABBED_TITLE, message.Message, Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
_proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);

@ -19,6 +19,11 @@ namespace NzbDrone.Core.Notifications.SendGrid
public override string Name => "SendGrid";
public override string Link => "https://sendgrid.com/";
public override void OnGrab(GrabMessage message)
{
_proxy.SendNotification(RELEASE_GRABBED_TITLE, message.Message, Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
_proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);

@ -16,6 +16,11 @@ namespace NzbDrone.Core.Notifications.Simplepush
public override string Name => "Simplepush";
public override string Link => "https://simplepush.io/";
public override void OnGrab(GrabMessage message)
{
_proxy.SendNotification(RELEASE_GRABBED_TITLE, message.Message, Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
_proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);

@ -16,6 +16,11 @@ namespace NzbDrone.Core.Notifications.Telegram
public override string Name => "Telegram";
public override string Link => "https://telegram.org/";
public override void OnGrab(GrabMessage message)
{
_proxy.SendNotification(RELEASE_GRABBED_TITLE, message.Message, Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
_proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);

@ -2,6 +2,7 @@ using System.Collections.Generic;
using FluentValidation.Results;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.HealthCheck;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Twitter
@ -18,6 +19,11 @@ namespace NzbDrone.Core.Notifications.Twitter
public override string Name => "Twitter";
public override string Link => "https://twitter.com/";
public override void OnGrab(GrabMessage message)
{
_twitterService.SendNotification($"Release Grabbed: {message.Message}", Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
_twitterService.SendNotification($"Health Issue: {healthCheck.Message}", Settings);

@ -18,6 +18,11 @@ namespace NzbDrone.Core.Notifications.Webhook
public override string Link => "https://wiki.servarr.com/prowlarr/settings#connect";
public override void OnGrab(GrabMessage grabMessage)
{
_proxy.SendWebhook(BuildGrabPayload(grabMessage), Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
_proxy.SendWebhook(BuildHealthPayload(healthCheck), Settings);

@ -14,6 +14,22 @@ namespace NzbDrone.Core.Notifications.Webhook
_configFileProvider = configFileProvider;
}
public WebhookGrabPayload BuildGrabPayload(GrabMessage message)
{
return new WebhookGrabPayload
{
EventType = WebhookEventType.Grab,
InstanceName = _configFileProvider.InstanceName,
Release = new WebhookRelease(message.Release),
Trigger = message.GrabTrigger,
Source = message.Source,
Host = message.Host,
DownloadClient = message.DownloadClientName,
DownloadClientType = message.DownloadClientType,
DownloadId = message.DownloadId
};
}
protected WebhookHealthPayload BuildHealthPayload(HealthCheck.HealthCheck healthCheck)
{
return new WebhookHealthPayload

@ -0,0 +1,15 @@
using NzbDrone.Core.Indexers.Events;
namespace NzbDrone.Core.Notifications.Webhook
{
public class WebhookGrabPayload : WebhookPayload
{
public WebhookRelease Release { get; set; }
public GrabTrigger Trigger { get; set; }
public string Source { get; set; }
public string Host { get; set; }
public string DownloadClient { get; set; }
public string DownloadClientType { get; set; }
public string DownloadId { get; set; }
}
}

@ -0,0 +1,22 @@
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Notifications.Webhook
{
public class WebhookRelease
{
public WebhookRelease()
{
}
public WebhookRelease(ReleaseInfo release)
{
ReleaseTitle = release.Title;
Indexer = release.Indexer;
Size = release.Size;
}
public string ReleaseTitle { get; set; }
public string Indexer { get; set; }
public long? Size { get; set; }
}
}

@ -55,7 +55,7 @@ namespace NzbDrone.Api.V1.Indexers
public async Task<IActionResult> GetNewznabResponse(int id, [FromQuery] NewznabRequest request)
{
var requestType = request.t;
request.source = UserAgentParser.ParseSource(Request.Headers["User-Agent"]);
request.source = Request.GetSource();
request.server = Request.GetServerUrl();
request.host = Request.GetHostName();
@ -232,7 +232,7 @@ namespace NzbDrone.Api.V1.Indexers
file = WebUtility.UrlDecode(file);
var source = UserAgentParser.ParseSource(Request.Headers["User-Agent"]);
var source = Request.GetSource();
var host = Request.GetHostName();
var unprotectedlLink = _downloadMappingService.ConvertToNormalLink(link);

@ -5,8 +5,11 @@ namespace Prowlarr.Api.V1.Notifications
public class NotificationResource : ProviderResource<NotificationResource>
{
public string Link { get; set; }
public bool OnGrab { get; set; }
public bool OnHealthIssue { get; set; }
public bool OnApplicationUpdate { get; set; }
public bool SupportsOnGrab { get; set; }
public bool IncludeManualGrabs { get; set; }
public bool SupportsOnHealthIssue { get; set; }
public bool IncludeHealthWarnings { get; set; }
public bool SupportsOnApplicationUpdate { get; set; }
@ -24,6 +27,9 @@ namespace Prowlarr.Api.V1.Notifications
var resource = base.ToResource(definition);
resource.OnGrab = definition.OnGrab;
resource.SupportsOnGrab = definition.SupportsOnGrab;
resource.IncludeManualGrabs = definition.IncludeManualGrabs;
resource.OnHealthIssue = definition.OnHealthIssue;
resource.SupportsOnHealthIssue = definition.SupportsOnHealthIssue;
resource.IncludeHealthWarnings = definition.IncludeHealthWarnings;
@ -42,6 +48,9 @@ namespace Prowlarr.Api.V1.Notifications
var definition = base.ToModel(resource);
definition.OnGrab = resource.OnGrab;
definition.SupportsOnGrab = resource.SupportsOnGrab;
definition.IncludeManualGrabs = resource.IncludeManualGrabs;
definition.OnHealthIssue = resource.OnHealthIssue;
definition.SupportsOnHealthIssue = resource.SupportsOnHealthIssue;
definition.IncludeHealthWarnings = resource.IncludeHealthWarnings;

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Routing.Constraints;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using Prowlarr.Http.REST;
@ -36,6 +37,21 @@ namespace Prowlarr.Api.V1.Search
public int? Seeders { get; set; }
public int? Leechers { get; set; }
public DownloadProtocol Protocol { get; set; }
public string FileName
{
get
{
var extension = "torrent";
if (Protocol == DownloadProtocol.Usenet)
{
extension = "nzb";
}
return $"{Title}.{extension}";
}
}
}
public static class ReleaseResourceMapper

@ -59,7 +59,7 @@ namespace Prowlarr.Api.V1.Search
var releaseInfo = _remoteReleaseCache.Find(GetCacheKey(release));
var indexerDef = _indexerFactory.Get(release.IndexerId);
var source = UserAgentParser.ParseSource(Request.Headers["User-Agent"]);
var source = Request.GetSource();
var host = Request.GetHostName();
try
@ -78,7 +78,7 @@ namespace Prowlarr.Api.V1.Search
[HttpPost("bulk")]
public ActionResult<ReleaseResource> GrabReleases(List<ReleaseResource> releases)
{
var source = UserAgentParser.ParseSource(Request.Headers["User-Agent"]);
var source = Request.GetSource();
var host = Request.GetHostName();
var groupedReleases = releases.GroupBy(r => r.IndexerId);

@ -4,9 +4,7 @@ using System.Linq;
using System.Net;
using Microsoft.AspNetCore.Http;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Exceptions;
namespace Prowlarr.Http.Extensions
{
@ -129,6 +127,16 @@ namespace Prowlarr.Http.Extensions
return remoteIP.ToString();
}
public static string GetSource(this HttpRequest request)
{
if (request.Headers.TryGetValue("X-Prowlarr-Client", out var source))
{
return "Prowlarr";
}
return NzbDrone.Common.Http.UserAgentParser.ParseSource(request.Headers["User-Agent"]);
}
public static string GetHostName(this HttpRequest request)
{
string ip = request.GetRemoteIP();

Loading…
Cancel
Save