mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-02-19 07:17:04 -05:00
Compare commits
7 Commits
v2.6.1
...
fix_pwa_in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8574c7c660 | ||
|
|
573dbcf882 | ||
|
|
94acd9afa4 | ||
|
|
65d25a72a9 | ||
|
|
97eb2fce44 | ||
|
|
701829001c | ||
|
|
8aeeca111c |
@@ -29,12 +29,24 @@ public class EventsController : ControllerBase
|
||||
[FromQuery] string? eventType = null,
|
||||
[FromQuery] DateTime? fromDate = null,
|
||||
[FromQuery] DateTime? toDate = null,
|
||||
[FromQuery] string? search = null)
|
||||
[FromQuery] string? search = null,
|
||||
[FromQuery] string? jobRunId = null)
|
||||
{
|
||||
// Validate pagination parameters
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1) pageSize = 100;
|
||||
if (pageSize > 1000) pageSize = 1000; // Cap at 1000 for performance
|
||||
if (page < 1)
|
||||
{
|
||||
page = 1;
|
||||
}
|
||||
|
||||
if (pageSize < 1)
|
||||
{
|
||||
pageSize = 100;
|
||||
}
|
||||
|
||||
if (pageSize > 1000)
|
||||
{
|
||||
pageSize = 1000; // Cap at 1000 for performance
|
||||
}
|
||||
|
||||
var query = _context.Events.AsQueryable();
|
||||
|
||||
@@ -62,6 +74,12 @@ public class EventsController : ControllerBase
|
||||
query = query.Where(e => e.Timestamp <= toDate.Value);
|
||||
}
|
||||
|
||||
// Apply job run ID exact-match filter
|
||||
if (!string.IsNullOrWhiteSpace(jobRunId) && Guid.TryParse(jobRunId, out var jobRunGuid))
|
||||
{
|
||||
query = query.Where(e => e.JobRunId == jobRunGuid);
|
||||
}
|
||||
|
||||
// Apply search filter if provided
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
@@ -69,7 +87,10 @@ public class EventsController : ControllerBase
|
||||
query = query.Where(e =>
|
||||
EF.Functions.Like(e.Message, pattern) ||
|
||||
EF.Functions.Like(e.Data, pattern) ||
|
||||
EF.Functions.Like(e.TrackingId.ToString(), pattern)
|
||||
EF.Functions.Like(e.TrackingId.ToString(), pattern) ||
|
||||
EF.Functions.Like(e.InstanceUrl, pattern) ||
|
||||
EF.Functions.Like(e.DownloadClientName, pattern) ||
|
||||
EF.Functions.Like(e.JobRunId.ToString(), pattern)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Cleanuparr.Api.Models;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Models;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -66,7 +66,9 @@ public class ManualEventsController : ControllerBase
|
||||
string pattern = EventsContext.GetLikePattern(search);
|
||||
query = query.Where(e =>
|
||||
EF.Functions.Like(e.Message, pattern) ||
|
||||
EF.Functions.Like(e.Data, pattern)
|
||||
EF.Functions.Like(e.Data, pattern) ||
|
||||
EF.Functions.Like(e.InstanceUrl, pattern) ||
|
||||
EF.Functions.Like(e.DownloadClientName, pattern)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
189
code/backend/Cleanuparr.Api/Controllers/StrikesController.cs
Normal file
189
code/backend/Cleanuparr.Api/Controllers/StrikesController.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.State;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Cleanuparr.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class StrikesController : ControllerBase
|
||||
{
|
||||
private readonly EventsContext _context;
|
||||
|
||||
public StrikesController(EventsContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets download items with their strikes (grouped), with pagination and filtering
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PaginatedResult<DownloadItemStrikesDto>>> GetStrikes(
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 50,
|
||||
[FromQuery] string? search = null,
|
||||
[FromQuery] string? type = null)
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1) pageSize = 50;
|
||||
if (pageSize > 100) pageSize = 100;
|
||||
|
||||
var query = _context.DownloadItems
|
||||
.Include(d => d.Strikes)
|
||||
.Where(d => d.Strikes.Any());
|
||||
|
||||
// Filter by strike type: only show items that have strikes of this type
|
||||
if (!string.IsNullOrWhiteSpace(type))
|
||||
{
|
||||
if (Enum.TryParse<StrikeType>(type, true, out var strikeType))
|
||||
query = query.Where(d => d.Strikes.Any(s => s.Type == strikeType));
|
||||
}
|
||||
|
||||
// Apply search filter on title or download hash
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
string pattern = EventsContext.GetLikePattern(search);
|
||||
query = query.Where(d =>
|
||||
EF.Functions.Like(d.Title, pattern) ||
|
||||
EF.Functions.Like(d.DownloadId, pattern));
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||
var skip = (page - 1) * pageSize;
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(d => d.Strikes.Max(s => s.CreatedAt))
|
||||
.Skip(skip)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
var dtos = items.Select(d => new DownloadItemStrikesDto
|
||||
{
|
||||
DownloadItemId = d.Id,
|
||||
DownloadId = d.DownloadId,
|
||||
Title = d.Title,
|
||||
TotalStrikes = d.Strikes.Count,
|
||||
StrikesByType = d.Strikes
|
||||
.GroupBy(s => s.Type)
|
||||
.ToDictionary(g => g.Key.ToString(), g => g.Count()),
|
||||
LatestStrikeAt = d.Strikes.Max(s => s.CreatedAt),
|
||||
FirstStrikeAt = d.Strikes.Min(s => s.CreatedAt),
|
||||
IsMarkedForRemoval = d.IsMarkedForRemoval,
|
||||
IsRemoved = d.IsRemoved,
|
||||
IsReturning = d.IsReturning,
|
||||
Strikes = d.Strikes
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.Select(s => new StrikeDetailDto
|
||||
{
|
||||
Id = s.Id,
|
||||
Type = s.Type.ToString(),
|
||||
CreatedAt = s.CreatedAt,
|
||||
LastDownloadedBytes = s.LastDownloadedBytes,
|
||||
JobRunId = s.JobRunId,
|
||||
}).ToList(),
|
||||
}).ToList();
|
||||
|
||||
return Ok(new PaginatedResult<DownloadItemStrikesDto>
|
||||
{
|
||||
Items = dtos,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
TotalCount = totalCount,
|
||||
TotalPages = totalPages,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the most recent individual strikes with download item info (for dashboard)
|
||||
/// </summary>
|
||||
[HttpGet("recent")]
|
||||
public async Task<ActionResult<List<RecentStrikeDto>>> GetRecentStrikes(
|
||||
[FromQuery] int count = 5)
|
||||
{
|
||||
if (count < 1) count = 1;
|
||||
if (count > 50) count = 50;
|
||||
|
||||
var strikes = await _context.Strikes
|
||||
.Include(s => s.DownloadItem)
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.Take(count)
|
||||
.Select(s => new RecentStrikeDto
|
||||
{
|
||||
Id = s.Id,
|
||||
Type = s.Type.ToString(),
|
||||
CreatedAt = s.CreatedAt,
|
||||
DownloadId = s.DownloadItem.DownloadId,
|
||||
Title = s.DownloadItem.Title,
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(strikes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all available strike types
|
||||
/// </summary>
|
||||
[HttpGet("types")]
|
||||
public ActionResult<List<string>> GetStrikeTypes()
|
||||
{
|
||||
var types = Enum.GetNames(typeof(StrikeType)).ToList();
|
||||
return Ok(types);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all strikes for a specific download item
|
||||
/// </summary>
|
||||
[HttpDelete("{downloadItemId:guid}")]
|
||||
public async Task<IActionResult> DeleteStrikesForItem(Guid downloadItemId)
|
||||
{
|
||||
var item = await _context.DownloadItems
|
||||
.Include(d => d.Strikes)
|
||||
.FirstOrDefaultAsync(d => d.Id == downloadItemId);
|
||||
|
||||
if (item == null)
|
||||
return NotFound();
|
||||
|
||||
_context.Strikes.RemoveRange(item.Strikes);
|
||||
_context.DownloadItems.Remove(item);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
public class DownloadItemStrikesDto
|
||||
{
|
||||
public Guid DownloadItemId { get; set; }
|
||||
public string DownloadId { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public int TotalStrikes { get; set; }
|
||||
public Dictionary<string, int> StrikesByType { get; set; } = new();
|
||||
public DateTime LatestStrikeAt { get; set; }
|
||||
public DateTime FirstStrikeAt { get; set; }
|
||||
public bool IsMarkedForRemoval { get; set; }
|
||||
public bool IsRemoved { get; set; }
|
||||
public bool IsReturning { get; set; }
|
||||
public List<StrikeDetailDto> Strikes { get; set; } = [];
|
||||
}
|
||||
|
||||
public class StrikeDetailDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public long? LastDownloadedBytes { get; set; }
|
||||
public Guid JobRunId { get; set; }
|
||||
}
|
||||
|
||||
public class RecentStrikeDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public string DownloadId { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -124,12 +124,18 @@ public static class ApiDI
|
||||
{
|
||||
name = "Cleanuparr",
|
||||
short_name = "Cleanuparr",
|
||||
description = "Automated cleanup for *arr applications and download clients",
|
||||
start_url = basePath,
|
||||
display = "standalone",
|
||||
background_color = "#ffffff",
|
||||
theme_color = "#ffffff",
|
||||
background_color = "#0e0a1a",
|
||||
theme_color = "#1a1135",
|
||||
icons = new[]
|
||||
{
|
||||
new {
|
||||
src = "icons/128.png",
|
||||
sizes = "128x128",
|
||||
type = "image/png"
|
||||
},
|
||||
new {
|
||||
src = "icons/icon-192x192.png",
|
||||
sizes = "192x192",
|
||||
|
||||
@@ -2,7 +2,7 @@ using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Cleanuparr.Api.Features.BlacklistSync.Contracts.Requests;
|
||||
using Cleanuparr.Infrastructure.Models;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.BlacklistSync;
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
using Cleanuparr.Api.Features.DownloadCleaner.Contracts.Requests;
|
||||
using Cleanuparr.Infrastructure.Models;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Utilities;
|
||||
using Cleanuparr.Persistence;
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed record CreateDownloadClientRequest
|
||||
|
||||
public DownloadClientType Type { get; init; }
|
||||
|
||||
public Uri? Host { get; init; }
|
||||
public string? Host { get; init; }
|
||||
|
||||
public string? Username { get; init; }
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed record CreateDownloadClientRequest
|
||||
|
||||
public string? UrlBase { get; init; }
|
||||
|
||||
public Uri? ExternalUrl { get; init; }
|
||||
public string? ExternalUrl { get; init; }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
@@ -33,10 +33,20 @@ public sealed record CreateDownloadClientRequest
|
||||
throw new ValidationException("Client name cannot be empty");
|
||||
}
|
||||
|
||||
if (Host is null)
|
||||
if (string.IsNullOrWhiteSpace(Host))
|
||||
{
|
||||
throw new ValidationException("Host cannot be empty");
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(Host, UriKind.RelativeOrAbsolute, out _))
|
||||
{
|
||||
throw new ValidationException("Host is not a valid URL");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ExternalUrl) && !Uri.TryCreate(ExternalUrl, UriKind.RelativeOrAbsolute, out _))
|
||||
{
|
||||
throw new ValidationException("External URL is not a valid URL");
|
||||
}
|
||||
}
|
||||
|
||||
public DownloadClientConfig ToEntity() => new()
|
||||
@@ -45,10 +55,10 @@ public sealed record CreateDownloadClientRequest
|
||||
Name = Name,
|
||||
TypeName = TypeName,
|
||||
Type = Type,
|
||||
Host = Host,
|
||||
Host = new Uri(Host!, UriKind.RelativeOrAbsolute),
|
||||
Username = Username,
|
||||
Password = Password,
|
||||
UrlBase = UrlBase,
|
||||
ExternalUrl = ExternalUrl,
|
||||
ExternalUrl = !string.IsNullOrWhiteSpace(ExternalUrl) ? new Uri(ExternalUrl, UriKind.RelativeOrAbsolute) : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public sealed record TestDownloadClientRequest
|
||||
|
||||
public DownloadClientType Type { get; init; }
|
||||
|
||||
public Uri? Host { get; init; }
|
||||
public string? Host { get; init; }
|
||||
|
||||
public string? Username { get; init; }
|
||||
|
||||
@@ -22,10 +22,15 @@ public sealed record TestDownloadClientRequest
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (Host is null)
|
||||
if (string.IsNullOrWhiteSpace(Host))
|
||||
{
|
||||
throw new ValidationException("Host cannot be empty");
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(Host, UriKind.RelativeOrAbsolute, out _))
|
||||
{
|
||||
throw new ValidationException("Host is not a valid URL");
|
||||
}
|
||||
}
|
||||
|
||||
public DownloadClientConfig ToTestConfig() => new()
|
||||
@@ -35,7 +40,7 @@ public sealed record TestDownloadClientRequest
|
||||
Name = "Test Client",
|
||||
TypeName = TypeName,
|
||||
Type = Type,
|
||||
Host = Host,
|
||||
Host = new Uri(Host!, UriKind.RelativeOrAbsolute),
|
||||
Username = Username,
|
||||
Password = Password,
|
||||
UrlBase = UrlBase,
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed record UpdateDownloadClientRequest
|
||||
|
||||
public DownloadClientType Type { get; init; }
|
||||
|
||||
public Uri? Host { get; init; }
|
||||
public string? Host { get; init; }
|
||||
|
||||
public string? Username { get; init; }
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed record UpdateDownloadClientRequest
|
||||
|
||||
public string? UrlBase { get; init; }
|
||||
|
||||
public Uri? ExternalUrl { get; init; }
|
||||
public string? ExternalUrl { get; init; }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
@@ -33,10 +33,20 @@ public sealed record UpdateDownloadClientRequest
|
||||
throw new ValidationException("Client name cannot be empty");
|
||||
}
|
||||
|
||||
if (Host is null)
|
||||
if (string.IsNullOrWhiteSpace(Host))
|
||||
{
|
||||
throw new ValidationException("Host cannot be empty");
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(Host, UriKind.RelativeOrAbsolute, out _))
|
||||
{
|
||||
throw new ValidationException("Host is not a valid URL");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ExternalUrl) && !Uri.TryCreate(ExternalUrl, UriKind.RelativeOrAbsolute, out _))
|
||||
{
|
||||
throw new ValidationException("External URL is not a valid URL");
|
||||
}
|
||||
}
|
||||
|
||||
public DownloadClientConfig ApplyTo(DownloadClientConfig existing) => existing with
|
||||
@@ -45,10 +55,10 @@ public sealed record UpdateDownloadClientRequest
|
||||
Name = Name,
|
||||
TypeName = TypeName,
|
||||
Type = Type,
|
||||
Host = Host,
|
||||
Host = new Uri(Host!, UriKind.RelativeOrAbsolute),
|
||||
Username = Username,
|
||||
Password = Password,
|
||||
UrlBase = UrlBase,
|
||||
ExternalUrl = ExternalUrl,
|
||||
ExternalUrl = !string.IsNullOrWhiteSpace(ExternalUrl) ? new Uri(ExternalUrl, UriKind.RelativeOrAbsolute) : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,10 +5,8 @@ using Cleanuparr.Api.Features.DownloadClient.Contracts.Requests;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Http.DynamicHttpClientSystem;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Cleanuparr.Api.Features.DownloadClient.Controllers;
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ public sealed record UpdateGeneralConfigRequest
|
||||
|
||||
public List<string> IgnoredDownloads { get; init; } = [];
|
||||
|
||||
public ushort StrikeInactivityWindowHours { get; init; } = 24;
|
||||
|
||||
public UpdateLoggingConfigRequest Log { get; init; } = new();
|
||||
|
||||
public GeneralConfig ApplyTo(GeneralConfig existingConfig, IServiceProvider services, ILogger logger)
|
||||
@@ -44,6 +46,7 @@ public sealed record UpdateGeneralConfigRequest
|
||||
existingConfig.StatusCheckEnabled = StatusCheckEnabled;
|
||||
existingConfig.EncryptionKey = EncryptionKey;
|
||||
existingConfig.IgnoredDownloads = IgnoredDownloads;
|
||||
existingConfig.StrikeInactivityWindowHours = StrikeInactivityWindowHours;
|
||||
|
||||
bool loggingChanged = Log.ApplyTo(existingConfig.Log);
|
||||
|
||||
@@ -61,6 +64,16 @@ public sealed record UpdateGeneralConfigRequest
|
||||
throw new ValidationException("HTTP_TIMEOUT must be greater than 0");
|
||||
}
|
||||
|
||||
if (config.StrikeInactivityWindowHours is 0)
|
||||
{
|
||||
throw new ValidationException("STRIKE_INACTIVITY_WINDOW_HOURS must be greater than 0");
|
||||
}
|
||||
|
||||
if (config.StrikeInactivityWindowHours > 168)
|
||||
{
|
||||
throw new ValidationException("STRIKE_INACTIVITY_WINDOW_HOURS must be less than or equal to 168");
|
||||
}
|
||||
|
||||
config.Log.Validate();
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,21 @@ public sealed class GeneralConfigController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("strikes/purge")]
|
||||
public async Task<IActionResult> PurgeAllStrikes(
|
||||
[FromServices] EventsContext eventsContext)
|
||||
{
|
||||
var deletedStrikes = await eventsContext.Strikes.ExecuteDeleteAsync();
|
||||
var deletedItems = await eventsContext.DownloadItems
|
||||
.Where(d => !d.Strikes.Any())
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
_logger.LogWarning("Purged all strikes: {strikes} strikes, {items} download items removed",
|
||||
deletedStrikes, deletedItems);
|
||||
|
||||
return Ok(new { DeletedStrikes = deletedStrikes, DeletedItems = deletedItems });
|
||||
}
|
||||
|
||||
private void ClearStrikesCacheIfNeeded(bool wasDryRun, bool isDryRun)
|
||||
{
|
||||
if (!wasDryRun || isDryRun)
|
||||
|
||||
@@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Cleanuparr.Api.Features.MalwareBlocker.Contracts.Requests;
|
||||
using Cleanuparr.Infrastructure.Models;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Utilities;
|
||||
using Cleanuparr.Persistence;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
using Cleanuparr.Api.Features.QueueCleaner.Contracts.Requests;
|
||||
using Cleanuparr.Infrastructure.Models;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Utilities;
|
||||
using Cleanuparr.Persistence;
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
using Cleanuparr.Infrastructure.Features.Jobs;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.Jobs;
|
||||
using Cleanuparr.Infrastructure.Helpers;
|
||||
using Cleanuparr.Infrastructure.Hubs;
|
||||
using Cleanuparr.Infrastructure.Models;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.State;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Quartz;
|
||||
using Serilog.Context;
|
||||
@@ -14,48 +19,73 @@ public sealed class GenericJob<T> : IJob
|
||||
{
|
||||
private readonly ILogger<GenericJob<T>> _logger;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
|
||||
|
||||
public GenericJob(ILogger<GenericJob<T>> logger, IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_scopeFactory = scopeFactory;
|
||||
}
|
||||
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
using var _ = LogContext.PushProperty("JobName", typeof(T).Name);
|
||||
|
||||
|
||||
Guid jobRunId = Guid.CreateVersion7();
|
||||
JobType jobType = Enum.Parse<JobType>(typeof(T).Name);
|
||||
JobRunStatus? status = null;
|
||||
|
||||
try
|
||||
{
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
var eventsContext = scope.ServiceProvider.GetRequiredService<EventsContext>();
|
||||
var hubContext = scope.ServiceProvider.GetRequiredService<IHubContext<AppHub>>();
|
||||
var jobManagementService = scope.ServiceProvider.GetRequiredService<IJobManagementService>();
|
||||
|
||||
await BroadcastJobStatus(hubContext, jobManagementService, false);
|
||||
|
||||
|
||||
var jobRun = new JobRun { Id = jobRunId, Type = jobType };
|
||||
eventsContext.JobRuns.Add(jobRun);
|
||||
await eventsContext.SaveChangesAsync();
|
||||
|
||||
ContextProvider.SetJobRunId(jobRunId);
|
||||
using var __ = LogContext.PushProperty(LogProperties.JobRunId, jobRunId.ToString());
|
||||
|
||||
await BroadcastJobStatus(hubContext, jobManagementService, jobType, false);
|
||||
|
||||
var handler = scope.ServiceProvider.GetRequiredService<T>();
|
||||
await handler.ExecuteAsync();
|
||||
|
||||
await BroadcastJobStatus(hubContext, jobManagementService, true);
|
||||
|
||||
status = JobRunStatus.Completed;
|
||||
await BroadcastJobStatus(hubContext, jobManagementService, jobType, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "{name} failed", typeof(T).Name);
|
||||
status = JobRunStatus.Failed;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await using var finalScope = _scopeFactory.CreateAsyncScope();
|
||||
var eventsContext = finalScope.ServiceProvider.GetRequiredService<EventsContext>();
|
||||
var jobRun = await eventsContext.JobRuns.FindAsync(jobRunId);
|
||||
if (jobRun is not null)
|
||||
{
|
||||
jobRun.CompletedAt = DateTime.UtcNow;
|
||||
jobRun.Status = status;
|
||||
await eventsContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BroadcastJobStatus(IHubContext<AppHub> hubContext, IJobManagementService jobManagementService, bool isFinished)
|
||||
|
||||
private async Task BroadcastJobStatus(IHubContext<AppHub> hubContext, IJobManagementService jobManagementService, JobType jobType, bool isFinished)
|
||||
{
|
||||
try
|
||||
{
|
||||
JobType jobType = Enum.Parse<JobType>(typeof(T).Name);
|
||||
JobInfo jobInfo = await jobManagementService.GetJob(jobType);
|
||||
|
||||
if (isFinished)
|
||||
{
|
||||
jobInfo.Status = "Scheduled";
|
||||
}
|
||||
|
||||
|
||||
await hubContext.Clients.All.SendAsync("JobStatusUpdate", jobInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Cleanuparr.Domain.Entities.HealthCheck;
|
||||
|
||||
public sealed record HealthCheckResult
|
||||
{
|
||||
public bool IsHealthy { get; set; }
|
||||
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
public TimeSpan ResponseTime { get; set; }
|
||||
}
|
||||
7
code/backend/Cleanuparr.Domain/Enums/JobRunStatus.cs
Normal file
7
code/backend/Cleanuparr.Domain/Enums/JobRunStatus.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Cleanuparr.Domain.Enums;
|
||||
|
||||
public enum JobRunStatus
|
||||
{
|
||||
Completed,
|
||||
Failed
|
||||
}
|
||||
9
code/backend/Cleanuparr.Domain/Enums/JobType.cs
Normal file
9
code/backend/Cleanuparr.Domain/Enums/JobType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Cleanuparr.Domain.Enums;
|
||||
|
||||
public enum JobType
|
||||
{
|
||||
QueueCleaner,
|
||||
MalwareBlocker,
|
||||
DownloadCleaner,
|
||||
BlacklistSynchronizer,
|
||||
}
|
||||
@@ -65,6 +65,9 @@ public class EventPublisherTests : IDisposable
|
||||
_loggerMock.Object,
|
||||
_notificationPublisherMock.Object,
|
||||
_dryRunInterceptorMock.Object);
|
||||
|
||||
// Setup JobRunId in context for tests
|
||||
ContextProvider.SetJobRunId(Guid.NewGuid());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -339,7 +342,7 @@ public class EventPublisherTests : IDisposable
|
||||
public async Task PublishQueueItemDeleted_SavesEventWithContextData()
|
||||
{
|
||||
// Arrange
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadName, "Test Download");
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, "Test Download");
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, "abc123");
|
||||
|
||||
// Act
|
||||
@@ -360,7 +363,7 @@ public class EventPublisherTests : IDisposable
|
||||
public async Task PublishQueueItemDeleted_SendsNotification()
|
||||
{
|
||||
// Arrange
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadName, "Test Download");
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, "Test Download");
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, "abc123");
|
||||
|
||||
// Act
|
||||
@@ -378,7 +381,7 @@ public class EventPublisherTests : IDisposable
|
||||
public async Task PublishDownloadCleaned_SavesEventWithContextData()
|
||||
{
|
||||
// Arrange
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadName, "Cleaned Download");
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, "Cleaned Download");
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, "def456");
|
||||
|
||||
// Act
|
||||
@@ -404,7 +407,7 @@ public class EventPublisherTests : IDisposable
|
||||
public async Task PublishDownloadCleaned_SendsNotification()
|
||||
{
|
||||
// Arrange
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadName, "Test");
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, "Test");
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, "xyz");
|
||||
|
||||
var ratio = 1.5;
|
||||
@@ -475,7 +478,7 @@ public class EventPublisherTests : IDisposable
|
||||
public async Task PublishCategoryChanged_SavesEventWithContextData()
|
||||
{
|
||||
// Arrange
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadName, "Category Test");
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, "Category Test");
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, "cat123");
|
||||
|
||||
// Act
|
||||
@@ -493,7 +496,7 @@ public class EventPublisherTests : IDisposable
|
||||
public async Task PublishCategoryChanged_WithTag_SavesCorrectMessage()
|
||||
{
|
||||
// Arrange
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadName, "Tag Test");
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, "Tag Test");
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, "tag123");
|
||||
|
||||
// Act
|
||||
@@ -509,7 +512,7 @@ public class EventPublisherTests : IDisposable
|
||||
public async Task PublishCategoryChanged_SendsNotification()
|
||||
{
|
||||
// Arrange
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadName, "Test");
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, "Test");
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, "xyz");
|
||||
|
||||
// Act
|
||||
|
||||
@@ -6,9 +6,7 @@ using Cleanuparr.Infrastructure.Features.MalwareBlocker;
|
||||
using Cleanuparr.Infrastructure.Http;
|
||||
using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Tests.Features.DownloadClient.TestHelpers;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
||||
@@ -17,14 +15,13 @@ namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient;
|
||||
public class DelugeServiceFixture : IDisposable
|
||||
{
|
||||
public Mock<ILogger<DelugeService>> Logger { get; }
|
||||
public MemoryCache Cache { get; }
|
||||
public Mock<IFilenameEvaluator> FilenameEvaluator { get; }
|
||||
public Mock<IStriker> Striker { get; }
|
||||
public Mock<IDryRunInterceptor> DryRunInterceptor { get; }
|
||||
public Mock<IHardLinkFileService> HardLinkFileService { get; }
|
||||
public Mock<IDynamicHttpClientProvider> HttpClientProvider { get; }
|
||||
public Mock<IEventPublisher> EventPublisher { get; }
|
||||
public BlocklistProvider BlocklistProvider { get; }
|
||||
public Mock<IBlocklistProvider> BlocklistProvider { get; }
|
||||
public Mock<IRuleEvaluator> RuleEvaluator { get; }
|
||||
public Mock<IRuleManager> RuleManager { get; }
|
||||
public Mock<IDelugeClientWrapper> ClientWrapper { get; }
|
||||
@@ -32,14 +29,13 @@ public class DelugeServiceFixture : IDisposable
|
||||
public DelugeServiceFixture()
|
||||
{
|
||||
Logger = new Mock<ILogger<DelugeService>>();
|
||||
Cache = new MemoryCache(new MemoryCacheOptions());
|
||||
FilenameEvaluator = new Mock<IFilenameEvaluator>();
|
||||
Striker = new Mock<IStriker>();
|
||||
DryRunInterceptor = new Mock<IDryRunInterceptor>();
|
||||
HardLinkFileService = new Mock<IHardLinkFileService>();
|
||||
HttpClientProvider = new Mock<IDynamicHttpClientProvider>();
|
||||
EventPublisher = new Mock<IEventPublisher>();
|
||||
BlocklistProvider = TestBlocklistProviderFactory.Create();
|
||||
BlocklistProvider = new Mock<IBlocklistProvider>();
|
||||
RuleEvaluator = new Mock<IRuleEvaluator>();
|
||||
RuleManager = new Mock<IRuleManager>();
|
||||
ClientWrapper = new Mock<IDelugeClientWrapper>();
|
||||
@@ -74,14 +70,13 @@ public class DelugeServiceFixture : IDisposable
|
||||
|
||||
return new DelugeService(
|
||||
Logger.Object,
|
||||
Cache,
|
||||
FilenameEvaluator.Object,
|
||||
Striker.Object,
|
||||
DryRunInterceptor.Object,
|
||||
HardLinkFileService.Object,
|
||||
HttpClientProvider.Object,
|
||||
EventPublisher.Object,
|
||||
BlocklistProvider,
|
||||
BlocklistProvider.Object,
|
||||
config,
|
||||
RuleEvaluator.Object,
|
||||
RuleManager.Object,
|
||||
@@ -112,7 +107,6 @@ public class DelugeServiceFixture : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Cache.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@ using Cleanuparr.Infrastructure.Features.MalwareBlocker;
|
||||
using Cleanuparr.Infrastructure.Http;
|
||||
using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Tests.Features.DownloadClient.TestHelpers;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
||||
@@ -17,14 +15,13 @@ namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient;
|
||||
public class QBitServiceFixture : IDisposable
|
||||
{
|
||||
public Mock<ILogger<QBitService>> Logger { get; }
|
||||
public MemoryCache Cache { get; }
|
||||
public Mock<IFilenameEvaluator> FilenameEvaluator { get; }
|
||||
public Mock<IStriker> Striker { get; }
|
||||
public Mock<IDryRunInterceptor> DryRunInterceptor { get; }
|
||||
public Mock<IHardLinkFileService> HardLinkFileService { get; }
|
||||
public Mock<IDynamicHttpClientProvider> HttpClientProvider { get; }
|
||||
public Mock<IEventPublisher> EventPublisher { get; }
|
||||
public BlocklistProvider BlocklistProvider { get; }
|
||||
public Mock<IBlocklistProvider> BlocklistProvider { get; }
|
||||
public Mock<IRuleEvaluator> RuleEvaluator { get; }
|
||||
public Mock<IRuleManager> RuleManager { get; }
|
||||
public Mock<IQBittorrentClientWrapper> ClientWrapper { get; }
|
||||
@@ -32,14 +29,13 @@ public class QBitServiceFixture : IDisposable
|
||||
public QBitServiceFixture()
|
||||
{
|
||||
Logger = new Mock<ILogger<QBitService>>();
|
||||
Cache = new MemoryCache(new MemoryCacheOptions());
|
||||
FilenameEvaluator = new Mock<IFilenameEvaluator>();
|
||||
Striker = new Mock<IStriker>();
|
||||
DryRunInterceptor = new Mock<IDryRunInterceptor>();
|
||||
HardLinkFileService = new Mock<IHardLinkFileService>();
|
||||
HttpClientProvider = new Mock<IDynamicHttpClientProvider>();
|
||||
EventPublisher = new Mock<IEventPublisher>();
|
||||
BlocklistProvider = TestBlocklistProviderFactory.Create();
|
||||
BlocklistProvider =new Mock<IBlocklistProvider>();
|
||||
RuleEvaluator = new Mock<IRuleEvaluator>();
|
||||
RuleManager = new Mock<IRuleManager>();
|
||||
ClientWrapper = new Mock<IQBittorrentClientWrapper>();
|
||||
@@ -76,14 +72,13 @@ public class QBitServiceFixture : IDisposable
|
||||
|
||||
return new QBitService(
|
||||
Logger.Object,
|
||||
Cache,
|
||||
FilenameEvaluator.Object,
|
||||
Striker.Object,
|
||||
DryRunInterceptor.Object,
|
||||
HardLinkFileService.Object,
|
||||
HttpClientProvider.Object,
|
||||
EventPublisher.Object,
|
||||
BlocklistProvider,
|
||||
BlocklistProvider.Object,
|
||||
config,
|
||||
RuleEvaluator.Object,
|
||||
RuleManager.Object,
|
||||
@@ -115,7 +110,6 @@ public class QBitServiceFixture : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Cache.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,7 +470,7 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
});
|
||||
|
||||
_fixture.Striker
|
||||
.Setup(x => x.StrikeAndCheckLimit(hash, It.IsAny<string>(), (ushort)3, StrikeType.DownloadingMetadata))
|
||||
.Setup(x => x.StrikeAndCheckLimit(hash, It.IsAny<string>(), (ushort)3, StrikeType.DownloadingMetadata, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
// Act
|
||||
@@ -479,7 +479,7 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
// Assert
|
||||
Assert.False(result.ShouldRemove);
|
||||
_fixture.Striker.Verify(
|
||||
x => x.StrikeAndCheckLimit(hash, It.IsAny<string>(), (ushort)3, StrikeType.DownloadingMetadata),
|
||||
x => x.StrikeAndCheckLimit(hash, It.IsAny<string>(), (ushort)3, StrikeType.DownloadingMetadata, It.IsAny<long?>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
@@ -533,7 +533,7 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
});
|
||||
|
||||
_fixture.Striker
|
||||
.Setup(x => x.StrikeAndCheckLimit(hash, It.IsAny<string>(), (ushort)3, StrikeType.DownloadingMetadata))
|
||||
.Setup(x => x.StrikeAndCheckLimit(hash, It.IsAny<string>(), (ushort)3, StrikeType.DownloadingMetadata, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true); // Strike limit exceeded
|
||||
|
||||
// Act
|
||||
@@ -600,7 +600,7 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
// Assert
|
||||
Assert.False(result.ShouldRemove);
|
||||
_fixture.Striker.Verify(
|
||||
x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), It.IsAny<StrikeType>()),
|
||||
x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), It.IsAny<StrikeType>(), It.IsAny<long?>()),
|
||||
Times.Never);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient.TestHelpers;
|
||||
|
||||
/// <summary>
|
||||
/// Test implementation of BlocklistProvider for testing purposes
|
||||
/// </summary>
|
||||
public static class TestBlocklistProviderFactory
|
||||
{
|
||||
public static BlocklistProvider Create()
|
||||
{
|
||||
var logger = new Mock<ILogger<BlocklistProvider>>().Object;
|
||||
var scopeFactory = new Mock<IServiceScopeFactory>().Object;
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
|
||||
return new BlocklistProvider(logger, scopeFactory, cache);
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,7 @@ using Cleanuparr.Infrastructure.Features.MalwareBlocker;
|
||||
using Cleanuparr.Infrastructure.Http;
|
||||
using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Tests.Features.DownloadClient.TestHelpers;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
||||
@@ -17,14 +15,13 @@ namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient;
|
||||
public class TransmissionServiceFixture : IDisposable
|
||||
{
|
||||
public Mock<ILogger<TransmissionService>> Logger { get; }
|
||||
public MemoryCache Cache { get; }
|
||||
public Mock<IFilenameEvaluator> FilenameEvaluator { get; }
|
||||
public Mock<IStriker> Striker { get; }
|
||||
public Mock<IDryRunInterceptor> DryRunInterceptor { get; }
|
||||
public Mock<IHardLinkFileService> HardLinkFileService { get; }
|
||||
public Mock<IDynamicHttpClientProvider> HttpClientProvider { get; }
|
||||
public Mock<IEventPublisher> EventPublisher { get; }
|
||||
public BlocklistProvider BlocklistProvider { get; }
|
||||
public Mock<IBlocklistProvider> BlocklistProvider { get; }
|
||||
public Mock<IRuleEvaluator> RuleEvaluator { get; }
|
||||
public Mock<IRuleManager> RuleManager { get; }
|
||||
public Mock<ITransmissionClientWrapper> ClientWrapper { get; }
|
||||
@@ -32,14 +29,13 @@ public class TransmissionServiceFixture : IDisposable
|
||||
public TransmissionServiceFixture()
|
||||
{
|
||||
Logger = new Mock<ILogger<TransmissionService>>();
|
||||
Cache = new MemoryCache(new MemoryCacheOptions());
|
||||
FilenameEvaluator = new Mock<IFilenameEvaluator>();
|
||||
Striker = new Mock<IStriker>();
|
||||
DryRunInterceptor = new Mock<IDryRunInterceptor>();
|
||||
HardLinkFileService = new Mock<IHardLinkFileService>();
|
||||
HttpClientProvider = new Mock<IDynamicHttpClientProvider>();
|
||||
EventPublisher = new Mock<IEventPublisher>();
|
||||
BlocklistProvider = TestBlocklistProviderFactory.Create();
|
||||
BlocklistProvider = new Mock<IBlocklistProvider>();
|
||||
RuleEvaluator = new Mock<IRuleEvaluator>();
|
||||
RuleManager = new Mock<IRuleManager>();
|
||||
ClientWrapper = new Mock<ITransmissionClientWrapper>();
|
||||
@@ -74,14 +70,13 @@ public class TransmissionServiceFixture : IDisposable
|
||||
|
||||
return new TransmissionService(
|
||||
Logger.Object,
|
||||
Cache,
|
||||
FilenameEvaluator.Object,
|
||||
Striker.Object,
|
||||
DryRunInterceptor.Object,
|
||||
HardLinkFileService.Object,
|
||||
HttpClientProvider.Object,
|
||||
EventPublisher.Object,
|
||||
BlocklistProvider,
|
||||
BlocklistProvider.Object,
|
||||
config,
|
||||
RuleEvaluator.Object,
|
||||
RuleManager.Object,
|
||||
@@ -112,7 +107,6 @@ public class TransmissionServiceFixture : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Cache.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@ using Cleanuparr.Infrastructure.Features.MalwareBlocker;
|
||||
using Cleanuparr.Infrastructure.Http;
|
||||
using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Tests.Features.DownloadClient.TestHelpers;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
||||
@@ -17,14 +15,13 @@ namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient;
|
||||
public class UTorrentServiceFixture : IDisposable
|
||||
{
|
||||
public Mock<ILogger<UTorrentService>> Logger { get; }
|
||||
public MemoryCache Cache { get; }
|
||||
public Mock<IFilenameEvaluator> FilenameEvaluator { get; }
|
||||
public Mock<IStriker> Striker { get; }
|
||||
public Mock<IDryRunInterceptor> DryRunInterceptor { get; }
|
||||
public Mock<IHardLinkFileService> HardLinkFileService { get; }
|
||||
public Mock<IDynamicHttpClientProvider> HttpClientProvider { get; }
|
||||
public Mock<IEventPublisher> EventPublisher { get; }
|
||||
public BlocklistProvider BlocklistProvider { get; }
|
||||
public Mock<IBlocklistProvider> BlocklistProvider { get; }
|
||||
public Mock<IRuleEvaluator> RuleEvaluator { get; }
|
||||
public Mock<IRuleManager> RuleManager { get; }
|
||||
public Mock<IUTorrentClientWrapper> ClientWrapper { get; }
|
||||
@@ -32,14 +29,13 @@ public class UTorrentServiceFixture : IDisposable
|
||||
public UTorrentServiceFixture()
|
||||
{
|
||||
Logger = new Mock<ILogger<UTorrentService>>();
|
||||
Cache = new MemoryCache(new MemoryCacheOptions());
|
||||
FilenameEvaluator = new Mock<IFilenameEvaluator>();
|
||||
Striker = new Mock<IStriker>();
|
||||
DryRunInterceptor = new Mock<IDryRunInterceptor>();
|
||||
HardLinkFileService = new Mock<IHardLinkFileService>();
|
||||
HttpClientProvider = new Mock<IDynamicHttpClientProvider>();
|
||||
EventPublisher = new Mock<IEventPublisher>();
|
||||
BlocklistProvider = TestBlocklistProviderFactory.Create();
|
||||
BlocklistProvider = new Mock<IBlocklistProvider>();
|
||||
RuleEvaluator = new Mock<IRuleEvaluator>();
|
||||
RuleManager = new Mock<IRuleManager>();
|
||||
ClientWrapper = new Mock<IUTorrentClientWrapper>();
|
||||
@@ -74,14 +70,13 @@ public class UTorrentServiceFixture : IDisposable
|
||||
|
||||
return new UTorrentService(
|
||||
Logger.Object,
|
||||
Cache,
|
||||
FilenameEvaluator.Object,
|
||||
Striker.Object,
|
||||
DryRunInterceptor.Object,
|
||||
HardLinkFileService.Object,
|
||||
HttpClientProvider.Object,
|
||||
EventPublisher.Object,
|
||||
BlocklistProvider,
|
||||
BlocklistProvider.Object,
|
||||
config,
|
||||
RuleEvaluator.Object,
|
||||
RuleManager.Object,
|
||||
@@ -112,7 +107,6 @@ public class UTorrentServiceFixture : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Cache.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,8 @@ public class DownloadHunterConsumerTests
|
||||
InstanceType = InstanceType.Lidarr,
|
||||
Instance = CreateArrInstance(),
|
||||
SearchItem = new SearchItem { Id = 999 },
|
||||
Record = CreateQueueRecord()
|
||||
Record = CreateQueueRecord(),
|
||||
JobRunId = Guid.NewGuid()
|
||||
};
|
||||
var contextMock = CreateConsumeContextMock(request);
|
||||
|
||||
@@ -128,7 +129,8 @@ public class DownloadHunterConsumerTests
|
||||
InstanceType = InstanceType.Radarr,
|
||||
Instance = CreateArrInstance(),
|
||||
SearchItem = new SearchItem { Id = 123 },
|
||||
Record = CreateQueueRecord()
|
||||
Record = CreateQueueRecord(),
|
||||
JobRunId = Guid.NewGuid()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -282,7 +282,8 @@ public class DownloadHunterTests : IDisposable
|
||||
InstanceType = instanceType,
|
||||
Instance = CreateArrInstance(),
|
||||
SearchItem = new SearchItem { Id = 123 },
|
||||
Record = CreateQueueRecord()
|
||||
Record = CreateQueueRecord(),
|
||||
JobRunId = Guid.NewGuid()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,8 @@ public class DownloadRemoverConsumerTests
|
||||
SearchItem = new SearchItem { Id = 456 },
|
||||
Record = CreateQueueRecord(),
|
||||
RemoveFromClient = true,
|
||||
DeleteReason = DeleteReason.Stalled
|
||||
DeleteReason = DeleteReason.Stalled,
|
||||
JobRunId = Guid.NewGuid()
|
||||
};
|
||||
var contextMock = CreateConsumeContextMock(request);
|
||||
|
||||
@@ -134,7 +135,8 @@ public class DownloadRemoverConsumerTests
|
||||
SearchItem = new SearchItem { Id = 789 },
|
||||
Record = CreateQueueRecord(),
|
||||
RemoveFromClient = false,
|
||||
DeleteReason = DeleteReason.FailedImport
|
||||
DeleteReason = DeleteReason.FailedImport,
|
||||
JobRunId = Guid.NewGuid()
|
||||
};
|
||||
var contextMock = CreateConsumeContextMock(request);
|
||||
|
||||
@@ -162,7 +164,8 @@ public class DownloadRemoverConsumerTests
|
||||
SearchItem = new SearchItem { Id = 111 },
|
||||
Record = CreateQueueRecord(),
|
||||
RemoveFromClient = true,
|
||||
DeleteReason = DeleteReason.SlowSpeed
|
||||
DeleteReason = DeleteReason.SlowSpeed,
|
||||
JobRunId = Guid.NewGuid()
|
||||
};
|
||||
var contextMock = CreateConsumeContextMock(request);
|
||||
|
||||
@@ -191,7 +194,8 @@ public class DownloadRemoverConsumerTests
|
||||
SearchItem = new SearchItem { Id = 123 },
|
||||
Record = CreateQueueRecord(),
|
||||
RemoveFromClient = true,
|
||||
DeleteReason = DeleteReason.Stalled
|
||||
DeleteReason = DeleteReason.Stalled,
|
||||
JobRunId = Guid.NewGuid()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications;
|
||||
using Cleanuparr.Infrastructure.Hubs;
|
||||
using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Infrastructure.Tests.Features.Jobs.TestHelpers;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Data.Models.Arr;
|
||||
using MassTransit;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -48,10 +48,7 @@ public class QueueItemRemoverTests : IDisposable
|
||||
.Returns(_arrClientMock.Object);
|
||||
|
||||
// Create real EventPublisher with mocked dependencies
|
||||
var eventsContextOptions = new DbContextOptionsBuilder<EventsContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
_eventsContext = new EventsContext(eventsContextOptions);
|
||||
_eventsContext = TestEventsContextFactory.Create();
|
||||
|
||||
var hubContextMock = new Mock<IHubContext<AppHub>>();
|
||||
var clientsMock = new Mock<IHubClients>();
|
||||
@@ -59,18 +56,10 @@ public class QueueItemRemoverTests : IDisposable
|
||||
hubContextMock.Setup(h => h.Clients).Returns(clientsMock.Object);
|
||||
|
||||
var dryRunInterceptorMock = new Mock<IDryRunInterceptor>();
|
||||
// Setup interceptor to execute the action with params using DynamicInvoke
|
||||
// Setup interceptor to skip actual database saves (these tests verify QueueItemRemover, not EventPublisher)
|
||||
dryRunInterceptorMock
|
||||
.Setup(d => d.InterceptAsync(It.IsAny<Delegate>(), It.IsAny<object[]>()))
|
||||
.Returns((Delegate action, object[] parameters) =>
|
||||
{
|
||||
var result = action.DynamicInvoke(parameters);
|
||||
if (result is Task task)
|
||||
{
|
||||
return task;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
_eventPublisher = new EventPublisher(
|
||||
_eventsContext,
|
||||
@@ -84,7 +73,8 @@ public class QueueItemRemoverTests : IDisposable
|
||||
_busMock.Object,
|
||||
_memoryCache,
|
||||
_arrClientFactoryMock.Object,
|
||||
_eventPublisher
|
||||
_eventPublisher,
|
||||
_eventsContext
|
||||
);
|
||||
|
||||
// Clear static RecurringHashes before each test
|
||||
@@ -455,7 +445,8 @@ public class QueueItemRemoverTests : IDisposable
|
||||
SearchItem = new SearchItem { Id = 123 },
|
||||
Record = CreateQueueRecord(),
|
||||
RemoveFromClient = removeFromClient,
|
||||
DeleteReason = deleteReason
|
||||
DeleteReason = deleteReason,
|
||||
JobRunId = Guid.NewGuid()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,9 @@ public class JobHandlerFixture : IDisposable
|
||||
|
||||
// Setup default behaviors
|
||||
SetupDefaultBehaviors();
|
||||
|
||||
// Setup JobRunId in context for tests
|
||||
ContextProvider.SetJobRunId(Guid.NewGuid());
|
||||
}
|
||||
|
||||
private void SetupDefaultBehaviors()
|
||||
@@ -56,6 +59,7 @@ public class JobHandlerFixture : IDisposable
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<Domain.Enums.EventSeverity>(),
|
||||
It.IsAny<object?>(),
|
||||
It.IsAny<Guid?>(),
|
||||
It.IsAny<Guid?>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
}
|
||||
@@ -123,6 +127,9 @@ public class JobHandlerFixture : IDisposable
|
||||
TimeProvider = new FakeTimeProvider();
|
||||
|
||||
SetupDefaultBehaviors();
|
||||
|
||||
// Setup fresh JobRunId for each test
|
||||
ContextProvider.SetJobRunId(Guid.NewGuid());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using Cleanuparr.Persistence;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.Jobs.TestHelpers;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating SQLite in-memory EventsContext instances for testing.
|
||||
/// SQLite in-memory supports ExecuteUpdateAsync, ExecuteDeleteAsync, and EF.Functions.Like,
|
||||
/// unlike the EF Core InMemory provider.
|
||||
/// </summary>
|
||||
public static class TestEventsContextFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new SQLite in-memory EventsContext with schema initialized
|
||||
/// </summary>
|
||||
public static EventsContext Create()
|
||||
{
|
||||
var connection = new SqliteConnection("DataSource=:memory:");
|
||||
connection.Open();
|
||||
|
||||
var options = new DbContextOptionsBuilder<EventsContext>()
|
||||
.UseSqlite(connection)
|
||||
.Options;
|
||||
|
||||
var context = new EventsContext(options);
|
||||
context.Database.EnsureCreated();
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ public class NotificationPublisherTests
|
||||
|
||||
private void SetupDownloadCleanerContext()
|
||||
{
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadName, "Test Download");
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, "Test Download");
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadClientUrl, new Uri("http://downloadclient.local"));
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, "HASH123");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Hubs;
|
||||
using Cleanuparr.Infrastructure.Models;
|
||||
using Cleanuparr.Infrastructure.Services;
|
||||
|
||||
@@ -7,25 +7,48 @@ using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Infrastructure.Services;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Cleanuparr.Persistence.Models.State;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Services;
|
||||
|
||||
public class RuleEvaluatorTests
|
||||
public class RuleEvaluatorTests : IDisposable
|
||||
{
|
||||
private readonly EventsContext _context;
|
||||
|
||||
public RuleEvaluatorTests()
|
||||
{
|
||||
_context = CreateInMemoryEventsContext();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Dispose();
|
||||
}
|
||||
|
||||
private static EventsContext CreateInMemoryEventsContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<EventsContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
return new EventsContext(options);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResetStrikes_ShouldRespectMinimumProgressThreshold()
|
||||
{
|
||||
// Arrange
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var stallRule = new StallRule
|
||||
{
|
||||
@@ -47,7 +70,7 @@ public class RuleEvaluatorTests
|
||||
.Returns(stallRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
strikerMock
|
||||
@@ -64,9 +87,14 @@ public class RuleEvaluatorTests
|
||||
torrentMock.SetupGet(t => t.CompletionPercentage).Returns(50);
|
||||
torrentMock.SetupGet(t => t.DownloadedBytes).Returns(() => downloadedBytes);
|
||||
|
||||
// Seed cache with initial observation (no reset expected)
|
||||
await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
strikerMock.Verify(x => x.ResetStrikeAsync(It.IsAny<string>(), It.IsAny<string>(), StrikeType.Stalled), Times.Never);
|
||||
// Seed database with a DownloadItem and initial strike (simulating first observation at 0 bytes)
|
||||
var downloadItem = new DownloadItem { DownloadId = "hash", Title = "Example Torrent" };
|
||||
context.DownloadItems.Add(downloadItem);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var initialStrike = new Strike { DownloadItemId = downloadItem.Id, Type = StrikeType.Stalled, LastDownloadedBytes = 0 };
|
||||
context.Strikes.Add(initialStrike);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Progress below threshold should not reset strikes
|
||||
downloadedBytes = ByteSize.Parse("1 MB").Bytes;
|
||||
@@ -84,10 +112,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingStallRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
@@ -98,7 +126,7 @@ public class RuleEvaluatorTests
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled), Times.Never);
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -106,10 +134,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Stall Apply", resetOnProgress: false, maxStrikes: 5);
|
||||
|
||||
@@ -118,7 +146,7 @@ public class RuleEvaluatorTests
|
||||
.Returns(stallRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
@@ -126,7 +154,7 @@ public class RuleEvaluatorTests
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)stallRule.MaxStrikes, StrikeType.Stalled), Times.Once);
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)stallRule.MaxStrikes, StrikeType.Stalled, It.IsAny<long?>()), Times.Once);
|
||||
strikerMock.Verify(x => x.ResetStrikeAsync(It.IsAny<string>(), It.IsAny<string>(), StrikeType.Stalled), Times.Never);
|
||||
}
|
||||
|
||||
@@ -135,10 +163,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Stall Remove", resetOnProgress: false, maxStrikes: 6);
|
||||
|
||||
@@ -147,7 +175,7 @@ public class RuleEvaluatorTests
|
||||
.Returns(stallRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
@@ -155,7 +183,7 @@ public class RuleEvaluatorTests
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
Assert.True(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)stallRule.MaxStrikes, StrikeType.Stalled), Times.Once);
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)stallRule.MaxStrikes, StrikeType.Stalled, It.IsAny<long?>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -163,10 +191,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var failingRule = CreateStallRule("Failing", resetOnProgress: false, maxStrikes: 4);
|
||||
|
||||
@@ -175,14 +203,14 @@ public class RuleEvaluatorTests
|
||||
.Returns(failingRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ThrowsAsync(new InvalidOperationException("boom"));
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => evaluator.EvaluateStallRulesAsync(torrentMock.Object));
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled), Times.Once);
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -190,10 +218,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
@@ -204,7 +232,7 @@ public class RuleEvaluatorTests
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime), Times.Never);
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -212,10 +240,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule("Slow Apply", resetOnProgress: false, maxStrikes: 3);
|
||||
|
||||
@@ -224,7 +252,7 @@ public class RuleEvaluatorTests
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
@@ -232,7 +260,7 @@ public class RuleEvaluatorTests
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)slowRule.MaxStrikes, StrikeType.SlowTime), Times.Once);
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)slowRule.MaxStrikes, StrikeType.SlowTime, It.IsAny<long?>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -240,10 +268,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule("Slow Remove", resetOnProgress: false, maxStrikes: 8);
|
||||
|
||||
@@ -252,7 +280,7 @@ public class RuleEvaluatorTests
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
@@ -260,7 +288,7 @@ public class RuleEvaluatorTests
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
Assert.True(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)slowRule.MaxStrikes, StrikeType.SlowTime), Times.Once);
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)slowRule.MaxStrikes, StrikeType.SlowTime, It.IsAny<long?>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -268,10 +296,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule("Slow Progress", resetOnProgress: true, maxStrikes: 4);
|
||||
|
||||
@@ -295,10 +323,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var failingRule = CreateSlowRule("Failing Slow", resetOnProgress: false, maxStrikes: 4);
|
||||
|
||||
@@ -307,14 +335,14 @@ public class RuleEvaluatorTests
|
||||
.Returns(failingRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()))
|
||||
.ThrowsAsync(new InvalidOperationException("slow fail"));
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => evaluator.EvaluateSlowRulesAsync(torrentMock.Object));
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime), Times.Once);
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -322,10 +350,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Speed Rule",
|
||||
@@ -339,7 +367,7 @@ public class RuleEvaluatorTests
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowSpeed))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowSpeed, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
@@ -348,7 +376,7 @@ public class RuleEvaluatorTests
|
||||
Assert.True(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(
|
||||
x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)slowRule.MaxStrikes, StrikeType.SlowSpeed),
|
||||
x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)slowRule.MaxStrikes, StrikeType.SlowSpeed, It.IsAny<long?>()),
|
||||
Times.Once);
|
||||
strikerMock.Verify(
|
||||
x => x.ResetStrikeAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<StrikeType>()),
|
||||
@@ -360,10 +388,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Both Rule",
|
||||
@@ -377,7 +405,7 @@ public class RuleEvaluatorTests
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowSpeed))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowSpeed, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
@@ -385,7 +413,7 @@ public class RuleEvaluatorTests
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
Assert.True(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)slowRule.MaxStrikes, StrikeType.SlowSpeed), Times.Once);
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)slowRule.MaxStrikes, StrikeType.SlowSpeed, It.IsAny<long?>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -393,10 +421,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
// Neither minSpeed nor maxTime set (maxTimeHours = 0, minSpeed = null)
|
||||
var slowRule = CreateSlowRule(
|
||||
@@ -415,7 +443,7 @@ public class RuleEvaluatorTests
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), It.IsAny<StrikeType>()), Times.Never);
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), It.IsAny<StrikeType>(), It.IsAny<long?>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -423,10 +451,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Speed Reset",
|
||||
@@ -455,10 +483,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Speed No Reset",
|
||||
@@ -483,10 +511,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Time No Reset",
|
||||
@@ -511,10 +539,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Speed Strike",
|
||||
@@ -528,7 +556,7 @@ public class RuleEvaluatorTests
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowSpeed))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowSpeed, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
@@ -537,7 +565,7 @@ public class RuleEvaluatorTests
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", 3, StrikeType.SlowSpeed), Times.Once);
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", 3, StrikeType.SlowSpeed, It.IsAny<long?>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -545,10 +573,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Time Strike",
|
||||
@@ -562,7 +590,7 @@ public class RuleEvaluatorTests
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
@@ -571,7 +599,7 @@ public class RuleEvaluatorTests
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", 5, StrikeType.SlowTime), Times.Once);
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", 5, StrikeType.SlowTime, It.IsAny<long?>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -579,10 +607,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("No Reset", resetOnProgress: false, maxStrikes: 3);
|
||||
|
||||
@@ -591,7 +619,7 @@ public class RuleEvaluatorTests
|
||||
.Returns(stallRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
long downloadedBytes = ByteSize.Parse("50 MB").Bytes;
|
||||
@@ -609,12 +637,22 @@ public class RuleEvaluatorTests
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WithProgressAndNoMinimumThreshold_ShouldReset()
|
||||
{
|
||||
// Arrange
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
// Seed database with a DownloadItem and initial strike (simulating first observation at 0 bytes)
|
||||
var downloadItem = new DownloadItem { DownloadId = "hash", Title = "Example Torrent" };
|
||||
context.DownloadItems.Add(downloadItem);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var initialStrike = new Strike { DownloadItemId = downloadItem.Id, Type = StrikeType.Stalled, LastDownloadedBytes = 0 };
|
||||
context.Strikes.Add(initialStrike);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Reset No Minimum", resetOnProgress: true, maxStrikes: 3, minimumProgress: null);
|
||||
|
||||
@@ -623,23 +661,19 @@ public class RuleEvaluatorTests
|
||||
.Returns(stallRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.ResetStrikeAsync(It.IsAny<string>(), It.IsAny<string>(), StrikeType.Stalled))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
long downloadedBytes = 0;
|
||||
// Act - Any progress should trigger reset when no minimum is set
|
||||
long downloadedBytes = ByteSize.Parse("1 KB").Bytes;
|
||||
var torrentMock = CreateTorrentMock(downloadedBytesFactory: () => downloadedBytes);
|
||||
|
||||
// Seed cache
|
||||
await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
strikerMock.Verify(x => x.ResetStrikeAsync(It.IsAny<string>(), It.IsAny<string>(), StrikeType.Stalled), Times.Never);
|
||||
|
||||
// Any progress should trigger reset when no minimum is set
|
||||
downloadedBytes = ByteSize.Parse("1 KB").Bytes;
|
||||
await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
// Assert
|
||||
strikerMock.Verify(x => x.ResetStrikeAsync("hash", "Example Torrent", StrikeType.Stalled), Times.Once);
|
||||
}
|
||||
|
||||
@@ -712,10 +746,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingStallRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
@@ -735,10 +769,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Test Rule", resetOnProgress: false, maxStrikes: 3, deletePrivateTorrentsFromClient: true);
|
||||
|
||||
@@ -747,7 +781,7 @@ public class RuleEvaluatorTests
|
||||
.Returns(stallRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
@@ -764,10 +798,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Delete True Rule", resetOnProgress: false, maxStrikes: 3, deletePrivateTorrentsFromClient: true);
|
||||
|
||||
@@ -776,7 +810,7 @@ public class RuleEvaluatorTests
|
||||
.Returns(stallRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
@@ -793,10 +827,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Delete False Rule", resetOnProgress: false, maxStrikes: 3, deletePrivateTorrentsFromClient: false);
|
||||
|
||||
@@ -805,7 +839,7 @@ public class RuleEvaluatorTests
|
||||
.Returns(stallRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
@@ -822,10 +856,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
@@ -845,10 +879,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule("Slow Delete True", resetOnProgress: false, maxStrikes: 3, maxTimeHours: 1, deletePrivateTorrentsFromClient: true);
|
||||
|
||||
@@ -857,7 +891,7 @@ public class RuleEvaluatorTests
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
@@ -874,10 +908,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule("Slow Delete False", resetOnProgress: false, maxStrikes: 3, maxTimeHours: 1, deletePrivateTorrentsFromClient: false);
|
||||
|
||||
@@ -886,7 +920,7 @@ public class RuleEvaluatorTests
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
@@ -903,10 +937,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
"Speed Delete True",
|
||||
@@ -921,7 +955,7 @@ public class RuleEvaluatorTests
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowSpeed))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowSpeed, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
@@ -939,10 +973,10 @@ public class RuleEvaluatorTests
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
using var memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, memoryCache, loggerMock.Object);
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule("Test Slow Rule", resetOnProgress: false, maxStrikes: 3, maxTimeHours: 1, deletePrivateTorrentsFromClient: true);
|
||||
|
||||
@@ -951,7 +985,7 @@ public class RuleEvaluatorTests
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime))
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
@@ -9,7 +9,6 @@ using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Persistence;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
@@ -19,18 +18,18 @@ namespace Cleanuparr.Infrastructure.Tests.Services;
|
||||
|
||||
public class StrikerTests : IDisposable
|
||||
{
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly EventsContext _strikerContext;
|
||||
private readonly ILogger<Striker> _logger;
|
||||
private readonly EventPublisher _eventPublisher;
|
||||
private readonly Striker _striker;
|
||||
|
||||
public StrikerTests()
|
||||
{
|
||||
_cache = new MemoryCache(new MemoryCacheOptions());
|
||||
_strikerContext = CreateInMemoryEventsContext();
|
||||
_logger = Substitute.For<ILogger<Striker>>();
|
||||
|
||||
// Create EventPublisher with mocked dependencies
|
||||
var eventsContext = CreateMockEventsContext();
|
||||
var eventsContext = CreateInMemoryEventsContext();
|
||||
var hubContext = Substitute.For<IHubContext<AppHub>>();
|
||||
var hubClients = Substitute.For<IHubClients>();
|
||||
var clientProxy = Substitute.For<IClientProxy>();
|
||||
@@ -53,11 +52,14 @@ public class StrikerTests : IDisposable
|
||||
notificationPublisher,
|
||||
dryRunInterceptor);
|
||||
|
||||
_striker = new Striker(_logger, _cache, _eventPublisher);
|
||||
_striker = new Striker(_logger, _strikerContext, _eventPublisher);
|
||||
|
||||
// Clear static state before each test
|
||||
Striker.RecurringHashes.Clear();
|
||||
|
||||
// Set up required JobRunId for tests
|
||||
ContextProvider.SetJobRunId(Guid.NewGuid());
|
||||
|
||||
// Set up required context for recurring item events and FailedImport strikes
|
||||
ContextProvider.Set(nameof(InstanceType), (object)InstanceType.Sonarr);
|
||||
ContextProvider.Set(ContextProvider.Keys.ArrInstanceUrl, new Uri("http://localhost:8989"));
|
||||
@@ -71,7 +73,7 @@ public class StrikerTests : IDisposable
|
||||
});
|
||||
}
|
||||
|
||||
private static EventsContext CreateMockEventsContext()
|
||||
private static EventsContext CreateInMemoryEventsContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<EventsContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
@@ -81,7 +83,7 @@ public class StrikerTests : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
_strikerContext.Dispose();
|
||||
Striker.RecurringHashes.Clear();
|
||||
}
|
||||
|
||||
@@ -336,4 +338,64 @@ public class StrikerTests : IDisposable
|
||||
Striker.RecurringHashes.Count.ShouldBe(1);
|
||||
Striker.RecurringHashes.ShouldContainKey(hash.ToLowerInvariant());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StrikeAndCheckLimit_CreatesNewStrikeRowForEachStrike()
|
||||
{
|
||||
// Arrange
|
||||
const string hash = "strike-rows-test";
|
||||
const string itemName = "Test Item";
|
||||
const ushort maxStrikes = 5;
|
||||
|
||||
// Act - Strike 3 times
|
||||
await _striker.StrikeAndCheckLimit(hash, itemName, maxStrikes, StrikeType.Stalled);
|
||||
await _striker.StrikeAndCheckLimit(hash, itemName, maxStrikes, StrikeType.Stalled);
|
||||
await _striker.StrikeAndCheckLimit(hash, itemName, maxStrikes, StrikeType.Stalled);
|
||||
|
||||
// Assert - Should have 3 strike rows
|
||||
var downloadItem = await _strikerContext.DownloadItems.FirstOrDefaultAsync(d => d.DownloadId == hash);
|
||||
downloadItem.ShouldNotBeNull();
|
||||
|
||||
var strikeCount = await _strikerContext.Strikes
|
||||
.CountAsync(s => s.DownloadItemId == downloadItem.Id && s.Type == StrikeType.Stalled);
|
||||
strikeCount.ShouldBe(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StrikeAndCheckLimit_StoresTitleOnDownloadItem()
|
||||
{
|
||||
// Arrange
|
||||
const string hash = "title-test";
|
||||
const string itemName = "My Movie Title 2024";
|
||||
const ushort maxStrikes = 3;
|
||||
|
||||
// Act
|
||||
await _striker.StrikeAndCheckLimit(hash, itemName, maxStrikes, StrikeType.Stalled);
|
||||
|
||||
// Assert
|
||||
var downloadItem = await _strikerContext.DownloadItems.FirstOrDefaultAsync(d => d.DownloadId == hash);
|
||||
downloadItem.ShouldNotBeNull();
|
||||
downloadItem.Title.ShouldBe(itemName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StrikeAndCheckLimit_UpdatesTitleOnDownloadItem_WhenTitleChanges()
|
||||
{
|
||||
// Arrange
|
||||
const string hash = "title-update-test";
|
||||
const string initialTitle = "Initial Title";
|
||||
const string updatedTitle = "Updated Title";
|
||||
const ushort maxStrikes = 5;
|
||||
|
||||
// Act - Strike with initial title
|
||||
await _striker.StrikeAndCheckLimit(hash, initialTitle, maxStrikes, StrikeType.Stalled);
|
||||
|
||||
// Strike with updated title
|
||||
await _striker.StrikeAndCheckLimit(hash, updatedTitle, maxStrikes, StrikeType.Stalled);
|
||||
|
||||
// Assert - Title should be updated
|
||||
var downloadItem = await _strikerContext.DownloadItems.FirstOrDefaultAsync(d => d.DownloadId == hash);
|
||||
downloadItem.ShouldNotBeNull();
|
||||
downloadItem.Title.ShouldBe(updatedTitle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Domain.Exceptions;
|
||||
using Cleanuparr.Infrastructure.Models;
|
||||
using Cleanuparr.Infrastructure.Utilities;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
// using Data.Models.Configuration.ContentBlocker;
|
||||
// using Data.Models.Configuration.DownloadCleaner;
|
||||
// using Data.Models.Configuration.QueueCleaner;
|
||||
// using Infrastructure.Interceptors;
|
||||
// using Infrastructure.Verticals.ContentBlocker;
|
||||
// using Infrastructure.Verticals.DownloadClient;
|
||||
// using Infrastructure.Verticals.Files;
|
||||
// using Infrastructure.Verticals.ItemStriker;
|
||||
// using Infrastructure.Verticals.Notifications;
|
||||
// using Microsoft.Extensions.Caching.Memory;
|
||||
// using Microsoft.Extensions.Logging;
|
||||
// using Microsoft.Extensions.Options;
|
||||
// using NSubstitute;
|
||||
//
|
||||
// namespace Infrastructure.Tests.Verticals.DownloadClient;
|
||||
//
|
||||
// public class DownloadServiceFixture : IDisposable
|
||||
// {
|
||||
// public ILogger<DownloadService> Logger { get; set; }
|
||||
// public IMemoryCache Cache { get; set; }
|
||||
// public IStriker Striker { get; set; }
|
||||
//
|
||||
// public DownloadServiceFixture()
|
||||
// {
|
||||
// Logger = Substitute.For<ILogger<DownloadService>>();
|
||||
// Cache = Substitute.For<IMemoryCache>();
|
||||
// Striker = Substitute.For<IStriker>();
|
||||
// }
|
||||
//
|
||||
// public TestDownloadService CreateSut(
|
||||
// QueueCleanerConfig? queueCleanerConfig = null,
|
||||
// ContentBlockerConfig? contentBlockerConfig = null
|
||||
// )
|
||||
// {
|
||||
// queueCleanerConfig ??= new QueueCleanerConfig
|
||||
// {
|
||||
// Enabled = true,
|
||||
// RunSequentially = true,
|
||||
// StalledResetStrikesOnProgress = true,
|
||||
// StalledMaxStrikes = 3
|
||||
// };
|
||||
//
|
||||
// var queueCleanerOptions = Substitute.For<IOptions<QueueCleanerConfig>>();
|
||||
// queueCleanerOptions.Value.Returns(queueCleanerConfig);
|
||||
//
|
||||
// contentBlockerConfig ??= new ContentBlockerConfig
|
||||
// {
|
||||
// Enabled = true
|
||||
// };
|
||||
//
|
||||
// var contentBlockerOptions = Substitute.For<IOptions<ContentBlockerConfig>>();
|
||||
// contentBlockerOptions.Value.Returns(contentBlockerConfig);
|
||||
//
|
||||
// var downloadCleanerOptions = Substitute.For<IOptions<DownloadCleanerConfig>>();
|
||||
// downloadCleanerOptions.Value.Returns(new DownloadCleanerConfig());
|
||||
//
|
||||
// var filenameEvaluator = Substitute.For<IFilenameEvaluator>();
|
||||
// var notifier = Substitute.For<INotificationPublisher>();
|
||||
// var dryRunInterceptor = Substitute.For<IDryRunInterceptor>();
|
||||
// var hardlinkFileService = Substitute.For<IHardLinkFileService>();
|
||||
//
|
||||
// return new TestDownloadService(
|
||||
// Logger,
|
||||
// queueCleanerOptions,
|
||||
// contentBlockerOptions,
|
||||
// downloadCleanerOptions,
|
||||
// Cache,
|
||||
// filenameEvaluator,
|
||||
// Striker,
|
||||
// notifier,
|
||||
// dryRunInterceptor,
|
||||
// hardlinkFileService
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// public void Dispose()
|
||||
// {
|
||||
// // Cleanup if needed
|
||||
// }
|
||||
// }
|
||||
@@ -1,214 +0,0 @@
|
||||
// using Data.Models.Configuration.DownloadCleaner;
|
||||
// using Data.Enums;
|
||||
// using Data.Models.Cache;
|
||||
// using Infrastructure.Helpers;
|
||||
// using Infrastructure.Verticals.Context;
|
||||
// using Infrastructure.Verticals.DownloadClient;
|
||||
// using NSubstitute;
|
||||
// using NSubstitute.ClearExtensions;
|
||||
// using Shouldly;
|
||||
//
|
||||
// namespace Infrastructure.Tests.Verticals.DownloadClient;
|
||||
//
|
||||
// public class DownloadServiceTests : IClassFixture<DownloadServiceFixture>
|
||||
// {
|
||||
// private readonly DownloadServiceFixture _fixture;
|
||||
//
|
||||
// public DownloadServiceTests(DownloadServiceFixture fixture)
|
||||
// {
|
||||
// _fixture = fixture;
|
||||
// _fixture.Cache.ClearSubstitute();
|
||||
// _fixture.Striker.ClearSubstitute();
|
||||
// }
|
||||
//
|
||||
// public class ResetStrikesOnProgressTests : DownloadServiceTests
|
||||
// {
|
||||
// public ResetStrikesOnProgressTests(DownloadServiceFixture fixture) : base(fixture)
|
||||
// {
|
||||
// }
|
||||
//
|
||||
// [Fact]
|
||||
// public void WhenStalledStrikeDisabled_ShouldNotResetStrikes()
|
||||
// {
|
||||
// // Arrange
|
||||
// TestDownloadService sut = _fixture.CreateSut(queueCleanerConfig: new()
|
||||
// {
|
||||
// Enabled = true,
|
||||
// RunSequentially = true,
|
||||
// StalledResetStrikesOnProgress = false,
|
||||
// });
|
||||
//
|
||||
// // Act
|
||||
// sut.ResetStalledStrikesOnProgress("test-hash", 100);
|
||||
//
|
||||
// // Assert
|
||||
// _fixture.Cache.ReceivedCalls().ShouldBeEmpty();
|
||||
// }
|
||||
//
|
||||
// [Fact]
|
||||
// public void WhenProgressMade_ShouldResetStrikes()
|
||||
// {
|
||||
// // Arrange
|
||||
// const string hash = "test-hash";
|
||||
// StalledCacheItem stalledCacheItem = new StalledCacheItem { Downloaded = 100 };
|
||||
//
|
||||
// _fixture.Cache.TryGetValue(Arg.Any<object>(), out Arg.Any<object?>())
|
||||
// .Returns(x =>
|
||||
// {
|
||||
// x[1] = stalledCacheItem;
|
||||
// return true;
|
||||
// });
|
||||
//
|
||||
// TestDownloadService sut = _fixture.CreateSut();
|
||||
//
|
||||
// // Act
|
||||
// sut.ResetStalledStrikesOnProgress(hash, 200);
|
||||
//
|
||||
// // Assert
|
||||
// _fixture.Cache.Received(1).Remove(CacheKeys.Strike(StrikeType.Stalled, hash));
|
||||
// }
|
||||
//
|
||||
// [Fact]
|
||||
// public void WhenNoProgress_ShouldNotResetStrikes()
|
||||
// {
|
||||
// // Arrange
|
||||
// const string hash = "test-hash";
|
||||
// StalledCacheItem stalledCacheItem = new StalledCacheItem { Downloaded = 200 };
|
||||
//
|
||||
// _fixture.Cache
|
||||
// .TryGetValue(Arg.Any<object>(), out Arg.Any<object?>())
|
||||
// .Returns(x =>
|
||||
// {
|
||||
// x[1] = stalledCacheItem;
|
||||
// return true;
|
||||
// });
|
||||
//
|
||||
// TestDownloadService sut = _fixture.CreateSut();
|
||||
//
|
||||
// // Act
|
||||
// sut.ResetStalledStrikesOnProgress(hash, 100);
|
||||
//
|
||||
// // Assert
|
||||
// _fixture.Cache.DidNotReceive().Remove(Arg.Any<object>());
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public class StrikeAndCheckLimitTests : DownloadServiceTests
|
||||
// {
|
||||
// public StrikeAndCheckLimitTests(DownloadServiceFixture fixture) : base(fixture)
|
||||
// {
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public class ShouldCleanDownloadTests : DownloadServiceTests
|
||||
// {
|
||||
// public ShouldCleanDownloadTests(DownloadServiceFixture fixture) : base(fixture)
|
||||
// {
|
||||
// ContextProvider.Set(ContextProvider.Keys.DownloadName, "test-download");
|
||||
// }
|
||||
//
|
||||
// [Fact]
|
||||
// public void WhenRatioAndMinSeedTimeReached_ShouldReturnTrue()
|
||||
// {
|
||||
// // Arrange
|
||||
// CleanCategory category = new()
|
||||
// {
|
||||
// Name = "test",
|
||||
// MaxRatio = 1.0,
|
||||
// MinSeedTime = 1,
|
||||
// MaxSeedTime = -1
|
||||
// };
|
||||
// const double ratio = 1.5;
|
||||
// TimeSpan seedingTime = TimeSpan.FromHours(2);
|
||||
//
|
||||
// TestDownloadService sut = _fixture.CreateSut();
|
||||
//
|
||||
// // Act
|
||||
// var result = sut.ShouldCleanDownload(ratio, seedingTime, category);
|
||||
//
|
||||
// // Assert
|
||||
// result.ShouldSatisfyAllConditions(
|
||||
// () => result.ShouldClean.ShouldBeTrue(),
|
||||
// () => result.Reason.ShouldBe(CleanReason.MaxRatioReached)
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// [Fact]
|
||||
// public void WhenRatioReachedAndMinSeedTimeNotReached_ShouldReturnFalse()
|
||||
// {
|
||||
// // Arrange
|
||||
// CleanCategory category = new()
|
||||
// {
|
||||
// Name = "test",
|
||||
// MaxRatio = 1.0,
|
||||
// MinSeedTime = 3,
|
||||
// MaxSeedTime = -1
|
||||
// };
|
||||
// const double ratio = 1.5;
|
||||
// TimeSpan seedingTime = TimeSpan.FromHours(2);
|
||||
//
|
||||
// TestDownloadService sut = _fixture.CreateSut();
|
||||
//
|
||||
// // Act
|
||||
// var result = sut.ShouldCleanDownload(ratio, seedingTime, category);
|
||||
//
|
||||
// // Assert
|
||||
// result.ShouldSatisfyAllConditions(
|
||||
// () => result.ShouldClean.ShouldBeFalse(),
|
||||
// () => result.Reason.ShouldBe(CleanReason.None)
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// [Fact]
|
||||
// public void WhenMaxSeedTimeReached_ShouldReturnTrue()
|
||||
// {
|
||||
// // Arrange
|
||||
// CleanCategory category = new()
|
||||
// {
|
||||
// Name = "test",
|
||||
// MaxRatio = -1,
|
||||
// MinSeedTime = 0,
|
||||
// MaxSeedTime = 1
|
||||
// };
|
||||
// const double ratio = 0.5;
|
||||
// TimeSpan seedingTime = TimeSpan.FromHours(2);
|
||||
//
|
||||
// TestDownloadService sut = _fixture.CreateSut();
|
||||
//
|
||||
// // Act
|
||||
// SeedingCheckResult result = sut.ShouldCleanDownload(ratio, seedingTime, category);
|
||||
//
|
||||
// // Assert
|
||||
// result.ShouldSatisfyAllConditions(
|
||||
// () => result.ShouldClean.ShouldBeTrue(),
|
||||
// () => result.Reason.ShouldBe(CleanReason.MaxSeedTimeReached)
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// [Fact]
|
||||
// public void WhenNeitherConditionMet_ShouldReturnFalse()
|
||||
// {
|
||||
// // Arrange
|
||||
// CleanCategory category = new()
|
||||
// {
|
||||
// Name = "test",
|
||||
// MaxRatio = 2.0,
|
||||
// MinSeedTime = 0,
|
||||
// MaxSeedTime = 3
|
||||
// };
|
||||
// const double ratio = 1.0;
|
||||
// TimeSpan seedingTime = TimeSpan.FromHours(1);
|
||||
//
|
||||
// TestDownloadService sut = _fixture.CreateSut();
|
||||
//
|
||||
// // Act
|
||||
// var result = sut.ShouldCleanDownload(ratio, seedingTime, category);
|
||||
//
|
||||
// // Assert
|
||||
// result.ShouldSatisfyAllConditions(
|
||||
// () => result.ShouldClean.ShouldBeFalse(),
|
||||
// () => result.Reason.ShouldBe(CleanReason.None)
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -14,7 +14,7 @@ public class EventCleanupService : BackgroundService
|
||||
private readonly ILogger<EventCleanupService> _logger;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly TimeSpan _cleanupInterval = TimeSpan.FromHours(4); // Run every 4 hours
|
||||
private readonly int _retentionDays = 30; // Keep events for 30 days
|
||||
private readonly int _eventRetentionDays = 30; // Keep events for 30 days
|
||||
|
||||
public EventCleanupService(ILogger<EventCleanupService> logger, IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
@@ -25,7 +25,7 @@ public class EventCleanupService : BackgroundService
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("Event cleanup service started. Interval: {interval}, Retention: {retention} days",
|
||||
_cleanupInterval, _retentionDays);
|
||||
_cleanupInterval, _eventRetentionDays);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -59,16 +59,19 @@ public class EventCleanupService : BackgroundService
|
||||
try
|
||||
{
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<EventsContext>();
|
||||
var eventsContext = scope.ServiceProvider.GetRequiredService<EventsContext>();
|
||||
var dataContext = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||
|
||||
var cutoffDate = DateTime.UtcNow.AddDays(-_retentionDays);
|
||||
await context.Events
|
||||
var cutoffDate = DateTime.UtcNow.AddDays(-_eventRetentionDays);
|
||||
await eventsContext.Events
|
||||
.Where(e => e.Timestamp < cutoffDate)
|
||||
.ExecuteDeleteAsync();
|
||||
await context.ManualEvents
|
||||
await eventsContext.ManualEvents
|
||||
.Where(e => e.Timestamp < cutoffDate)
|
||||
.Where(e => e.IsResolved)
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
await CleanupStrikesAsync(eventsContext, dataContext);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -76,6 +79,48 @@ public class EventCleanupService : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CleanupStrikesAsync(EventsContext eventsContext, DataContext dataContext)
|
||||
{
|
||||
var config = await dataContext.GeneralConfigs
|
||||
.AsNoTracking()
|
||||
.FirstAsync();
|
||||
|
||||
var inactivityWindowHours = config.StrikeInactivityWindowHours;
|
||||
var cutoffDate = DateTime.UtcNow.AddHours(-inactivityWindowHours);
|
||||
|
||||
// Sliding window: find items whose most recent strike is older than the inactivity window.
|
||||
// As long as a download keeps receiving new strikes, all its strikes are preserved.
|
||||
var inactiveItemIds = await eventsContext.Strikes
|
||||
.GroupBy(s => s.DownloadItemId)
|
||||
.Where(g => g.Max(s => s.CreatedAt) < cutoffDate)
|
||||
.Select(g => g.Key)
|
||||
.ToListAsync();
|
||||
|
||||
if (inactiveItemIds.Count > 0)
|
||||
{
|
||||
var deletedStrikesCount = await eventsContext.Strikes
|
||||
.Where(s => inactiveItemIds.Contains(s.DownloadItemId))
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
if (deletedStrikesCount > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Cleaned up {count} strikes from {items} inactive items (no new strikes for {hours} hours)",
|
||||
deletedStrikesCount, inactiveItemIds.Count, inactivityWindowHours);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up orphaned DownloadItems (those with no strikes)
|
||||
int deletedDownloadItemsCount = await eventsContext.DownloadItems
|
||||
.Where(d => !d.Strikes.Any())
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
if (deletedDownloadItemsCount > 0)
|
||||
{
|
||||
_logger.LogTrace("Cleaned up {count} download items with 0 strikes", deletedDownloadItemsCount);
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Event cleanup service stopping...");
|
||||
|
||||
@@ -43,7 +43,7 @@ public class EventPublisher : IEventPublisher
|
||||
/// <summary>
|
||||
/// Generic method for publishing events to database and SignalR clients
|
||||
/// </summary>
|
||||
public async Task PublishAsync(EventType eventType, string message, EventSeverity severity, object? data = null, Guid? trackingId = null)
|
||||
public async Task PublishAsync(EventType eventType, string message, EventSeverity severity, object? data = null, Guid? trackingId = null, Guid? strikeId = null)
|
||||
{
|
||||
AppEvent eventEntity = new()
|
||||
{
|
||||
@@ -54,7 +54,13 @@ public class EventPublisher : IEventPublisher
|
||||
{
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
}) : null,
|
||||
TrackingId = trackingId
|
||||
TrackingId = trackingId,
|
||||
StrikeId = strikeId,
|
||||
JobRunId = ContextProvider.TryGetJobRunId(),
|
||||
InstanceType = ContextProvider.Get(nameof(InstanceType)) is InstanceType it ? it : null,
|
||||
InstanceUrl = (ContextProvider.Get(ContextProvider.Keys.ArrInstanceUrl) as Uri)?.ToString(),
|
||||
DownloadClientType = ContextProvider.Get(ContextProvider.Keys.DownloadClientType) is DownloadClientTypeName dct ? dct : null,
|
||||
DownloadClientName = ContextProvider.Get(ContextProvider.Keys.DownloadClientName) as string,
|
||||
};
|
||||
|
||||
// Save to database with dry run interception
|
||||
@@ -65,7 +71,7 @@ public class EventPublisher : IEventPublisher
|
||||
|
||||
_logger.LogTrace("Published event: {eventType}", eventType);
|
||||
}
|
||||
|
||||
|
||||
public async Task PublishManualAsync(string message, EventSeverity severity, object? data = null)
|
||||
{
|
||||
ManualEvent eventEntity = new()
|
||||
@@ -76,21 +82,26 @@ public class EventPublisher : IEventPublisher
|
||||
{
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
}) : null,
|
||||
JobRunId = ContextProvider.TryGetJobRunId(),
|
||||
InstanceType = ContextProvider.Get(nameof(InstanceType)) is InstanceType it ? it : null,
|
||||
InstanceUrl = (ContextProvider.Get(ContextProvider.Keys.ArrInstanceUrl) as Uri)?.ToString(),
|
||||
DownloadClientType = ContextProvider.Get(ContextProvider.Keys.DownloadClientType) is DownloadClientTypeName dct ? dct : null,
|
||||
DownloadClientName = ContextProvider.Get(ContextProvider.Keys.DownloadClientName) as string,
|
||||
};
|
||||
|
||||
|
||||
// Save to database with dry run interception
|
||||
await _dryRunInterceptor.InterceptAsync(SaveManualEventToDatabase, eventEntity);
|
||||
|
||||
|
||||
// Always send to SignalR clients (not affected by dry run)
|
||||
await NotifyClientsAsync(eventEntity);
|
||||
|
||||
|
||||
_logger.LogTrace("Published manual event: {message}", message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a strike event with context data and notifications
|
||||
/// </summary>
|
||||
public async Task PublishStrike(StrikeType strikeType, int strikeCount, string hash, string itemName)
|
||||
public async Task PublishStrike(StrikeType strikeType, int strikeCount, string hash, string itemName, Guid? strikeId = null)
|
||||
{
|
||||
// Determine the appropriate EventType based on StrikeType
|
||||
EventType eventType = strikeType switch
|
||||
@@ -133,7 +144,11 @@ public class EventPublisher : IEventPublisher
|
||||
eventType,
|
||||
$"Item '{itemName}' has been struck {strikeCount} times for reason '{strikeType}'",
|
||||
EventSeverity.Important,
|
||||
data: data);
|
||||
data: data,
|
||||
strikeId: strikeId);
|
||||
|
||||
// Broadcast strike to SignalR clients for real-time dashboard updates
|
||||
await BroadcastStrikeAsync(strikeId, strikeType, hash, itemName);
|
||||
|
||||
// Send notification (uses ContextProvider internally)
|
||||
await _notificationPublisher.NotifyStrike(strikeType, strikeCount);
|
||||
@@ -145,7 +160,7 @@ public class EventPublisher : IEventPublisher
|
||||
public async Task PublishQueueItemDeleted(bool removeFromClient, DeleteReason deleteReason)
|
||||
{
|
||||
// Get context data for the event
|
||||
string downloadName = ContextProvider.Get<string>(ContextProvider.Keys.DownloadName);
|
||||
string itemName = ContextProvider.Get<string>(ContextProvider.Keys.ItemName);
|
||||
string hash = ContextProvider.Get<string>(ContextProvider.Keys.Hash);
|
||||
|
||||
// Publish the event
|
||||
@@ -153,7 +168,7 @@ public class EventPublisher : IEventPublisher
|
||||
EventType.QueueItemDeleted,
|
||||
$"Deleting item from queue with reason: {deleteReason}",
|
||||
EventSeverity.Important,
|
||||
data: new { downloadName, hash, removeFromClient, deleteReason });
|
||||
data: new { itemName, hash, removeFromClient, deleteReason });
|
||||
|
||||
// Send notification (uses ContextProvider internally)
|
||||
await _notificationPublisher.NotifyQueueItemDeleted(removeFromClient, deleteReason);
|
||||
@@ -165,7 +180,7 @@ public class EventPublisher : IEventPublisher
|
||||
public async Task PublishDownloadCleaned(double ratio, TimeSpan seedingTime, string categoryName, CleanReason reason)
|
||||
{
|
||||
// Get context data for the event
|
||||
string downloadName = ContextProvider.Get<string>(ContextProvider.Keys.DownloadName);
|
||||
string itemName = ContextProvider.Get<string>(ContextProvider.Keys.ItemName);
|
||||
string hash = ContextProvider.Get<string>(ContextProvider.Keys.Hash);
|
||||
|
||||
// Publish the event
|
||||
@@ -173,7 +188,7 @@ public class EventPublisher : IEventPublisher
|
||||
EventType.DownloadCleaned,
|
||||
$"Cleaned item from download client with reason: {reason}",
|
||||
EventSeverity.Important,
|
||||
data: new { downloadName, hash, categoryName, ratio, seedingTime = seedingTime.TotalHours, reason });
|
||||
data: new { itemName, hash, categoryName, ratio, seedingTime = seedingTime.TotalHours, reason });
|
||||
|
||||
// Send notification (uses ContextProvider internally)
|
||||
await _notificationPublisher.NotifyDownloadCleaned(ratio, seedingTime, categoryName, reason);
|
||||
@@ -185,7 +200,7 @@ public class EventPublisher : IEventPublisher
|
||||
public async Task PublishCategoryChanged(string oldCategory, string newCategory, bool isTag = false)
|
||||
{
|
||||
// Get context data for the event
|
||||
string downloadName = ContextProvider.Get<string>(ContextProvider.Keys.DownloadName);
|
||||
string itemName = ContextProvider.Get<string>(ContextProvider.Keys.ItemName);
|
||||
string hash = ContextProvider.Get<string>(ContextProvider.Keys.Hash);
|
||||
|
||||
// Publish the event
|
||||
@@ -193,7 +208,7 @@ public class EventPublisher : IEventPublisher
|
||||
EventType.CategoryChanged,
|
||||
isTag ? $"Tag '{newCategory}' added to download" : $"Category changed from '{oldCategory}' to '{newCategory}'",
|
||||
EventSeverity.Information,
|
||||
data: new { downloadName, hash, oldCategory, newCategory, isTag });
|
||||
data: new { itemName, hash, oldCategory, newCategory, isTag });
|
||||
|
||||
// Send notification (uses ContextProvider internally)
|
||||
await _notificationPublisher.NotifyCategoryChanged(oldCategory, newCategory, isTag);
|
||||
@@ -204,14 +219,10 @@ public class EventPublisher : IEventPublisher
|
||||
/// </summary>
|
||||
public async Task PublishRecurringItem(string hash, string itemName, int strikeCount)
|
||||
{
|
||||
var instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||
var instanceUrl = ContextProvider.Get<Uri>(ContextProvider.Keys.ArrInstanceUrl);
|
||||
|
||||
// Publish the event
|
||||
await PublishManualAsync(
|
||||
"Download keeps coming back after deletion\nTo prevent further issues, please consult the prerequisites: https://cleanuparr.github.io/Cleanuparr/docs/installation/",
|
||||
EventSeverity.Important,
|
||||
data: new { itemName, hash, strikeCount, instanceType, instanceUrl }
|
||||
data: new { itemName, hash, strikeCount }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -220,13 +231,10 @@ public class EventPublisher : IEventPublisher
|
||||
/// </summary>
|
||||
public async Task PublishSearchNotTriggered(string hash, string itemName)
|
||||
{
|
||||
var instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||
var instanceUrl = ContextProvider.Get<Uri>(ContextProvider.Keys.ArrInstanceUrl);
|
||||
|
||||
await PublishManualAsync(
|
||||
"Replacement search was not triggered after removal because the item keeps coming back\nPlease trigger a manual search if needed",
|
||||
EventSeverity.Warning,
|
||||
data: new { itemName, hash, instanceType, instanceUrl }
|
||||
data: new { itemName, hash }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -267,4 +275,24 @@ public class EventPublisher : IEventPublisher
|
||||
_logger.LogError(ex, "Failed to send event {eventId} to SignalR clients", appEventEntity.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BroadcastStrikeAsync(Guid? strikeId, StrikeType strikeType, string hash, string itemName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var strike = new
|
||||
{
|
||||
Id = strikeId ?? Guid.Empty,
|
||||
Type = strikeType.ToString(),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
DownloadId = hash,
|
||||
Title = itemName,
|
||||
};
|
||||
await _appHubContext.Clients.All.SendAsync("StrikeReceived", strike);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to send strike to SignalR clients");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,11 @@ namespace Cleanuparr.Infrastructure.Events.Interfaces;
|
||||
|
||||
public interface IEventPublisher
|
||||
{
|
||||
Task PublishAsync(EventType eventType, string message, EventSeverity severity, object? data = null, Guid? trackingId = null);
|
||||
Task PublishAsync(EventType eventType, string message, EventSeverity severity, object? data = null, Guid? trackingId = null, Guid? strikeId = null);
|
||||
|
||||
Task PublishManualAsync(string message, EventSeverity severity, object? data = null);
|
||||
|
||||
Task PublishStrike(StrikeType strikeType, int strikeCount, string hash, string itemName);
|
||||
Task PublishStrike(StrikeType strikeType, int strikeCount, string hash, string itemName, Guid? strikeId = null);
|
||||
|
||||
Task PublishQueueItemDeleted(bool removeFromClient, DeleteReason deleteReason);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Context;
|
||||
|
||||
@@ -34,12 +34,23 @@ public static class ContextProvider
|
||||
return Get<T>(key);
|
||||
}
|
||||
|
||||
public const string JobRunIdKey = "JobRunId";
|
||||
|
||||
public static Guid GetJobRunId() =>
|
||||
Get(JobRunIdKey) as Guid? ?? throw new InvalidOperationException("JobRunId not set in context");
|
||||
|
||||
public static Guid? TryGetJobRunId() => Get(JobRunIdKey) as Guid?;
|
||||
|
||||
public static void SetJobRunId(Guid id) => Set(JobRunIdKey, id);
|
||||
|
||||
public static class Keys
|
||||
{
|
||||
public const string Version = "version";
|
||||
public const string DownloadName = "downloadName";
|
||||
public const string ItemName = "itemName";
|
||||
public const string Hash = "hash";
|
||||
public const string DownloadClientUrl = "downloadClientUrl";
|
||||
public const string DownloadClientType = "downloadClientType";
|
||||
public const string DownloadClientName = "downloadClientName";
|
||||
public const string ArrInstanceUrl = "arrInstanceUrl";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Cleanuparr.Domain.Entities.Deluge.Response;
|
||||
using Cleanuparr.Domain.Entities.HealthCheck;
|
||||
using Cleanuparr.Domain.Exceptions;
|
||||
using Cleanuparr.Infrastructure.Events;
|
||||
using Cleanuparr.Infrastructure.Events.Interfaces;
|
||||
@@ -20,7 +21,6 @@ public partial class DelugeService : DownloadService, IDelugeService
|
||||
|
||||
public DelugeService(
|
||||
ILogger<DelugeService> logger,
|
||||
IMemoryCache cache,
|
||||
IFilenameEvaluator filenameEvaluator,
|
||||
IStriker striker,
|
||||
IDryRunInterceptor dryRunInterceptor,
|
||||
@@ -32,7 +32,7 @@ public partial class DelugeService : DownloadService, IDelugeService
|
||||
IRuleEvaluator ruleEvaluator,
|
||||
IRuleManager ruleManager
|
||||
) : base(
|
||||
logger, cache,
|
||||
logger,
|
||||
filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
)
|
||||
@@ -44,7 +44,6 @@ public partial class DelugeService : DownloadService, IDelugeService
|
||||
// Internal constructor for testing
|
||||
internal DelugeService(
|
||||
ILogger<DelugeService> logger,
|
||||
IMemoryCache cache,
|
||||
IFilenameEvaluator filenameEvaluator,
|
||||
IStriker striker,
|
||||
IDryRunInterceptor dryRunInterceptor,
|
||||
@@ -57,7 +56,7 @@ public partial class DelugeService : DownloadService, IDelugeService
|
||||
IRuleManager ruleManager,
|
||||
IDelugeClientWrapper clientWrapper
|
||||
) : base(
|
||||
logger, cache,
|
||||
logger,
|
||||
filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
)
|
||||
|
||||
@@ -72,9 +72,11 @@ public partial class DelugeService
|
||||
continue;
|
||||
}
|
||||
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadClientUrl, _downloadClientConfig.ExternalOrInternalUrl);
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadClientType, _downloadClientConfig.TypeName);
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadClientName, _downloadClientConfig.Name);
|
||||
|
||||
DelugeContents? contents;
|
||||
try
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Entities.HealthCheck;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Events;
|
||||
using Cleanuparr.Infrastructure.Events.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.Files;
|
||||
@@ -11,26 +11,15 @@ using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
|
||||
public class HealthCheckResult
|
||||
{
|
||||
public bool IsHealthy { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public TimeSpan ResponseTime { get; set; }
|
||||
}
|
||||
|
||||
public abstract class DownloadService : IDownloadService
|
||||
{
|
||||
protected readonly ILogger<DownloadService> _logger;
|
||||
protected readonly IMemoryCache _cache;
|
||||
protected readonly IFilenameEvaluator _filenameEvaluator;
|
||||
protected readonly IStriker _striker;
|
||||
protected readonly MemoryCacheEntryOptions _cacheOptions;
|
||||
protected readonly IDryRunInterceptor _dryRunInterceptor;
|
||||
protected readonly IHardLinkFileService _hardLinkFileService;
|
||||
protected readonly IEventPublisher _eventPublisher;
|
||||
@@ -42,7 +31,6 @@ public abstract class DownloadService : IDownloadService
|
||||
|
||||
protected DownloadService(
|
||||
ILogger<DownloadService> logger,
|
||||
IMemoryCache cache,
|
||||
IFilenameEvaluator filenameEvaluator,
|
||||
IStriker striker,
|
||||
IDryRunInterceptor dryRunInterceptor,
|
||||
@@ -56,15 +44,12 @@ public abstract class DownloadService : IDownloadService
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_cache = cache;
|
||||
_filenameEvaluator = filenameEvaluator;
|
||||
_striker = striker;
|
||||
_dryRunInterceptor = dryRunInterceptor;
|
||||
_hardLinkFileService = hardLinkFileService;
|
||||
_eventPublisher = eventPublisher;
|
||||
_blocklistProvider = blocklistProvider;
|
||||
_cacheOptions = new MemoryCacheEntryOptions()
|
||||
.SetSlidingExpiration(StaticConfiguration.TriggerValue + Constants.CacheLimitBuffer);
|
||||
_downloadClientConfig = downloadClientConfig;
|
||||
_httpClient = httpClientProvider.CreateClient(downloadClientConfig);
|
||||
_ruleEvaluator = ruleEvaluator;
|
||||
@@ -124,9 +109,11 @@ public abstract class DownloadService : IDownloadService
|
||||
continue;
|
||||
}
|
||||
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadClientUrl, _downloadClientConfig.ExternalOrInternalUrl);
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadClientType, _downloadClientConfig.TypeName);
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadClientName, _downloadClientConfig.Name);
|
||||
|
||||
TimeSpan seedingTime = TimeSpan.FromSeconds(torrent.SeedingTimeSeconds);
|
||||
SeedingCheckResult result = ShouldCleanDownload(torrent.Ratio, seedingTime, category);
|
||||
@@ -220,7 +207,7 @@ public abstract class DownloadService : IDownloadService
|
||||
return false;
|
||||
}
|
||||
|
||||
string downloadName = ContextProvider.Get<string>(ContextProvider.Keys.DownloadName);
|
||||
string downloadName = ContextProvider.Get<string>(ContextProvider.Keys.ItemName);
|
||||
TimeSpan minSeedingTime = TimeSpan.FromHours(category.MinSeedTime);
|
||||
|
||||
if (category.MinSeedTime > 0 && seedingTime < minSeedingTime)
|
||||
@@ -246,7 +233,7 @@ public abstract class DownloadService : IDownloadService
|
||||
return false;
|
||||
}
|
||||
|
||||
string downloadName = ContextProvider.Get<string>(ContextProvider.Keys.DownloadName);
|
||||
string downloadName = ContextProvider.Get<string>(ContextProvider.Keys.ItemName);
|
||||
TimeSpan maxSeedingTime = TimeSpan.FromHours(category.MaxSeedTime);
|
||||
|
||||
if (category.MaxSeedTime > 0 && seedingTime < maxSeedingTime)
|
||||
|
||||
@@ -61,7 +61,6 @@ public sealed class DownloadServiceFactory : IDownloadServiceFactory
|
||||
private QBitService CreateQBitService(DownloadClientConfig downloadClientConfig)
|
||||
{
|
||||
var logger = _serviceProvider.GetRequiredService<ILogger<QBitService>>();
|
||||
var cache = _serviceProvider.GetRequiredService<IMemoryCache>();
|
||||
var filenameEvaluator = _serviceProvider.GetRequiredService<IFilenameEvaluator>();
|
||||
var striker = _serviceProvider.GetRequiredService<IStriker>();
|
||||
var dryRunInterceptor = _serviceProvider.GetRequiredService<IDryRunInterceptor>();
|
||||
@@ -75,7 +74,7 @@ public sealed class DownloadServiceFactory : IDownloadServiceFactory
|
||||
|
||||
// Create the QBitService instance
|
||||
QBitService service = new(
|
||||
logger, cache, filenameEvaluator, striker, dryRunInterceptor,
|
||||
logger, filenameEvaluator, striker, dryRunInterceptor,
|
||||
hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
);
|
||||
|
||||
@@ -86,7 +85,6 @@ public sealed class DownloadServiceFactory : IDownloadServiceFactory
|
||||
{
|
||||
var logger = _serviceProvider.GetRequiredService<ILogger<DelugeService>>();
|
||||
var filenameEvaluator = _serviceProvider.GetRequiredService<IFilenameEvaluator>();
|
||||
var cache = _serviceProvider.GetRequiredService<IMemoryCache>();
|
||||
var striker = _serviceProvider.GetRequiredService<IStriker>();
|
||||
var dryRunInterceptor = _serviceProvider.GetRequiredService<IDryRunInterceptor>();
|
||||
var hardLinkFileService = _serviceProvider.GetRequiredService<IHardLinkFileService>();
|
||||
@@ -99,7 +97,7 @@ public sealed class DownloadServiceFactory : IDownloadServiceFactory
|
||||
|
||||
// Create the DelugeService instance
|
||||
DelugeService service = new(
|
||||
logger, cache, filenameEvaluator, striker, dryRunInterceptor,
|
||||
logger, filenameEvaluator, striker, dryRunInterceptor,
|
||||
hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
);
|
||||
|
||||
@@ -109,7 +107,6 @@ public sealed class DownloadServiceFactory : IDownloadServiceFactory
|
||||
private TransmissionService CreateTransmissionService(DownloadClientConfig downloadClientConfig)
|
||||
{
|
||||
var logger = _serviceProvider.GetRequiredService<ILogger<TransmissionService>>();
|
||||
var cache = _serviceProvider.GetRequiredService<IMemoryCache>();
|
||||
var filenameEvaluator = _serviceProvider.GetRequiredService<IFilenameEvaluator>();
|
||||
var striker = _serviceProvider.GetRequiredService<IStriker>();
|
||||
var dryRunInterceptor = _serviceProvider.GetRequiredService<IDryRunInterceptor>();
|
||||
@@ -123,7 +120,7 @@ public sealed class DownloadServiceFactory : IDownloadServiceFactory
|
||||
|
||||
// Create the TransmissionService instance
|
||||
TransmissionService service = new(
|
||||
logger, cache, filenameEvaluator, striker, dryRunInterceptor,
|
||||
logger, filenameEvaluator, striker, dryRunInterceptor,
|
||||
hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Domain.Entities.HealthCheck;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Cleanuparr.Domain.Entities.HealthCheck;
|
||||
using Cleanuparr.Infrastructure.Events;
|
||||
using Cleanuparr.Infrastructure.Events.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.Files;
|
||||
@@ -21,7 +22,6 @@ public partial class QBitService : DownloadService, IQBitService
|
||||
|
||||
public QBitService(
|
||||
ILogger<QBitService> logger,
|
||||
IMemoryCache cache,
|
||||
IFilenameEvaluator filenameEvaluator,
|
||||
IStriker striker,
|
||||
IDryRunInterceptor dryRunInterceptor,
|
||||
@@ -33,7 +33,7 @@ public partial class QBitService : DownloadService, IQBitService
|
||||
IRuleEvaluator ruleEvaluator,
|
||||
IRuleManager ruleManager
|
||||
) : base(
|
||||
logger, cache, filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
logger, filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
)
|
||||
{
|
||||
@@ -44,7 +44,6 @@ public partial class QBitService : DownloadService, IQBitService
|
||||
// Internal constructor for testing
|
||||
internal QBitService(
|
||||
ILogger<QBitService> logger,
|
||||
IMemoryCache cache,
|
||||
IFilenameEvaluator filenameEvaluator,
|
||||
IStriker striker,
|
||||
IDryRunInterceptor dryRunInterceptor,
|
||||
@@ -57,7 +56,7 @@ public partial class QBitService : DownloadService, IQBitService
|
||||
IRuleManager ruleManager,
|
||||
IQBittorrentClientWrapper clientWrapper
|
||||
) : base(
|
||||
logger, cache, filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
logger, filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
)
|
||||
{
|
||||
|
||||
@@ -104,9 +104,11 @@ public partial class QBitService
|
||||
continue;
|
||||
}
|
||||
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadClientUrl, _downloadClientConfig.ExternalOrInternalUrl);
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadClientType, _downloadClientConfig.TypeName);
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadClientName, _downloadClientConfig.Name);
|
||||
bool hasHardlinks = false;
|
||||
bool hasErrors = false;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Cleanuparr.Domain.Entities.HealthCheck;
|
||||
using Cleanuparr.Infrastructure.Events;
|
||||
using Cleanuparr.Infrastructure.Events.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.Files;
|
||||
@@ -39,7 +40,6 @@ public partial class TransmissionService : DownloadService, ITransmissionService
|
||||
|
||||
public TransmissionService(
|
||||
ILogger<TransmissionService> logger,
|
||||
IMemoryCache cache,
|
||||
IFilenameEvaluator filenameEvaluator,
|
||||
IStriker striker,
|
||||
IDryRunInterceptor dryRunInterceptor,
|
||||
@@ -51,7 +51,7 @@ public partial class TransmissionService : DownloadService, ITransmissionService
|
||||
IRuleEvaluator ruleEvaluator,
|
||||
IRuleManager ruleManager
|
||||
) : base(
|
||||
logger, cache,
|
||||
logger,
|
||||
filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
)
|
||||
@@ -70,7 +70,6 @@ public partial class TransmissionService : DownloadService, ITransmissionService
|
||||
// Internal constructor for testing
|
||||
internal TransmissionService(
|
||||
ILogger<TransmissionService> logger,
|
||||
IMemoryCache cache,
|
||||
IFilenameEvaluator filenameEvaluator,
|
||||
IStriker striker,
|
||||
IDryRunInterceptor dryRunInterceptor,
|
||||
@@ -83,7 +82,7 @@ public partial class TransmissionService : DownloadService, ITransmissionService
|
||||
IRuleManager ruleManager,
|
||||
ITransmissionClientWrapper clientWrapper
|
||||
) : base(
|
||||
logger, cache,
|
||||
logger,
|
||||
filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
)
|
||||
|
||||
@@ -66,9 +66,11 @@ public partial class TransmissionService
|
||||
continue;
|
||||
}
|
||||
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadClientUrl, _downloadClientConfig.ExternalOrInternalUrl);
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadClientType, _downloadClientConfig.TypeName);
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadClientName, _downloadClientConfig.Name);
|
||||
|
||||
if (torrent.Info.Files is null || torrent.Info.FileStats is null)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Cleanuparr.Infrastructure.Events;
|
||||
using Cleanuparr.Domain.Entities.HealthCheck;
|
||||
using Cleanuparr.Infrastructure.Events.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.Files;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
@@ -35,7 +35,7 @@ public partial class UTorrentService : DownloadService, IUTorrentService
|
||||
IRuleEvaluator ruleEvaluator,
|
||||
IRuleManager ruleManager
|
||||
) : base(
|
||||
logger, cache,
|
||||
logger,
|
||||
filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
)
|
||||
@@ -63,7 +63,6 @@ public partial class UTorrentService : DownloadService, IUTorrentService
|
||||
// Internal constructor for testing
|
||||
internal UTorrentService(
|
||||
ILogger<UTorrentService> logger,
|
||||
IMemoryCache cache,
|
||||
IFilenameEvaluator filenameEvaluator,
|
||||
IStriker striker,
|
||||
IDryRunInterceptor dryRunInterceptor,
|
||||
@@ -76,7 +75,7 @@ public partial class UTorrentService : DownloadService, IUTorrentService
|
||||
IRuleManager ruleManager,
|
||||
IUTorrentClientWrapper clientWrapper
|
||||
) : base(
|
||||
logger, cache,
|
||||
logger,
|
||||
filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
)
|
||||
|
||||
@@ -62,9 +62,11 @@ public partial class UTorrentService
|
||||
continue;
|
||||
}
|
||||
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadClientUrl, _downloadClientConfig.ExternalOrInternalUrl);
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadClientType, _downloadClientConfig.TypeName);
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadClientName, _downloadClientConfig.Name);
|
||||
|
||||
List<UTorrentFile>? files = await _client.GetTorrentFilesAsync(torrent.Hash);
|
||||
|
||||
|
||||
@@ -15,4 +15,6 @@ public sealed record DownloadHuntRequest<T>
|
||||
public required T SearchItem { get; init; }
|
||||
|
||||
public required QueueRecord Record { get; init; }
|
||||
|
||||
public required Guid JobRunId { get; init; }
|
||||
}
|
||||
@@ -17,6 +17,8 @@ public sealed record QueueItemRemoveRequest<T>
|
||||
public required QueueRecord Record { get; init; }
|
||||
|
||||
public required bool RemoveFromClient { get; init; }
|
||||
|
||||
|
||||
public required DeleteReason DeleteReason { get; init; }
|
||||
|
||||
public required Guid JobRunId { get; init; }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Net;
|
||||
using System.Net;
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Events;
|
||||
@@ -11,9 +11,11 @@ using Cleanuparr.Infrastructure.Features.DownloadRemover.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadRemover.Models;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Infrastructure.Helpers;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Data.Models.Arr;
|
||||
using MassTransit;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -26,13 +28,15 @@ public sealed class QueueItemRemover : IQueueItemRemover
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IArrClientFactory _arrClientFactory;
|
||||
private readonly IEventPublisher _eventPublisher;
|
||||
private readonly EventsContext _eventsContext;
|
||||
|
||||
public QueueItemRemover(
|
||||
ILogger<QueueItemRemover> logger,
|
||||
IBus messageBus,
|
||||
IMemoryCache cache,
|
||||
IArrClientFactory arrClientFactory,
|
||||
IEventPublisher eventPublisher
|
||||
IEventPublisher eventPublisher,
|
||||
EventsContext eventsContext
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -40,6 +44,7 @@ public sealed class QueueItemRemover : IQueueItemRemover
|
||||
_cache = cache;
|
||||
_arrClientFactory = arrClientFactory;
|
||||
_eventPublisher = eventPublisher;
|
||||
_eventsContext = eventsContext;
|
||||
}
|
||||
|
||||
public async Task RemoveQueueItemAsync<T>(QueueItemRemoveRequest<T> request)
|
||||
@@ -50,8 +55,18 @@ public sealed class QueueItemRemover : IQueueItemRemover
|
||||
var arrClient = _arrClientFactory.GetClient(request.InstanceType, request.Instance.Version);
|
||||
await arrClient.DeleteQueueItemAsync(request.Instance, request.Record, request.RemoveFromClient, request.DeleteReason);
|
||||
|
||||
// Mark the download item as removed in the database
|
||||
await _eventsContext.DownloadItems
|
||||
.Where(x => EF.Functions.Like(x.DownloadId, request.Record.DownloadId))
|
||||
.ExecuteUpdateAsync(setter =>
|
||||
{
|
||||
setter.SetProperty(x => x.IsRemoved, true);
|
||||
setter.SetProperty(x => x.IsMarkedForRemoval, false);
|
||||
});
|
||||
|
||||
// Set context for EventPublisher
|
||||
ContextProvider.Set(ContextProvider.Keys.DownloadName, request.Record.Title);
|
||||
ContextProvider.SetJobRunId(request.JobRunId);
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, request.Record.Title);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, request.Record.DownloadId);
|
||||
ContextProvider.Set(nameof(QueueRecord), request.Record);
|
||||
ContextProvider.Set(ContextProvider.Keys.ArrInstanceUrl, request.Instance.ExternalUrl ?? request.Instance.Url);
|
||||
@@ -75,7 +90,8 @@ public sealed class QueueItemRemover : IQueueItemRemover
|
||||
InstanceType = request.InstanceType,
|
||||
Instance = request.Instance,
|
||||
SearchItem = request.SearchItem,
|
||||
Record = request.Record
|
||||
Record = request.Record,
|
||||
JobRunId = request.JobRunId
|
||||
});
|
||||
}
|
||||
catch (HttpRequestException exception)
|
||||
|
||||
@@ -11,7 +11,9 @@ public interface IStriker
|
||||
/// <param name="itemName">The name of the item</param>
|
||||
/// <param name="maxStrikes">The maximum number of strikes</param>
|
||||
/// <param name="strikeType">The strike type</param>
|
||||
/// <param name="lastDownloadedBytes">Optional: bytes downloaded at time of strike (for progress tracking)</param>
|
||||
/// <returns>True if the limit has been reached, otherwise false</returns>
|
||||
Task<bool> StrikeAndCheckLimit(string hash, string itemName, ushort maxStrikes, StrikeType strikeType);
|
||||
Task<bool> StrikeAndCheckLimit(string hash, string itemName, ushort maxStrikes, StrikeType strikeType, long? lastDownloadedBytes = null);
|
||||
|
||||
Task ResetStrikeAsync(string hash, string itemName, StrikeType strikeType);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Concurrent;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Events;
|
||||
using Cleanuparr.Infrastructure.Events.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Helpers;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.State;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
@@ -12,47 +12,62 @@ namespace Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
public sealed class Striker : IStriker
|
||||
{
|
||||
private readonly ILogger<Striker> _logger;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly MemoryCacheEntryOptions _cacheOptions;
|
||||
private readonly EventsContext _context;
|
||||
private readonly IEventPublisher _eventPublisher;
|
||||
|
||||
public static readonly ConcurrentDictionary<string, string?> RecurringHashes = [];
|
||||
|
||||
public Striker(ILogger<Striker> logger, IMemoryCache cache, IEventPublisher eventPublisher)
|
||||
public Striker(ILogger<Striker> logger, EventsContext context, IEventPublisher eventPublisher)
|
||||
{
|
||||
_logger = logger;
|
||||
_cache = cache;
|
||||
_context = context;
|
||||
_eventPublisher = eventPublisher;
|
||||
_cacheOptions = new MemoryCacheEntryOptions()
|
||||
.SetSlidingExpiration(StaticConfiguration.TriggerValue + Constants.CacheLimitBuffer);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<bool> StrikeAndCheckLimit(string hash, string itemName, ushort maxStrikes, StrikeType strikeType)
|
||||
|
||||
public async Task<bool> StrikeAndCheckLimit(string hash, string itemName, ushort maxStrikes, StrikeType strikeType, long? lastDownloadedBytes = null)
|
||||
{
|
||||
if (maxStrikes is 0)
|
||||
{
|
||||
_logger.LogTrace("skip striking for {reason} | max strikes is 0 | {name}", strikeType, itemName);
|
||||
return false;
|
||||
}
|
||||
|
||||
string key = CacheKeys.Strike(strikeType, hash);
|
||||
|
||||
if (!_cache.TryGetValue(key, out int strikeCount))
|
||||
|
||||
var downloadItem = await GetOrCreateDownloadItemAsync(hash, itemName);
|
||||
|
||||
int existingStrikeCount = await _context.Strikes
|
||||
.CountAsync(s => s.DownloadItemId == downloadItem.Id && s.Type == strikeType);
|
||||
|
||||
var strike = new Strike
|
||||
{
|
||||
strikeCount = 1;
|
||||
}
|
||||
else
|
||||
DownloadItemId = downloadItem.Id,
|
||||
JobRunId = ContextProvider.GetJobRunId(),
|
||||
Type = strikeType,
|
||||
LastDownloadedBytes = lastDownloadedBytes
|
||||
};
|
||||
_context.Strikes.Add(strike);
|
||||
|
||||
int strikeCount = existingStrikeCount + 1;
|
||||
|
||||
// If item was previously removed and gets a new strike, it has returned
|
||||
if (downloadItem.IsRemoved)
|
||||
{
|
||||
++strikeCount;
|
||||
downloadItem.IsReturning = true;
|
||||
downloadItem.IsRemoved = false;
|
||||
downloadItem.IsMarkedForRemoval = false;
|
||||
}
|
||||
|
||||
|
||||
// Mark for removal when strike limit reached
|
||||
if (strikeCount >= maxStrikes)
|
||||
{
|
||||
downloadItem.IsMarkedForRemoval = true;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Item on strike number {strike} | reason {reason} | {name}", strikeCount, strikeType.ToString(), itemName);
|
||||
|
||||
await _eventPublisher.PublishStrike(strikeType, strikeCount, hash, itemName);
|
||||
|
||||
_cache.Set(key, strikeCount, _cacheOptions);
|
||||
|
||||
await _eventPublisher.PublishStrike(strikeType, strikeCount, hash, itemName, strike.Id);
|
||||
|
||||
if (strikeCount < maxStrikes)
|
||||
{
|
||||
return false;
|
||||
@@ -61,7 +76,7 @@ public sealed class Striker : IStriker
|
||||
if (strikeCount > maxStrikes)
|
||||
{
|
||||
_logger.LogWarning("Blocked item keeps coming back | {name}", itemName);
|
||||
|
||||
|
||||
RecurringHashes.TryAdd(hash.ToLowerInvariant(), null);
|
||||
await _eventPublisher.PublishRecurringItem(hash, itemName, strikeCount);
|
||||
}
|
||||
@@ -71,17 +86,51 @@ public sealed class Striker : IStriker
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task ResetStrikeAsync(string hash, string itemName, StrikeType strikeType)
|
||||
public async Task ResetStrikeAsync(string hash, string itemName, StrikeType strikeType)
|
||||
{
|
||||
string key = CacheKeys.Strike(strikeType, hash);
|
||||
var downloadItem = await _context.DownloadItems
|
||||
.FirstOrDefaultAsync(d => d.DownloadId == hash);
|
||||
|
||||
if (_cache.TryGetValue(key, out int strikeCount) && strikeCount > 0)
|
||||
if (downloadItem is null)
|
||||
{
|
||||
_logger.LogTrace("Progress detected | resetting {reason} strikes from {strikeCount} to 0 | {name}", strikeType, strikeCount, itemName);
|
||||
return;
|
||||
}
|
||||
|
||||
_cache.Remove(key);
|
||||
var strikesToDelete = await _context.Strikes
|
||||
.Where(s => s.DownloadItemId == downloadItem.Id && s.Type == strikeType)
|
||||
.ToListAsync();
|
||||
|
||||
return Task.CompletedTask;
|
||||
if (strikesToDelete.Count > 0)
|
||||
{
|
||||
_context.Strikes.RemoveRange(strikesToDelete);
|
||||
await _context.SaveChangesAsync();
|
||||
_logger.LogTrace("Progress detected | resetting {reason} strikes from {strikeCount} to 0 | {name}", strikeType, strikesToDelete.Count, itemName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<DownloadItem> GetOrCreateDownloadItemAsync(string hash, string itemName)
|
||||
{
|
||||
var downloadItem = await _context.DownloadItems
|
||||
.FirstOrDefaultAsync(d => d.DownloadId == hash);
|
||||
|
||||
if (downloadItem is not null)
|
||||
{
|
||||
if (downloadItem.Title != itemName)
|
||||
{
|
||||
downloadItem.Title = itemName;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
return downloadItem;
|
||||
}
|
||||
|
||||
downloadItem = new DownloadItem
|
||||
{
|
||||
DownloadId = hash,
|
||||
Title = itemName
|
||||
};
|
||||
_context.DownloadItems.Add(downloadItem);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return downloadItem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,9 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
|
||||
foreach (var downloadService in downloadServices)
|
||||
{
|
||||
using var dcType = LogContext.PushProperty(LogProperties.DownloadClientType, downloadService.ClientConfig.Type.ToString());
|
||||
using var dcName = LogContext.PushProperty(LogProperties.DownloadClientName, downloadService.ClientConfig.Name);
|
||||
|
||||
try
|
||||
{
|
||||
await downloadService.LoginAsync();
|
||||
@@ -142,9 +145,10 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance)
|
||||
{
|
||||
using var _ = LogContext.PushProperty(LogProperties.Category, instance.ArrConfig.Type.ToString());
|
||||
|
||||
using var _2 = LogContext.PushProperty(LogProperties.InstanceName, instance.Name);
|
||||
|
||||
IArrClient arrClient = _arrClientFactory.GetClient(instance.ArrConfig.Type, instance.Version);
|
||||
|
||||
|
||||
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
|
||||
{
|
||||
var groups = items
|
||||
@@ -209,6 +213,9 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
// Process each client with its own filtered downloads
|
||||
foreach (var (downloadService, downloadsToChangeCategory) in downloadServiceWithDownloads)
|
||||
{
|
||||
using var dcType = LogContext.PushProperty(LogProperties.DownloadClientType, downloadService.ClientConfig.Type.ToString());
|
||||
using var dcName = LogContext.PushProperty(LogProperties.DownloadClientName, downloadService.ClientConfig.Name);
|
||||
|
||||
try
|
||||
{
|
||||
await downloadService.CreateCategoryAsync(config.UnlinkedTargetCategory);
|
||||
@@ -222,7 +229,7 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
downloadService.ClientConfig.Name
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
await downloadService.ChangeCategoryForNoHardLinksAsync(downloadsToChangeCategory);
|
||||
@@ -275,6 +282,9 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
// Process cleaning for each client
|
||||
foreach (var (downloadService, downloadsToClean) in downloadServiceWithDownloads)
|
||||
{
|
||||
using var dcType = LogContext.PushProperty(LogProperties.DownloadClientType, downloadService.ClientConfig.Type.ToString());
|
||||
using var dcName = LogContext.PushProperty(LogProperties.DownloadClientName, downloadService.ClientConfig.Name);
|
||||
|
||||
try
|
||||
{
|
||||
await downloadService.CleanDownloadsAsync(downloadsToClean, config.Categories);
|
||||
|
||||
@@ -149,7 +149,8 @@ public abstract class GenericHandler : IHandler
|
||||
Record = record,
|
||||
SearchItem = (SeriesSearchItem)GetRecordSearchItem(instanceType, instance.Version, record, isPack),
|
||||
RemoveFromClient = removeFromClient,
|
||||
DeleteReason = deleteReason
|
||||
DeleteReason = deleteReason,
|
||||
JobRunId = ContextProvider.GetJobRunId()
|
||||
};
|
||||
|
||||
await _messageBus.Publish(removeRequest);
|
||||
@@ -163,14 +164,16 @@ public abstract class GenericHandler : IHandler
|
||||
Record = record,
|
||||
SearchItem = GetRecordSearchItem(instanceType, instance.Version, record, isPack),
|
||||
RemoveFromClient = removeFromClient,
|
||||
DeleteReason = deleteReason
|
||||
DeleteReason = deleteReason,
|
||||
JobRunId = ContextProvider.GetJobRunId()
|
||||
};
|
||||
|
||||
await _messageBus.Publish(removeRequest);
|
||||
}
|
||||
|
||||
_logger.LogInformation("item marked for removal | {title} | {url}", record.Title, instance.Url);
|
||||
await _eventPublisher.PublishAsync(EventType.DownloadMarkedForDeletion, "Download marked for deletion", EventSeverity.Important);
|
||||
await _eventPublisher.PublishAsync(EventType.DownloadMarkedForDeletion, "Download marked for deletion", EventSeverity.Important,
|
||||
data: new { itemName = record.Title, hash = record.DownloadId });
|
||||
}
|
||||
|
||||
protected SearchItem GetRecordSearchItem(InstanceType type, float version, QueueRecord record, bool isPack = false)
|
||||
|
||||
@@ -96,14 +96,15 @@ public sealed class MalwareBlocker : GenericHandler
|
||||
ignoredDownloads.AddRange(ContextProvider.Get<ContentBlockerConfig>().IgnoredDownloads);
|
||||
|
||||
using var _ = LogContext.PushProperty(LogProperties.Category, instance.ArrConfig.Type.ToString());
|
||||
|
||||
using var _2 = LogContext.PushProperty(LogProperties.InstanceName, instance.Name);
|
||||
|
||||
IArrClient arrClient = _arrClientFactory.GetClient(instance.ArrConfig.Type, instance.Version);
|
||||
|
||||
// push to context
|
||||
ContextProvider.Set(ContextProvider.Keys.ArrInstanceUrl, instance.ExternalUrl ?? instance.Url);
|
||||
ContextProvider.Set(nameof(InstanceType), instance.ArrConfig.Type);
|
||||
ContextProvider.Set(ContextProvider.Keys.Version, instance.Version);
|
||||
|
||||
|
||||
IReadOnlyList<IDownloadService> downloadServices = await GetInitializedDownloadServicesAsync();
|
||||
|
||||
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
|
||||
|
||||
@@ -86,9 +86,10 @@ public sealed class QueueCleaner : GenericHandler
|
||||
ignoredDownloads.AddRange(queueCleanerConfig.IgnoredDownloads);
|
||||
|
||||
using var _ = LogContext.PushProperty(LogProperties.Category, instance.ArrConfig.Type.ToString());
|
||||
|
||||
using var _2 = LogContext.PushProperty(LogProperties.InstanceName, instance.Name);
|
||||
|
||||
IArrClient arrClient = _arrClientFactory.GetClient(instance.ArrConfig.Type, instance.Version);
|
||||
|
||||
|
||||
// push to context
|
||||
ContextProvider.Set(ContextProvider.Keys.ArrInstanceUrl, instance.ExternalUrl ?? instance.Url);
|
||||
ContextProvider.Set(nameof(InstanceType), instance.ArrConfig.Type);
|
||||
|
||||
@@ -177,7 +177,7 @@ public class NotificationPublisher : INotificationPublisher
|
||||
|
||||
private static NotificationContext BuildDownloadCleanedContext(double ratio, TimeSpan seedingTime, string categoryName, CleanReason reason)
|
||||
{
|
||||
var downloadName = ContextProvider.Get<string>(ContextProvider.Keys.DownloadName);
|
||||
var downloadName = ContextProvider.Get<string>(ContextProvider.Keys.ItemName);
|
||||
var hash = ContextProvider.Get<string>(ContextProvider.Keys.Hash);
|
||||
var clientUrl = ContextProvider.Get<Uri>(ContextProvider.Keys.DownloadClientUrl);
|
||||
|
||||
@@ -200,7 +200,7 @@ public class NotificationPublisher : INotificationPublisher
|
||||
|
||||
private NotificationContext BuildCategoryChangedContext(string oldCategory, string newCategory, bool isTag)
|
||||
{
|
||||
string downloadName = ContextProvider.Get<string>(ContextProvider.Keys.DownloadName);
|
||||
string downloadName = ContextProvider.Get<string>(ContextProvider.Keys.ItemName);
|
||||
Uri clientUrl = ContextProvider.Get<Uri>(ContextProvider.Keys.DownloadClientUrl);
|
||||
|
||||
NotificationContext context = new()
|
||||
|
||||
@@ -4,4 +4,8 @@ public static class LogProperties
|
||||
{
|
||||
public const string Category = "Category";
|
||||
public const string JobName = "JobName";
|
||||
public const string InstanceName = "InstanceName";
|
||||
public const string DownloadClientType = "DownloadClientType";
|
||||
public const string DownloadClientName = "DownloadClientName";
|
||||
public const string JobRunId = "JobRunId";
|
||||
}
|
||||
@@ -38,7 +38,7 @@ public class AppHub : Hub
|
||||
{
|
||||
var logs = _logSink.GetRecentLogs();
|
||||
await Clients.Caller.SendAsync("LogsReceived", logs);
|
||||
_logger.LogDebug("Sent {count} recent logs to client {connectionId}", logs.Count(), Context.ConnectionId);
|
||||
// _logger.LogDebug("Sent {count} recent logs to client {connectionId}", logs.Count(), Context.ConnectionId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -59,7 +59,7 @@ public class AppHub : Hub
|
||||
.ToListAsync();
|
||||
|
||||
await Clients.Caller.SendAsync("EventsReceived", events);
|
||||
_logger.LogDebug("Sent {count} recent events to client {connectionId}", events.Count, Context.ConnectionId);
|
||||
// _logger.LogDebug("Sent {count} recent events to client {connectionId}", events.Count, Context.ConnectionId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -81,7 +81,7 @@ public class AppHub : Hub
|
||||
.ToListAsync();
|
||||
|
||||
await Clients.Caller.SendAsync("ManualEventsReceived", manualEvents);
|
||||
_logger.LogDebug("Sent {count} recent manual events to client {connectionId}", manualEvents.Count, Context.ConnectionId);
|
||||
// _logger.LogDebug("Sent {count} recent manual events to client {connectionId}", manualEvents.Count, Context.ConnectionId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -89,6 +89,35 @@ public class AppHub : Hub
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client requests recent strikes
|
||||
/// </summary>
|
||||
public async Task GetRecentStrikes(int count = 5)
|
||||
{
|
||||
try
|
||||
{
|
||||
var strikes = await _context.Strikes
|
||||
.Include(s => s.DownloadItem)
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.Take(Math.Min(count, 50))
|
||||
.Select(s => new
|
||||
{
|
||||
s.Id,
|
||||
Type = s.Type.ToString(),
|
||||
s.CreatedAt,
|
||||
DownloadId = s.DownloadItem.DownloadId,
|
||||
Title = s.DownloadItem.Title,
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
await Clients.Caller.SendAsync("StrikesReceived", strikes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to send recent strikes to client");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client requests current job statuses
|
||||
/// </summary>
|
||||
@@ -110,7 +139,7 @@ public class AppHub : Hub
|
||||
/// </summary>
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
_logger.LogTrace("Client connected to AppHub: {ConnectionId}", Context.ConnectionId);
|
||||
// _logger.LogTrace("Client connected to AppHub: {ConnectionId}", Context.ConnectionId);
|
||||
|
||||
var status = _statusSnapshot.Current;
|
||||
if (status.CurrentVersion is not null || status.LatestVersion is not null)
|
||||
@@ -126,7 +155,7 @@ public class AppHub : Hub
|
||||
/// </summary>
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
_logger.LogTrace("Client disconnected from AppHub: {ConnectionId}", Context.ConnectionId);
|
||||
// _logger.LogTrace("Client disconnected from AppHub: {ConnectionId}", Context.ConnectionId);
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using Cleanuparr.Infrastructure.Helpers;
|
||||
using Cleanuparr.Infrastructure.Hubs;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Serilog;
|
||||
@@ -48,8 +49,12 @@ public class SignalRLogSink : ILogEventSink
|
||||
Level = logEvent.Level.ToString(),
|
||||
Message = stringWriter.ToString(),
|
||||
Exception = logEvent.Exception?.ToString(),
|
||||
JobName = GetPropertyValue(logEvent, "JobName"),
|
||||
Category = GetPropertyValue(logEvent, "Category", "SYSTEM"),
|
||||
JobName = GetPropertyValue(logEvent, LogProperties.JobName),
|
||||
Category = GetPropertyValue(logEvent, LogProperties.Category, "SYSTEM"),
|
||||
InstanceName = GetPropertyValue(logEvent, LogProperties.InstanceName),
|
||||
DownloadClientType = GetPropertyValue(logEvent, LogProperties.DownloadClientType),
|
||||
DownloadClientName = GetPropertyValue(logEvent, LogProperties.DownloadClientName),
|
||||
JobRunId = GetPropertyValue(logEvent, LogProperties.JobRunId),
|
||||
};
|
||||
|
||||
// Add to buffer for new clients
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace Cleanuparr.Infrastructure.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the supported job types in the application
|
||||
/// </summary>
|
||||
public enum JobType
|
||||
{
|
||||
QueueCleaner,
|
||||
MalwareBlocker,
|
||||
DownloadCleaner,
|
||||
BlacklistSynchronizer,
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Models;
|
||||
using Quartz;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Models;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Utilities;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Entities.Cache;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Infrastructure.Helpers;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using Cleanuparr.Persistence.Models.State;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Services;
|
||||
@@ -17,29 +17,25 @@ public class RuleEvaluator : IRuleEvaluator
|
||||
{
|
||||
private readonly IRuleManager _ruleManager;
|
||||
private readonly IStriker _striker;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly MemoryCacheEntryOptions _cacheOptions;
|
||||
private readonly EventsContext _context;
|
||||
private readonly ILogger<RuleEvaluator> _logger;
|
||||
|
||||
public RuleEvaluator(
|
||||
IRuleManager ruleManager,
|
||||
IStriker striker,
|
||||
IMemoryCache cache,
|
||||
EventsContext context,
|
||||
ILogger<RuleEvaluator> logger)
|
||||
{
|
||||
_ruleManager = ruleManager;
|
||||
_striker = striker;
|
||||
_cache = cache;
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
_cacheOptions = new MemoryCacheEntryOptions()
|
||||
.SetSlidingExpiration(StaticConfiguration.TriggerValue + Constants.CacheLimitBuffer);
|
||||
}
|
||||
|
||||
public async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> EvaluateStallRulesAsync(ITorrentItemWrapper torrent)
|
||||
{
|
||||
_logger.LogTrace("Evaluating stall rules | {name}", torrent.Name);
|
||||
|
||||
// Get matching stall rules in priority order
|
||||
var rule = _ruleManager.GetMatchingStallRule(torrent);
|
||||
|
||||
if (rule is null)
|
||||
@@ -57,12 +53,13 @@ public class RuleEvaluator : IRuleEvaluator
|
||||
rule.MinimumProgressByteSize?.Bytes
|
||||
);
|
||||
|
||||
// Apply strike and check if torrent should be removed
|
||||
long currentDownloaded = Math.Max(0, torrent.DownloadedBytes);
|
||||
bool shouldRemove = await _striker.StrikeAndCheckLimit(
|
||||
torrent.Hash,
|
||||
torrent.Name,
|
||||
(ushort)rule.MaxStrikes,
|
||||
StrikeType.Stalled
|
||||
StrikeType.Stalled,
|
||||
currentDownloaded
|
||||
);
|
||||
|
||||
if (shouldRemove)
|
||||
@@ -77,7 +74,6 @@ public class RuleEvaluator : IRuleEvaluator
|
||||
{
|
||||
_logger.LogTrace("Evaluating slow rules | {name}", torrent.Name);
|
||||
|
||||
// Get matching slow rules in priority order
|
||||
SlowRule? rule = _ruleManager.GetMatchingSlowRule(torrent);
|
||||
|
||||
if (rule is null)
|
||||
@@ -89,7 +85,6 @@ public class RuleEvaluator : IRuleEvaluator
|
||||
_logger.LogTrace("Applying slow rule {rule} | {name}", rule.Name, torrent.Name);
|
||||
ContextProvider.Set<QueueRule>(rule);
|
||||
|
||||
// Check if slow speed
|
||||
if (!string.IsNullOrWhiteSpace(rule.MinSpeed))
|
||||
{
|
||||
ByteSize minSpeed = rule.MinSpeedByteSize;
|
||||
@@ -114,7 +109,6 @@ public class RuleEvaluator : IRuleEvaluator
|
||||
}
|
||||
}
|
||||
|
||||
// Check if slow time
|
||||
if (rule.MaxTimeHours > 0)
|
||||
{
|
||||
SmartTimeSpan maxTime = SmartTimeSpan.FromHours(rule.MaxTimeHours);
|
||||
@@ -153,7 +147,8 @@ public class RuleEvaluator : IRuleEvaluator
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HasStalledDownloadProgress(torrent, StrikeType.Stalled, out long previous, out long current))
|
||||
var (hasProgress, previous, current) = await GetDownloadProgressAsync(torrent);
|
||||
if (!hasProgress)
|
||||
{
|
||||
_logger.LogTrace("No progress detected | strikes are not reset | {name}", torrent.Name);
|
||||
return;
|
||||
@@ -171,10 +166,10 @@ public class RuleEvaluator : IRuleEvaluator
|
||||
minimumProgressBytes,
|
||||
torrent.Name
|
||||
);
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_logger.LogTrace(
|
||||
"Progress detected | strikes are reset | progress: {progress}b | minimum: {minimum}b | {name}",
|
||||
progressBytes,
|
||||
@@ -204,34 +199,36 @@ public class RuleEvaluator : IRuleEvaluator
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
await _striker.ResetStrikeAsync(torrent.Hash, torrent.Name, strikeType);
|
||||
}
|
||||
|
||||
private bool HasStalledDownloadProgress(ITorrentItemWrapper torrent, StrikeType strikeType, out long previousDownloaded, out long currentDownloaded)
|
||||
private async Task<(bool HasProgress, long PreviousDownloaded, long CurrentDownloaded)> GetDownloadProgressAsync(ITorrentItemWrapper torrent)
|
||||
{
|
||||
previousDownloaded = 0;
|
||||
currentDownloaded = Math.Max(0, torrent.DownloadedBytes);
|
||||
long currentDownloaded = Math.Max(0, torrent.DownloadedBytes);
|
||||
|
||||
string cacheKey = CacheKeys.StrikeItem(torrent.Hash, strikeType);
|
||||
var downloadItem = await _context.DownloadItems
|
||||
.FirstOrDefaultAsync(d => d.DownloadId == torrent.Hash);
|
||||
|
||||
if (!_cache.TryGetValue(cacheKey, out StalledCacheItem? cachedItem) || cachedItem is null)
|
||||
if (downloadItem is null)
|
||||
{
|
||||
cachedItem = new StalledCacheItem { Downloaded = currentDownloaded };
|
||||
_cache.Set(cacheKey, cachedItem, _cacheOptions);
|
||||
return false;
|
||||
return (false, 0, currentDownloaded);
|
||||
}
|
||||
|
||||
previousDownloaded = cachedItem.Downloaded;
|
||||
// Get the most recent strike for this download item (Stalled type) to check progress
|
||||
var mostRecentStrike = await _context.Strikes
|
||||
.Where(s => s.DownloadItemId == downloadItem.Id && s.Type == StrikeType.Stalled)
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
bool progressed = currentDownloaded > cachedItem.Downloaded;
|
||||
|
||||
if (progressed || currentDownloaded != cachedItem.Downloaded)
|
||||
if (mostRecentStrike is null)
|
||||
{
|
||||
cachedItem.Downloaded = currentDownloaded;
|
||||
_cache.Set(cacheKey, cachedItem, _cacheOptions);
|
||||
return (false, 0, currentDownloaded);
|
||||
}
|
||||
|
||||
return progressed;
|
||||
long previousDownloaded = mostRecentStrike.LastDownloadedBytes ?? 0;
|
||||
bool progressed = currentDownloaded > previousDownloaded;
|
||||
|
||||
return (progressed, previousDownloaded, currentDownloaded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Domain.Exceptions;
|
||||
using Cleanuparr.Infrastructure.Models;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
|
||||
@@ -12,6 +12,7 @@ using Cleanuparr.Persistence.Models.Configuration.BlacklistSync;
|
||||
using Cleanuparr.Persistence.Models.State;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Serilog.Events;
|
||||
|
||||
@@ -202,16 +203,32 @@ public class DataContext : DbContext
|
||||
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
// Use OriginalString for Uri properties to preserve the exact input (including embedded credentials)
|
||||
foreach (var property in entityType.GetProperties().Where(p => p.ClrType == typeof(Uri)))
|
||||
{
|
||||
property.SetValueConverter(
|
||||
new ValueConverter<Uri, string>(
|
||||
v => v.OriginalString,
|
||||
v => new Uri(v, UriKind.RelativeOrAbsolute)));
|
||||
|
||||
property.SetValueComparer(new ValueComparer<Uri>(
|
||||
(u1, u2) => u1 != null && u2 != null
|
||||
? u1.OriginalString == u2.OriginalString
|
||||
: u1 == null && u2 == null,
|
||||
u => u == null ? 0 : u.OriginalString.GetHashCode(),
|
||||
u => u == null ? null! : new Uri(u.OriginalString, UriKind.RelativeOrAbsolute)));
|
||||
}
|
||||
|
||||
var enumProperties = entityType.ClrType.GetProperties()
|
||||
.Where(p => p.PropertyType.IsEnum ||
|
||||
(p.PropertyType.IsGenericType &&
|
||||
p.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>) &&
|
||||
.Where(p => p.PropertyType.IsEnum ||
|
||||
(p.PropertyType.IsGenericType &&
|
||||
p.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>) &&
|
||||
p.PropertyType.GetGenericArguments()[0].IsEnum));
|
||||
|
||||
foreach (var property in enumProperties)
|
||||
{
|
||||
var enumType = property.PropertyType.IsEnum
|
||||
? property.PropertyType
|
||||
var enumType = property.PropertyType.IsEnum
|
||||
? property.PropertyType
|
||||
: property.PropertyType.GetGenericArguments()[0];
|
||||
|
||||
var converterType = typeof(LowercaseEnumConverter<>).MakeGenericType(enumType);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Persistence.Converters;
|
||||
using Cleanuparr.Persistence.Models.Events;
|
||||
using Cleanuparr.Persistence.Models.State;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
@@ -12,8 +14,14 @@ namespace Cleanuparr.Persistence;
|
||||
public class EventsContext : DbContext
|
||||
{
|
||||
public DbSet<AppEvent> Events { get; set; }
|
||||
|
||||
|
||||
public DbSet<ManualEvent> ManualEvents { get; set; }
|
||||
|
||||
public DbSet<Strike> Strikes { get; set; }
|
||||
|
||||
public DbSet<DownloadItem> DownloadItems { get; set; }
|
||||
|
||||
public DbSet<JobRun> JobRuns { get; set; }
|
||||
|
||||
public EventsContext()
|
||||
{
|
||||
@@ -50,6 +58,29 @@ public class EventsContext : DbContext
|
||||
{
|
||||
entity.Property(e => e.Timestamp)
|
||||
.HasConversion(new UtcDateTimeConverter());
|
||||
|
||||
entity.HasOne(e => e.Strike)
|
||||
.WithMany()
|
||||
.HasForeignKey(e => e.StrikeId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Strike>(entity =>
|
||||
{
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasConversion(new UtcDateTimeConverter());
|
||||
|
||||
entity.Property(e => e.Type)
|
||||
.HasConversion(new LowercaseEnumConverter<StrikeType>());
|
||||
});
|
||||
|
||||
modelBuilder.Entity<JobRun>(entity =>
|
||||
{
|
||||
entity.Property(e => e.StartedAt)
|
||||
.HasConversion(new UtcDateTimeConverter());
|
||||
|
||||
entity.Property(e => e.CompletedAt)
|
||||
.HasConversion(new UtcDateTimeConverter());
|
||||
});
|
||||
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
|
||||
1285
code/backend/Cleanuparr.Persistence/Migrations/Data/20260213155055_AddStrikeInactivityWindow.Designer.cs
generated
Normal file
1285
code/backend/Cleanuparr.Persistence/Migrations/Data/20260213155055_AddStrikeInactivityWindow.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddStrikeInactivityWindow : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<ushort>(
|
||||
name: "strike_inactivity_window_hours",
|
||||
table: "general_configs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: (ushort)24);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "strike_inactivity_window_hours",
|
||||
table: "general_configs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,6 +314,10 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("status_check_enabled");
|
||||
|
||||
b.Property<ushort>("StrikeInactivityWindowHours")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("strike_inactivity_window_hours");
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "Log", "Cleanuparr.Persistence.Models.Configuration.General.GeneralConfig.Log#LoggingConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
210
code/backend/Cleanuparr.Persistence/Migrations/Events/20251220204209_AddStrikes.Designer.cs
generated
Normal file
210
code/backend/Cleanuparr.Persistence/Migrations/Events/20251220204209_AddStrikes.Designer.cs
generated
Normal file
@@ -0,0 +1,210 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Cleanuparr.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Events
|
||||
{
|
||||
[DbContext(typeof(EventsContext))]
|
||||
[Migration("20251220204209_AddStrikes")]
|
||||
partial class AddStrikes
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.AppEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("data");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("event_type");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<Guid?>("TrackingId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tracking_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_events");
|
||||
|
||||
b.HasIndex("EventType")
|
||||
.HasDatabaseName("ix_events_event_type");
|
||||
|
||||
b.HasIndex("Message")
|
||||
.HasDatabaseName("ix_events_message");
|
||||
|
||||
b.HasIndex("Severity")
|
||||
.HasDatabaseName("ix_events_severity");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_events_timestamp");
|
||||
|
||||
b.ToTable("events", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.ManualEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("data");
|
||||
|
||||
b.Property<bool>("IsResolved")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_resolved");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_manual_events");
|
||||
|
||||
b.HasIndex("IsResolved")
|
||||
.HasDatabaseName("ix_manual_events_is_resolved");
|
||||
|
||||
b.HasIndex("Message")
|
||||
.HasDatabaseName("ix_manual_events_message");
|
||||
|
||||
b.HasIndex("Severity")
|
||||
.HasDatabaseName("ix_manual_events_severity");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_manual_events_timestamp");
|
||||
|
||||
b.ToTable("manual_events", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.DownloadItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("DownloadId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_download_items");
|
||||
|
||||
b.HasIndex("DownloadId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_download_items_download_id");
|
||||
|
||||
b.ToTable("download_items", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.Strike", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Guid>("DownloadItemId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_item_id");
|
||||
|
||||
b.Property<long?>("LastDownloadedBytes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("last_downloaded_bytes");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_strikes");
|
||||
|
||||
b.HasIndex("CreatedAt")
|
||||
.HasDatabaseName("ix_strikes_created_at");
|
||||
|
||||
b.HasIndex("DownloadItemId", "Type")
|
||||
.HasDatabaseName("ix_strikes_download_item_id_type");
|
||||
|
||||
b.ToTable("strikes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.Strike", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.DownloadItem", "DownloadItem")
|
||||
.WithMany("Strikes")
|
||||
.HasForeignKey("DownloadItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_strikes_download_items_download_item_id");
|
||||
|
||||
b.Navigation("DownloadItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.DownloadItem", b =>
|
||||
{
|
||||
b.Navigation("Strikes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Events
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddStrikes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "download_items",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
download_id = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
title = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_download_items", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "strikes",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
download_item_id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
type = table.Column<string>(type: "TEXT", nullable: false),
|
||||
created_at = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
last_downloaded_bytes = table.Column<long>(type: "INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_strikes", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_strikes_download_items_download_item_id",
|
||||
column: x => x.download_item_id,
|
||||
principalTable: "download_items",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_download_items_download_id",
|
||||
table: "download_items",
|
||||
column: "download_id",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_strikes_created_at",
|
||||
table: "strikes",
|
||||
column: "created_at");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_strikes_download_item_id_type",
|
||||
table: "strikes",
|
||||
columns: new[] { "download_item_id", "type" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "strikes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "download_items");
|
||||
}
|
||||
}
|
||||
}
|
||||
365
code/backend/Cleanuparr.Persistence/Migrations/Events/20260213090744_AddEventContextColumns.Designer.cs
generated
Normal file
365
code/backend/Cleanuparr.Persistence/Migrations/Events/20260213090744_AddEventContextColumns.Designer.cs
generated
Normal file
@@ -0,0 +1,365 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Cleanuparr.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Events
|
||||
{
|
||||
[DbContext(typeof(EventsContext))]
|
||||
[Migration("20260213090744_AddEventContextColumns")]
|
||||
partial class AddEventContextColumns
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.1");
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.AppEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("data");
|
||||
|
||||
b.Property<string>("DownloadClientName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_name");
|
||||
|
||||
b.Property<string>("DownloadClientType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_type");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("event_type");
|
||||
|
||||
b.Property<string>("InstanceType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.Property<string>("InstanceUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_url");
|
||||
|
||||
b.Property<Guid?>("JobRunId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<Guid?>("StrikeId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("strike_id");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<Guid?>("TrackingId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tracking_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_events");
|
||||
|
||||
b.HasIndex("DownloadClientType")
|
||||
.HasDatabaseName("ix_events_download_client_type");
|
||||
|
||||
b.HasIndex("EventType")
|
||||
.HasDatabaseName("ix_events_event_type");
|
||||
|
||||
b.HasIndex("InstanceType")
|
||||
.HasDatabaseName("ix_events_instance_type");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_events_job_run_id");
|
||||
|
||||
b.HasIndex("Message")
|
||||
.HasDatabaseName("ix_events_message");
|
||||
|
||||
b.HasIndex("Severity")
|
||||
.HasDatabaseName("ix_events_severity");
|
||||
|
||||
b.HasIndex("StrikeId")
|
||||
.HasDatabaseName("ix_events_strike_id");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_events_timestamp");
|
||||
|
||||
b.ToTable("events", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.ManualEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("data");
|
||||
|
||||
b.Property<string>("DownloadClientName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_name");
|
||||
|
||||
b.Property<string>("DownloadClientType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_type");
|
||||
|
||||
b.Property<string>("InstanceType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.Property<string>("InstanceUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_url");
|
||||
|
||||
b.Property<bool>("IsResolved")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_resolved");
|
||||
|
||||
b.Property<Guid?>("JobRunId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_manual_events");
|
||||
|
||||
b.HasIndex("InstanceType")
|
||||
.HasDatabaseName("ix_manual_events_instance_type");
|
||||
|
||||
b.HasIndex("IsResolved")
|
||||
.HasDatabaseName("ix_manual_events_is_resolved");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_manual_events_job_run_id");
|
||||
|
||||
b.HasIndex("Message")
|
||||
.HasDatabaseName("ix_manual_events_message");
|
||||
|
||||
b.HasIndex("Severity")
|
||||
.HasDatabaseName("ix_manual_events_severity");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_manual_events_timestamp");
|
||||
|
||||
b.ToTable("manual_events", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.DownloadItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("DownloadId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_download_items");
|
||||
|
||||
b.HasIndex("DownloadId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_download_items_download_id");
|
||||
|
||||
b.ToTable("download_items", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.JobRun", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<DateTime>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_job_runs");
|
||||
|
||||
b.HasIndex("StartedAt")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_job_runs_started_at");
|
||||
|
||||
b.HasIndex("Type")
|
||||
.HasDatabaseName("ix_job_runs_type");
|
||||
|
||||
b.ToTable("job_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.Strike", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Guid>("DownloadItemId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_item_id");
|
||||
|
||||
b.Property<Guid>("JobRunId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<long?>("LastDownloadedBytes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("last_downloaded_bytes");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_strikes");
|
||||
|
||||
b.HasIndex("CreatedAt")
|
||||
.HasDatabaseName("ix_strikes_created_at");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_strikes_job_run_id");
|
||||
|
||||
b.HasIndex("DownloadItemId", "Type")
|
||||
.HasDatabaseName("ix_strikes_download_item_id_type");
|
||||
|
||||
b.ToTable("strikes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.AppEvent", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("JobRunId")
|
||||
.HasConstraintName("fk_events_job_runs_job_run_id");
|
||||
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.Strike", "Strike")
|
||||
.WithMany()
|
||||
.HasForeignKey("StrikeId")
|
||||
.HasConstraintName("fk_events_strikes_strike_id");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
|
||||
b.Navigation("Strike");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.ManualEvent", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("ManualEvents")
|
||||
.HasForeignKey("JobRunId")
|
||||
.HasConstraintName("fk_manual_events_job_runs_job_run_id");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.Strike", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.DownloadItem", "DownloadItem")
|
||||
.WithMany("Strikes")
|
||||
.HasForeignKey("DownloadItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_strikes_download_items_download_item_id");
|
||||
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("Strikes")
|
||||
.HasForeignKey("JobRunId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_strikes_job_runs_job_run_id");
|
||||
|
||||
b.Navigation("DownloadItem");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.DownloadItem", b =>
|
||||
{
|
||||
b.Navigation("Strikes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.JobRun", b =>
|
||||
{
|
||||
b.Navigation("Events");
|
||||
|
||||
b.Navigation("ManualEvents");
|
||||
|
||||
b.Navigation("Strikes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Events
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddEventContextColumns : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "job_run_id",
|
||||
table: "strikes",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "download_client_name",
|
||||
table: "manual_events",
|
||||
type: "TEXT",
|
||||
maxLength: 200,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "download_client_type",
|
||||
table: "manual_events",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "instance_type",
|
||||
table: "manual_events",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "instance_url",
|
||||
table: "manual_events",
|
||||
type: "TEXT",
|
||||
maxLength: 500,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "job_run_id",
|
||||
table: "manual_events",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "download_client_name",
|
||||
table: "events",
|
||||
type: "TEXT",
|
||||
maxLength: 200,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "download_client_type",
|
||||
table: "events",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "instance_type",
|
||||
table: "events",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "instance_url",
|
||||
table: "events",
|
||||
type: "TEXT",
|
||||
maxLength: 500,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "job_run_id",
|
||||
table: "events",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "strike_id",
|
||||
table: "events",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "job_runs",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
type = table.Column<string>(type: "TEXT", nullable: false),
|
||||
started_at = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
completed_at = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
status = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_job_runs", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_strikes_job_run_id",
|
||||
table: "strikes",
|
||||
column: "job_run_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_manual_events_instance_type",
|
||||
table: "manual_events",
|
||||
column: "instance_type");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_manual_events_job_run_id",
|
||||
table: "manual_events",
|
||||
column: "job_run_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_events_download_client_type",
|
||||
table: "events",
|
||||
column: "download_client_type");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_events_instance_type",
|
||||
table: "events",
|
||||
column: "instance_type");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_events_job_run_id",
|
||||
table: "events",
|
||||
column: "job_run_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_events_strike_id",
|
||||
table: "events",
|
||||
column: "strike_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_job_runs_started_at",
|
||||
table: "job_runs",
|
||||
column: "started_at",
|
||||
descending: new bool[0]);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_job_runs_type",
|
||||
table: "job_runs",
|
||||
column: "type");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_events_job_runs_job_run_id",
|
||||
table: "events",
|
||||
column: "job_run_id",
|
||||
principalTable: "job_runs",
|
||||
principalColumn: "id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_events_strikes_strike_id",
|
||||
table: "events",
|
||||
column: "strike_id",
|
||||
principalTable: "strikes",
|
||||
principalColumn: "id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_manual_events_job_runs_job_run_id",
|
||||
table: "manual_events",
|
||||
column: "job_run_id",
|
||||
principalTable: "job_runs",
|
||||
principalColumn: "id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_strikes_job_runs_job_run_id",
|
||||
table: "strikes",
|
||||
column: "job_run_id",
|
||||
principalTable: "job_runs",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_events_job_runs_job_run_id",
|
||||
table: "events");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_events_strikes_strike_id",
|
||||
table: "events");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_manual_events_job_runs_job_run_id",
|
||||
table: "manual_events");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_strikes_job_runs_job_run_id",
|
||||
table: "strikes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "job_runs");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_strikes_job_run_id",
|
||||
table: "strikes");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_manual_events_instance_type",
|
||||
table: "manual_events");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_manual_events_job_run_id",
|
||||
table: "manual_events");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_events_download_client_type",
|
||||
table: "events");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_events_instance_type",
|
||||
table: "events");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_events_job_run_id",
|
||||
table: "events");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_events_strike_id",
|
||||
table: "events");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "job_run_id",
|
||||
table: "strikes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "download_client_name",
|
||||
table: "manual_events");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "download_client_type",
|
||||
table: "manual_events");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "instance_type",
|
||||
table: "manual_events");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "instance_url",
|
||||
table: "manual_events");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "job_run_id",
|
||||
table: "manual_events");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "download_client_name",
|
||||
table: "events");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "download_client_type",
|
||||
table: "events");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "instance_type",
|
||||
table: "events");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "instance_url",
|
||||
table: "events");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "job_run_id",
|
||||
table: "events");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "strike_id",
|
||||
table: "events");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Cleanuparr.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Events
|
||||
{
|
||||
[DbContext(typeof(EventsContext))]
|
||||
[Migration("20260213160156_AddOnStrikeDeleteBehavior")]
|
||||
partial class AddOnStrikeDeleteBehavior
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.1");
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.AppEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("data");
|
||||
|
||||
b.Property<string>("DownloadClientName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_name");
|
||||
|
||||
b.Property<string>("DownloadClientType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_type");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("event_type");
|
||||
|
||||
b.Property<string>("InstanceType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.Property<string>("InstanceUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_url");
|
||||
|
||||
b.Property<Guid?>("JobRunId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<Guid?>("StrikeId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("strike_id");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<Guid?>("TrackingId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tracking_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_events");
|
||||
|
||||
b.HasIndex("DownloadClientType")
|
||||
.HasDatabaseName("ix_events_download_client_type");
|
||||
|
||||
b.HasIndex("EventType")
|
||||
.HasDatabaseName("ix_events_event_type");
|
||||
|
||||
b.HasIndex("InstanceType")
|
||||
.HasDatabaseName("ix_events_instance_type");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_events_job_run_id");
|
||||
|
||||
b.HasIndex("Message")
|
||||
.HasDatabaseName("ix_events_message");
|
||||
|
||||
b.HasIndex("Severity")
|
||||
.HasDatabaseName("ix_events_severity");
|
||||
|
||||
b.HasIndex("StrikeId")
|
||||
.HasDatabaseName("ix_events_strike_id");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_events_timestamp");
|
||||
|
||||
b.ToTable("events", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.ManualEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("data");
|
||||
|
||||
b.Property<string>("DownloadClientName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_name");
|
||||
|
||||
b.Property<string>("DownloadClientType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_type");
|
||||
|
||||
b.Property<string>("InstanceType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.Property<string>("InstanceUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_url");
|
||||
|
||||
b.Property<bool>("IsResolved")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_resolved");
|
||||
|
||||
b.Property<Guid?>("JobRunId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_manual_events");
|
||||
|
||||
b.HasIndex("InstanceType")
|
||||
.HasDatabaseName("ix_manual_events_instance_type");
|
||||
|
||||
b.HasIndex("IsResolved")
|
||||
.HasDatabaseName("ix_manual_events_is_resolved");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_manual_events_job_run_id");
|
||||
|
||||
b.HasIndex("Message")
|
||||
.HasDatabaseName("ix_manual_events_message");
|
||||
|
||||
b.HasIndex("Severity")
|
||||
.HasDatabaseName("ix_manual_events_severity");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_manual_events_timestamp");
|
||||
|
||||
b.ToTable("manual_events", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.DownloadItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("DownloadId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_download_items");
|
||||
|
||||
b.HasIndex("DownloadId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_download_items_download_id");
|
||||
|
||||
b.ToTable("download_items", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.JobRun", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<DateTime>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_job_runs");
|
||||
|
||||
b.HasIndex("StartedAt")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_job_runs_started_at");
|
||||
|
||||
b.HasIndex("Type")
|
||||
.HasDatabaseName("ix_job_runs_type");
|
||||
|
||||
b.ToTable("job_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.Strike", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Guid>("DownloadItemId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_item_id");
|
||||
|
||||
b.Property<Guid>("JobRunId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<long?>("LastDownloadedBytes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("last_downloaded_bytes");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_strikes");
|
||||
|
||||
b.HasIndex("CreatedAt")
|
||||
.HasDatabaseName("ix_strikes_created_at");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_strikes_job_run_id");
|
||||
|
||||
b.HasIndex("DownloadItemId", "Type")
|
||||
.HasDatabaseName("ix_strikes_download_item_id_type");
|
||||
|
||||
b.ToTable("strikes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.AppEvent", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("JobRunId")
|
||||
.HasConstraintName("fk_events_job_runs_job_run_id");
|
||||
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.Strike", "Strike")
|
||||
.WithMany()
|
||||
.HasForeignKey("StrikeId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_events_strikes_strike_id");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
|
||||
b.Navigation("Strike");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.ManualEvent", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("ManualEvents")
|
||||
.HasForeignKey("JobRunId")
|
||||
.HasConstraintName("fk_manual_events_job_runs_job_run_id");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.Strike", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.DownloadItem", "DownloadItem")
|
||||
.WithMany("Strikes")
|
||||
.HasForeignKey("DownloadItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_strikes_download_items_download_item_id");
|
||||
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("Strikes")
|
||||
.HasForeignKey("JobRunId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_strikes_job_runs_job_run_id");
|
||||
|
||||
b.Navigation("DownloadItem");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.DownloadItem", b =>
|
||||
{
|
||||
b.Navigation("Strikes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.JobRun", b =>
|
||||
{
|
||||
b.Navigation("Events");
|
||||
|
||||
b.Navigation("ManualEvents");
|
||||
|
||||
b.Navigation("Strikes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Events
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddOnStrikeDeleteBehavior : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_events_strikes_strike_id",
|
||||
table: "events");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_events_strikes_strike_id",
|
||||
table: "events",
|
||||
column: "strike_id",
|
||||
principalTable: "strikes",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_events_strikes_strike_id",
|
||||
table: "events");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_events_strikes_strike_id",
|
||||
table: "events",
|
||||
column: "strike_id",
|
||||
principalTable: "strikes",
|
||||
principalColumn: "id");
|
||||
}
|
||||
}
|
||||
}
|
||||
378
code/backend/Cleanuparr.Persistence/Migrations/Events/20260214230732_AddDownloadItemStatuses.Designer.cs
generated
Normal file
378
code/backend/Cleanuparr.Persistence/Migrations/Events/20260214230732_AddDownloadItemStatuses.Designer.cs
generated
Normal file
@@ -0,0 +1,378 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Cleanuparr.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Events
|
||||
{
|
||||
[DbContext(typeof(EventsContext))]
|
||||
[Migration("20260214230732_AddDownloadItemStatuses")]
|
||||
partial class AddDownloadItemStatuses
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.1");
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.AppEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("data");
|
||||
|
||||
b.Property<string>("DownloadClientName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_name");
|
||||
|
||||
b.Property<string>("DownloadClientType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_type");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("event_type");
|
||||
|
||||
b.Property<string>("InstanceType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.Property<string>("InstanceUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_url");
|
||||
|
||||
b.Property<Guid?>("JobRunId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<Guid?>("StrikeId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("strike_id");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<Guid?>("TrackingId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tracking_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_events");
|
||||
|
||||
b.HasIndex("DownloadClientType")
|
||||
.HasDatabaseName("ix_events_download_client_type");
|
||||
|
||||
b.HasIndex("EventType")
|
||||
.HasDatabaseName("ix_events_event_type");
|
||||
|
||||
b.HasIndex("InstanceType")
|
||||
.HasDatabaseName("ix_events_instance_type");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_events_job_run_id");
|
||||
|
||||
b.HasIndex("Message")
|
||||
.HasDatabaseName("ix_events_message");
|
||||
|
||||
b.HasIndex("Severity")
|
||||
.HasDatabaseName("ix_events_severity");
|
||||
|
||||
b.HasIndex("StrikeId")
|
||||
.HasDatabaseName("ix_events_strike_id");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_events_timestamp");
|
||||
|
||||
b.ToTable("events", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.ManualEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("data");
|
||||
|
||||
b.Property<string>("DownloadClientName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_name");
|
||||
|
||||
b.Property<string>("DownloadClientType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_type");
|
||||
|
||||
b.Property<string>("InstanceType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.Property<string>("InstanceUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_url");
|
||||
|
||||
b.Property<bool>("IsResolved")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_resolved");
|
||||
|
||||
b.Property<Guid?>("JobRunId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_manual_events");
|
||||
|
||||
b.HasIndex("InstanceType")
|
||||
.HasDatabaseName("ix_manual_events_instance_type");
|
||||
|
||||
b.HasIndex("IsResolved")
|
||||
.HasDatabaseName("ix_manual_events_is_resolved");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_manual_events_job_run_id");
|
||||
|
||||
b.HasIndex("Message")
|
||||
.HasDatabaseName("ix_manual_events_message");
|
||||
|
||||
b.HasIndex("Severity")
|
||||
.HasDatabaseName("ix_manual_events_severity");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_manual_events_timestamp");
|
||||
|
||||
b.ToTable("manual_events", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.DownloadItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("DownloadId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_id");
|
||||
|
||||
b.Property<bool>("IsMarkedForRemoval")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_marked_for_removal");
|
||||
|
||||
b.Property<bool>("IsRemoved")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_removed");
|
||||
|
||||
b.Property<bool>("IsReturning")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_returning");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_download_items");
|
||||
|
||||
b.HasIndex("DownloadId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_download_items_download_id");
|
||||
|
||||
b.ToTable("download_items", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.JobRun", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<DateTime>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_job_runs");
|
||||
|
||||
b.HasIndex("StartedAt")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_job_runs_started_at");
|
||||
|
||||
b.HasIndex("Type")
|
||||
.HasDatabaseName("ix_job_runs_type");
|
||||
|
||||
b.ToTable("job_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.Strike", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Guid>("DownloadItemId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_item_id");
|
||||
|
||||
b.Property<Guid>("JobRunId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<long?>("LastDownloadedBytes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("last_downloaded_bytes");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_strikes");
|
||||
|
||||
b.HasIndex("CreatedAt")
|
||||
.HasDatabaseName("ix_strikes_created_at");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_strikes_job_run_id");
|
||||
|
||||
b.HasIndex("DownloadItemId", "Type")
|
||||
.HasDatabaseName("ix_strikes_download_item_id_type");
|
||||
|
||||
b.ToTable("strikes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.AppEvent", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("JobRunId")
|
||||
.HasConstraintName("fk_events_job_runs_job_run_id");
|
||||
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.Strike", "Strike")
|
||||
.WithMany()
|
||||
.HasForeignKey("StrikeId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_events_strikes_strike_id");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
|
||||
b.Navigation("Strike");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.ManualEvent", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("ManualEvents")
|
||||
.HasForeignKey("JobRunId")
|
||||
.HasConstraintName("fk_manual_events_job_runs_job_run_id");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.Strike", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.DownloadItem", "DownloadItem")
|
||||
.WithMany("Strikes")
|
||||
.HasForeignKey("DownloadItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_strikes_download_items_download_item_id");
|
||||
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("Strikes")
|
||||
.HasForeignKey("JobRunId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_strikes_job_runs_job_run_id");
|
||||
|
||||
b.Navigation("DownloadItem");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.DownloadItem", b =>
|
||||
{
|
||||
b.Navigation("Strikes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.JobRun", b =>
|
||||
{
|
||||
b.Navigation("Events");
|
||||
|
||||
b.Navigation("ManualEvents");
|
||||
|
||||
b.Navigation("Strikes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Events
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDownloadItemStatuses : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "is_marked_for_removal",
|
||||
table: "download_items",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "is_removed",
|
||||
table: "download_items",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "is_returning",
|
||||
table: "download_items",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "is_marked_for_removal",
|
||||
table: "download_items");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "is_removed",
|
||||
table: "download_items");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "is_returning",
|
||||
table: "download_items");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace Cleanuparr.Persistence.Migrations.Events
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.1");
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.AppEvent", b =>
|
||||
{
|
||||
@@ -28,11 +28,33 @@ namespace Cleanuparr.Persistence.Migrations.Events
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("data");
|
||||
|
||||
b.Property<string>("DownloadClientName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_name");
|
||||
|
||||
b.Property<string>("DownloadClientType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_type");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("event_type");
|
||||
|
||||
b.Property<string>("InstanceType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.Property<string>("InstanceUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_url");
|
||||
|
||||
b.Property<Guid?>("JobRunId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
@@ -44,6 +66,10 @@ namespace Cleanuparr.Persistence.Migrations.Events
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<Guid?>("StrikeId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("strike_id");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("timestamp");
|
||||
@@ -55,15 +81,27 @@ namespace Cleanuparr.Persistence.Migrations.Events
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_events");
|
||||
|
||||
b.HasIndex("DownloadClientType")
|
||||
.HasDatabaseName("ix_events_download_client_type");
|
||||
|
||||
b.HasIndex("EventType")
|
||||
.HasDatabaseName("ix_events_event_type");
|
||||
|
||||
b.HasIndex("InstanceType")
|
||||
.HasDatabaseName("ix_events_instance_type");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_events_job_run_id");
|
||||
|
||||
b.HasIndex("Message")
|
||||
.HasDatabaseName("ix_events_message");
|
||||
|
||||
b.HasIndex("Severity")
|
||||
.HasDatabaseName("ix_events_severity");
|
||||
|
||||
b.HasIndex("StrikeId")
|
||||
.HasDatabaseName("ix_events_strike_id");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_events_timestamp");
|
||||
@@ -82,10 +120,32 @@ namespace Cleanuparr.Persistence.Migrations.Events
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("data");
|
||||
|
||||
b.Property<string>("DownloadClientName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_name");
|
||||
|
||||
b.Property<string>("DownloadClientType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_type");
|
||||
|
||||
b.Property<string>("InstanceType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.Property<string>("InstanceUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_url");
|
||||
|
||||
b.Property<bool>("IsResolved")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_resolved");
|
||||
|
||||
b.Property<Guid?>("JobRunId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
@@ -104,9 +164,15 @@ namespace Cleanuparr.Persistence.Migrations.Events
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_manual_events");
|
||||
|
||||
b.HasIndex("InstanceType")
|
||||
.HasDatabaseName("ix_manual_events_instance_type");
|
||||
|
||||
b.HasIndex("IsResolved")
|
||||
.HasDatabaseName("ix_manual_events_is_resolved");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_manual_events_job_run_id");
|
||||
|
||||
b.HasIndex("Message")
|
||||
.HasDatabaseName("ix_manual_events_message");
|
||||
|
||||
@@ -119,6 +185,190 @@ namespace Cleanuparr.Persistence.Migrations.Events
|
||||
|
||||
b.ToTable("manual_events", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.DownloadItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("DownloadId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_id");
|
||||
|
||||
b.Property<bool>("IsMarkedForRemoval")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_marked_for_removal");
|
||||
|
||||
b.Property<bool>("IsRemoved")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_removed");
|
||||
|
||||
b.Property<bool>("IsReturning")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_returning");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_download_items");
|
||||
|
||||
b.HasIndex("DownloadId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_download_items_download_id");
|
||||
|
||||
b.ToTable("download_items", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.JobRun", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<DateTime>("StartedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_job_runs");
|
||||
|
||||
b.HasIndex("StartedAt")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_job_runs_started_at");
|
||||
|
||||
b.HasIndex("Type")
|
||||
.HasDatabaseName("ix_job_runs_type");
|
||||
|
||||
b.ToTable("job_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.Strike", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Guid>("DownloadItemId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_item_id");
|
||||
|
||||
b.Property<Guid>("JobRunId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<long?>("LastDownloadedBytes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("last_downloaded_bytes");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_strikes");
|
||||
|
||||
b.HasIndex("CreatedAt")
|
||||
.HasDatabaseName("ix_strikes_created_at");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_strikes_job_run_id");
|
||||
|
||||
b.HasIndex("DownloadItemId", "Type")
|
||||
.HasDatabaseName("ix_strikes_download_item_id_type");
|
||||
|
||||
b.ToTable("strikes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.AppEvent", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("JobRunId")
|
||||
.HasConstraintName("fk_events_job_runs_job_run_id");
|
||||
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.Strike", "Strike")
|
||||
.WithMany()
|
||||
.HasForeignKey("StrikeId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_events_strikes_strike_id");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
|
||||
b.Navigation("Strike");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.ManualEvent", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("ManualEvents")
|
||||
.HasForeignKey("JobRunId")
|
||||
.HasConstraintName("fk_manual_events_job_runs_job_run_id");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.Strike", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.DownloadItem", "DownloadItem")
|
||||
.WithMany("Strikes")
|
||||
.HasForeignKey("DownloadItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_strikes_download_items_download_item_id");
|
||||
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("Strikes")
|
||||
.HasForeignKey("JobRunId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_strikes_job_runs_job_run_id");
|
||||
|
||||
b.Navigation("DownloadItem");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.DownloadItem", b =>
|
||||
{
|
||||
b.Navigation("Strikes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.JobRun", b =>
|
||||
{
|
||||
b.Navigation("Events");
|
||||
|
||||
b.Navigation("ManualEvents");
|
||||
|
||||
b.Navigation("Strikes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ public sealed record GeneralConfig : IConfig
|
||||
|
||||
public List<string> IgnoredDownloads { get; set; } = [];
|
||||
|
||||
public ushort StrikeInactivityWindowHours { get; set; } = 24;
|
||||
|
||||
public LoggingConfig Log { get; set; } = new();
|
||||
|
||||
public void Validate()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Persistence.Models.State;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Cleanuparr.Persistence.Models.Events;
|
||||
@@ -11,28 +13,64 @@ namespace Cleanuparr.Persistence.Models.Events;
|
||||
[Index(nameof(EventType))]
|
||||
[Index(nameof(Severity))]
|
||||
[Index(nameof(Message))]
|
||||
[Index(nameof(StrikeId))]
|
||||
[Index(nameof(JobRunId))]
|
||||
[Index(nameof(InstanceType))]
|
||||
[Index(nameof(DownloadClientType))]
|
||||
public class AppEvent : IEvent
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; } = Guid.CreateVersion7();
|
||||
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
|
||||
|
||||
[Required]
|
||||
public EventType EventType { get; set; }
|
||||
|
||||
|
||||
[Required]
|
||||
[MaxLength(1000)]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? Data { get; set; }
|
||||
|
||||
|
||||
[Required]
|
||||
public required EventSeverity Severity { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Optional correlation ID to link related events
|
||||
/// </summary>
|
||||
public Guid? TrackingId { get; set; }
|
||||
|
||||
public Guid? StrikeId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Strike? Strike { get; set; }
|
||||
|
||||
public Guid? JobRunId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public JobRun? JobRun { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of arr instance that generated this event (e.g., Sonarr, Radarr)
|
||||
/// </summary>
|
||||
public InstanceType? InstanceType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The URL of the arr instance that generated this event
|
||||
/// </summary>
|
||||
[MaxLength(500)]
|
||||
public string? InstanceUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of download client involved in this event
|
||||
/// </summary>
|
||||
public DownloadClientTypeName? DownloadClientType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the download client involved in this event
|
||||
/// </summary>
|
||||
[MaxLength(200)]
|
||||
public string? DownloadClientName { get; set; }
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Persistence.Models.State;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Cleanuparr.Persistence.Models.Events;
|
||||
@@ -11,21 +13,50 @@ namespace Cleanuparr.Persistence.Models.Events;
|
||||
[Index(nameof(Severity))]
|
||||
[Index(nameof(Message))]
|
||||
[Index(nameof(IsResolved))]
|
||||
[Index(nameof(JobRunId))]
|
||||
[Index(nameof(InstanceType))]
|
||||
public class ManualEvent
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; } = Guid.CreateVersion7();
|
||||
|
||||
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Required]
|
||||
[MaxLength(1000)]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
|
||||
public string? Data { get; set; }
|
||||
|
||||
[Required]
|
||||
public required EventSeverity Severity { get; set; }
|
||||
|
||||
|
||||
public bool IsResolved { get; set; }
|
||||
|
||||
public Guid? JobRunId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public JobRun? JobRun { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of arr instance that generated this event
|
||||
/// </summary>
|
||||
public InstanceType? InstanceType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The URL of the arr instance that generated this event
|
||||
/// </summary>
|
||||
[MaxLength(500)]
|
||||
public string? InstanceUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of download client involved in this event
|
||||
/// </summary>
|
||||
public DownloadClientTypeName? DownloadClientType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the download client involved in this event
|
||||
/// </summary>
|
||||
[MaxLength(200)]
|
||||
public string? DownloadClientName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Cleanuparr.Persistence.Models.State;
|
||||
|
||||
[Index(nameof(DownloadId), IsUnique = true)]
|
||||
public class DownloadItem
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; } = Guid.CreateVersion7();
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public required string DownloadId { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(500)]
|
||||
public required string Title { get; set; }
|
||||
|
||||
public bool IsMarkedForRemoval { get; set; }
|
||||
public bool IsRemoved { get; set; }
|
||||
public bool IsReturning { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public List<Strike> Strikes { get; set; } = [];
|
||||
}
|
||||
33
code/backend/Cleanuparr.Persistence/Models/State/JobRun.cs
Normal file
33
code/backend/Cleanuparr.Persistence/Models/State/JobRun.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Persistence.Models.Events;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Cleanuparr.Persistence.Models.State;
|
||||
|
||||
[Index(nameof(StartedAt), IsDescending = [true])]
|
||||
[Index(nameof(Type))]
|
||||
public class JobRun
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public required JobType Type { get; set; }
|
||||
|
||||
public DateTime StartedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime? CompletedAt { get; set; }
|
||||
|
||||
public JobRunStatus? Status { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public List<Strike> Strikes { get; set; } = [];
|
||||
|
||||
[JsonIgnore]
|
||||
public List<AppEvent> Events { get; set; } = [];
|
||||
|
||||
[JsonIgnore]
|
||||
public List<ManualEvent> ManualEvents { get; set; } = [];
|
||||
}
|
||||
34
code/backend/Cleanuparr.Persistence/Models/State/Strike.cs
Normal file
34
code/backend/Cleanuparr.Persistence/Models/State/Strike.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Cleanuparr.Persistence.Models.State;
|
||||
|
||||
[Index(nameof(DownloadItemId), nameof(Type))]
|
||||
[Index(nameof(CreatedAt))]
|
||||
[Index(nameof(JobRunId))]
|
||||
public class Strike
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; } = Guid.CreateVersion7();
|
||||
|
||||
[Required]
|
||||
public Guid DownloadItemId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public DownloadItem DownloadItem { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
public Guid JobRunId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public JobRun JobRun { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
public required StrikeType Type { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public long? LastDownloadedBytes { get; set; }
|
||||
}
|
||||
@@ -6,7 +6,6 @@ public static class Constants
|
||||
{
|
||||
public static readonly TimeSpan TriggerMaxLimit = TimeSpan.FromHours(6);
|
||||
public static readonly TimeSpan TriggerMinLimit = TimeSpan.FromSeconds(30);
|
||||
public static readonly TimeSpan CacheLimitBuffer = TimeSpan.FromHours(2);
|
||||
|
||||
public const string HttpClientWithRetryName = "retry";
|
||||
|
||||
|
||||
BIN
code/frontend/public/icons/icon-192x192.png
Normal file
BIN
code/frontend/public/icons/icon-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
code/frontend/public/icons/icon-512x512.png
Normal file
BIN
code/frontend/public/icons/icon-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -11,6 +11,16 @@
|
||||
"src": "icons/128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
1
code/frontend/public/sw.js
Normal file
1
code/frontend/public/sw.js
Normal file
@@ -0,0 +1 @@
|
||||
self.addEventListener('fetch', () => {});
|
||||
@@ -30,6 +30,13 @@ export const routes: Routes = [
|
||||
(m) => m.EventsComponent,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'strikes',
|
||||
loadComponent: () =>
|
||||
import('@features/strikes/strikes.component').then(
|
||||
(m) => m.StrikesComponent,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
children: [
|
||||
|
||||
@@ -18,6 +18,7 @@ export class EventsApi {
|
||||
if (filter.fromDate) params = params.set('fromDate', filter.fromDate);
|
||||
if (filter.toDate) params = params.set('toDate', filter.toDate);
|
||||
if (filter.search) params = params.set('search', filter.search);
|
||||
if (filter.jobRunId) params = params.set('jobRunId', filter.jobRunId);
|
||||
}
|
||||
return this.http.get<PaginatedResult<AppEvent>>('/api/events', { params });
|
||||
}
|
||||
|
||||
@@ -14,4 +14,8 @@ export class GeneralConfigApi {
|
||||
update(config: GeneralConfig): Observable<void> {
|
||||
return this.http.put<void>('/api/configuration/general', config);
|
||||
}
|
||||
|
||||
purgeStrikes(): Observable<{ deletedStrikes: number; deletedItems: number }> {
|
||||
return this.http.post<{ deletedStrikes: number; deletedItems: number }>('/api/configuration/strikes/purge', {});
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user