1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-19 21:46:43 -04:00

New: Optional message for marking as failed via API

Closes #7775
This commit is contained in:
Mark McDowall
2025-09-29 20:46:07 -07:00
parent 858c690543
commit a5ea19ddfb
19 changed files with 121 additions and 29 deletions
@@ -16,13 +16,19 @@ interface BlocklistDetailsModalProps {
protocol: DownloadProtocol; protocol: DownloadProtocol;
indexer?: string; indexer?: string;
message?: string; message?: string;
source?: string;
onModalClose: () => void; onModalClose: () => void;
} }
function BlocklistDetailsModal(props: BlocklistDetailsModalProps) { function BlocklistDetailsModal({
const { isOpen, sourceTitle, protocol, indexer, message, onModalClose } = isOpen,
props; sourceTitle,
protocol,
indexer,
message,
source,
onModalClose,
}: BlocklistDetailsModalProps) {
return ( return (
<Modal isOpen={isOpen} onModalClose={onModalClose}> <Modal isOpen={isOpen} onModalClose={onModalClose}>
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
@@ -50,6 +56,9 @@ function BlocklistDetailsModal(props: BlocklistDetailsModalProps) {
data={message} data={message}
/> />
) : null} ) : null}
{source ? (
<DescriptionListItem title={translate('Source')} data={source} />
) : null}
</DescriptionList> </DescriptionList>
</ModalBody> </ModalBody>
@@ -37,6 +37,7 @@ function BlocklistRow(props: BlocklistRowProps) {
protocol, protocol,
indexer, indexer,
message, message,
source,
isSelected, isSelected,
columns, columns,
onSelectedChange, onSelectedChange,
@@ -154,6 +155,7 @@ function BlocklistRow(props: BlocklistRowProps) {
protocol={protocol} protocol={protocol}
indexer={indexer} indexer={indexer}
message={message} message={message}
source={source}
onModalClose={handleDetailsModalClose} onModalClose={handleDetailsModalClose}
/> />
</TableRow> </TableRow>
@@ -174,7 +174,7 @@ function HistoryDetails(props: HistoryDetailsProps) {
} }
if (eventType === 'downloadFailed') { if (eventType === 'downloadFailed') {
const { message, indexer } = data as DownloadFailedHistory; const { indexer, message, source } = data as DownloadFailedHistory;
return ( return (
<DescriptionList> <DescriptionList>
@@ -195,6 +195,10 @@ function HistoryDetails(props: HistoryDetailsProps) {
{message ? ( {message ? (
<DescriptionListItem title={translate('Message')} data={message} /> <DescriptionListItem title={translate('Message')} data={message} />
) : null} ) : null}
{source ? (
<DescriptionListItem title={translate('Source')} data={source} />
) : null}
</DescriptionList> </DescriptionList>
); );
} }
@@ -20,6 +20,7 @@ function useApiMutation<T, TData>(options: MutationOptions<T, TData>) {
headers: { headers: {
...options.headers, ...options.headers,
'X-Api-Key': window.Sonarr.apiKey, 'X-Api-Key': window.Sonarr.apiKey,
'X-Sonarr-Client': 'Sonarr',
}, },
}; };
}, [options]); }, [options]);
@@ -26,6 +26,7 @@ const useApiQuery = <T>(options: QueryOptions<T>) => {
headers: { headers: {
...options.headers, ...options.headers,
'X-Api-Key': window.Sonarr.apiKey, 'X-Api-Key': window.Sonarr.apiKey,
'X-Sonarr-Client': 'Sonarr',
}, },
}, },
}; };
@@ -64,6 +64,7 @@ const usePagedApiQuery = <T>(options: PagedQueryOptions<T>) => {
headers: { headers: {
...options.headers, ...options.headers,
'X-Api-Key': window.Sonarr.apiKey, 'X-Api-Key': window.Sonarr.apiKey,
'X-Sonarr-Client': 'Sonarr',
}, },
}, },
}; };
+1
View File
@@ -15,6 +15,7 @@ interface Blocklist extends ModelBase {
seriesId?: number; seriesId?: number;
indexer?: string; indexer?: string;
message?: string; message?: string;
source?: string;
} }
export default Blocklist; export default Blocklist;
+1
View File
@@ -37,6 +37,7 @@ export interface GrabbedHistoryData {
export interface DownloadFailedHistory { export interface DownloadFailedHistory {
message: string; message: string;
indexer?: string; indexer?: string;
source?: string;
} }
export interface DownloadFolderImportedHistory { export interface DownloadFolderImportedHistory {
@@ -0,0 +1,23 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Test.Common;
namespace NzbDrone.Common.Test.Http;
[TestFixture]
public class UserAgentParserFixture : TestBase
{
// Ref *Arr `_userAgent = $"{BuildInfo.AppName}/{BuildInfo.Version} ({osName} {osVersion})";`
// Ref Mylar `Mylar3/' +str(hash) +'(' +vers +') +http://www.github.com/mylar3/mylar3/`
[TestCase("Mylar3/ 3ee23rh23irqfq (13123123) http://www.github.com/mylar3/mylar3/", "Mylar3")]
[TestCase("Lidarr/1.0.0.2300 (ubuntu 20.04)", "Lidarr")]
[TestCase("Radarr/1.0.0.2300 (ubuntu 20.04)", "Radarr")]
[TestCase("Readarr/1.0.0.2300 (ubuntu 20.04)", "Readarr")]
[TestCase("Sonarr/3.0.6.9999 (ubuntu 20.04)", "Sonarr")]
[TestCase("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36", "Other")]
public void should_parse_user_agent(string userAgent, string parsedAgent)
{
UserAgentParser.ParseSource(userAgent).Should().Be(parsedAgent);
}
}
@@ -1,7 +1,12 @@
using System.Text.RegularExpressions;
namespace NzbDrone.Common.Http namespace NzbDrone.Common.Http
{ {
public static class UserAgentParser public static class UserAgentParser
{ {
private static readonly Regex AppSourceRegex = new(@"(?<agent>[a-z0-9]*)\/.*(?:\(.*\))?",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
public static string SimplifyUserAgent(string userAgent) public static string SimplifyUserAgent(string userAgent)
{ {
if (userAgent == null || userAgent.StartsWith("Mozilla/5.0")) if (userAgent == null || userAgent.StartsWith("Mozilla/5.0"))
@@ -11,5 +16,17 @@ namespace NzbDrone.Common.Http
return userAgent; return userAgent;
} }
public static string ParseSource(string userAgent)
{
var match = AppSourceRegex.Match(SimplifyUserAgent(userAgent) ?? string.Empty);
if (match.Groups["agent"].Success)
{
return match.Groups["agent"].Value;
}
return "Other";
}
} }
} }
@@ -24,6 +24,7 @@ namespace NzbDrone.Core.Blocklisting
public IndexerFlags IndexerFlags { get; set; } public IndexerFlags IndexerFlags { get; set; }
public ReleaseType ReleaseType { get; set; } public ReleaseType ReleaseType { get; set; }
public string Message { get; set; } public string Message { get; set; }
public string Source { get; set; }
public string TorrentInfoHash { get; set; } public string TorrentInfoHash { get; set; }
public List<Language> Languages { get; set; } public List<Language> Languages { get; set; }
} }
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Blocklisting
bool Blocklisted(int seriesId, ReleaseInfo release); bool Blocklisted(int seriesId, ReleaseInfo release);
bool BlocklistedTorrentHash(int seriesId, string hash); bool BlocklistedTorrentHash(int seriesId, string hash);
PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec); PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec);
void Block(RemoteEpisode remoteEpisode, string message); void Block(RemoteEpisode remoteEpisode, string message, string source);
void Delete(int id); void Delete(int id);
void Delete(List<int> ids); void Delete(List<int> ids);
} }
@@ -71,7 +71,7 @@ namespace NzbDrone.Core.Blocklisting
return _blocklistRepository.GetPaged(pagingSpec); return _blocklistRepository.GetPaged(pagingSpec);
} }
public void Block(RemoteEpisode remoteEpisode, string message) public void Block(RemoteEpisode remoteEpisode, string message, string source)
{ {
var blocklist = new Blocklist var blocklist = new Blocklist
{ {
@@ -85,6 +85,7 @@ namespace NzbDrone.Core.Blocklisting
Indexer = remoteEpisode.Release.Indexer, Indexer = remoteEpisode.Release.Indexer,
Protocol = remoteEpisode.Release.DownloadProtocol, Protocol = remoteEpisode.Release.DownloadProtocol,
Message = message, Message = message,
Source = source,
Languages = remoteEpisode.ParsedEpisodeInfo.Languages Languages = remoteEpisode.ParsedEpisodeInfo.Languages
}; };
@@ -185,6 +186,7 @@ namespace NzbDrone.Core.Blocklisting
Indexer = message.Data.GetValueOrDefault("indexer"), Indexer = message.Data.GetValueOrDefault("indexer"),
Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")), Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")),
Message = message.Message, Message = message.Message,
Source = message.Source,
Languages = message.Languages, Languages = message.Languages,
TorrentInfoHash = message.TrackedDownload?.Protocol == DownloadProtocol.Torrent TorrentInfoHash = message.TrackedDownload?.Protocol == DownloadProtocol.Torrent
? message.TrackedDownload.DownloadItem.DownloadId ? message.TrackedDownload.DownloadItem.DownloadId
@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(223)]
public class add_source_to_blocklist : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Blocklist").AddColumn("Source").AsString().Nullable();
}
}
}
@@ -21,6 +21,7 @@ namespace NzbDrone.Core.Download
public string DownloadClient { get; set; } public string DownloadClient { get; set; }
public string DownloadId { get; set; } public string DownloadId { get; set; }
public string Message { get; set; } public string Message { get; set; }
public string Source { get; set; }
public Dictionary<string, string> Data { get; set; } public Dictionary<string, string> Data { get; set; }
public TrackedDownload TrackedDownload { get; set; } public TrackedDownload TrackedDownload { get; set; }
public List<Language> Languages { get; set; } public List<Language> Languages { get; set; }
@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.History; using NzbDrone.Core.History;
@@ -11,8 +12,8 @@ namespace NzbDrone.Core.Download
{ {
public interface IFailedDownloadService public interface IFailedDownloadService
{ {
void MarkAsFailed(int historyId, bool skipRedownload = false); void MarkAsFailed(int historyId, string message = null, string source = null, bool skipRedownload = false);
void MarkAsFailed(TrackedDownload trackedDownload, bool skipRedownload = false); void MarkAsFailed(TrackedDownload trackedDownload, string message = null, string source = null, bool skipRedownload = false);
void Check(TrackedDownload trackedDownload); void Check(TrackedDownload trackedDownload);
void ProcessFailed(TrackedDownload trackedDownload); void ProcessFailed(TrackedDownload trackedDownload);
} }
@@ -30,15 +31,16 @@ namespace NzbDrone.Core.Download
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
} }
public void MarkAsFailed(int historyId, bool skipRedownload = false) public void MarkAsFailed(int historyId, string message, string source = null, bool skipRedownload = false)
{ {
var history = _historyService.Get(historyId); message ??= "Manually marked as failed";
var history = _historyService.Get(historyId);
var downloadId = history.DownloadId; var downloadId = history.DownloadId;
if (downloadId.IsNullOrWhiteSpace()) if (downloadId.IsNullOrWhiteSpace())
{ {
PublishDownloadFailedEvent(history, new List<int> { history.EpisodeId }, "Manually marked as failed", skipRedownload: skipRedownload); PublishDownloadFailedEvent(history, new List<int> { history.EpisodeId }, message, source, skipRedownload: skipRedownload);
return; return;
} }
@@ -55,16 +57,16 @@ namespace NzbDrone.Core.Download
grabbedHistory.AddRange(GetGrabbedHistory(downloadId)); grabbedHistory.AddRange(GetGrabbedHistory(downloadId));
grabbedHistory = grabbedHistory.DistinctBy(h => h.Id).ToList(); grabbedHistory = grabbedHistory.DistinctBy(h => h.Id).ToList();
PublishDownloadFailedEvent(history, GetEpisodeIds(grabbedHistory), "Manually marked as failed"); PublishDownloadFailedEvent(history, GetEpisodeIds(grabbedHistory), message, source);
} }
public void MarkAsFailed(TrackedDownload trackedDownload, bool skipRedownload = false) public void MarkAsFailed(TrackedDownload trackedDownload, string message, string source = null, bool skipRedownload = false)
{ {
var history = GetGrabbedHistory(trackedDownload.DownloadItem.DownloadId); var history = GetGrabbedHistory(trackedDownload.DownloadItem.DownloadId);
if (history.Any()) if (history.Any())
{ {
PublishDownloadFailedEvent(history.First(), GetEpisodeIds(history), "Manually marked as failed", trackedDownload, skipRedownload: skipRedownload); PublishDownloadFailedEvent(history.First(), GetEpisodeIds(history), message ?? "Manually marked as failed", source, trackedDownload, skipRedownload: skipRedownload);
} }
} }
@@ -117,10 +119,10 @@ namespace NzbDrone.Core.Download
} }
trackedDownload.State = TrackedDownloadState.Failed; trackedDownload.State = TrackedDownloadState.Failed;
PublishDownloadFailedEvent(grabbedItems.First(), GetEpisodeIds(grabbedItems), failure, trackedDownload); PublishDownloadFailedEvent(grabbedItems.First(), GetEpisodeIds(grabbedItems), failure, $"{BuildInfo.AppName} Failed Download Handling", trackedDownload);
} }
private void PublishDownloadFailedEvent(EpisodeHistory historyItem, List<int> episodeIds, string message, TrackedDownload trackedDownload = null, bool skipRedownload = false) private void PublishDownloadFailedEvent(EpisodeHistory historyItem, List<int> episodeIds, string message, string source, TrackedDownload trackedDownload = null, bool skipRedownload = false)
{ {
Enum.TryParse(historyItem.Data.GetValueOrDefault(EpisodeHistory.RELEASE_SOURCE, ReleaseSourceType.Unknown.ToString()), out ReleaseSourceType releaseSource); Enum.TryParse(historyItem.Data.GetValueOrDefault(EpisodeHistory.RELEASE_SOURCE, ReleaseSourceType.Unknown.ToString()), out ReleaseSourceType releaseSource);
@@ -133,6 +135,7 @@ namespace NzbDrone.Core.Download
DownloadClient = historyItem.Data.GetValueOrDefault(EpisodeHistory.DOWNLOAD_CLIENT), DownloadClient = historyItem.Data.GetValueOrDefault(EpisodeHistory.DOWNLOAD_CLIENT),
DownloadId = historyItem.DownloadId, DownloadId = historyItem.DownloadId,
Message = message, Message = message,
Source = source,
Data = historyItem.Data, Data = historyItem.Data,
TrackedDownload = trackedDownload, TrackedDownload = trackedDownload,
Languages = historyItem.Languages, Languages = historyItem.Languages,
@@ -249,6 +249,7 @@ namespace NzbDrone.Core.History
history.Data.Add("DownloadClient", message.DownloadClient); history.Data.Add("DownloadClient", message.DownloadClient);
history.Data.Add("DownloadClientName", message.TrackedDownload?.DownloadItem.DownloadClientInfo.Name); history.Data.Add("DownloadClientName", message.TrackedDownload?.DownloadItem.DownloadClientInfo.Name);
history.Data.Add("Message", message.Message); history.Data.Add("Message", message.Message);
history.Data.Add("Source", message.Source);
history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteEpisode?.ParsedEpisodeInfo?.ReleaseGroup ?? message.Data.GetValueOrDefault(EpisodeHistory.RELEASE_GROUP)); history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteEpisode?.ParsedEpisodeInfo?.ReleaseGroup ?? message.Data.GetValueOrDefault(EpisodeHistory.RELEASE_GROUP));
history.Data.Add("Size", message.TrackedDownload?.DownloadItem.TotalSize.ToString() ?? message.Data.GetValueOrDefault(EpisodeHistory.SIZE)); history.Data.Add("Size", message.TrackedDownload?.DownloadItem.TotalSize.ToString() ?? message.Data.GetValueOrDefault(EpisodeHistory.SIZE));
history.Data.Add("Indexer", message.TrackedDownload?.RemoteEpisode?.Release?.Indexer ?? message.Data.GetValueOrDefault(EpisodeHistory.INDEXER)); history.Data.Add("Indexer", message.TrackedDownload?.RemoteEpisode?.Release?.Indexer ?? message.Data.GetValueOrDefault(EpisodeHistory.INDEXER));
+2 -2
View File
@@ -323,7 +323,7 @@ namespace Sonarr.Api.V3.Queue
{ {
if (blocklist) if (blocklist)
{ {
_blocklistService.Block(pendingRelease.RemoteEpisode, "Pending release manually blocklisted"); _blocklistService.Block(pendingRelease.RemoteEpisode, "Pending release manually blocklisted", null);
} }
_pendingReleaseService.RemovePendingQueueItemsObsolete(pendingRelease.Id); _pendingReleaseService.RemovePendingQueueItemsObsolete(pendingRelease.Id);
@@ -356,7 +356,7 @@ namespace Sonarr.Api.V3.Queue
if (blocklist) if (blocklist)
{ {
_failedDownloadService.MarkAsFailed(trackedDownload, skipRedownload); _failedDownloadService.MarkAsFailed(trackedDownload, null, null, skipRedownload);
} }
if (!removeFromClient && !blocklist && !changeCategory) if (!removeFromClient && !blocklist && !changeCategory)
+10 -10
View File
@@ -68,13 +68,13 @@ namespace Sonarr.Api.V5.Queue
} }
[RestDeleteById] [RestDeleteById]
public ActionResult RemoveAction(int id, bool removeFromClient = true, bool blocklist = false, bool skipRedownload = false, bool changeCategory = false) public ActionResult RemoveAction(int id, string? message = null, bool removeFromClient = true, bool blocklist = false, bool skipRedownload = false, bool changeCategory = false)
{ {
var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id);
if (pendingRelease != null) if (pendingRelease != null)
{ {
Remove(pendingRelease, blocklist); Remove(pendingRelease, message, blocklist);
return Deleted(); return Deleted();
} }
@@ -86,14 +86,14 @@ namespace Sonarr.Api.V5.Queue
throw new NotFoundException(); throw new NotFoundException();
} }
Remove(trackedDownload, removeFromClient, blocklist, skipRedownload, changeCategory); Remove(trackedDownload, message, removeFromClient, blocklist, skipRedownload, changeCategory);
_trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId); _trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId);
return Deleted(); return Deleted();
} }
[HttpDelete("bulk")] [HttpDelete("bulk")]
public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] bool removeFromClient = true, [FromQuery] bool blocklist = false, [FromQuery] bool skipRedownload = false, [FromQuery] bool changeCategory = false) public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] string? message, [FromQuery] bool removeFromClient = true, [FromQuery] bool blocklist = false, [FromQuery] bool skipRedownload = false, [FromQuery] bool changeCategory = false)
{ {
var trackedDownloadIds = new List<string>(); var trackedDownloadIds = new List<string>();
var pendingToRemove = new List<NzbDrone.Core.Queue.Queue>(); var pendingToRemove = new List<NzbDrone.Core.Queue.Queue>();
@@ -119,12 +119,12 @@ namespace Sonarr.Api.V5.Queue
foreach (var pendingRelease in pendingToRemove.DistinctBy(p => p.Id)) foreach (var pendingRelease in pendingToRemove.DistinctBy(p => p.Id))
{ {
Remove(pendingRelease, blocklist); Remove(pendingRelease, message, blocklist);
} }
foreach (var trackedDownload in trackedToRemove.DistinctBy(t => t.DownloadItem.DownloadId)) foreach (var trackedDownload in trackedToRemove.DistinctBy(t => t.DownloadItem.DownloadId))
{ {
Remove(trackedDownload, removeFromClient, blocklist, skipRedownload, changeCategory); Remove(trackedDownload, message, removeFromClient, blocklist, skipRedownload, changeCategory);
trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId); trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId);
} }
@@ -314,17 +314,17 @@ namespace Sonarr.Api.V5.Queue
} }
} }
private void Remove(NzbDrone.Core.Queue.Queue pendingRelease, bool blocklist) private void Remove(NzbDrone.Core.Queue.Queue pendingRelease, string? message, bool blocklist)
{ {
if (blocklist) if (blocklist)
{ {
_blocklistService.Block(pendingRelease.RemoteEpisode, "Pending release manually blocklisted"); _blocklistService.Block(pendingRelease.RemoteEpisode, message ?? "Pending release manually blocklisted", Request.GetSource());
} }
_pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id); _pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id);
} }
private TrackedDownload? Remove(TrackedDownload trackedDownload, bool removeFromClient, bool blocklist, bool skipRedownload, bool changeCategory) private TrackedDownload? Remove(TrackedDownload trackedDownload, string? message, bool removeFromClient, bool blocklist, bool skipRedownload, bool changeCategory)
{ {
if (removeFromClient) if (removeFromClient)
{ {
@@ -351,7 +351,7 @@ namespace Sonarr.Api.V5.Queue
if (blocklist) if (blocklist)
{ {
_failedDownloadService.MarkAsFailed(trackedDownload, skipRedownload); _failedDownloadService.MarkAsFailed(trackedDownload, message, Request.GetSource(), skipRedownload);
} }
if (!removeFromClient && !blocklist && !changeCategory) if (!removeFromClient && !blocklist && !changeCategory)
@@ -97,6 +97,16 @@ namespace Sonarr.Http.Extensions
return remoteIP.ToString(); return remoteIP.ToString();
} }
public static string GetSource(this HttpRequest request)
{
if (request.Headers.TryGetValue("X-Sonarr-Client", out var source))
{
return source;
}
return NzbDrone.Common.Http.UserAgentParser.ParseSource(request.Headers["User-Agent"]);
}
public static void DisableCache(this IHeaderDictionary headers) public static void DisableCache(this IHeaderDictionary headers)
{ {
headers.Remove("Last-Modified"); headers.Remove("Last-Modified");