mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-09-12 05:19:43 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a44f226e8a | ||
|
|
edafde5810 | ||
|
|
54cd037cd2 | ||
|
|
2ebf67d44d | ||
|
|
d2d294b93c | ||
|
|
39eb91ac48 | ||
|
|
53376d94d9 | ||
|
|
f51973bb7b |
No files matched your search
@@ -74,9 +74,9 @@ jobs:
|
||||
token: ${{ env.REPO_READONLY_PAT }}
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
dotnet-version: 10.0.200
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
|
||||
@@ -84,9 +84,9 @@ jobs:
|
||||
path: code/frontend/dist/ui/browser
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
dotnet-version: 10.0.200
|
||||
|
||||
- name: Restore .NET dependencies
|
||||
run: |
|
||||
|
||||
@@ -68,9 +68,9 @@ jobs:
|
||||
path: code/frontend/dist/ui/browser
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
dotnet-version: 10.0.200
|
||||
|
||||
- name: Restore .NET dependencies
|
||||
run: |
|
||||
|
||||
@@ -20,6 +20,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create directory for static files
|
||||
run: |
|
||||
mkdir -p Cloudflare/static
|
||||
|
||||
- name: Copy root static files to Cloudflare static directory
|
||||
run: |
|
||||
|
||||
@@ -29,9 +29,9 @@ jobs:
|
||||
timeout-minutes: 1
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
dotnet-version: 10.0.200
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
thepirateheaven.org
|
||||
RARBG.work
|
||||
@@ -31,6 +31,16 @@ public class AuthControllerTests : IClassFixture<CustomWebApplicationFactory>
|
||||
body.GetProperty("setupCompleted").GetBoolean().ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact, TestPriority(0)]
|
||||
public async Task AuthEndpoints_AlwaysReturnNoCacheHeaders()
|
||||
{
|
||||
var response = await _client.GetAsync("/api/auth/status");
|
||||
|
||||
response.Headers.CacheControl.ShouldNotBeNull();
|
||||
response.Headers.CacheControl!.NoCache.ShouldBeTrue();
|
||||
response.Headers.CacheControl!.NoStore.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact, TestPriority(1)]
|
||||
public async Task Setup_CreateAccount_ReturnsCreated()
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Cleanuparr.Api.Filters;
|
||||
using Cleanuparr.Api.Json;
|
||||
using Cleanuparr.Infrastructure.Health;
|
||||
using Cleanuparr.Infrastructure.Hubs;
|
||||
@@ -64,10 +65,10 @@ public static class ApiDI
|
||||
// Enable compression
|
||||
app.UseResponseCompression();
|
||||
|
||||
// Serve static files with caching
|
||||
// Serve static files without caching
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
OnPrepareResponse = _ => {}
|
||||
OnPrepareResponse = ctx => NoCacheAttribute.Apply(ctx.Context.Response.Headers)
|
||||
});
|
||||
|
||||
// Add the global exception handling middleware first
|
||||
@@ -119,6 +120,7 @@ public static class ApiDI
|
||||
);
|
||||
|
||||
context.Response.ContentType = "text/html";
|
||||
NoCacheAttribute.Apply(context.Response.Headers);
|
||||
await context.Response.WriteAsync(indexContent, Encoding.UTF8);
|
||||
}).AllowAnonymous();
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using Cleanuparr.Api.Features.Auth.Contracts.Requests;
|
||||
using Cleanuparr.Api.Features.Auth.Contracts.Responses;
|
||||
using Cleanuparr.Api.Filters;
|
||||
using Cleanuparr.Infrastructure.Features.Auth;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Auth;
|
||||
@@ -14,6 +15,7 @@ namespace Cleanuparr.Api.Features.Auth.Controllers;
|
||||
[ApiController]
|
||||
[Route("api/account")]
|
||||
[Authorize]
|
||||
[NoCache]
|
||||
public sealed class AccountController : ControllerBase
|
||||
{
|
||||
private readonly UsersContext _usersContext;
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Security.Cryptography;
|
||||
using Cleanuparr.Api.Auth;
|
||||
using Cleanuparr.Api.Features.Auth.Contracts.Requests;
|
||||
using Cleanuparr.Api.Features.Auth.Contracts.Responses;
|
||||
using Cleanuparr.Api.Filters;
|
||||
using Cleanuparr.Infrastructure.Features.Auth;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Auth;
|
||||
@@ -14,6 +15,7 @@ namespace Cleanuparr.Api.Features.Auth.Controllers;
|
||||
[ApiController]
|
||||
[Route("api/auth")]
|
||||
[AllowAnonymous]
|
||||
[NoCache]
|
||||
public sealed class AuthController : ControllerBase
|
||||
{
|
||||
private readonly UsersContext _usersContext;
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ public sealed record UpdateMalwareBlockerConfigRequest
|
||||
|
||||
public bool DeletePrivate { get; init; }
|
||||
|
||||
public bool DeleteKnownMalware { get; init; }
|
||||
public bool ProcessNoContentId { get; init; }
|
||||
|
||||
public BlocklistSettings Sonarr { get; init; } = new();
|
||||
|
||||
@@ -37,7 +37,7 @@ public sealed record UpdateMalwareBlockerConfigRequest
|
||||
config.UseAdvancedScheduling = UseAdvancedScheduling;
|
||||
config.IgnorePrivate = IgnorePrivate;
|
||||
config.DeletePrivate = DeletePrivate;
|
||||
config.DeleteKnownMalware = DeleteKnownMalware;
|
||||
config.ProcessNoContentId = ProcessNoContentId;
|
||||
config.Sonarr = Sonarr;
|
||||
config.Radarr = Radarr;
|
||||
config.Lidarr = Lidarr;
|
||||
|
||||
+3
-1
@@ -13,6 +13,8 @@ public sealed record UpdateQueueCleanerConfigRequest
|
||||
public FailedImportConfig FailedImport { get; init; } = new();
|
||||
|
||||
public ushort DownloadingMetadataMaxStrikes { get; init; }
|
||||
|
||||
|
||||
public bool ProcessNoContentId { get; init; }
|
||||
|
||||
public List<string> IgnoredDownloads { get; set; } = [];
|
||||
}
|
||||
+1
@@ -69,6 +69,7 @@ public sealed class QueueCleanerConfigController : ControllerBase
|
||||
oldConfig.UseAdvancedScheduling = newConfigDto.UseAdvancedScheduling;
|
||||
oldConfig.FailedImport = newConfigDto.FailedImport;
|
||||
oldConfig.DownloadingMetadataMaxStrikes = newConfigDto.DownloadingMetadataMaxStrikes;
|
||||
oldConfig.ProcessNoContentId = newConfigDto.ProcessNoContentId;
|
||||
oldConfig.IgnoredDownloads = newConfigDto.IgnoredDownloads;
|
||||
|
||||
oldConfig.Validate();
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace Cleanuparr.Api.Filters;
|
||||
|
||||
/// <summary>
|
||||
/// Prevents caching of sensitive responses by setting appropriate HTTP headers.
|
||||
/// Applies Cache-Control: no-cache, no-store, Pragma: no-cache, and a past Expires date
|
||||
/// for maximum compatibility with HTTP/1.0 and HTTP/1.1 clients and intermediaries.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public sealed class NoCacheAttribute : ActionFilterAttribute
|
||||
{
|
||||
public static void Apply(IHeaderDictionary headers)
|
||||
{
|
||||
headers.CacheControl = "no-cache, no-store";
|
||||
headers.Pragma = "no-cache";
|
||||
headers.Expires = "Thu, 01 Jan 1970 00:00:00 GMT";
|
||||
}
|
||||
|
||||
public override void OnResultExecuting(ResultExecutingContext context)
|
||||
{
|
||||
Apply(context.HttpContext.Response.Headers);
|
||||
base.OnResultExecuting(context);
|
||||
}
|
||||
}
|
||||
@@ -11,5 +11,4 @@ public enum DeleteReason
|
||||
AllFilesSkipped,
|
||||
AllFilesSkippedByQBit,
|
||||
AllFilesBlocked,
|
||||
MalwareFileFound,
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using Cleanuparr.Infrastructure.Features.Arr;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.Arr;
|
||||
|
||||
@@ -35,144 +33,4 @@ public class WhisparrV2ClientTests
|
||||
_dryRunInterceptorMock.Object
|
||||
);
|
||||
}
|
||||
|
||||
#region IsRecordValid Tests
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenEpisodeIdIsZero_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Episode",
|
||||
DownloadId = "abc123",
|
||||
Protocol = "torrent",
|
||||
EpisodeId = 0,
|
||||
SeriesId = 1
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
_loggerMock.Verify(
|
||||
x => x.Log(
|
||||
LogLevel.Debug,
|
||||
It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("episode id and/or series id missing")),
|
||||
It.IsAny<Exception?>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenSeriesIdIsZero_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Episode",
|
||||
DownloadId = "abc123",
|
||||
Protocol = "torrent",
|
||||
EpisodeId = 1,
|
||||
SeriesId = 0
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenBothIdsAreZero_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Episode",
|
||||
DownloadId = "abc123",
|
||||
Protocol = "torrent",
|
||||
EpisodeId = 0,
|
||||
SeriesId = 0
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenBothIdsAreSet_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Episode",
|
||||
DownloadId = "abc123",
|
||||
Protocol = "torrent",
|
||||
EpisodeId = 42,
|
||||
SeriesId = 10
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenDownloadIdIsNull_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Episode",
|
||||
DownloadId = null!,
|
||||
Protocol = "torrent",
|
||||
EpisodeId = 42,
|
||||
SeriesId = 10
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenDownloadIdIsEmpty_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Episode",
|
||||
DownloadId = "",
|
||||
Protocol = "torrent",
|
||||
EpisodeId = 42,
|
||||
SeriesId = 10
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using Cleanuparr.Infrastructure.Features.Arr;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.Arr;
|
||||
|
||||
@@ -35,98 +33,4 @@ public class WhisparrV3ClientTests
|
||||
_dryRunInterceptorMock.Object
|
||||
);
|
||||
}
|
||||
|
||||
#region IsRecordValid Tests
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenMovieIdIsZero_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Movie",
|
||||
DownloadId = "abc123",
|
||||
Protocol = "torrent",
|
||||
MovieId = 0
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
_loggerMock.Verify(
|
||||
x => x.Log(
|
||||
LogLevel.Debug,
|
||||
It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("movie id missing")),
|
||||
It.IsAny<Exception?>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenMovieIdIsSet_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Movie",
|
||||
DownloadId = "abc123",
|
||||
Protocol = "torrent",
|
||||
MovieId = 42
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenDownloadIdIsNull_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Movie",
|
||||
DownloadId = null!,
|
||||
Protocol = "torrent",
|
||||
MovieId = 42
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenDownloadIdIsEmpty_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Movie",
|
||||
DownloadId = "",
|
||||
Protocol = "torrent",
|
||||
MovieId = 42
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
+52
-2
@@ -275,6 +275,55 @@ public class QueueItemRemoverTests : IDisposable
|
||||
|
||||
#endregion
|
||||
|
||||
#region RemoveQueueItemAsync - SkipSearch Tests
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveQueueItemAsync_WhenSkipSearch_DoesNotPublishHuntRequest()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateRemoveRequest(skipSearch: true);
|
||||
|
||||
_arrClientMock
|
||||
.Setup(c => c.DeleteQueueItemAsync(
|
||||
It.IsAny<ArrInstance>(),
|
||||
It.IsAny<QueueRecord>(),
|
||||
It.IsAny<bool>(),
|
||||
It.IsAny<DeleteReason>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await _queueItemRemover.RemoveQueueItemAsync(request);
|
||||
|
||||
// Assert
|
||||
_busMock.Verify(b => b.Publish(
|
||||
It.IsAny<DownloadHuntRequest<SearchItem>>(),
|
||||
It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveQueueItemAsync_WhenSkipSearch_AndHashIsNotRecurring_DoesNotModifyRecurringHashes()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateRemoveRequest(skipSearch: true);
|
||||
var hash = request.Record.DownloadId.ToLowerInvariant();
|
||||
|
||||
_arrClientMock
|
||||
.Setup(c => c.DeleteQueueItemAsync(
|
||||
It.IsAny<ArrInstance>(),
|
||||
It.IsAny<QueueRecord>(),
|
||||
It.IsAny<bool>(),
|
||||
It.IsAny<DeleteReason>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await _queueItemRemover.RemoveQueueItemAsync(request);
|
||||
|
||||
// Assert - hash was never in recurring, should still not be there
|
||||
Assert.False(Striker.RecurringHashes.ContainsKey(hash));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RemoveQueueItemAsync - HTTP Error Tests
|
||||
|
||||
[Fact]
|
||||
@@ -377,7 +426,6 @@ public class QueueItemRemoverTests : IDisposable
|
||||
[InlineData(DeleteReason.SlowSpeed)]
|
||||
[InlineData(DeleteReason.SlowTime)]
|
||||
[InlineData(DeleteReason.DownloadingMetadata)]
|
||||
[InlineData(DeleteReason.MalwareFileFound)]
|
||||
public async Task RemoveQueueItemAsync_PassesCorrectDeleteReason(DeleteReason deleteReason)
|
||||
{
|
||||
// Arrange
|
||||
@@ -436,7 +484,8 @@ public class QueueItemRemoverTests : IDisposable
|
||||
private static QueueItemRemoveRequest<SearchItem> CreateRemoveRequest(
|
||||
InstanceType instanceType = InstanceType.Sonarr,
|
||||
bool removeFromClient = true,
|
||||
DeleteReason deleteReason = DeleteReason.Stalled)
|
||||
DeleteReason deleteReason = DeleteReason.Stalled,
|
||||
bool skipSearch = false)
|
||||
{
|
||||
return new QueueItemRemoveRequest<SearchItem>
|
||||
{
|
||||
@@ -446,6 +495,7 @@ public class QueueItemRemoverTests : IDisposable
|
||||
Record = CreateQueueRecord(),
|
||||
RemoveFromClient = removeFromClient,
|
||||
DeleteReason = deleteReason,
|
||||
SkipSearch = skipSearch,
|
||||
JobRunId = Guid.NewGuid()
|
||||
};
|
||||
}
|
||||
|
||||
+192
-19
@@ -159,24 +159,21 @@ public class MalwareBlockerTests : IDisposable
|
||||
_fixture.ArrClientFactory.Verify(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteInternalAsync_WhenDeleteKnownMalwareEnabled_ProcessesAllArrs()
|
||||
[Theory]
|
||||
[InlineData(InstanceType.Radarr)]
|
||||
[InlineData(InstanceType.Lidarr)]
|
||||
[InlineData(InstanceType.Readarr)]
|
||||
[InlineData(InstanceType.Whisparr)]
|
||||
public async Task ExecuteInternalAsync_WhenArrTypeEnabled_ProcessesCorrectInstances(InstanceType instanceType)
|
||||
{
|
||||
// Arrange
|
||||
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
||||
|
||||
var contentBlockerConfig = _fixture.DataContext.ContentBlockerConfigs.First();
|
||||
contentBlockerConfig.DeleteKnownMalware = true;
|
||||
// Need at least one blocklist enabled for processing to occur
|
||||
contentBlockerConfig.Sonarr = new BlocklistSettings { Enabled = true };
|
||||
_fixture.DataContext.SaveChanges();
|
||||
|
||||
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
||||
TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
|
||||
EnableBlocklist(instanceType);
|
||||
AddArrInstance(instanceType);
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(It.IsAny<InstanceType>(), It.IsAny<float>()))
|
||||
.Setup(x => x.GetClient(instanceType, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -192,9 +189,8 @@ public class MalwareBlockerTests : IDisposable
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert - Sonarr and Radarr processed because DeleteKnownMalware is true
|
||||
_fixture.ArrClientFactory.Verify(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()), Times.Once);
|
||||
_fixture.ArrClientFactory.Verify(x => x.GetClient(InstanceType.Radarr, It.IsAny<float>()), Times.Once);
|
||||
// Assert
|
||||
_fixture.ArrClientFactory.Verify(x => x.GetClient(instanceType, It.IsAny<float>()), Times.Once);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -215,6 +211,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
@@ -225,7 +222,9 @@ public class MalwareBlockerTests : IDisposable
|
||||
Id = 1,
|
||||
DownloadId = "ignored-download-id",
|
||||
Title = "Ignored Download",
|
||||
Protocol = "torrent"
|
||||
Protocol = "torrent",
|
||||
SeriesId = 1,
|
||||
EpisodeId = 1
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -267,6 +266,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
@@ -277,7 +277,9 @@ public class MalwareBlockerTests : IDisposable
|
||||
Id = 1,
|
||||
DownloadId = "torrent-download-id",
|
||||
Title = "Torrent Download",
|
||||
Protocol = "torrent"
|
||||
Protocol = "torrent",
|
||||
SeriesId = 1,
|
||||
EpisodeId = 1
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -325,6 +327,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
@@ -401,6 +404,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
@@ -472,6 +476,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
@@ -482,7 +487,9 @@ public class MalwareBlockerTests : IDisposable
|
||||
Id = 1,
|
||||
DownloadId = "missing-download-id",
|
||||
Title = "Missing Download",
|
||||
Protocol = "torrent"
|
||||
Protocol = "torrent",
|
||||
SeriesId = 1,
|
||||
EpisodeId = 1
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -526,6 +533,142 @@ public class MalwareBlockerTests : IDisposable
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessInstanceAsync_SkipsItem_WhenMissingContentId_AndProcessNoContentIdIsFalse()
|
||||
{
|
||||
// Arrange
|
||||
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
||||
EnableSonarrBlocklist();
|
||||
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(false);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
DownloadId = "no-content-id-download",
|
||||
Title = "No Content ID Download",
|
||||
Protocol = "torrent"
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
.Setup(x => x.Iterate(
|
||||
It.IsAny<IArrClient>(),
|
||||
It.IsAny<ArrInstance>(),
|
||||
It.IsAny<Func<IReadOnlyList<QueueRecord>, Task>>()
|
||||
))
|
||||
.Returns(async (IArrClient client, ArrInstance instance, Func<IReadOnlyList<QueueRecord>, Task> callback) =>
|
||||
{
|
||||
await callback([queueRecord]);
|
||||
});
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
_logger.Verify(
|
||||
x => x.Log(
|
||||
LogLevel.Information,
|
||||
It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("skip | item is missing the content id")),
|
||||
It.IsAny<Exception?>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
|
||||
_fixture.MessageBus.Verify(
|
||||
x => x.Publish(
|
||||
It.IsAny<QueueItemRemoveRequest<SeriesSearchItem>>(),
|
||||
It.IsAny<CancellationToken>()
|
||||
),
|
||||
Times.Never
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessInstanceAsync_WhenMissingContentId_AndProcessNoContentIdIsTrue_PublishesRemoveRequestWithSkipSearch()
|
||||
{
|
||||
// Arrange
|
||||
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
||||
EnableSonarrBlocklist();
|
||||
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
||||
|
||||
var contentBlockerConfig = _fixture.DataContext.ContentBlockerConfigs.First();
|
||||
contentBlockerConfig.ProcessNoContentId = true;
|
||||
_fixture.DataContext.SaveChanges();
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(false);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
DownloadId = "no-content-id-download",
|
||||
Title = "No Content ID Download",
|
||||
Protocol = "torrent"
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
.Setup(x => x.Iterate(
|
||||
It.IsAny<IArrClient>(),
|
||||
It.IsAny<ArrInstance>(),
|
||||
It.IsAny<Func<IReadOnlyList<QueueRecord>, Task>>()
|
||||
))
|
||||
.Returns(async (IArrClient client, ArrInstance instance, Func<IReadOnlyList<QueueRecord>, Task> callback) =>
|
||||
{
|
||||
await callback([queueRecord]);
|
||||
});
|
||||
|
||||
var mockDownloadService = _fixture.CreateMockDownloadService();
|
||||
mockDownloadService
|
||||
.Setup(x => x.BlockUnwantedFilesAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<List<string>>()
|
||||
))
|
||||
.ReturnsAsync(new BlockFilesResult
|
||||
{
|
||||
Found = true,
|
||||
ShouldRemove = true,
|
||||
IsPrivate = false,
|
||||
DeleteReason = DeleteReason.AllFilesBlocked
|
||||
});
|
||||
|
||||
_fixture.DownloadServiceFactory
|
||||
.Setup(x => x.GetDownloadService(It.IsAny<DownloadClientConfig>()))
|
||||
.Returns(mockDownloadService.Object);
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert - SkipSearch must be true because the item has no content ID
|
||||
_fixture.MessageBus.Verify(
|
||||
x => x.Publish(
|
||||
It.Is<QueueItemRemoveRequest<SeriesSearchItem>>(r =>
|
||||
r.SkipSearch == true &&
|
||||
r.DeleteReason == DeleteReason.AllFilesBlocked
|
||||
),
|
||||
It.IsAny<CancellationToken>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Error Handling Tests
|
||||
@@ -540,6 +683,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
@@ -550,7 +694,9 @@ public class MalwareBlockerTests : IDisposable
|
||||
Id = 1,
|
||||
DownloadId = "error-download-id",
|
||||
Title = "Error Download",
|
||||
Protocol = "torrent"
|
||||
Protocol = "torrent",
|
||||
SeriesId = 1,
|
||||
EpisodeId = 1
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -605,5 +751,32 @@ public class MalwareBlockerTests : IDisposable
|
||||
_fixture.DataContext.SaveChanges();
|
||||
}
|
||||
|
||||
private void EnableBlocklist(InstanceType instanceType)
|
||||
{
|
||||
var config = _fixture.DataContext.ContentBlockerConfigs.First();
|
||||
var settings = new BlocklistSettings { Enabled = true };
|
||||
switch (instanceType)
|
||||
{
|
||||
case InstanceType.Radarr: config.Radarr = settings; break;
|
||||
case InstanceType.Lidarr: config.Lidarr = settings; break;
|
||||
case InstanceType.Readarr: config.Readarr = settings; break;
|
||||
case InstanceType.Whisparr: config.Whisparr = settings; break;
|
||||
default: throw new ArgumentOutOfRangeException(nameof(instanceType));
|
||||
}
|
||||
_fixture.DataContext.SaveChanges();
|
||||
}
|
||||
|
||||
private void AddArrInstance(InstanceType instanceType)
|
||||
{
|
||||
switch (instanceType)
|
||||
{
|
||||
case InstanceType.Radarr: TestDataContextFactory.AddRadarrInstance(_fixture.DataContext); break;
|
||||
case InstanceType.Lidarr: TestDataContextFactory.AddLidarrInstance(_fixture.DataContext); break;
|
||||
case InstanceType.Readarr: TestDataContextFactory.AddReadarrInstance(_fixture.DataContext); break;
|
||||
case InstanceType.Whisparr: TestDataContextFactory.AddWhisparrInstance(_fixture.DataContext); break;
|
||||
default: throw new ArgumentOutOfRangeException(nameof(instanceType));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -220,6 +220,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
@@ -230,7 +231,9 @@ public class QueueCleanerTests : IDisposable
|
||||
Id = 1,
|
||||
DownloadId = "ignored-download-id",
|
||||
Title = "Ignored Download",
|
||||
Protocol = "torrent"
|
||||
Protocol = "torrent",
|
||||
SeriesId = 1,
|
||||
EpisodeId = 1
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -275,6 +278,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
@@ -285,7 +289,9 @@ public class QueueCleanerTests : IDisposable
|
||||
Id = 1,
|
||||
DownloadId = "cached-download-id",
|
||||
Title = "Cached Download",
|
||||
Protocol = "torrent"
|
||||
Protocol = "torrent",
|
||||
SeriesId = 1,
|
||||
EpisodeId = 1
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -326,6 +332,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.ShouldRemoveFromQueue(
|
||||
It.IsAny<InstanceType>(),
|
||||
It.IsAny<QueueRecord>(),
|
||||
@@ -342,7 +349,9 @@ public class QueueCleanerTests : IDisposable
|
||||
Id = 1,
|
||||
DownloadId = "torrent-download-id",
|
||||
Title = "Torrent Download",
|
||||
Protocol = "torrent"
|
||||
Protocol = "torrent",
|
||||
SeriesId = 1,
|
||||
EpisodeId = 1
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -389,6 +398,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
@@ -458,6 +468,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.ShouldRemoveFromQueue(
|
||||
It.IsAny<InstanceType>(),
|
||||
It.IsAny<QueueRecord>(),
|
||||
@@ -474,7 +485,9 @@ public class QueueCleanerTests : IDisposable
|
||||
Id = 1,
|
||||
DownloadId = "missing-download-id",
|
||||
Title = "Missing Download",
|
||||
Protocol = "torrent"
|
||||
Protocol = "torrent",
|
||||
SeriesId = 1,
|
||||
EpisodeId = 1
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -527,6 +540,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.ShouldRemoveFromQueue(
|
||||
It.IsAny<InstanceType>(),
|
||||
It.IsAny<QueueRecord>(),
|
||||
@@ -543,7 +557,9 @@ public class QueueCleanerTests : IDisposable
|
||||
Id = 1,
|
||||
DownloadId = "download-id",
|
||||
Title = "Test Download",
|
||||
Protocol = "torrent"
|
||||
Protocol = "torrent",
|
||||
SeriesId = 1,
|
||||
EpisodeId = 1
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -595,6 +611,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.ShouldRemoveFromQueue(
|
||||
It.IsAny<InstanceType>(),
|
||||
It.IsAny<QueueRecord>(),
|
||||
@@ -656,6 +673,147 @@ public class QueueCleanerTests : IDisposable
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessInstanceAsync_SkipsItem_WhenMissingContentId_AndProcessNoContentIdIsFalse()
|
||||
{
|
||||
// Arrange
|
||||
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
||||
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(false);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
DownloadId = "no-content-id-download",
|
||||
Title = "No Content ID Download",
|
||||
Protocol = "torrent"
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
.Setup(x => x.Iterate(
|
||||
It.IsAny<IArrClient>(),
|
||||
It.IsAny<ArrInstance>(),
|
||||
It.IsAny<Func<IReadOnlyList<QueueRecord>, Task>>()
|
||||
))
|
||||
.Returns(async (IArrClient client, ArrInstance instance, Func<IReadOnlyList<QueueRecord>, Task> callback) =>
|
||||
{
|
||||
await callback([queueRecord]);
|
||||
});
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
_logger.Verify(
|
||||
x => x.Log(
|
||||
LogLevel.Information,
|
||||
It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("skip | item is missing the content id")),
|
||||
It.IsAny<Exception?>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
|
||||
_fixture.MessageBus.Verify(
|
||||
x => x.Publish(
|
||||
It.IsAny<QueueItemRemoveRequest<SeriesSearchItem>>(),
|
||||
It.IsAny<CancellationToken>()
|
||||
),
|
||||
Times.Never
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessInstanceAsync_WhenMissingContentId_AndProcessNoContentIdIsTrue_PublishesRemoveRequestWithSkipSearch()
|
||||
{
|
||||
// Arrange
|
||||
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
||||
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
||||
|
||||
var queueCleanerConfig = _fixture.DataContext.QueueCleanerConfigs.First();
|
||||
queueCleanerConfig.ProcessNoContentId = true;
|
||||
_fixture.DataContext.SaveChanges();
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(false);
|
||||
mockArrClient.Setup(x => x.ShouldRemoveFromQueue(
|
||||
It.IsAny<InstanceType>(),
|
||||
It.IsAny<QueueRecord>(),
|
||||
It.IsAny<bool>(),
|
||||
It.IsAny<short>()
|
||||
)).ReturnsAsync(false);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
DownloadId = "no-content-id-download",
|
||||
Title = "No Content ID Download",
|
||||
Protocol = "torrent"
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
.Setup(x => x.Iterate(
|
||||
It.IsAny<IArrClient>(),
|
||||
It.IsAny<ArrInstance>(),
|
||||
It.IsAny<Func<IReadOnlyList<QueueRecord>, Task>>()
|
||||
))
|
||||
.Returns(async (IArrClient client, ArrInstance instance, Func<IReadOnlyList<QueueRecord>, Task> callback) =>
|
||||
{
|
||||
await callback([queueRecord]);
|
||||
});
|
||||
|
||||
var mockDownloadService = _fixture.CreateMockDownloadService();
|
||||
mockDownloadService
|
||||
.Setup(x => x.ShouldRemoveFromArrQueueAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<List<string>>()
|
||||
))
|
||||
.ReturnsAsync(new DownloadCheckResult
|
||||
{
|
||||
Found = true,
|
||||
ShouldRemove = true,
|
||||
IsPrivate = false,
|
||||
DeleteFromClient = true,
|
||||
DeleteReason = DeleteReason.Stalled
|
||||
});
|
||||
|
||||
_fixture.DownloadServiceFactory
|
||||
.Setup(x => x.GetDownloadService(It.IsAny<DownloadClientConfig>()))
|
||||
.Returns(mockDownloadService.Object);
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert - SkipSearch must be true because the item has no content ID
|
||||
_fixture.MessageBus.Verify(
|
||||
x => x.Publish(
|
||||
It.Is<QueueItemRemoveRequest<SeriesSearchItem>>(r =>
|
||||
r.SkipSearch == true &&
|
||||
r.DeleteReason == DeleteReason.Stalled
|
||||
),
|
||||
It.IsAny<CancellationToken>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Error Handling Tests
|
||||
@@ -669,6 +827,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.ShouldRemoveFromQueue(
|
||||
It.IsAny<InstanceType>(),
|
||||
It.IsAny<QueueRecord>(),
|
||||
@@ -685,7 +844,9 @@ public class QueueCleanerTests : IDisposable
|
||||
Id = 1,
|
||||
DownloadId = "error-download-id",
|
||||
Title = "Error Download",
|
||||
Protocol = "torrent"
|
||||
Protocol = "torrent",
|
||||
SeriesId = 1,
|
||||
EpisodeId = 1
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -744,6 +905,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Radarr, It.IsAny<float>()))
|
||||
@@ -833,6 +995,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Radarr, It.IsAny<float>()))
|
||||
@@ -905,6 +1068,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Lidarr, It.IsAny<float>()))
|
||||
@@ -977,6 +1141,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Readarr, It.IsAny<float>()))
|
||||
@@ -1049,6 +1214,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Whisparr, 2f))
|
||||
@@ -1124,6 +1290,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Whisparr, 3f))
|
||||
@@ -1196,6 +1363,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
mockArrClient.Setup(x => x.HasContentId(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Whisparr, 2f))
|
||||
|
||||
-1
@@ -75,7 +75,6 @@ public static class TestDataContextFactory
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
IgnoredDownloads = [],
|
||||
DeleteKnownMalware = false,
|
||||
DeletePrivate = false,
|
||||
Sonarr = new BlocklistSettings { Enabled = false },
|
||||
Radarr = new BlocklistSettings { Enabled = false },
|
||||
|
||||
-28
@@ -118,34 +118,6 @@ public class BlocklistProviderTests : IDisposable
|
||||
result.Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMalwarePatterns_NotInCache_ReturnsEmptyBag()
|
||||
{
|
||||
// Act
|
||||
var result = _provider.GetMalwarePatterns();
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMalwarePatterns_InCache_ReturnsCachedPatterns()
|
||||
{
|
||||
// Arrange
|
||||
var patterns = new ConcurrentBag<string> { "known_malware.exe", "trojan*", "virus.dll" };
|
||||
_cache.Set(CacheKeys.KnownMalwarePatterns(), patterns);
|
||||
|
||||
// Act
|
||||
var result = _provider.GetMalwarePatterns();
|
||||
|
||||
// Assert
|
||||
result.Count.ShouldBe(3);
|
||||
result.ShouldContain("known_malware.exe");
|
||||
result.ShouldContain("trojan*");
|
||||
result.ShouldContain("virus.dll");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(InstanceType.Sonarr)]
|
||||
[InlineData(InstanceType.Radarr)]
|
||||
|
||||
+2
-2
@@ -271,12 +271,12 @@ public class NotificationPublisherTests
|
||||
.Returns(providerMock.Object);
|
||||
|
||||
// Act
|
||||
await _publisher.NotifyQueueItemDeleted(false, DeleteReason.MalwareFileFound);
|
||||
await _publisher.NotifyQueueItemDeleted(false, DeleteReason.AllFilesBlocked);
|
||||
|
||||
// Assert
|
||||
providerMock.Verify(p => p.SendNotificationAsync(It.Is<NotificationContext>(
|
||||
c => c.Data["Removed from client?"] == "False" &&
|
||||
c.Data["Reason"] == "MalwareFileFound")), Times.Once);
|
||||
c.Data["Reason"] == "AllFilesBlocked")), Times.Once);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -232,7 +232,7 @@ public class EventPublisher : IEventPublisher
|
||||
public async Task PublishSearchNotTriggered(string hash, string itemName)
|
||||
{
|
||||
await PublishManualAsync(
|
||||
"Replacement search was not triggered after removal because the item keeps coming back\nPlease trigger a manual search if needed",
|
||||
"Replacement search was not triggered after removal\nPlease trigger a manual search if needed",
|
||||
EventSeverity.Warning,
|
||||
data: new { itemName, hash }
|
||||
);
|
||||
|
||||
@@ -158,7 +158,7 @@ public abstract class ArrClient : IArrClient
|
||||
|
||||
public abstract Task SearchItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items);
|
||||
|
||||
public virtual bool IsRecordValid(QueueRecord record)
|
||||
public bool IsRecordValid(QueueRecord record)
|
||||
{
|
||||
if (string.IsNullOrEmpty(record.DownloadId))
|
||||
{
|
||||
@@ -169,6 +169,8 @@ public abstract class ArrClient : IArrClient
|
||||
return true;
|
||||
}
|
||||
|
||||
public abstract bool HasContentId(QueueRecord record);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HealthCheckAsync(ArrInstance arrInstance)
|
||||
{
|
||||
|
||||
@@ -17,6 +17,13 @@ public interface IArrClient
|
||||
|
||||
bool IsRecordValid(QueueRecord record);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the record has an id (movie id, tv show id etc.)
|
||||
/// </summary>
|
||||
/// <param name="record">The record to check</param>
|
||||
/// <returns>True if the record has an id, false otherwise</returns>
|
||||
bool HasContentId(QueueRecord record);
|
||||
|
||||
/// <summary>
|
||||
/// Tests the connection to an Arr instance
|
||||
/// </summary>
|
||||
|
||||
@@ -87,16 +87,7 @@ public class LidarrClient : ArrClient, ILidarrClient
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsRecordValid(QueueRecord record)
|
||||
{
|
||||
if (record.ArtistId is 0 || record.AlbumId is 0)
|
||||
{
|
||||
_logger.LogDebug("skip | artist id and/or album id missing | {title}", record.Title);
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.IsRecordValid(record);
|
||||
}
|
||||
public override bool HasContentId(QueueRecord record) => record.ArtistId is not 0 && record.AlbumId is not 0;
|
||||
|
||||
private static string GetSearchLog(
|
||||
Uri instanceUrl,
|
||||
|
||||
@@ -92,16 +92,7 @@ public class RadarrClient : ArrClient, IRadarrClient
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsRecordValid(QueueRecord record)
|
||||
{
|
||||
if (record.MovieId is 0)
|
||||
{
|
||||
_logger.LogDebug("skip | movie id missing | {title}", record.Title);
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.IsRecordValid(record);
|
||||
}
|
||||
public override bool HasContentId(QueueRecord record) => record.MovieId is not 0;
|
||||
|
||||
private static string GetSearchLog(Uri instanceUrl, RadarrCommand command, bool success, string? logContext)
|
||||
{
|
||||
|
||||
@@ -92,16 +92,7 @@ public class ReadarrClient : ArrClient, IReadarrClient
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsRecordValid(QueueRecord record)
|
||||
{
|
||||
if (record.AuthorId is 0 || record.BookId is 0)
|
||||
{
|
||||
_logger.LogDebug("skip | author id and/or book id missing | {title}", record.Title);
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.IsRecordValid(record);
|
||||
}
|
||||
public override bool HasContentId(QueueRecord record) => record.AuthorId is not 0 && record.BookId is not 0;
|
||||
|
||||
private static string GetSearchLog(Uri instanceUrl, ReadarrCommand command, bool success, string? logContext)
|
||||
{
|
||||
|
||||
@@ -90,16 +90,7 @@ public class SonarrClient : ArrClient, ISonarrClient
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsRecordValid(QueueRecord record)
|
||||
{
|
||||
if (record.EpisodeId is 0 || record.SeriesId is 0)
|
||||
{
|
||||
_logger.LogDebug("skip | episode id and/or series id missing | {title}", record.Title);
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.IsRecordValid(record);
|
||||
}
|
||||
public override bool HasContentId(QueueRecord record) => record.EpisodeId is not 0 && record.SeriesId is not 0;
|
||||
|
||||
private static string GetSearchLog(
|
||||
SeriesSearchType searchType,
|
||||
|
||||
@@ -90,16 +90,7 @@ public class WhisparrV2Client : ArrClient, IWhisparrV2Client
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsRecordValid(QueueRecord record)
|
||||
{
|
||||
if (record.EpisodeId is 0 || record.SeriesId is 0)
|
||||
{
|
||||
_logger.LogDebug("skip | episode id and/or series id missing | {title}", record.Title);
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.IsRecordValid(record);
|
||||
}
|
||||
public override bool HasContentId(QueueRecord record) => record.EpisodeId is not 0 && record.SeriesId is not 0;
|
||||
|
||||
private static string GetSearchLog(
|
||||
SeriesSearchType searchType,
|
||||
|
||||
@@ -93,16 +93,7 @@ public class WhisparrV3Client : ArrClient, IWhisparrV3Client
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsRecordValid(QueueRecord record)
|
||||
{
|
||||
if (record.MovieId is 0)
|
||||
{
|
||||
_logger.LogDebug("skip | movie id missing | {title}", record.Title);
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.IsRecordValid(record);
|
||||
}
|
||||
public override bool HasContentId(QueueRecord record) => record.MovieId is not 0;
|
||||
|
||||
private static string GetSearchLog(Uri instanceUrl, WhisparrV3Command command, bool success, string? logContext)
|
||||
{
|
||||
|
||||
-8
@@ -68,7 +68,6 @@ public partial class DelugeService
|
||||
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
|
||||
ConcurrentBag<string> patterns = _blocklistProvider.GetPatterns(instanceType);
|
||||
ConcurrentBag<Regex> regexes = _blocklistProvider.GetRegexes(instanceType);
|
||||
ConcurrentBag<string> malwarePatterns = _blocklistProvider.GetMalwarePatterns();
|
||||
|
||||
ProcessFiles(contents.Contents, (name, file) =>
|
||||
{
|
||||
@@ -79,13 +78,6 @@ public partial class DelugeService
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (malwareBlockerConfig.DeleteKnownMalware && _filenameEvaluator.IsKnownMalware(name, malwarePatterns))
|
||||
{
|
||||
_logger.LogInformation("malware file found | {file} | {title}", file.Path, download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.MalwareFileFound;
|
||||
}
|
||||
|
||||
if (file.Priority is 0)
|
||||
{
|
||||
|
||||
+1
-10
@@ -73,8 +73,7 @@ public partial class QBitService
|
||||
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
|
||||
ConcurrentBag<string> patterns = _blocklistProvider.GetPatterns(instanceType);
|
||||
ConcurrentBag<Regex> regexes = _blocklistProvider.GetRegexes(instanceType);
|
||||
ConcurrentBag<string> malwarePatterns = _blocklistProvider.GetMalwarePatterns();
|
||||
|
||||
|
||||
foreach (TorrentContent file in files)
|
||||
{
|
||||
if (!file.Index.HasValue)
|
||||
@@ -84,14 +83,6 @@ public partial class QBitService
|
||||
}
|
||||
|
||||
totalFiles++;
|
||||
|
||||
if (malwareBlockerConfig.DeleteKnownMalware && _filenameEvaluator.IsKnownMalware(file.Name, malwarePatterns))
|
||||
{
|
||||
_logger.LogInformation("malware file found | {file} | {title}", file.Name, download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.MalwareFileFound;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (file.Priority is TorrentContentPriority.Skip)
|
||||
{
|
||||
|
||||
-8
@@ -71,7 +71,6 @@ public partial class RTorrentService
|
||||
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
|
||||
ConcurrentBag<string> patterns = _blocklistProvider.GetPatterns(instanceType);
|
||||
ConcurrentBag<Regex> regexes = _blocklistProvider.GetRegexes(instanceType);
|
||||
ConcurrentBag<string> malwarePatterns = _blocklistProvider.GetMalwarePatterns();
|
||||
|
||||
List<(int Index, int Priority)> priorityUpdates = [];
|
||||
|
||||
@@ -85,13 +84,6 @@ public partial class RTorrentService
|
||||
continue;
|
||||
}
|
||||
|
||||
if (malwareBlockerConfig.DeleteKnownMalware && _filenameEvaluator.IsKnownMalware(fileName, malwarePatterns))
|
||||
{
|
||||
_logger.LogInformation("malware file found | {file} | {title}", file.Path, download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.MalwareFileFound;
|
||||
}
|
||||
|
||||
if (file.Priority == 0)
|
||||
{
|
||||
_logger.LogTrace("File is already skipped | {file}", file.Path);
|
||||
|
||||
+2
-11
@@ -56,8 +56,7 @@ public partial class TransmissionService
|
||||
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
|
||||
ConcurrentBag<string> patterns = _blocklistProvider.GetPatterns(instanceType);
|
||||
ConcurrentBag<Regex> regexes = _blocklistProvider.GetRegexes(instanceType);
|
||||
ConcurrentBag<string> malwarePatterns = _blocklistProvider.GetMalwarePatterns();
|
||||
|
||||
|
||||
for (int i = 0; i < download.Files.Length; i++)
|
||||
{
|
||||
if (download.FileStats?[i].Wanted == null)
|
||||
@@ -67,15 +66,7 @@ public partial class TransmissionService
|
||||
}
|
||||
|
||||
totalFiles++;
|
||||
|
||||
if (malwareBlockerConfig.DeleteKnownMalware && _filenameEvaluator.IsKnownMalware(download.Files[i].Name, malwarePatterns))
|
||||
{
|
||||
_logger.LogInformation("malware file found | {file} | {title}", download.Files[i].Name, download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.MalwareFileFound;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
if (!download.FileStats[i].Wanted.Value)
|
||||
{
|
||||
_logger.LogTrace("File is already skipped | {file}", download.Files[i].Name);
|
||||
|
||||
-9
@@ -61,18 +61,9 @@ public partial class UTorrentService
|
||||
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
|
||||
ConcurrentBag<string> patterns = _blocklistProvider.GetPatterns(instanceType);
|
||||
ConcurrentBag<Regex> regexes = _blocklistProvider.GetRegexes(instanceType);
|
||||
ConcurrentBag<string> malwarePatterns = _blocklistProvider.GetMalwarePatterns();
|
||||
|
||||
for (int i = 0; i < files.Count; i++)
|
||||
{
|
||||
if (malwareBlockerConfig.DeleteKnownMalware && _filenameEvaluator.IsKnownMalware(files[i].Name, malwarePatterns))
|
||||
{
|
||||
_logger.LogInformation("malware file found | {file} | {title}", files[i].Name, download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.MalwareFileFound;
|
||||
return result;
|
||||
}
|
||||
|
||||
var file = files[i];
|
||||
|
||||
if (file.Priority == 0) // Already skipped
|
||||
|
||||
+2
@@ -21,4 +21,6 @@ public sealed record QueueItemRemoveRequest<T>
|
||||
public required DeleteReason DeleteReason { get; init; }
|
||||
|
||||
public required Guid JobRunId { get; init; }
|
||||
|
||||
public bool SkipSearch { get; init; }
|
||||
}
|
||||
@@ -73,15 +73,20 @@ public sealed class QueueItemRemover : IQueueItemRemover
|
||||
ContextProvider.Set(nameof(InstanceType), request.InstanceType);
|
||||
ContextProvider.Set(ContextProvider.Keys.Version, request.Instance.Version);
|
||||
|
||||
// Use the new centralized EventPublisher method
|
||||
await _eventPublisher.PublishQueueItemDeleted(request.RemoveFromClient, request.DeleteReason);
|
||||
|
||||
// If recurring, do not search for replacement
|
||||
string hash = request.Record.DownloadId.ToLowerInvariant();
|
||||
if (Striker.RecurringHashes.ContainsKey(hash))
|
||||
var isRecurring = Striker.RecurringHashes.ContainsKey(hash);
|
||||
|
||||
if (isRecurring || request.SkipSearch)
|
||||
{
|
||||
await _eventPublisher.PublishSearchNotTriggered(request.Record.DownloadId, request.Record.Title);
|
||||
Striker.RecurringHashes.Remove(hash, out _);
|
||||
|
||||
if (isRecurring)
|
||||
{
|
||||
Striker.RecurringHashes.Remove(hash, out _);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -131,7 +131,8 @@ public abstract class GenericHandler : IHandler
|
||||
QueueRecord record,
|
||||
bool isPack,
|
||||
bool removeFromClient,
|
||||
DeleteReason deleteReason
|
||||
DeleteReason deleteReason,
|
||||
bool skipSearch = false
|
||||
)
|
||||
{
|
||||
if (_cache.TryGetValue(downloadRemovalKey, out bool _))
|
||||
@@ -139,7 +140,7 @@ public abstract class GenericHandler : IHandler
|
||||
_logger.LogDebug("skip removal request | already marked for removal | {title}", record.Title);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (instanceType is InstanceType.Sonarr || (instanceType is InstanceType.Whisparr && instance.Version is 2))
|
||||
{
|
||||
QueueItemRemoveRequest<SeriesSearchItem> removeRequest = new()
|
||||
@@ -150,7 +151,8 @@ public abstract class GenericHandler : IHandler
|
||||
SearchItem = (SeriesSearchItem)GetRecordSearchItem(instanceType, instance.Version, record, isPack),
|
||||
RemoveFromClient = removeFromClient,
|
||||
DeleteReason = deleteReason,
|
||||
JobRunId = ContextProvider.GetJobRunId()
|
||||
JobRunId = ContextProvider.GetJobRunId(),
|
||||
SkipSearch = skipSearch
|
||||
};
|
||||
|
||||
await _messageBus.Publish(removeRequest);
|
||||
@@ -165,7 +167,8 @@ public abstract class GenericHandler : IHandler
|
||||
SearchItem = GetRecordSearchItem(instanceType, instance.Version, record, isPack),
|
||||
RemoveFromClient = removeFromClient,
|
||||
DeleteReason = deleteReason,
|
||||
JobRunId = ContextProvider.GetJobRunId()
|
||||
JobRunId = ContextProvider.GetJobRunId(),
|
||||
SkipSearch = skipSearch
|
||||
};
|
||||
|
||||
await _messageBus.Publish(removeRequest);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Events.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
@@ -48,9 +48,13 @@ public sealed class MalwareBlocker : GenericHandler
|
||||
return;
|
||||
}
|
||||
|
||||
var config = ContextProvider.Get<ContentBlockerConfig>();
|
||||
ContentBlockerConfig malwareBlockerConfig = ContextProvider.Get<ContentBlockerConfig>();
|
||||
|
||||
if (!config.Sonarr.Enabled && !config.Radarr.Enabled && !config.Lidarr.Enabled && !config.Readarr.Enabled && !config.Whisparr.Enabled)
|
||||
if (!malwareBlockerConfig.Sonarr.Enabled &&
|
||||
!malwareBlockerConfig.Radarr.Enabled &&
|
||||
!malwareBlockerConfig.Lidarr.Enabled &&
|
||||
!malwareBlockerConfig.Readarr.Enabled &&
|
||||
!malwareBlockerConfig.Whisparr.Enabled)
|
||||
{
|
||||
_logger.LogWarning("No blocklists are enabled");
|
||||
return;
|
||||
@@ -64,27 +68,27 @@ public sealed class MalwareBlocker : GenericHandler
|
||||
var readarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Readarr));
|
||||
var whisparrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Whisparr));
|
||||
|
||||
if (config.Sonarr.Enabled || config.DeleteKnownMalware)
|
||||
if (malwareBlockerConfig.Sonarr.Enabled)
|
||||
{
|
||||
await ProcessArrConfigAsync(sonarrConfig);
|
||||
}
|
||||
|
||||
if (config.Radarr.Enabled || config.DeleteKnownMalware)
|
||||
|
||||
if (malwareBlockerConfig.Radarr.Enabled)
|
||||
{
|
||||
await ProcessArrConfigAsync(radarrConfig);
|
||||
}
|
||||
|
||||
if (config.Lidarr.Enabled || config.DeleteKnownMalware)
|
||||
|
||||
if (malwareBlockerConfig.Lidarr.Enabled)
|
||||
{
|
||||
await ProcessArrConfigAsync(lidarrConfig);
|
||||
}
|
||||
|
||||
if (config.Readarr.Enabled || config.DeleteKnownMalware)
|
||||
|
||||
if (malwareBlockerConfig.Readarr.Enabled)
|
||||
{
|
||||
await ProcessArrConfigAsync(readarrConfig);
|
||||
}
|
||||
|
||||
if (config.Whisparr.Enabled || config.DeleteKnownMalware)
|
||||
|
||||
if (malwareBlockerConfig.Whisparr.Enabled)
|
||||
{
|
||||
await ProcessArrConfigAsync(whisparrConfig);
|
||||
}
|
||||
@@ -107,33 +111,43 @@ public sealed class MalwareBlocker : GenericHandler
|
||||
|
||||
IReadOnlyList<IDownloadService> downloadServices = await GetInitializedDownloadServicesAsync();
|
||||
|
||||
var config = ContextProvider.Get<ContentBlockerConfig>();
|
||||
|
||||
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
|
||||
{
|
||||
var groups = items
|
||||
.GroupBy(x => x.DownloadId)
|
||||
.ToList();
|
||||
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (group.Any(x => !arrClient.IsRecordValid(x)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QueueRecord record = group.First();
|
||||
|
||||
_logger.LogTrace("processing | {title} | {id}", record.Title, record.DownloadId);
|
||||
|
||||
|
||||
if (!arrClient.IsRecordValid(record))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (ignoredDownloads.Contains(record.DownloadId, StringComparer.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation("skip | {title} | ignored", record.Title);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogTrace("processing | {title} | {id}", record.Title, record.DownloadId);
|
||||
|
||||
bool hasContentId = arrClient.HasContentId(record);
|
||||
|
||||
if (!hasContentId)
|
||||
{
|
||||
if (!config.ProcessNoContentId)
|
||||
{
|
||||
_logger.LogInformation("skip | item is missing the content id | {title}", record.Title);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogDebug("item is missing the content id | {title}", record.Title);
|
||||
}
|
||||
|
||||
string downloadRemovalKey = CacheKeys.DownloadMarkedForRemoval(record.DownloadId, instance.Url);
|
||||
|
||||
@@ -196,8 +210,6 @@ public sealed class MalwareBlocker : GenericHandler
|
||||
continue;
|
||||
}
|
||||
|
||||
var config = ContextProvider.Get<ContentBlockerConfig>();
|
||||
|
||||
bool removeFromClient = true;
|
||||
|
||||
if (result.IsPrivate && !config.DeletePrivate)
|
||||
@@ -212,7 +224,8 @@ public sealed class MalwareBlocker : GenericHandler
|
||||
record,
|
||||
group.Count() > 1,
|
||||
removeFromClient,
|
||||
result.DeleteReason
|
||||
result.DeleteReason,
|
||||
skipSearch: !hasContentId
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -106,18 +106,11 @@ public sealed class QueueCleaner : GenericHandler
|
||||
var groups = items
|
||||
.GroupBy(x => x.DownloadId)
|
||||
.ToList();
|
||||
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (group.Any(x => !arrClient.IsRecordValid(x)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QueueRecord record = group.First();
|
||||
|
||||
_logger.LogTrace("processing | {title} | {id}", record.Title, record.DownloadId);
|
||||
|
||||
|
||||
if (!arrClient.IsRecordValid(record))
|
||||
{
|
||||
continue;
|
||||
@@ -129,6 +122,21 @@ public sealed class QueueCleaner : GenericHandler
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogDebug("processing | {title} | {id}", record.Title, record.DownloadId);
|
||||
|
||||
bool hasContentId = arrClient.HasContentId(record);
|
||||
|
||||
if (!hasContentId)
|
||||
{
|
||||
if (!queueCleanerConfig.ProcessNoContentId)
|
||||
{
|
||||
_logger.LogInformation("skip | item is missing the content id | {title}", record.Title);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogDebug("item is missing the content id | {title}", record.Title);
|
||||
}
|
||||
|
||||
string downloadRemovalKey = CacheKeys.DownloadMarkedForRemoval(record.DownloadId, instance.Url);
|
||||
|
||||
if (_cache.TryGetValue(downloadRemovalKey, out bool _))
|
||||
@@ -190,7 +198,8 @@ public sealed class QueueCleaner : GenericHandler
|
||||
record,
|
||||
group.Count() > 1,
|
||||
removeFromClient,
|
||||
downloadCheckResult.DeleteReason
|
||||
downloadCheckResult.DeleteReason,
|
||||
skipSearch: !hasContentId
|
||||
);
|
||||
|
||||
continue;
|
||||
@@ -218,7 +227,8 @@ public sealed class QueueCleaner : GenericHandler
|
||||
record,
|
||||
group.Count() > 1,
|
||||
removeFromClient,
|
||||
DeleteReason.FailedImport
|
||||
DeleteReason.FailedImport,
|
||||
skipSearch: !hasContentId
|
||||
);
|
||||
|
||||
continue;
|
||||
|
||||
+4
-54
@@ -23,8 +23,6 @@ public sealed class BlocklistProvider : IBlocklistProvider
|
||||
private readonly Dictionary<string, DateTime> _lastLoadTimes = new();
|
||||
private const int DefaultLoadIntervalHours = 4;
|
||||
private const int FastLoadIntervalMinutes = 5;
|
||||
private const string MalwareListUrl = "https://cleanuparr.pages.dev/static/known_malware_file_name_patterns";
|
||||
private const string MalwareListKey = "MALWARE_PATTERNS";
|
||||
|
||||
public BlocklistProvider(
|
||||
ILogger<BlocklistProvider> logger,
|
||||
@@ -72,10 +70,7 @@ public sealed class BlocklistProvider : IBlocklistProvider
|
||||
changedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Always check and update malware patterns
|
||||
await LoadMalwarePatternsAsync(fileReader);
|
||||
|
||||
|
||||
if (changedCount > 0)
|
||||
{
|
||||
_logger.LogInformation("Successfully loaded {count} blocklists", changedCount);
|
||||
@@ -109,17 +104,10 @@ public sealed class BlocklistProvider : IBlocklistProvider
|
||||
public ConcurrentBag<Regex> GetRegexes(InstanceType instanceType)
|
||||
{
|
||||
_cache.TryGetValue(CacheKeys.BlocklistRegexes(instanceType), out ConcurrentBag<Regex>? regexes);
|
||||
|
||||
|
||||
return regexes ?? [];
|
||||
}
|
||||
|
||||
public ConcurrentBag<string> GetMalwarePatterns()
|
||||
{
|
||||
_cache.TryGetValue(CacheKeys.KnownMalwarePatterns(), out ConcurrentBag<string>? patterns);
|
||||
|
||||
return patterns ?? [];
|
||||
}
|
||||
|
||||
|
||||
private async Task<bool> EnsureInstanceLoadedAsync(BlocklistSettings settings, InstanceType instanceType, FileReader fileReader)
|
||||
{
|
||||
if (!settings.Enabled || string.IsNullOrEmpty(settings.BlocklistPath))
|
||||
@@ -165,47 +153,9 @@ public sealed class BlocklistProvider : IBlocklistProvider
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return DateTime.UtcNow - lastLoad >= interval;
|
||||
}
|
||||
|
||||
private async Task LoadMalwarePatternsAsync(FileReader fileReader)
|
||||
{
|
||||
var malwareInterval = TimeSpan.FromMinutes(FastLoadIntervalMinutes);
|
||||
|
||||
if (!ShouldReloadBlocklist(MalwareListKey, malwareInterval))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Loading malware patterns");
|
||||
|
||||
string[] filePatterns = await fileReader.ReadContentAsync(MalwareListUrl);
|
||||
|
||||
long startTime = Stopwatch.GetTimestamp();
|
||||
ParallelOptions options = new() { MaxDegreeOfParallelism = 5 };
|
||||
ConcurrentBag<string> patterns = [];
|
||||
|
||||
Parallel.ForEach(filePatterns, options, pattern =>
|
||||
{
|
||||
patterns.Add(pattern);
|
||||
});
|
||||
|
||||
TimeSpan elapsed = Stopwatch.GetElapsedTime(startTime);
|
||||
|
||||
_cache.Set(CacheKeys.KnownMalwarePatterns(), patterns);
|
||||
_lastLoadTimes[MalwareListKey] = DateTime.UtcNow;
|
||||
|
||||
_logger.LogDebug("loaded {count} known malware patterns", patterns.Count);
|
||||
_logger.LogDebug("malware patterns loaded in {elapsed} ms", elapsed.TotalMilliseconds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load malware patterns from {url}", MalwareListUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadPatternsAndRegexesAsync(BlocklistSettings blocklistSettings, InstanceType instanceType, FileReader fileReader)
|
||||
{
|
||||
|
||||
@@ -20,16 +20,6 @@ public class FilenameEvaluator : IFilenameEvaluator
|
||||
return IsValidAgainstPatterns(filename, type, patterns) && IsValidAgainstRegexes(filename, type, regexes);
|
||||
}
|
||||
|
||||
public bool IsKnownMalware(string filename, ConcurrentBag<string> malwarePatterns)
|
||||
{
|
||||
if (malwarePatterns.Count is 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return malwarePatterns.Any(pattern => filename.Contains(pattern, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool IsValidAgainstPatterns(string filename, BlocklistType type, ConcurrentBag<string> patterns)
|
||||
{
|
||||
if (patterns.Count is 0)
|
||||
|
||||
@@ -13,6 +13,4 @@ public interface IBlocklistProvider
|
||||
ConcurrentBag<string> GetPatterns(InstanceType instanceType);
|
||||
|
||||
ConcurrentBag<Regex> GetRegexes(InstanceType instanceType);
|
||||
|
||||
ConcurrentBag<string> GetMalwarePatterns();
|
||||
}
|
||||
@@ -7,6 +7,4 @@ namespace Cleanuparr.Infrastructure.Features.MalwareBlocker;
|
||||
public interface IFilenameEvaluator
|
||||
{
|
||||
bool IsValid(string filename, BlocklistType type, ConcurrentBag<string> patterns, ConcurrentBag<Regex> regexes);
|
||||
|
||||
bool IsKnownMalware(string filename, ConcurrentBag<string> malwarePatterns);
|
||||
}
|
||||
@@ -8,8 +8,6 @@ public static class CacheKeys
|
||||
public static string BlocklistPatterns(InstanceType instanceType) => $"{instanceType.ToString()}_patterns";
|
||||
public static string BlocklistRegexes(InstanceType instanceType) => $"{instanceType.ToString()}_regexes";
|
||||
|
||||
public static string KnownMalwarePatterns() => "KNOWN_MALWARE_PATTERNS";
|
||||
|
||||
public static string IgnoredDownloads(string name) => $"{name}_ignored";
|
||||
|
||||
public static string DownloadMarkedForRemoval(string hash, Uri url) => $"remove_{hash.ToLowerInvariant()}_{url}";
|
||||
|
||||
+1312
File diff suppressed because it is too large.
Load diff
+40
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddProcessMissingContentId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "process_no_content_id",
|
||||
table: "queue_cleaner_configs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "process_no_content_id",
|
||||
table: "content_blocker_configs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "process_no_content_id",
|
||||
table: "queue_cleaner_configs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "process_no_content_id",
|
||||
table: "content_blocker_configs");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1300
File diff suppressed because it is too large.
Load diff
+29
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveKnownMalwareOption : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "delete_known_malware",
|
||||
table: "content_blocker_configs");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "delete_known_malware",
|
||||
table: "content_blocker_configs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1300
File diff suppressed because it is too large.
Load diff
+40
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveProcessMissingId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "process_no_content_id",
|
||||
table: "queue_cleaner_configs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "process_no_content_id",
|
||||
table: "content_blocker_configs");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "process_no_content_id",
|
||||
table: "queue_cleaner_configs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "process_no_content_id",
|
||||
table: "content_blocker_configs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1308
File diff suppressed because it is too large.
Load diff
+40
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddProcessMissingId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "process_no_content_id",
|
||||
table: "queue_cleaner_configs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "process_no_content_id",
|
||||
table: "content_blocker_configs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "process_no_content_id",
|
||||
table: "queue_cleaner_configs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "process_no_content_id",
|
||||
table: "content_blocker_configs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -389,10 +389,6 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("DeleteKnownMalware")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_known_malware");
|
||||
|
||||
b.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_private");
|
||||
@@ -410,6 +406,10 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ignored_downloads");
|
||||
|
||||
b.Property<bool>("ProcessNoContentId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("process_no_content_id");
|
||||
|
||||
b.Property<bool>("UseAdvancedScheduling")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_advanced_scheduling");
|
||||
@@ -920,6 +920,10 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ignored_downloads");
|
||||
|
||||
b.Property<bool>("ProcessNoContentId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("process_no_content_id");
|
||||
|
||||
b.Property<bool>("UseAdvancedScheduling")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_advanced_scheduling");
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@ public sealed record ContentBlockerConfig : IJobConfig
|
||||
public bool IgnorePrivate { get; set; }
|
||||
|
||||
public bool DeletePrivate { get; set; }
|
||||
|
||||
public bool DeleteKnownMalware { get; set; }
|
||||
|
||||
public bool ProcessNoContentId { get; set; }
|
||||
|
||||
public BlocklistSettings Sonarr { get; set; } = new();
|
||||
|
||||
|
||||
+3
-1
@@ -22,7 +22,9 @@ public sealed record QueueCleanerConfig : IJobConfig
|
||||
public FailedImportConfig FailedImport { get; set; } = new();
|
||||
|
||||
public List<string> IgnoredDownloads { get; set; } = [];
|
||||
|
||||
|
||||
public bool ProcessNoContentId { get; set; }
|
||||
|
||||
public ushort DownloadingMetadataMaxStrikes { get; set; }
|
||||
|
||||
public List<StallRule> StallRules { get; set; } = [];
|
||||
|
||||
Generated
+243
-226
@@ -279,13 +279,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/architect": {
|
||||
"version": "0.2102.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.0.tgz",
|
||||
"integrity": "sha512-kYFwTNzToG2SJMxj2f41w3QRtdqlrFuF+bpZrtIaHOP078Ktld8EPIp9KqB0Y46Vvs69ifby5Q1/wPD9wA3iaw==",
|
||||
"version": "0.2102.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.1.tgz",
|
||||
"integrity": "sha512-x2Qqz6oLYvEh9UBUG0AP1A4zROO/VP+k+zM9+4c2uZw1uqoBQFmutqgzncjVU7cR9R0RApgx9JRZHDFtQru68w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "21.2.0",
|
||||
"@angular-devkit/core": "21.2.1",
|
||||
"rxjs": "7.8.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -298,9 +298,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/core": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.0.tgz",
|
||||
"integrity": "sha512-HZdTn46Ca6qbb9Zef8R/+TWsk6mNKRm4rJyL3PxHP6HnVCwSPNZ0LNN9BjVREBs+UlRdXqBGFBZh5D1nBgu5GQ==",
|
||||
"version": "21.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.1.tgz",
|
||||
"integrity": "sha512-TpXGjERqVPN8EPt7LdmWAwh0oNQ/6uWFutzGZiXhJy81n1zb1O1XrqhRAmvP1cAo5O+na6IV2JkkCmxL6F8GUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "8.18.0",
|
||||
@@ -325,13 +325,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/schematics": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.0.tgz",
|
||||
"integrity": "sha512-3kn3FI5v7BQ7Zct6raek+WgvyDwOJ8wElbyC903GxMQCDBRGGcevhHvTAIHhknihEsrgplzPhTlWeMbk1JfdFg==",
|
||||
"version": "21.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.1.tgz",
|
||||
"integrity": "sha512-CWoamHaasAHMjHcYqxbj0tMnoXxdGotcAz2SpiuWtH28Lnf5xfbTaJn/lwdMP8Wdh4tgA+uYh2l45A5auCwmkw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "21.2.0",
|
||||
"@angular-devkit/core": "21.2.1",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"magic-string": "0.30.21",
|
||||
"ora": "9.3.0",
|
||||
@@ -344,9 +344,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-eslint/builder": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.2.0.tgz",
|
||||
"integrity": "sha512-wcp3J9cbrDwSeI/o1D/DSvMQa8zpKjc5WhRGTx33omhWijCfiVNEAiBLWiEx5Sb/dWcoX8yFNWY5jSgFVy9Sjw==",
|
||||
"version": "21.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.3.0.tgz",
|
||||
"integrity": "sha512-26QUUouei52biUFAlJSrWNAU9tuF2miKwd8uHdxWwCF31xz+OxC5+NfudWvt1AFaYow7gWueX1QX3rNNtSPDrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -355,89 +355,89 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/cli": ">= 21.0.0 < 22.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-eslint/bundled-angular-compiler": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.2.0.tgz",
|
||||
"integrity": "sha512-J0DWL+j6t9ItFIyIADvzHGqwDA1qfVJ9bx+oTmJ/Hlo7cUpIRoXpcTXpug0CEEABFH0RfDu6PDG2b0FoZ1+7bg==",
|
||||
"version": "21.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.3.0.tgz",
|
||||
"integrity": "sha512-l521I24J9gJxyMbRkrM24Tc7W8J8BP+TDAmVs2nT8+lXbS3kg8QpWBRtd+hNUgq6o+vt+lKBkytnEfu8OiqeRg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@angular-eslint/eslint-plugin": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.2.0.tgz",
|
||||
"integrity": "sha512-X2Qn2viDsjm91CEMxNrxDH3qkKpp6un0C1F1BW2p/m9J4AUVfOcXwWz9UpHFSHTRQ+YlTJbiH1ZwwAPeKhFaxA==",
|
||||
"version": "21.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.3.0.tgz",
|
||||
"integrity": "sha512-Whf/AUUBekOlfSJRS78m76YGrBQAZ3waXE7oOdlW5xEQvn8jBDN9EGuNnjg/syZzvzjK4ZpYC4g1XYXrc+fQIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular-eslint/bundled-angular-compiler": "21.2.0",
|
||||
"@angular-eslint/utils": "21.2.0",
|
||||
"@angular-eslint/bundled-angular-compiler": "21.3.0",
|
||||
"@angular-eslint/utils": "21.3.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/utils": "^7.11.0 || ^8.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-eslint/eslint-plugin-template": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.2.0.tgz",
|
||||
"integrity": "sha512-lJ13Dj0DjR6YiceQR0sRbyWzSzOQ6uZPwK9CJUF3wuZjYAUvL1D61zaU9QrVLtf89NVOxv+dYZHDdu3IDeIqbA==",
|
||||
"version": "21.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.3.0.tgz",
|
||||
"integrity": "sha512-lVixd/KypPWgA/5/pUOhJV9MTcaHjYZEqyOi+IiLk+h+maGxn6/s6Ot+20n+XGS85zAgOY+qUw6EEQ11hoojIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular-eslint/bundled-angular-compiler": "21.2.0",
|
||||
"@angular-eslint/utils": "21.2.0",
|
||||
"@angular-eslint/bundled-angular-compiler": "21.3.0",
|
||||
"@angular-eslint/utils": "21.3.0",
|
||||
"aria-query": "5.3.2",
|
||||
"axobject-query": "4.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular-eslint/template-parser": "21.2.0",
|
||||
"@angular-eslint/template-parser": "21.3.0",
|
||||
"@typescript-eslint/types": "^7.11.0 || ^8.0.0",
|
||||
"@typescript-eslint/utils": "^7.11.0 || ^8.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-eslint/template-parser": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.2.0.tgz",
|
||||
"integrity": "sha512-TCb3qYOC/uXKZCo56cJ6N9sHeWdFhyVqrbbYfFjTi09081T6jllgHDZL5Ms7gOMNY8KywWGGbhxwvzeA0RwTgA==",
|
||||
"version": "21.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.3.0.tgz",
|
||||
"integrity": "sha512-ysyou1zAY6M6rSZNdIcYKGd4nk6TCapamyFNB3ivmTlVZ0O35TS9o/rJ0aUttuHgDp+Ysgs3ql+LA746PXgCyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@angular-eslint/bundled-angular-compiler": "21.2.0",
|
||||
"eslint-scope": "^9.0.0"
|
||||
"@angular-eslint/bundled-angular-compiler": "21.3.0",
|
||||
"eslint-scope": "^9.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-eslint/utils": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.2.0.tgz",
|
||||
"integrity": "sha512-E19/hkuvHoNFvctBkmEiGWpy2bbC6cgbr3GNVrn2nGtbI4jnwnDFCGHv50I4LBfvj0PA9E6TWe73ejJ5qoMJWQ==",
|
||||
"version": "21.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.3.0.tgz",
|
||||
"integrity": "sha512-oNigH6w3l+owTMboj/uFG0tHOy43uH8BpQRtBOQL1/s2+5in/BJ2Fjobv3SyizxTgeJ1FhRefbkT8GmVjK7jAA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular-eslint/bundled-angular-compiler": "21.2.0"
|
||||
"@angular-eslint/bundled-angular-compiler": "21.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/utils": "^7.11.0 || ^8.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/animations": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.0.tgz",
|
||||
"integrity": "sha512-SzlLoMT/r5wKqPicx5okCAiN5UD5+VE7x/F1G6gSJCcnBfbK5PqHPUmDnMW4jw9Ode06KZDT7ntstn6fG+Ld8w==",
|
||||
"version": "21.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.2.tgz",
|
||||
"integrity": "sha512-h+BKgBDI04Euu+SfEuvOOT/oFO31yg/DgFLvOBFTxab9dWoaeR3j4JsjMUTRJ+HmvAeo1jUm1F5vUf2Bn63Q+A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -447,18 +447,18 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "21.2.0"
|
||||
"@angular/core": "21.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/build": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.0.tgz",
|
||||
"integrity": "sha512-K0EqiHz2y7TSyD4adWD0+C/P9khKlrsSWavXWxGRvoSJC/H3I3SK5Z6BWwftBibXR1Fis7njwvl5IGAlQrDchA==",
|
||||
"version": "21.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.1.tgz",
|
||||
"integrity": "sha512-cUpLNHJp9taII/FOcJHHfQYlMcZSRaf6eIxgSNS6Xfx1CeGoJNDN+J8+GFk+H1CPJt1EvbfyZ+dE5DbsgTD/QQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "2.3.0",
|
||||
"@angular-devkit/architect": "0.2102.0",
|
||||
"@angular-devkit/architect": "0.2102.1",
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/helper-annotate-as-pure": "7.27.3",
|
||||
"@babel/helper-split-export-declaration": "7.24.7",
|
||||
@@ -501,7 +501,7 @@
|
||||
"@angular/platform-browser": "^21.0.0",
|
||||
"@angular/platform-server": "^21.0.0",
|
||||
"@angular/service-worker": "^21.0.0",
|
||||
"@angular/ssr": "^21.2.0",
|
||||
"@angular/ssr": "^21.2.1",
|
||||
"karma": "^6.4.0",
|
||||
"less": "^4.2.0",
|
||||
"ng-packagr": "^21.0.0",
|
||||
@@ -551,9 +551,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/cdk": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.2.0.tgz",
|
||||
"integrity": "sha512-1P0TNL1F51NC7JAaXabaAHY7Y1zBloLSZXfml1POa4a116V+y/QZfPGsxM0LwD1qSSXhSb2LNl7duTtJAP39bA==",
|
||||
"version": "21.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.2.1.tgz",
|
||||
"integrity": "sha512-JUFV8qLnO7CU5v4W0HzXSQrFkkJ4RH/qqdwrf9lup7YEnsLxB7cTGhsVisc9pWKAJsoNZ4pXCVOkqKc1mFL7dw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parse5": "^8.0.0",
|
||||
@@ -567,20 +567,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/cli": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.0.tgz",
|
||||
"integrity": "sha512-yaGEpckqgOemcHkoWeH92i9eNrcbr9iE/dnxL+Du6s9spTAXJ2jjtYfszhmowuQZkCK5rjecMb8ctNtHlaGCjg==",
|
||||
"version": "21.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.1.tgz",
|
||||
"integrity": "sha512-5SRfMTgwFj1zXOpfeZWHsxZBni0J4Xz7/CbewG47D6DmbstOrSdgt6eNzJ62R650t0G9dpri2YvToZgImtbjOQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/architect": "0.2102.0",
|
||||
"@angular-devkit/core": "21.2.0",
|
||||
"@angular-devkit/schematics": "21.2.0",
|
||||
"@angular-devkit/architect": "0.2102.1",
|
||||
"@angular-devkit/core": "21.2.1",
|
||||
"@angular-devkit/schematics": "21.2.1",
|
||||
"@inquirer/prompts": "7.10.1",
|
||||
"@listr2/prompt-adapter-inquirer": "3.0.5",
|
||||
"@modelcontextprotocol/sdk": "1.26.0",
|
||||
"@schematics/angular": "21.2.0",
|
||||
"@schematics/angular": "21.2.1",
|
||||
"@yarnpkg/lockfile": "1.1.0",
|
||||
"algoliasearch": "5.48.1",
|
||||
"ini": "6.0.0",
|
||||
@@ -603,9 +603,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/common": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.0.tgz",
|
||||
"integrity": "sha512-6zJMPi0i/XDniEgv3/t2BjuDHiOG44lgIR5PYyxqGpgJ0kqB5hku/0TuentNEi1VnBYgthnfhjek7c+lakXmhw==",
|
||||
"version": "21.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.2.tgz",
|
||||
"integrity": "sha512-xpVYV+MgqWzdjTCFxe3uJGpFOc84YrO4H4oX9HkzI5yQ5OLkQlndtq+OAUK8e330iacg4XHArft3SNDj1LaFfg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -615,14 +615,14 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "21.2.0",
|
||||
"@angular/core": "21.2.2",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/compiler": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.0.tgz",
|
||||
"integrity": "sha512-0RPkma8UVNpse/VJcXT9w6SKzTMz4J/uMGj0l9enM1frg9xrx1fwi/lLmaVV9Nr9LfqPjQdxNFFlvaBB7g/2zg==",
|
||||
"version": "21.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.2.tgz",
|
||||
"integrity": "sha512-k7P0EH8I/Iwf2uRalSqhfokFbItTwdH7CmBJ7RKTRIH4FcrQcnqHetNKUMCOYXZtnlHIAnTpG+C+T4+6GTpYFg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -633,9 +633,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/compiler-cli": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.0.tgz",
|
||||
"integrity": "sha512-gZd58p0/JjgdxMX3v+LjCB6e3dBIfNVr/YzXoh55TfffdBCUQY94hl1+DFQkJ72K5EX+1zbaz03dIm30kw1bGw==",
|
||||
"version": "21.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.2.tgz",
|
||||
"integrity": "sha512-TFg2wXUZ1FdUikNyR27PxuCXuqqlJhL6Mr/cBYuc4HbtBfgKw5FLffbI/iLubBEs55W5ApuYpBVuXKGoZp9SRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -657,7 +657,7 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/compiler": "21.2.0",
|
||||
"@angular/compiler": "21.2.2",
|
||||
"typescript": ">=5.9 <6.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -667,9 +667,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/core": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.0.tgz",
|
||||
"integrity": "sha512-VnTbmZq3g3Q+s3nCZ8VUDMLjMezOg/bqUxAJ/DrRWCrEcTP5JO3mrNPs3FHj+qlB0T+BQP7uQv6QTzPVKybwoA==",
|
||||
"version": "21.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.2.tgz",
|
||||
"integrity": "sha512-ljiyiFjR6dgK27CNlOcMrjsDPYKFf2Rl89WLwGEGMOj0cJg/PSLQqpW/fbSkSB3SDgwG/WhXQ4Wrw525OKMupg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -679,7 +679,7 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/compiler": "21.2.0",
|
||||
"@angular/compiler": "21.2.2",
|
||||
"rxjs": "^6.5.3 || ^7.4.0",
|
||||
"zone.js": "~0.15.0 || ~0.16.0"
|
||||
},
|
||||
@@ -693,9 +693,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/forms": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.0.tgz",
|
||||
"integrity": "sha512-NduUtPWLauH/FLayEDkLyaKAGqKzXbcfO7468LOWCXN3crhNVQyIWRQPOUcdpoJwDAGLpN85m3DhJhXNnA9c5w==",
|
||||
"version": "21.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.2.tgz",
|
||||
"integrity": "sha512-uiuL8uy4OpcbB0zRZ8TcvYXxe+GZC+XkCAptfrG+yAyzm4cOrqCilOZsMG6bVdOb2sjc2islk9CVDZZOju28/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
@@ -705,16 +705,16 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "21.2.0",
|
||||
"@angular/core": "21.2.0",
|
||||
"@angular/platform-browser": "21.2.0",
|
||||
"@angular/common": "21.2.2",
|
||||
"@angular/core": "21.2.2",
|
||||
"@angular/platform-browser": "21.2.2",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/platform-browser": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.0.tgz",
|
||||
"integrity": "sha512-IUGukpvvT2B5Dl76qzk6rY7UIHUT9u4BhT2AwVz+5JqcX9KwQtYD17Gt7wj6bvIgCXKWG+CfN8Zd9DECOCYWjg==",
|
||||
"version": "21.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.2.tgz",
|
||||
"integrity": "sha512-6cHfHi/lRCUPNGO0eJeYRIpu8vM+CMMS2Wv/psOUwvl/5+RC92hfBEZxzQiF/5X9A170bJabaMFQC5fA7pkF2g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -724,9 +724,9 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/animations": "21.2.0",
|
||||
"@angular/common": "21.2.0",
|
||||
"@angular/core": "21.2.0"
|
||||
"@angular/animations": "21.2.2",
|
||||
"@angular/common": "21.2.2",
|
||||
"@angular/core": "21.2.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@angular/animations": {
|
||||
@@ -735,9 +735,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/router": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.0.tgz",
|
||||
"integrity": "sha512-siliJ+jJRUCRZ0cdkqc7zww9Didz56Z0Z2YPIuR2n5TZLiuJY+jAf6xotXKp/v6v8XoGJwLiRNipGgNDRIAlWA==",
|
||||
"version": "21.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.2.tgz",
|
||||
"integrity": "sha512-mpVPI6AiIlZ4z6sSM5WOzCMwTka9yQ9TEVj9+ZyfAs5U9RnKDa8DZIoQ6BFbh3h/v4DArvT5q1Ji8aIlabmWcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
@@ -746,9 +746,9 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "21.2.0",
|
||||
"@angular/core": "21.2.0",
|
||||
"@angular/platform-browser": "21.2.0",
|
||||
"@angular/common": "21.2.2",
|
||||
"@angular/core": "21.2.2",
|
||||
"@angular/platform-browser": "21.2.2",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
@@ -1548,15 +1548,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
|
||||
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
|
||||
"version": "0.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
|
||||
"integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^2.1.7",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.1.2"
|
||||
"minimatch": "^3.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1620,9 +1620,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz",
|
||||
"integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==",
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
|
||||
"integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1633,7 +1633,7 @@
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"js-yaml": "^4.1.1",
|
||||
"minimatch": "^3.1.3",
|
||||
"minimatch": "^3.1.5",
|
||||
"strip-json-comments": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1709,9 +1709,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.39.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz",
|
||||
"integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==",
|
||||
"version": "9.39.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
|
||||
"integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1767,9 +1767,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
|
||||
"integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
|
||||
"version": "1.19.11",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
|
||||
"integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4013,14 +4013,14 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@schematics/angular": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.0.tgz",
|
||||
"integrity": "sha512-GQUIeGzZwCT9/W5MAkKnkwETROPbA1eRmy3JF56jLmvr95tJnypGOG8jGYy0d+tcEVujIouh48r4J3bJQg5mrw==",
|
||||
"version": "21.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.1.tgz",
|
||||
"integrity": "sha512-DjrHRMoILhbZ6tc7aNZWuHA1wCm1iU/JN1TxAwNEyIBgyU3Fx8Z5baK4w0TCpOIPt0RLWVgP2L7kka9aXWCUFA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "21.2.0",
|
||||
"@angular-devkit/schematics": "21.2.0",
|
||||
"@angular-devkit/core": "21.2.1",
|
||||
"@angular-devkit/schematics": "21.2.1",
|
||||
"jsonc-parser": "3.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4427,17 +4427,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
|
||||
"integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz",
|
||||
"integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/type-utils": "8.56.1",
|
||||
"@typescript-eslint/utils": "8.56.1",
|
||||
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||
"@typescript-eslint/scope-manager": "8.57.0",
|
||||
"@typescript-eslint/type-utils": "8.57.0",
|
||||
"@typescript-eslint/utils": "8.57.0",
|
||||
"@typescript-eslint/visitor-keys": "8.57.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
@@ -4450,23 +4450,23 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.57.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz",
|
||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz",
|
||||
"integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/typescript-estree": "8.56.1",
|
||||
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||
"@typescript-eslint/scope-manager": "8.57.0",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/typescript-estree": "8.57.0",
|
||||
"@typescript-eslint/visitor-keys": "8.57.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4482,14 +4482,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz",
|
||||
"integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz",
|
||||
"integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.56.1",
|
||||
"@typescript-eslint/types": "^8.56.1",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.57.0",
|
||||
"@typescript-eslint/types": "^8.57.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4504,14 +4504,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz",
|
||||
"integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz",
|
||||
"integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/visitor-keys": "8.56.1"
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/visitor-keys": "8.57.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -4522,9 +4522,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz",
|
||||
"integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz",
|
||||
"integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4539,15 +4539,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz",
|
||||
"integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz",
|
||||
"integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/typescript-estree": "8.56.1",
|
||||
"@typescript-eslint/utils": "8.56.1",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/typescript-estree": "8.57.0",
|
||||
"@typescript-eslint/utils": "8.57.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
},
|
||||
@@ -4564,9 +4564,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz",
|
||||
"integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz",
|
||||
"integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -4579,16 +4579,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz",
|
||||
"integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz",
|
||||
"integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.56.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||
"@typescript-eslint/project-service": "8.57.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.57.0",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/visitor-keys": "8.57.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -4607,17 +4607,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz",
|
||||
"integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz",
|
||||
"integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/typescript-estree": "8.56.1"
|
||||
"@typescript-eslint/scope-manager": "8.57.0",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/typescript-estree": "8.57.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -4632,13 +4632,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz",
|
||||
"integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz",
|
||||
"integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -5120,9 +5120,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001774",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz",
|
||||
"integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==",
|
||||
"version": "1.0.30001777",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz",
|
||||
"integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -5219,14 +5219,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cli-truncate": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz",
|
||||
"integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz",
|
||||
"integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"slice-ansi": "^7.1.0",
|
||||
"string-width": "^8.0.0"
|
||||
"slice-ansi": "^8.0.0",
|
||||
"string-width": "^8.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -5596,9 +5596,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.302",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
|
||||
"integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
|
||||
"version": "1.5.307",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz",
|
||||
"integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -5620,9 +5620,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.19.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
|
||||
"integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==",
|
||||
"version": "5.20.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
|
||||
"integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
@@ -5781,26 +5781,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.39.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz",
|
||||
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
||||
"version": "9.39.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
|
||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.21.1",
|
||||
"@eslint/config-array": "^0.21.2",
|
||||
"@eslint/config-helpers": "^0.4.2",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.39.3",
|
||||
"@eslint/eslintrc": "^3.3.5",
|
||||
"@eslint/js": "9.39.4",
|
||||
"@eslint/plugin-kit": "^0.4.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
"@types/estree": "^1.0.6",
|
||||
"ajv": "^6.12.4",
|
||||
"ajv": "^6.14.0",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"debug": "^4.3.2",
|
||||
@@ -5819,7 +5819,7 @@
|
||||
"is-glob": "^4.0.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"minimatch": "^3.1.2",
|
||||
"minimatch": "^3.1.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"optionator": "^0.9.3"
|
||||
},
|
||||
@@ -5858,9 +5858,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "9.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz",
|
||||
"integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==",
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
|
||||
"integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
@@ -6159,13 +6159,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
|
||||
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz",
|
||||
"integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.0.1"
|
||||
"ip-address": "10.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -6308,9 +6308,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
|
||||
"integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -6549,9 +6549,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz",
|
||||
"integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==",
|
||||
"version": "4.12.7",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz",
|
||||
"integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -6712,9 +6712,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
|
||||
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
|
||||
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -6763,9 +6763,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -6896,9 +6896,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
|
||||
"integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.1.tgz",
|
||||
"integrity": "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -7445,6 +7445,23 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/log-update/node_modules/slice-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.2.1",
|
||||
"is-fullwidth-code-point": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/log-update/node_modules/string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
@@ -7936,9 +7953,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
||||
"version": "2.0.36",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
|
||||
"integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -8476,9 +8493,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"version": "8.5.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -9317,17 +9334,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/slice-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz",
|
||||
"integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.2.1",
|
||||
"is-fullwidth-code-point": "^5.0.0"
|
||||
"ansi-styles": "^6.2.3",
|
||||
"is-fullwidth-code-point": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
|
||||
@@ -9563,9 +9580,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.9",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz",
|
||||
"integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==",
|
||||
"version": "7.5.11",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
|
||||
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -15,6 +15,7 @@ export class DocumentationService {
|
||||
'ignoredDownloads': 'ignored-downloads',
|
||||
'useAdvancedScheduling': 'scheduling-mode',
|
||||
'cronExpression': 'cron-expression',
|
||||
'processNoContentId': 'process-downloads-with-no-content-id',
|
||||
'failedImport.maxStrikes': 'failed-import-max-strikes',
|
||||
'failedImport.ignorePrivate': 'failed-import-ignore-private',
|
||||
'failedImport.deletePrivate': 'failed-import-delete-private',
|
||||
@@ -87,7 +88,7 @@ export class DocumentationService {
|
||||
'cronExpression': 'cron-expression',
|
||||
'ignorePrivate': 'ignore-private',
|
||||
'deletePrivate': 'delete-private',
|
||||
'deleteKnownMalware': 'delete-known-malware',
|
||||
'processNoContentId': 'process-downloads-with-no-content-id',
|
||||
'sonarr.enabled': 'enable-blocklist',
|
||||
'sonarr.blocklistPath': 'blocklist-path',
|
||||
'sonarr.blocklistType': 'blocklist-type',
|
||||
|
||||
+3
-3
@@ -23,9 +23,6 @@
|
||||
hint="When enabled, the Malware blocker will run according to the schedule"
|
||||
helpKey="malware-blocker:enabled" />
|
||||
@if (enabled()) {
|
||||
<app-toggle label="Delete Known Malware" [(checked)]="deleteKnownMalware"
|
||||
hint="When enabled, downloads matching known malware patterns will be deleted"
|
||||
helpKey="malware-blocker:deleteKnownMalware" />
|
||||
<app-toggle label="Ignore Private Torrents" [(checked)]="ignorePrivate"
|
||||
hint="When enabled, private torrents will not be processed"
|
||||
helpKey="malware-blocker:ignorePrivate" />
|
||||
@@ -33,6 +30,9 @@
|
||||
[disabled]="deletePrivateDisabled()"
|
||||
hint="Disable this if you want to keep private torrents in the download client even if they are removed from the arrs"
|
||||
helpKey="malware-blocker:deletePrivate" />
|
||||
<app-toggle label="Process downloads with no content ID" [(checked)]="processNoContentId"
|
||||
hint="Process downloads from the queue that are not linked to any content in the arr app. Cleanuparr will not be able to trigger a search for a replacement when this happens."
|
||||
helpKey="malware-blocker:processNoContentId" />
|
||||
|
||||
<div class="form-divider"></div>
|
||||
|
||||
|
||||
+4
-4
@@ -62,7 +62,7 @@ export class MalwareBlockerComponent implements OnInit, HasPendingChanges {
|
||||
readonly ignoredDownloads = signal<string[]>([]);
|
||||
readonly ignorePrivate = signal(false);
|
||||
readonly deletePrivate = signal(false);
|
||||
readonly deleteKnownMalware = signal(false);
|
||||
readonly processNoContentId = signal(false);
|
||||
readonly arrExpanded = signal(false);
|
||||
|
||||
readonly scheduleIntervalOptions = computed(() => {
|
||||
@@ -164,7 +164,7 @@ export class MalwareBlockerComponent implements OnInit, HasPendingChanges {
|
||||
this.ignoredDownloads.set(config.ignoredDownloads ?? []);
|
||||
this.ignorePrivate.set(config.ignorePrivate);
|
||||
this.deletePrivate.set(config.deletePrivate);
|
||||
this.deleteKnownMalware.set(config.deleteKnownMalware);
|
||||
this.processNoContentId.set(config.processNoContentId);
|
||||
|
||||
const blocklists: Record<string, any> = {};
|
||||
for (const name of ARR_NAMES) {
|
||||
@@ -220,7 +220,7 @@ export class MalwareBlockerComponent implements OnInit, HasPendingChanges {
|
||||
ignoredDownloads: this.ignoredDownloads(),
|
||||
ignorePrivate: this.ignorePrivate(),
|
||||
deletePrivate: this.deletePrivate(),
|
||||
deleteKnownMalware: this.deleteKnownMalware(),
|
||||
processNoContentId: this.processNoContentId(),
|
||||
sonarr: { enabled: blocklists['sonarr'].enabled, blocklistPath: blocklists['sonarr'].blocklistPath, blocklistType: blocklists['sonarr'].blocklistType as BlocklistType },
|
||||
radarr: { enabled: blocklists['radarr'].enabled, blocklistPath: blocklists['radarr'].blocklistPath, blocklistType: blocklists['radarr'].blocklistType as BlocklistType },
|
||||
lidarr: { enabled: blocklists['lidarr'].enabled, blocklistPath: blocklists['lidarr'].blocklistPath, blocklistType: blocklists['lidarr'].blocklistType as BlocklistType },
|
||||
@@ -256,7 +256,7 @@ export class MalwareBlockerComponent implements OnInit, HasPendingChanges {
|
||||
ignoredDownloads: this.ignoredDownloads(),
|
||||
ignorePrivate: this.ignorePrivate(),
|
||||
deletePrivate: this.deletePrivate(),
|
||||
deleteKnownMalware: this.deleteKnownMalware(),
|
||||
processNoContentId: this.processNoContentId(),
|
||||
arrBlocklists: this.arrBlocklists(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
hint="When enabled, the queue cleaner will run according to the schedule"
|
||||
helpKey="queue-cleaner:enabled" />
|
||||
@if (enabled()) {
|
||||
<app-toggle label="Process downloads with no content ID" [(checked)]="processNoContentId"
|
||||
hint="Process downloads from the queue that are not linked to any content in the arr app. Cleanuparr will not be able to trigger a search for a replacement when this happens."
|
||||
helpKey="queue-cleaner:processNoContentId" />
|
||||
<div class="form-divider"></div>
|
||||
<app-toggle label="Advanced Scheduling" [(checked)]="useAdvancedScheduling"
|
||||
hint="Choose between basic scheduling or advanced cron expression"
|
||||
|
||||
@@ -83,6 +83,7 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
readonly scheduleEvery = signal<unknown>(5);
|
||||
readonly scheduleUnit = signal<unknown>(ScheduleUnit.Minutes);
|
||||
readonly ignoredDownloads = signal<string[]>([]);
|
||||
readonly processNoContentId = signal(false);
|
||||
|
||||
readonly scheduleIntervalOptions = computed(() => {
|
||||
const unit = this.scheduleUnit() as ScheduleUnit;
|
||||
@@ -290,6 +291,7 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
this.scheduleUnit.set(parsed.type);
|
||||
}
|
||||
this.ignoredDownloads.set(config.ignoredDownloads ?? []);
|
||||
this.processNoContentId.set(config.processNoContentId);
|
||||
this.failedMaxStrikes.set(config.failedImport.maxStrikes);
|
||||
this.failedIgnorePrivate.set(config.failedImport.ignorePrivate);
|
||||
this.failedDeletePrivate.set(config.failedImport.deletePrivate);
|
||||
@@ -511,6 +513,7 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
useAdvancedScheduling: this.useAdvancedScheduling(),
|
||||
cronExpression,
|
||||
ignoredDownloads: this.ignoredDownloads(),
|
||||
processNoContentId: this.processNoContentId(),
|
||||
failedImport: {
|
||||
maxStrikes: this.failedMaxStrikes() ?? 3,
|
||||
ignorePrivate: this.failedIgnorePrivate(),
|
||||
@@ -546,6 +549,7 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
scheduleEvery: this.scheduleEvery(),
|
||||
scheduleUnit: this.scheduleUnit(),
|
||||
ignoredDownloads: this.ignoredDownloads(),
|
||||
processNoContentId: this.processNoContentId(),
|
||||
failedMaxStrikes: this.failedMaxStrikes(),
|
||||
failedIgnorePrivate: this.failedIgnorePrivate(),
|
||||
failedDeletePrivate: this.failedDeletePrivate(),
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface MalwareBlockerConfig {
|
||||
ignoredDownloads: string[];
|
||||
ignorePrivate: boolean;
|
||||
deletePrivate: boolean;
|
||||
deleteKnownMalware: boolean;
|
||||
processNoContentId: boolean;
|
||||
sonarr: BlocklistSettings;
|
||||
radarr: BlocklistSettings;
|
||||
lidarr: BlocklistSettings;
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface QueueCleanerConfig {
|
||||
useAdvancedScheduling: boolean;
|
||||
jobSchedule?: JobSchedule;
|
||||
ignoredDownloads: string[];
|
||||
processNoContentId: boolean;
|
||||
failedImport: FailedImportConfig;
|
||||
downloadingMetadataMaxStrikes: number;
|
||||
stallRules?: StallRule[];
|
||||
|
||||
@@ -30,10 +30,10 @@
|
||||
<button class="chip-add" type="button" (click)="commitInput()" aria-label="Add item">+</button>
|
||||
}
|
||||
</div>
|
||||
@if (error()) {
|
||||
<span class="chip-error">{{ error() }}</span>
|
||||
} @else if (uncommittedError()) {
|
||||
@if (uncommittedError()) {
|
||||
<span class="chip-error">{{ uncommittedError() }}</span>
|
||||
} @else if (error()) {
|
||||
<span class="chip-error">{{ error() }}</span>
|
||||
} @else if (hint()) {
|
||||
<span class="chip-hint">{{ hint() }}</span>
|
||||
}
|
||||
@@ -69,7 +69,6 @@ Advanced download management and automation features for your *arr applications
|
||||
>
|
||||
|
||||
- Remove and block downloads blocked by qBittorrent or by Cleanuparr's **Malware Blocker**.
|
||||
- Remove and block known malware based on patterns found by the community.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ sidebar_position: 2
|
||||
|
||||
import {
|
||||
ConfigSection,
|
||||
Note,
|
||||
Important,
|
||||
Warning,
|
||||
ElementNavigator,
|
||||
@@ -106,20 +107,17 @@ Setting this to true means private torrents will be permanently deleted, potenti
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
title="Delete Known Malware"
|
||||
icon="🦠"
|
||||
title="Process downloads with no content ID"
|
||||
icon="🔍"
|
||||
>
|
||||
|
||||
When enabled, downloads that match known malware patterns will be automatically deleted from the download client.
|
||||
When enabled, downloads that are not linked to any content in the *arr app will be processed normally. If such a download is removed, Cleanuparr will **not** be able to trigger a search for a replacement, since the content ID is unknown.
|
||||
|
||||
**Malware Detection Source:**
|
||||
- List is automatically fetched from: `https://cleanuparr.pages.dev/static/known_malware_file_name_patterns`
|
||||
- Updates automatically every **5 minutes**
|
||||
- Contains filename patterns known to be associated with malware
|
||||
<Note>
|
||||
This setting will enable the processing of items that appear in the logs with the following message:
|
||||
|
||||
<Warning>
|
||||
This feature permanently deletes downloads that match malware patterns. While the patterns are carefully curated, false positives are possible. Monitor logs carefully when first enabling this feature.
|
||||
</Warning>
|
||||
`skip | item is missing the content id`
|
||||
</Note>
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ sidebar_position: 3
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import {
|
||||
ConfigSection,
|
||||
Note,
|
||||
Important,
|
||||
Warning,
|
||||
ElementNavigator,
|
||||
@@ -82,6 +83,21 @@ mytracker.com
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
title="Process downloads with no content ID"
|
||||
icon="🔍"
|
||||
>
|
||||
|
||||
When enabled, downloads that are not linked to any content in the *arr app will be processed normally. If such a download is removed, Cleanuparr will **not** be able to trigger a search for a replacement, since the content ID is unknown.
|
||||
|
||||
<Note>
|
||||
This setting will enable the processing of items that appear in the logs with the following message:
|
||||
|
||||
`skip | item is missing the content id`
|
||||
</Note>
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
</div>
|
||||
|
||||
<PrefixedSection prefix="failed-import">
|
||||
|
||||
Reference in new issue
Block a user