mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 10:48:49 -04:00
270 lines
9.6 KiB
C#
270 lines
9.6 KiB
C#
using System.Net;
|
|
using Cleanuparr.Domain.Entities.Arr;
|
|
using Cleanuparr.Domain.Entities.Arr.Queue;
|
|
using Cleanuparr.Domain.Entities.LazyLibrarian;
|
|
using Cleanuparr.Domain.Enums;
|
|
using Cleanuparr.Infrastructure.Events.Interfaces;
|
|
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
|
using Cleanuparr.Infrastructure.Features.Context;
|
|
using Cleanuparr.Infrastructure.Features.DownloadRemover.Interfaces;
|
|
using Cleanuparr.Infrastructure.Features.DownloadRemover.Models;
|
|
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
|
using Cleanuparr.Infrastructure.Features.LazyLibrarian;
|
|
using Cleanuparr.Infrastructure.Helpers;
|
|
using Cleanuparr.Persistence;
|
|
using Cleanuparr.Persistence.Models.Configuration.Seeker;
|
|
using Cleanuparr.Persistence.Models.State;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Cleanuparr.Infrastructure.Features.DownloadRemover;
|
|
|
|
public sealed class QueueItemRemover : IQueueItemRemover
|
|
{
|
|
private readonly ILogger<QueueItemRemover> _logger;
|
|
private readonly IMemoryCache _cache;
|
|
private readonly IArrClientFactory _arrClientFactory;
|
|
private readonly IEventPublisher _eventPublisher;
|
|
private readonly EventsContext _eventsContext;
|
|
private readonly DataContext _dataContext;
|
|
private readonly ILazyLibrarianService _lazyLibrarianService;
|
|
|
|
public QueueItemRemover(
|
|
ILogger<QueueItemRemover> logger,
|
|
IMemoryCache cache,
|
|
IArrClientFactory arrClientFactory,
|
|
IEventPublisher eventPublisher,
|
|
EventsContext eventsContext,
|
|
DataContext dataContext,
|
|
ILazyLibrarianService lazyLibrarianService
|
|
)
|
|
{
|
|
_logger = logger;
|
|
_cache = cache;
|
|
_arrClientFactory = arrClientFactory;
|
|
_eventPublisher = eventPublisher;
|
|
_eventsContext = eventsContext;
|
|
_dataContext = dataContext;
|
|
_lazyLibrarianService = lazyLibrarianService;
|
|
}
|
|
|
|
public async Task RemoveQueueItemAsync(QueueItemRemoveRequest request)
|
|
{
|
|
try
|
|
{
|
|
switch (request.Target)
|
|
{
|
|
case ArrRemovalTarget target:
|
|
await RemoveViaArrAsync(request, target);
|
|
break;
|
|
|
|
case LazyLibrarianRemovalTarget target:
|
|
await RemoveViaLazyLibrarianAsync(request, target);
|
|
break;
|
|
|
|
default:
|
|
throw new NotSupportedException($"removal target {request.Target.GetType().Name} is not supported");
|
|
}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
// A failed removal leaves the download in the queue.
|
|
await ClearRemovalFlagAsync(request.Target.DownloadId);
|
|
|
|
if (exception is HttpRequestException { StatusCode: HttpStatusCode.NotFound })
|
|
{
|
|
throw new Exception($"Item might have already been deleted by your {request.Instance.ArrConfig.Type} instance", exception);
|
|
}
|
|
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
_cache.Remove(CacheKeys.DownloadMarkedForRemoval(request.Target.DownloadId, request.Instance.Url));
|
|
}
|
|
}
|
|
|
|
private async Task RemoveViaArrAsync(QueueItemRemoveRequest request, ArrRemovalTarget target)
|
|
{
|
|
InstanceType instanceType = request.Instance.ArrConfig.Type;
|
|
IArrClient arrClient = _arrClientFactory.GetClient(instanceType, request.Instance.Version);
|
|
bool sent = await arrClient.DeleteQueueItemAsync(request.Instance, target.Record, target.RemoveFromClient, target.ChangeCategory, request.DeleteReason);
|
|
|
|
await MarkDownloadRemovedAsync(target.DownloadId, sent);
|
|
|
|
SetRemovalContext(request, target, instanceType);
|
|
ContextProvider.Set(nameof(QueueRecord), target.Record);
|
|
|
|
await _eventPublisher.PublishQueueItemDeleted(target.RemoveFromClient, request.DeleteReason);
|
|
|
|
if (!await ShouldQueueSearchAsync(request, target))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_eventsContext.SearchQueue.Add(new SearchQueueItem
|
|
{
|
|
ArrInstanceId = request.Instance.Id,
|
|
ItemId = target.SearchItem.Id,
|
|
SeriesId = (target.SearchItem as SeriesSearchItem)?.SeriesId,
|
|
SearchType = (target.SearchItem as SeriesSearchItem)?.SearchType.ToString(),
|
|
Title = target.Title,
|
|
});
|
|
|
|
await _eventsContext.SaveChangesAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// queueBook leaves the wanted row snatched, and a snatched row blocks every search.
|
|
/// getDownloadProgress is what clears it.
|
|
/// </summary>
|
|
private async Task RemoveViaLazyLibrarianAsync(QueueItemRemoveRequest request, LazyLibrarianRemovalTarget target)
|
|
{
|
|
LazyLibrarianQueueItem item = target.Item;
|
|
|
|
SetRemovalContext(request, target, InstanceType.LazyLibrarian);
|
|
|
|
bool snatchCleared = await TryClearSnatchAsync(request, target);
|
|
|
|
bool sent = await _lazyLibrarianService.ResetItemAsync(request.Instance, item);
|
|
await MarkDownloadRemovedAsync(target.DownloadId, sent);
|
|
|
|
_logger.LogInformation(
|
|
"queue item reset in LazyLibrarian with reason {Reason} | {Url} | {Title}",
|
|
request.DeleteReason.ToString(), request.Instance.Url, target.Title
|
|
);
|
|
|
|
await _eventPublisher.PublishQueueItemDeleted(target.RemovedFromClient, request.DeleteReason);
|
|
|
|
if (!snatchCleared)
|
|
{
|
|
await _eventPublisher.PublishSearchNotTriggered(target.DownloadId, target.Title);
|
|
return;
|
|
}
|
|
|
|
if (!await ShouldQueueSearchAsync(request, target))
|
|
{
|
|
return;
|
|
}
|
|
|
|
await _lazyLibrarianService.TriggerSearchAsync(request.Instance, item);
|
|
|
|
_logger.LogInformation(
|
|
"book search triggered | {Url} | book id: {Id}",
|
|
request.Instance.Url, string.Join(',', item.Books.Select(book => book.BookId))
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// LazyLibrarian marks the wanted row aborted when it polls the client and finds nothing.
|
|
/// Progress -1 means it did. Anything else leaves the row snatched, which blocks the search.
|
|
/// </summary>
|
|
private async Task<bool> TryClearSnatchAsync(QueueItemRemoveRequest request, LazyLibrarianRemovalTarget target)
|
|
{
|
|
if (!target.RemovedFromClient)
|
|
{
|
|
_logger.LogInformation(
|
|
"search not triggered | the download is still in the download client | {Title}",
|
|
target.Title
|
|
);
|
|
|
|
return false;
|
|
}
|
|
|
|
LazyLibrarianDownloadProgress? progress =
|
|
await _lazyLibrarianService.GetDownloadProgressAsync(request.Instance, target.Item);
|
|
|
|
if (progress is null)
|
|
{
|
|
_logger.LogWarning("search not triggered | no download progress reported | {Title}", target.Title);
|
|
return false;
|
|
}
|
|
|
|
if (progress.Progress is -1)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
_logger.LogWarning(
|
|
"search not triggered | LazyLibrarian still reports the snatch | {Title} | progress: {Progress}",
|
|
target.Title, progress.Progress
|
|
);
|
|
|
|
return false;
|
|
}
|
|
|
|
private static void SetRemovalContext(QueueItemRemoveRequest request, RemovalTarget target, InstanceType instanceType)
|
|
{
|
|
ContextProvider.SetJobRunId(request.JobRunId);
|
|
ContextProvider.Set(ContextProvider.Keys.ItemName, target.Title);
|
|
ContextProvider.Set(ContextProvider.Keys.Hash, target.DownloadId);
|
|
ContextProvider.Set(ContextProvider.Keys.ArrInstanceUrl, request.Instance.ExternalOrInternalUrl);
|
|
ContextProvider.Set(nameof(InstanceType), instanceType);
|
|
ContextProvider.Set(ContextProvider.Keys.ArrInstanceId, request.Instance.Id);
|
|
ContextProvider.Set(ContextProvider.Keys.Version, request.Instance.Version);
|
|
|
|
if (request.DownloadClient is not null)
|
|
{
|
|
ContextProvider.SetDownloadClient(request.DownloadClient);
|
|
}
|
|
}
|
|
|
|
/// <remarks>
|
|
/// A skipped request leaves the download queued.
|
|
/// Marking it removed would turn its next strike into a false return.
|
|
/// </remarks>
|
|
private async Task MarkDownloadRemovedAsync(string downloadId, bool sent)
|
|
{
|
|
if (!sent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await _eventsContext.DownloadItems
|
|
.Where(x => x.DownloadId == downloadId)
|
|
.ExecuteUpdateAsync(setter =>
|
|
{
|
|
setter.SetProperty(x => x.IsRemoved, true);
|
|
setter.SetProperty(x => x.IsMarkedForRemoval, false);
|
|
});
|
|
}
|
|
|
|
private async Task ClearRemovalFlagAsync(string downloadId)
|
|
{
|
|
await _eventsContext.DownloadItems
|
|
.Where(x => x.DownloadId == downloadId && x.IsMarkedForRemoval)
|
|
.ExecuteUpdateAsync(setter => setter.SetProperty(x => x.IsMarkedForRemoval, false));
|
|
}
|
|
|
|
private async Task<bool> ShouldQueueSearchAsync(QueueItemRemoveRequest request, RemovalTarget target)
|
|
{
|
|
string hash = target.DownloadId.ToLowerInvariant();
|
|
bool isRecurring = Striker.RecurringHashes.ContainsKey(hash);
|
|
|
|
if (isRecurring || request.SkipSearch)
|
|
{
|
|
await _eventPublisher.PublishSearchNotTriggered(target.DownloadId, target.Title);
|
|
|
|
if (isRecurring)
|
|
{
|
|
Striker.RecurringHashes.Remove(hash, out _);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
SeekerConfig seekerConfig = await _dataContext.SeekerConfigs
|
|
.AsNoTracking()
|
|
.FirstAsync();
|
|
|
|
if (!seekerConfig.SearchEnabled)
|
|
{
|
|
_logger.LogDebug("Search not triggered | {Name}", target.Title);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|