Remove stalled downloads (#21)

This commit is contained in:
Marius Nechifor
2024-12-16 23:20:32 +02:00
parent 0a6ec21c95
commit 64bb9fc513
53 changed files with 522 additions and 160 deletions
+59 -5
View File
@@ -1,8 +1,10 @@
using Common.Configuration;
using Common.Configuration.Arr;
using Common.Configuration.Arr;
using Common.Configuration.Logging;
using Domain.Arr.Queue;
using Common.Configuration.QueueCleaner;
using Domain.Enums;
using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
@@ -14,17 +16,28 @@ public abstract class ArrClient
protected readonly ILogger<ArrClient> _logger;
protected readonly HttpClient _httpClient;
protected readonly LoggingConfig _loggingConfig;
protected readonly QueueCleanerConfig _queueCleanerConfig;
protected readonly Striker _striker;
protected ArrClient(ILogger<ArrClient> logger, IHttpClientFactory httpClientFactory, IOptions<LoggingConfig> loggingConfig)
protected ArrClient(
ILogger<ArrClient> logger,
IHttpClientFactory httpClientFactory,
IOptions<LoggingConfig> loggingConfig,
IOptions<QueueCleanerConfig> queueCleanerConfig,
Striker striker
)
{
_logger = logger;
_striker = striker;
_httpClient = httpClientFactory.CreateClient();
_loggingConfig = loggingConfig.Value;
_queueCleanerConfig = queueCleanerConfig.Value;
_striker = striker;
}
public virtual async Task<QueueListResponse> GetQueueItemsAsync(ArrInstance arrInstance, int page)
{
Uri uri = new(arrInstance.Url, $"/api/v3/queue?page={page}&pageSize=200&sortKey=timeleft");
Uri uri = new(arrInstance.Url, GetQueueUrlPath(page));
using HttpRequestMessage request = new(HttpMethod.Get, uri);
SetApiKey(request, arrInstance.ApiKey);
@@ -52,6 +65,28 @@ public abstract class ArrClient
return queueResponse;
}
public virtual bool ShouldRemoveFromQueue(QueueRecord record)
{
bool hasWarn() => record.TrackedDownloadStatus
.Equals("warning", StringComparison.InvariantCultureIgnoreCase);
bool isImportBlocked() => record.TrackedDownloadState
.Equals("importBlocked", StringComparison.InvariantCultureIgnoreCase);
bool isImportPending() => record.TrackedDownloadState
.Equals("importPending", StringComparison.InvariantCultureIgnoreCase);
if (hasWarn() && (isImportBlocked() || isImportPending()))
{
return _striker.StrikeAndCheckLimit(
record.DownloadId,
record.Title,
_queueCleanerConfig.ImportFailedMaxStrikes,
StrikeType.ImportFailed
);
}
return false;
}
public virtual async Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord queueRecord)
{
Uri uri = new(arrInstance.Url, $"/api/v3/queue/{queueRecord.Id}?removeFromClient=true&blocklist=true&skipRedownload=true&changeCategory=false");
@@ -76,6 +111,25 @@ public abstract class ArrClient
public abstract Task RefreshItemsAsync(ArrInstance arrInstance, ArrConfig config, HashSet<SearchItem>? items);
public virtual bool IsRecordValid(QueueRecord record)
{
if (string.IsNullOrEmpty(record.DownloadId))
{
_logger.LogDebug("skip | download id is null for {title}", record.Title);
return false;
}
if (record.DownloadId.Equals(record.Title, StringComparison.InvariantCultureIgnoreCase))
{
_logger.LogDebug("skip | item is not ready yet | {title}", record.Title);
return false;
}
return true;
}
protected abstract string GetQueueUrlPath(int page);
protected virtual void SetApiKey(HttpRequestMessage request, string apiKey)
{
request.Headers.Add("x-api-key", apiKey);
@@ -1,6 +1,6 @@
using Common.Configuration;
using Common.Configuration.Arr;
using Domain.Arr.Queue;
using Domain.Models.Arr.Queue;
using Microsoft.Extensions.Logging;
namespace Infrastructure.Verticals.Arr;
@@ -1,8 +1,12 @@
using System.Text;
using Common.Configuration.Arr;
using Common.Configuration.Logging;
using Common.Configuration.QueueCleaner;
using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
using Domain.Models.Radarr;
using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
@@ -14,11 +18,18 @@ public sealed class RadarrClient : ArrClient
public RadarrClient(
ILogger<ArrClient> logger,
IHttpClientFactory httpClientFactory,
IOptions<LoggingConfig> loggingConfig
) : base(logger, httpClientFactory, loggingConfig)
IOptions<LoggingConfig> loggingConfig,
IOptions<QueueCleanerConfig> queueCleanerConfig,
Striker striker
) : base(logger, httpClientFactory, loggingConfig, queueCleanerConfig, striker)
{
}
protected override string GetQueueUrlPath(int page)
{
return $"/api/v3/queue?page={page}&pageSize=200&includeUnknownMovieItems=true&includeMovie=true";
}
public override async Task RefreshItemsAsync(ArrInstance arrInstance, ArrConfig config, HashSet<SearchItem>? items)
{
if (items?.Count is null or 0)
@@ -59,6 +70,17 @@ public sealed class RadarrClient : ArrClient
}
}
public override bool IsRecordValid(QueueRecord record)
{
if (record.MovieId is 0)
{
_logger.LogDebug("skip | item information missing | {title}", record.Title);
return false;
}
return base.IsRecordValid(record);
}
private static string GetSearchLog(Uri instanceUrl, RadarrCommand command, bool success, string? logContext)
{
string status = success ? "triggered" : "failed";
@@ -1,8 +1,12 @@
using System.Text;
using Common.Configuration.Arr;
using Common.Configuration.Logging;
using Common.Configuration.QueueCleaner;
using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
using Domain.Models.Sonarr;
using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
@@ -14,10 +18,17 @@ public sealed class SonarrClient : ArrClient
public SonarrClient(
ILogger<SonarrClient> logger,
IHttpClientFactory httpClientFactory,
IOptions<LoggingConfig> loggingConfig
) : base(logger, httpClientFactory, loggingConfig)
IOptions<LoggingConfig> loggingConfig,
IOptions<QueueCleanerConfig> queueCleanerConfig,
Striker striker
) : base(logger, httpClientFactory, loggingConfig, queueCleanerConfig, striker)
{
}
protected override string GetQueueUrlPath(int page)
{
return $"/api/v3/queue?page={page}&pageSize=200&includeUnknownSeriesItems=true&includeSeries=true";
}
public override async Task RefreshItemsAsync(ArrInstance arrInstance, ArrConfig config, HashSet<SearchItem>? items)
{
@@ -26,11 +37,9 @@ public sealed class SonarrClient : ArrClient
return;
}
SonarrConfig sonarrConfig = (SonarrConfig)config;
Uri uri = new(arrInstance.Url, "/api/v3/command");
foreach (SonarrCommand command in GetSearchCommands(sonarrConfig.SearchType, items))
foreach (SonarrCommand command in GetSearchCommands(items.Cast<SonarrSearchItem>().ToHashSet()))
{
using HttpRequestMessage request = new(HttpMethod.Post, uri);
request.Content = new StringContent(
@@ -41,22 +50,33 @@ public sealed class SonarrClient : ArrClient
SetApiKey(request, arrInstance.ApiKey);
using HttpResponseMessage response = await _httpClient.SendAsync(request);
string? logContext = await ComputeCommandLogContextAsync(arrInstance, command, sonarrConfig.SearchType);
string? logContext = await ComputeCommandLogContextAsync(arrInstance, command, command.SearchType);
try
{
response.EnsureSuccessStatusCode();
_logger.LogInformation("{log}", GetSearchLog(sonarrConfig.SearchType, arrInstance.Url, command, true, logContext));
_logger.LogInformation("{log}", GetSearchLog(command.SearchType, arrInstance.Url, command, true, logContext));
}
catch
{
_logger.LogError("{log}", GetSearchLog(sonarrConfig.SearchType, arrInstance.Url, command, false, logContext));
_logger.LogError("{log}", GetSearchLog(command.SearchType, arrInstance.Url, command, false, logContext));
throw;
}
}
}
public override bool IsRecordValid(QueueRecord record)
{
if (record.EpisodeId is 0 || record.SeriesId is 0)
{
_logger.LogDebug("skip | item information missing | {title}", record.Title);
return false;
}
return base.IsRecordValid(record);
}
private static string GetSearchLog(
SonarrSearchType searchType,
Uri instanceUrl,
@@ -191,7 +211,7 @@ public sealed class SonarrClient : ArrClient
return JsonConvert.DeserializeObject<Series>(responseBody);
}
private List<SonarrCommand> GetSearchCommands(SonarrSearchType searchType, HashSet<SearchItem> items)
private List<SonarrCommand> GetSearchCommands(HashSet<SonarrSearchItem> items)
{
const string episodeSearch = "EpisodeSearch";
const string seasonSearch = "SeasonSearch";
@@ -199,13 +219,13 @@ public sealed class SonarrClient : ArrClient
List<SonarrCommand> commands = new();
foreach (SearchItem item in items)
foreach (SonarrSearchItem item in items)
{
SonarrCommand command = searchType is SonarrSearchType.Episode
SonarrCommand command = item.SearchType is SonarrSearchType.Episode
? commands.FirstOrDefault() ?? new() { Name = episodeSearch, EpisodeIds = new() }
: new();
switch (searchType)
switch (item.SearchType)
{
case SonarrSearchType.Episode when command.EpisodeIds is null:
command.EpisodeIds = [item.Id];
@@ -227,15 +247,16 @@ public sealed class SonarrClient : ArrClient
break;
default:
throw new ArgumentOutOfRangeException(nameof(searchType), searchType, null);
throw new ArgumentOutOfRangeException(nameof(item.SearchType), item.SearchType, null);
}
if (searchType is SonarrSearchType.Episode && commands.Count > 0)
if (item.SearchType is SonarrSearchType.Episode && commands.Count > 0)
{
// only one command will be generated for episodes search
continue;
}
command.SearchType = item.SearchType;
commands.Add(command);
}