+
+
+
+
+ {
+ onGrab.value &&
+
+
+
+ }
+
{
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;
diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js
index 5e3b172a2..f12c9b72b 100644
--- a/frontend/src/Store/Actions/releaseActions.js
+++ b/frontend/src/Store/Actions/releaseActions.js
@@ -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,
diff --git a/frontend/src/Utilities/createAjaxRequest.js b/frontend/src/Utilities/createAjaxRequest.js
index a91619703..11a40418a 100644
--- a/frontend/src/Utilities/createAjaxRequest.js
+++ b/frontend/src/Utilities/createAjaxRequest.js
@@ -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);
}
diff --git a/src/NzbDrone.Common/Http/UserAgentParser.cs b/src/NzbDrone.Common/Http/UserAgentParser.cs
index 8eb5f6d6a..a73b627b8 100644
--- a/src/NzbDrone.Common/Http/UserAgentParser.cs
+++ b/src/NzbDrone.Common/Http/UserAgentParser.cs
@@ -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
{
diff --git a/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs
index 35fe385e8..366687860 100644
--- a/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs
+++ b/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs
@@ -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
@@ -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();
}
}
}
diff --git a/src/NzbDrone.Core/Datastore/Migration/029_add_on_grab_to_notifications.cs b/src/NzbDrone.Core/Datastore/Migration/029_add_on_grab_to_notifications.cs
new file mode 100644
index 000000000..079ec92d4
--- /dev/null
+++ b/src/NzbDrone.Core/Datastore/Migration/029_add_on_grab_to_notifications.cs
@@ -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();
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs
index b26c016e6..81337e682 100644
--- a/src/NzbDrone.Core/Datastore/TableMapping.cs
+++ b/src/NzbDrone.Core/Datastore/TableMapping.cs
@@ -66,6 +66,7 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity("Notifications").RegisterModel()
.Ignore(x => x.ImplementationName)
+ .Ignore(i => i.SupportsOnGrab)
.Ignore(i => i.SupportsOnHealthIssue)
.Ignore(i => i.SupportsOnApplicationUpdate);
diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs
index d28b801c4..49085b6e4 100644
--- a/src/NzbDrone.Core/Download/DownloadService.cs
+++ b/src/NzbDrone.Core/Download/DownloadService.cs
@@ -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 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();
+ 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);
}
}
}
diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs
index 27c53d29c..676e28fee 100644
--- a/src/NzbDrone.Core/History/HistoryService.cs
+++ b/src/NzbDrone.Core/History/HistoryService.cs
@@ -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
};
diff --git a/src/NzbDrone.Core/Indexers/Events/IndexerDownloadEvent.cs b/src/NzbDrone.Core/Indexers/Events/IndexerDownloadEvent.cs
index ed895adf5..ae1495dd0 100644
--- a/src/NzbDrone.Core/Indexers/Events/IndexerDownloadEvent.cs
+++ b/src/NzbDrone.Core/Indexers/Events/IndexerDownloadEvent.cs
@@ -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
+ }
}
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index 952022e76..60ddf3018 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -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",
diff --git a/src/NzbDrone.Core/Notifications/Apprise/Apprise.cs b/src/NzbDrone.Core/Notifications/Apprise/Apprise.cs
index 4eae3476a..91b237728 100644
--- a/src/NzbDrone.Core/Notifications/Apprise/Apprise.cs
+++ b/src/NzbDrone.Core/Notifications/Apprise/Apprise.cs
@@ -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}");
diff --git a/src/NzbDrone.Core/Notifications/Boxcar/Boxcar.cs b/src/NzbDrone.Core/Notifications/Boxcar/Boxcar.cs
index db6908019..c4caa0435 100644
--- a/src/NzbDrone.Core/Notifications/Boxcar/Boxcar.cs
+++ b/src/NzbDrone.Core/Notifications/Boxcar/Boxcar.cs
@@ -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);
diff --git a/src/NzbDrone.Core/Notifications/Discord/Discord.cs b/src/NzbDrone.Core/Notifications/Discord/Discord.cs
index 2985af45a..ca69464a4 100644
--- a/src/NzbDrone.Core/Notifications/Discord/Discord.cs
+++ b/src/NzbDrone.Core/Notifications/Discord/Discord.cs
@@ -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()
+ };
+
+ 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