mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-01-01 18:37:48 -05:00
Compare commits
8 Commits
update_pac
...
add_whispa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e465582a01 | ||
|
|
01238759b6 | ||
|
|
acb98db17f | ||
|
|
8c16a8b9dd | ||
|
|
b71b268b08 | ||
|
|
a708d22b27 | ||
|
|
a9a3b08ad6 | ||
|
|
1d1e8679e4 |
2
.github/workflows/build-executable.yml
vendored
2
.github/workflows/build-executable.yml
vendored
@@ -76,7 +76,7 @@ jobs:
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
|
||||
2
.github/workflows/build-macos-installer.yml
vendored
2
.github/workflows/build-macos-installer.yml
vendored
@@ -86,7 +86,7 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Restore .NET dependencies
|
||||
run: |
|
||||
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Restore .NET dependencies
|
||||
run: |
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
|
||||
@@ -19,7 +19,7 @@ This helps us avoid redundant work, git conflicts, and contributions that may no
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
|
||||
- [.NET 9.0 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)
|
||||
- [Node.js 18+](https://nodejs.org/)
|
||||
- [Git](https://git-scm.com/)
|
||||
- (Optional) [Make](https://www.gnu.org/software/make/) for database migrations
|
||||
|
||||
11
README.md
11
README.md
@@ -28,20 +28,23 @@ Cleanuparr was created primarily to address malicious files, such as `*.lnk` or
|
||||
> - Notify on strike or download removal.
|
||||
> - Ignore certain torrent hashes, categories, tags or trackers from being processed by Cleanuparr.
|
||||
|
||||
## Sponsored by GitAds
|
||||
[](https://gitads.dev/v1/ad-track?source=cleanuparr/cleanuparr@github)
|
||||
|
||||
## Screenshots
|
||||
|
||||
https://cleanuparr.github.io/Cleanuparr/docs/screenshots
|
||||
|
||||
## 🎯 Supported Applications
|
||||
|
||||
### *Arr Applications
|
||||
### *Arr Applications (latest version)
|
||||
- **Sonarr**
|
||||
- **Radarr**
|
||||
- **Lidarr**
|
||||
- **Readarr**
|
||||
- **Whisparr**
|
||||
- **Whisparr v2**
|
||||
|
||||
### Download Clients
|
||||
### Download Clients (latest version)
|
||||
- **qBittorrent**
|
||||
- **Transmission**
|
||||
- **Deluge**
|
||||
@@ -116,4 +119,4 @@ Special thanks for inspiration go to:
|
||||
# Buy me a coffee
|
||||
If I made your life just a tiny bit easier, consider buying me a coffee!
|
||||
|
||||
<a href="https://buymeacoffee.com/flaminel" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
|
||||
<a href="https://buymeacoffee.com/flaminel" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
|
||||
@@ -15,7 +15,7 @@ COPY frontend/ .
|
||||
RUN npm run build
|
||||
|
||||
# Build .NET backend
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-bookworm-slim AS build
|
||||
ARG TARGETARCH
|
||||
ARG VERSION=0.0.1
|
||||
ARG PACKAGES_USERNAME
|
||||
@@ -42,7 +42,7 @@ RUN --mount=type=cache,target=/root/.nuget/packages,sharing=locked \
|
||||
/p:DebugSymbols=false
|
||||
|
||||
# Runtime stage
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0-bookworm-slim
|
||||
|
||||
# Install required packages for user management, timezone support, and Python for Apprise CLI
|
||||
RUN apt-get update && apt-get install -y \
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<AssemblyName>Cleanuparr</AssemblyName>
|
||||
<Version Condition="'$(Version)' == ''">0.0.1</Version>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
@@ -24,14 +24,14 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MassTransit" Version="8.5.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.0" />
|
||||
<PackageReference Include="Quartz" Version="3.15.1" />
|
||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.15.1" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.1" />
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System.Diagnostics;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Arr;
|
||||
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Persistence;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -176,7 +174,7 @@ public class StatusController : ControllerBase
|
||||
{
|
||||
try
|
||||
{
|
||||
var sonarrClient = _arrClientFactory.GetClient(InstanceType.Sonarr);
|
||||
var sonarrClient = _arrClientFactory.GetClient(InstanceType.Sonarr, instance.Version);
|
||||
await sonarrClient.HealthCheckAsync(instance);
|
||||
|
||||
sonarrStatus.Add(new
|
||||
@@ -208,7 +206,7 @@ public class StatusController : ControllerBase
|
||||
{
|
||||
try
|
||||
{
|
||||
var radarrClient = _arrClientFactory.GetClient(InstanceType.Radarr);
|
||||
var radarrClient = _arrClientFactory.GetClient(InstanceType.Radarr, instance.Version);
|
||||
await radarrClient.HealthCheckAsync(instance);
|
||||
|
||||
radarrStatus.Add(new
|
||||
@@ -240,7 +238,7 @@ public class StatusController : ControllerBase
|
||||
{
|
||||
try
|
||||
{
|
||||
var lidarrClient = _arrClientFactory.GetClient(InstanceType.Lidarr);
|
||||
var lidarrClient = _arrClientFactory.GetClient(InstanceType.Lidarr, instance.Version);
|
||||
await lidarrClient.HealthCheckAsync(instance);
|
||||
|
||||
lidarrStatus.Add(new
|
||||
|
||||
@@ -34,7 +34,8 @@ public static class ServicesDI
|
||||
.AddScoped<IRadarrClient, RadarrClient>()
|
||||
.AddScoped<ILidarrClient, LidarrClient>()
|
||||
.AddScoped<IReadarrClient, ReadarrClient>()
|
||||
.AddScoped<IWhisparrClient, WhisparrClient>()
|
||||
.AddScoped<IWhisparrV2Client, WhisparrV2Client>()
|
||||
.AddScoped<IWhisparrV3Client, WhisparrV3Client>()
|
||||
.AddScoped<IArrClientFactory, ArrClientFactory>()
|
||||
.AddScoped<QueueCleaner>()
|
||||
.AddScoped<BlacklistSynchronizer>()
|
||||
|
||||
@@ -18,6 +18,9 @@ public sealed record ArrInstanceRequest
|
||||
[Required]
|
||||
public required string ApiKey { get; init; }
|
||||
|
||||
[Required]
|
||||
public required float Version { get; init; }
|
||||
|
||||
public ArrInstance ToEntity(Guid configId) => new()
|
||||
{
|
||||
Enabled = Enabled,
|
||||
@@ -25,6 +28,7 @@ public sealed record ArrInstanceRequest
|
||||
Url = new Uri(Url),
|
||||
ApiKey = ApiKey,
|
||||
ArrConfigId = configId,
|
||||
Version = Version,
|
||||
};
|
||||
|
||||
public void ApplyTo(ArrInstance instance)
|
||||
@@ -33,5 +37,6 @@ public sealed record ArrInstanceRequest
|
||||
instance.Name = Name;
|
||||
instance.Url = new Uri(Url);
|
||||
instance.ApiKey = ApiKey;
|
||||
instance.Version = Version;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ public sealed record TestArrInstanceRequest
|
||||
|
||||
[Required]
|
||||
public required string ApiKey { get; init; }
|
||||
|
||||
[Required]
|
||||
public required float Version { get; init; }
|
||||
|
||||
public ArrInstance ToTestInstance() => new()
|
||||
{
|
||||
@@ -20,5 +23,6 @@ public sealed record TestArrInstanceRequest
|
||||
Url = new Uri(Url),
|
||||
ApiKey = ApiKey,
|
||||
ArrConfigId = Guid.Empty,
|
||||
Version = Version,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Cleanuparr.Api.Features.Arr.Contracts.Requests;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Arr.Dtos;
|
||||
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Mapster;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Cleanuparr.Api.Features.Arr.Controllers;
|
||||
|
||||
@@ -289,7 +283,7 @@ public sealed class ArrConfigController : ControllerBase
|
||||
try
|
||||
{
|
||||
var testInstance = request.ToTestInstance();
|
||||
var client = _arrClientFactory.GetClient(type);
|
||||
var client = _arrClientFactory.GetClient(type, request.Version);
|
||||
await client.HealthCheckAsync(testInstance);
|
||||
|
||||
return Ok(new { Message = $"Connection to {type} instance successful" });
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json.Serialization;
|
||||
using Cleanuparr.Api;
|
||||
@@ -33,12 +34,24 @@ builder.Configuration
|
||||
int.TryParse(builder.Configuration.GetValue<string>("PORT"), out int port);
|
||||
port = port is 0 ? 11011 : port;
|
||||
|
||||
string? bindAddress = builder.Configuration.GetValue<string>("BIND_ADDRESS");
|
||||
|
||||
if (!builder.Environment.IsDevelopment())
|
||||
{
|
||||
// If no port is configured, default to 11011
|
||||
builder.WebHost.ConfigureKestrel(options =>
|
||||
{
|
||||
options.ListenAnyIP(port);
|
||||
if (string.IsNullOrEmpty(bindAddress) || bindAddress is "0.0.0.0" || bindAddress is "*")
|
||||
{
|
||||
options.ListenAnyIP(port);
|
||||
}
|
||||
else if (IPAddress.TryParse(bindAddress, out var ipAddress))
|
||||
{
|
||||
options.Listen(ipAddress, port);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Invalid BIND_ADDRESS: '{bindAddress}'");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -124,7 +137,7 @@ if (basePath is not null)
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation("Server configuration: PORT={port}, BASE_PATH={basePath}", port, basePath ?? "/");
|
||||
logger.LogInformation("Server configuration: BIND_ADDRESS={bindAddress}, PORT={port}, BASE_PATH={basePath}", bindAddress ?? "0.0.0.0", port, basePath ?? "/");
|
||||
|
||||
// Initialize the host
|
||||
app.Init();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -2,17 +2,17 @@ namespace Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
|
||||
public sealed record QueueRecord
|
||||
{
|
||||
// Sonarr and Whisparr
|
||||
// Sonarr and Whisparr v2
|
||||
public long SeriesId { get; init; }
|
||||
public long EpisodeId { get; init; }
|
||||
public long SeasonNumber { get; init; }
|
||||
|
||||
public QueueSeries? Series { get; init; }
|
||||
|
||||
// Radarr
|
||||
// Radarr and Whisparr v3
|
||||
public long MovieId { get; init; }
|
||||
|
||||
public QueueSeries? Movie { get; init; }
|
||||
public QueueMovie? Movie { get; init; }
|
||||
|
||||
// Lidarr
|
||||
public long ArtistId { get; init; }
|
||||
|
||||
@@ -2,7 +2,7 @@ using Cleanuparr.Domain.Enums;
|
||||
|
||||
namespace Cleanuparr.Domain.Entities.Whisparr;
|
||||
|
||||
public sealed record WhisparrCommand
|
||||
public sealed record WhisparrV2Command
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Cleanuparr.Domain.Entities.Whisparr;
|
||||
|
||||
public sealed record WhisparrV3Command
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
|
||||
public required List<long> MovieIds { get; init; }
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
@@ -19,9 +19,9 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
@@ -31,7 +31,7 @@
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -12,7 +12,8 @@ public class ArrClientFactoryTests
|
||||
private readonly Mock<IRadarrClient> _radarrClientMock;
|
||||
private readonly Mock<ILidarrClient> _lidarrClientMock;
|
||||
private readonly Mock<IReadarrClient> _readarrClientMock;
|
||||
private readonly Mock<IWhisparrClient> _whisparrClientMock;
|
||||
private readonly Mock<IWhisparrV2Client> _whisparrClientMock;
|
||||
private readonly Mock<IWhisparrV3Client> _whisparrV3ClientMock;
|
||||
private readonly ArrClientFactory _factory;
|
||||
|
||||
public ArrClientFactoryTests()
|
||||
@@ -21,14 +22,16 @@ public class ArrClientFactoryTests
|
||||
_radarrClientMock = new Mock<IRadarrClient>();
|
||||
_lidarrClientMock = new Mock<ILidarrClient>();
|
||||
_readarrClientMock = new Mock<IReadarrClient>();
|
||||
_whisparrClientMock = new Mock<IWhisparrClient>();
|
||||
_whisparrClientMock = new Mock<IWhisparrV2Client>();
|
||||
_whisparrV3ClientMock = new Mock<IWhisparrV3Client>();
|
||||
|
||||
_factory = new ArrClientFactory(
|
||||
_sonarrClientMock.Object,
|
||||
_radarrClientMock.Object,
|
||||
_lidarrClientMock.Object,
|
||||
_readarrClientMock.Object,
|
||||
_whisparrClientMock.Object
|
||||
_whisparrClientMock.Object,
|
||||
_whisparrV3ClientMock.Object
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +41,7 @@ public class ArrClientFactoryTests
|
||||
public void GetClient_Sonarr_ReturnsSonarrClient()
|
||||
{
|
||||
// Act
|
||||
var result = _factory.GetClient(InstanceType.Sonarr);
|
||||
var result = _factory.GetClient(InstanceType.Sonarr, 0);
|
||||
|
||||
// Assert
|
||||
Assert.Same(_sonarrClientMock.Object, result);
|
||||
@@ -48,7 +51,7 @@ public class ArrClientFactoryTests
|
||||
public void GetClient_Radarr_ReturnsRadarrClient()
|
||||
{
|
||||
// Act
|
||||
var result = _factory.GetClient(InstanceType.Radarr);
|
||||
var result = _factory.GetClient(InstanceType.Radarr, 0);
|
||||
|
||||
// Assert
|
||||
Assert.Same(_radarrClientMock.Object, result);
|
||||
@@ -58,7 +61,7 @@ public class ArrClientFactoryTests
|
||||
public void GetClient_Lidarr_ReturnsLidarrClient()
|
||||
{
|
||||
// Act
|
||||
var result = _factory.GetClient(InstanceType.Lidarr);
|
||||
var result = _factory.GetClient(InstanceType.Lidarr, 0);
|
||||
|
||||
// Assert
|
||||
Assert.Same(_lidarrClientMock.Object, result);
|
||||
@@ -68,7 +71,7 @@ public class ArrClientFactoryTests
|
||||
public void GetClient_Readarr_ReturnsReadarrClient()
|
||||
{
|
||||
// Act
|
||||
var result = _factory.GetClient(InstanceType.Readarr);
|
||||
var result = _factory.GetClient(InstanceType.Readarr, 0);
|
||||
|
||||
// Assert
|
||||
Assert.Same(_readarrClientMock.Object, result);
|
||||
@@ -78,12 +81,22 @@ public class ArrClientFactoryTests
|
||||
public void GetClient_Whisparr_ReturnsWhisparrClient()
|
||||
{
|
||||
// Act
|
||||
var result = _factory.GetClient(InstanceType.Whisparr);
|
||||
var result = _factory.GetClient(InstanceType.Whisparr, 2);
|
||||
|
||||
// Assert
|
||||
Assert.Same(_whisparrClientMock.Object, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetClient_WhisparrV3_ReturnsWhisparrClient()
|
||||
{
|
||||
// Act
|
||||
var result = _factory.GetClient(InstanceType.Whisparr, 3);
|
||||
|
||||
// Assert
|
||||
Assert.Same(_whisparrV3ClientMock.Object, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetClient_UnsupportedType_ThrowsNotImplementedException()
|
||||
{
|
||||
@@ -91,21 +104,17 @@ public class ArrClientFactoryTests
|
||||
var unsupportedType = (InstanceType)999;
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<NotImplementedException>(() => _factory.GetClient(unsupportedType));
|
||||
var exception = Assert.Throws<NotImplementedException>(() => _factory.GetClient(unsupportedType, It.IsAny<float>()));
|
||||
Assert.Contains("not yet supported", exception.Message);
|
||||
Assert.Contains("999", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(InstanceType.Sonarr)]
|
||||
[InlineData(InstanceType.Radarr)]
|
||||
[InlineData(InstanceType.Lidarr)]
|
||||
[InlineData(InstanceType.Readarr)]
|
||||
[InlineData(InstanceType.Whisparr)]
|
||||
public void GetClient_AllSupportedTypes_ReturnsNonNullClient(InstanceType instanceType)
|
||||
[MemberData(nameof(InstancesData))]
|
||||
public void GetClient_AllSupportedTypes_ReturnsNonNullClient(InstanceType instanceType, float? version)
|
||||
{
|
||||
// Act
|
||||
var result = _factory.GetClient(instanceType);
|
||||
var result = _factory.GetClient(instanceType, version ?? 0f);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
@@ -113,20 +122,26 @@ public class ArrClientFactoryTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(InstanceType.Sonarr)]
|
||||
[InlineData(InstanceType.Radarr)]
|
||||
[InlineData(InstanceType.Lidarr)]
|
||||
[InlineData(InstanceType.Readarr)]
|
||||
[InlineData(InstanceType.Whisparr)]
|
||||
public void GetClient_CalledMultipleTimes_ReturnsSameInstance(InstanceType instanceType)
|
||||
[MemberData(nameof(InstancesData))]
|
||||
public void GetClient_CalledMultipleTimes_ReturnsSameInstance(InstanceType instanceType, float? version)
|
||||
{
|
||||
// Act
|
||||
var result1 = _factory.GetClient(instanceType);
|
||||
var result2 = _factory.GetClient(instanceType);
|
||||
var result1 = _factory.GetClient(instanceType, version ?? 0f);
|
||||
var result2 = _factory.GetClient(instanceType, version ?? 0f);
|
||||
|
||||
// Assert
|
||||
Assert.Same(result1, result2);
|
||||
}
|
||||
|
||||
public static IEnumerable<object?[]> InstancesData =>
|
||||
[
|
||||
[InstanceType.Sonarr, null],
|
||||
[InstanceType.Radarr, null],
|
||||
[InstanceType.Lidarr, null],
|
||||
[InstanceType.Readarr, null],
|
||||
[InstanceType.Whisparr, 2f],
|
||||
[InstanceType.Whisparr, 3f]
|
||||
];
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ public class DownloadHunterTests : IDisposable
|
||||
_fakeTimeProvider = new FakeTimeProvider();
|
||||
|
||||
_arrClientFactoryMock
|
||||
.Setup(f => f.GetClient(It.IsAny<InstanceType>()))
|
||||
.Setup(f => f.GetClient(It.IsAny<InstanceType>(), It.IsAny<float>()))
|
||||
.Returns(_arrClientMock.Object);
|
||||
|
||||
_downloadHunter = new Infrastructure.Features.DownloadHunter.DownloadHunter(
|
||||
@@ -71,7 +71,7 @@ public class DownloadHunterTests : IDisposable
|
||||
await _downloadHunter.HuntDownloadsAsync(request);
|
||||
|
||||
// Assert
|
||||
_arrClientFactoryMock.Verify(f => f.GetClient(It.IsAny<InstanceType>()), Times.Never);
|
||||
_arrClientFactoryMock.Verify(f => f.GetClient(It.IsAny<InstanceType>(), It.IsAny<float>()), Times.Never);
|
||||
_arrClientMock.Verify(c => c.SearchItemsAsync(It.IsAny<ArrInstance>(), It.IsAny<HashSet<SearchItem>>()), Times.Never);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ public class DownloadHunterTests : IDisposable
|
||||
await task;
|
||||
|
||||
// Assert
|
||||
_arrClientFactoryMock.Verify(f => f.GetClient(request.InstanceType), Times.Once);
|
||||
_arrClientFactoryMock.Verify(f => f.GetClient(request.InstanceType, It.IsAny<float>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -148,7 +148,7 @@ public class DownloadHunterTests : IDisposable
|
||||
await task;
|
||||
|
||||
// Assert
|
||||
_arrClientFactoryMock.Verify(f => f.GetClient(instanceType), Times.Once);
|
||||
_arrClientFactoryMock.Verify(f => f.GetClient(instanceType, It.IsAny<float>()), Times.Once);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -292,7 +292,8 @@ public class DownloadHunterTests : IDisposable
|
||||
{
|
||||
Name = "Test Instance",
|
||||
Url = new Uri("http://arr.local"),
|
||||
ApiKey = "test-api-key"
|
||||
ApiKey = "test-api-key",
|
||||
Version = 0
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ public class QueueItemRemoverTests : IDisposable
|
||||
_arrClientMock = new Mock<IArrClient>();
|
||||
|
||||
_arrClientFactoryMock
|
||||
.Setup(f => f.GetClient(It.IsAny<InstanceType>()))
|
||||
.Setup(f => f.GetClient(It.IsAny<InstanceType>(), It.IsAny<float>()))
|
||||
.Returns(_arrClientMock.Object);
|
||||
|
||||
// Create real EventPublisher with mocked dependencies
|
||||
@@ -205,7 +205,7 @@ public class QueueItemRemoverTests : IDisposable
|
||||
await _queueItemRemover.RemoveQueueItemAsync(request);
|
||||
|
||||
// Assert
|
||||
_arrClientFactoryMock.Verify(f => f.GetClient(instanceType), Times.Once);
|
||||
_arrClientFactoryMock.Verify(f => f.GetClient(instanceType, It.IsAny<float>()), Times.Once);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -249,7 +249,7 @@ public class DownloadCleanerTests : IDisposable
|
||||
// Setup arr client to return queue record with matching download ID
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(It.IsAny<InstanceType>()))
|
||||
.Setup(x => x.GetClient(It.IsAny<InstanceType>(), It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
@@ -322,7 +322,7 @@ public class DownloadCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(It.IsAny<InstanceType>()))
|
||||
.Setup(x => x.GetClient(It.IsAny<InstanceType>(), It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -340,11 +340,11 @@ public class DownloadCleanerTests : IDisposable
|
||||
|
||||
// Assert - both instances should be processed
|
||||
_fixture.ArrClientFactory.Verify(
|
||||
x => x.GetClient(InstanceType.Sonarr),
|
||||
x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()),
|
||||
Times.Once
|
||||
);
|
||||
_fixture.ArrClientFactory.Verify(
|
||||
x => x.GetClient(InstanceType.Radarr),
|
||||
x => x.GetClient(InstanceType.Radarr, It.IsAny<float>()),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
@@ -502,7 +502,7 @@ public class DownloadCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecords = new List<QueueRecord>
|
||||
@@ -878,7 +878,7 @@ public class DownloadCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
// Make the arr queue iterator throw an exception
|
||||
|
||||
@@ -109,7 +109,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(It.IsAny<InstanceType>()))
|
||||
.Setup(x => x.GetClient(It.IsAny<InstanceType>(), It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -139,7 +139,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -156,7 +156,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
_fixture.ArrClientFactory.Verify(x => x.GetClient(InstanceType.Sonarr), Times.Once);
|
||||
_fixture.ArrClientFactory.Verify(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -176,7 +176,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(It.IsAny<InstanceType>()))
|
||||
.Setup(x => x.GetClient(It.IsAny<InstanceType>(), It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -193,8 +193,8 @@ public class MalwareBlockerTests : IDisposable
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert - Sonarr and Radarr processed because DeleteKnownMalware is true
|
||||
_fixture.ArrClientFactory.Verify(x => x.GetClient(InstanceType.Sonarr), Times.Once);
|
||||
_fixture.ArrClientFactory.Verify(x => x.GetClient(InstanceType.Radarr), Times.Once);
|
||||
_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);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -217,7 +217,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
@@ -269,7 +269,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
@@ -327,7 +327,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
@@ -403,7 +403,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
@@ -474,7 +474,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
@@ -542,7 +542,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
|
||||
@@ -62,7 +62,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(It.IsAny<InstanceType>()))
|
||||
.Setup(x => x.GetClient(It.IsAny<InstanceType>(), It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -122,7 +122,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(It.IsAny<InstanceType>()))
|
||||
.Setup(x => x.GetClient(It.IsAny<InstanceType>(), It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -182,7 +182,7 @@ public class QueueCleanerTests : IDisposable
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(It.IsAny<InstanceType>()))
|
||||
.Setup(x => x.GetClient(It.IsAny<InstanceType>(), It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
@@ -199,8 +199,8 @@ public class QueueCleanerTests : IDisposable
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
_fixture.ArrClientFactory.Verify(x => x.GetClient(InstanceType.Sonarr), Times.Once);
|
||||
_fixture.ArrClientFactory.Verify(x => x.GetClient(InstanceType.Radarr), Times.Once);
|
||||
_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);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -222,7 +222,7 @@ public class QueueCleanerTests : IDisposable
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
@@ -277,7 +277,7 @@ public class QueueCleanerTests : IDisposable
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
@@ -334,7 +334,7 @@ public class QueueCleanerTests : IDisposable
|
||||
)).ReturnsAsync(false);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
@@ -391,7 +391,7 @@ public class QueueCleanerTests : IDisposable
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
@@ -466,7 +466,7 @@ public class QueueCleanerTests : IDisposable
|
||||
)).ReturnsAsync(false);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
@@ -535,7 +535,7 @@ public class QueueCleanerTests : IDisposable
|
||||
)).ReturnsAsync(false);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
@@ -603,7 +603,7 @@ public class QueueCleanerTests : IDisposable
|
||||
)).ReturnsAsync(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
@@ -677,7 +677,7 @@ public class QueueCleanerTests : IDisposable
|
||||
)).ReturnsAsync(false);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
@@ -746,7 +746,7 @@ public class QueueCleanerTests : IDisposable
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Radarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Radarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
@@ -835,7 +835,7 @@ public class QueueCleanerTests : IDisposable
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Radarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Radarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
@@ -907,7 +907,7 @@ public class QueueCleanerTests : IDisposable
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Lidarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Lidarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
@@ -979,7 +979,7 @@ public class QueueCleanerTests : IDisposable
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Readarr))
|
||||
.Setup(x => x.GetClient(InstanceType.Readarr, It.IsAny<float>()))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
|
||||
@@ -58,8 +58,9 @@ public class NotificationPublisherTests
|
||||
};
|
||||
|
||||
ContextProvider.Set(nameof(QueueRecord), record);
|
||||
ContextProvider.Set(nameof(InstanceType), (object)instanceType);
|
||||
ContextProvider.Set(nameof(InstanceType), instanceType);
|
||||
ContextProvider.Set(nameof(ArrInstance) + nameof(ArrInstance.Url), new Uri("http://sonarr.local"));
|
||||
ContextProvider.Set("version", 1f);
|
||||
}
|
||||
|
||||
private void SetupDownloadCleanerContext()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
@@ -13,15 +13,15 @@
|
||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||
<PackageReference Include="MassTransit.Abstractions" Version="8.5.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Mono.Unix" Version="7.1.0-final.1.21458.1" />
|
||||
<PackageReference Include="Quartz" Version="3.15.1" />
|
||||
<PackageReference Include="Serilog.Expressions" Version="5.0.0" />
|
||||
<PackageReference Include="System.Threading.RateLimiting" Version="10.0.1" />
|
||||
<PackageReference Include="System.Threading.RateLimiting" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -9,31 +9,35 @@ public sealed class ArrClientFactory : IArrClientFactory
|
||||
private readonly IRadarrClient _radarrClient;
|
||||
private readonly ILidarrClient _lidarrClient;
|
||||
private readonly IReadarrClient _readarrClient;
|
||||
private readonly IWhisparrClient _whisparrClient;
|
||||
private readonly IWhisparrV2Client _whisparrV2Client;
|
||||
private readonly IWhisparrV3Client _whisparrV3Client;
|
||||
|
||||
public ArrClientFactory(
|
||||
ISonarrClient sonarrClient,
|
||||
IRadarrClient radarrClient,
|
||||
ILidarrClient lidarrClient,
|
||||
IReadarrClient readarrClient,
|
||||
IWhisparrClient whisparrClient
|
||||
IWhisparrV2Client whisparrV2Client,
|
||||
IWhisparrV3Client whisparrV3Client
|
||||
)
|
||||
{
|
||||
_sonarrClient = sonarrClient;
|
||||
_radarrClient = radarrClient;
|
||||
_lidarrClient = lidarrClient;
|
||||
_readarrClient = readarrClient;
|
||||
_whisparrClient = whisparrClient;
|
||||
_whisparrV2Client = whisparrV2Client;
|
||||
_whisparrV3Client = whisparrV3Client;
|
||||
}
|
||||
|
||||
public IArrClient GetClient(InstanceType type) =>
|
||||
public IArrClient GetClient(InstanceType type, float instanceVersion) =>
|
||||
type switch
|
||||
{
|
||||
InstanceType.Sonarr => _sonarrClient,
|
||||
InstanceType.Radarr => _radarrClient,
|
||||
InstanceType.Lidarr => _lidarrClient,
|
||||
InstanceType.Readarr => _readarrClient,
|
||||
InstanceType.Whisparr => _whisparrClient,
|
||||
InstanceType.Whisparr when instanceVersion is 2 => _whisparrV2Client,
|
||||
InstanceType.Whisparr when instanceVersion is 3 => _whisparrV3Client,
|
||||
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
|
||||
};
|
||||
}
|
||||
@@ -2,14 +2,6 @@ using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Arr.Dtos;
|
||||
|
||||
/// <summary>
|
||||
/// DTO for updating Sonarr configuration basic settings (instances managed separately)
|
||||
/// </summary>
|
||||
public record UpdateSonarrConfigDto
|
||||
{
|
||||
public short FailedImportMaxStrikes { get; init; } = -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for Arr instances that can handle both existing (with ID) and new (without ID) instances
|
||||
/// </summary>
|
||||
@@ -22,6 +14,8 @@ public record ArrInstanceDto
|
||||
|
||||
public bool Enabled { get; init; } = true;
|
||||
|
||||
public float Version { get; set; }
|
||||
|
||||
[Required]
|
||||
public required string Name { get; init; }
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.Arr.Dtos;
|
||||
|
||||
/// <summary>
|
||||
/// DTO for updating Lidarr configuration basic settings (instances managed separately)
|
||||
/// </summary>
|
||||
public record UpdateLidarrConfigDto
|
||||
{
|
||||
public short FailedImportMaxStrikes { get; init; } = -1;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.Arr.Dtos;
|
||||
|
||||
/// <summary>
|
||||
/// DTO for updating Radarr configuration basic settings (instances managed separately)
|
||||
/// </summary>
|
||||
public record UpdateRadarrConfigDto
|
||||
{
|
||||
public short FailedImportMaxStrikes { get; init; } = -1;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.Arr.Dtos;
|
||||
|
||||
/// <summary>
|
||||
/// DTO for updating Readarr configuration basic settings (instances managed separately)
|
||||
/// </summary>
|
||||
public record UpdateReadarrConfigDto
|
||||
{
|
||||
public short FailedImportMaxStrikes { get; init; } = -1;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.Arr.Dtos;
|
||||
|
||||
/// <summary>
|
||||
/// DTO for updating Whisparr configuration basic settings (instances managed separately)
|
||||
/// </summary>
|
||||
public record UpdateWhisparrConfigDto
|
||||
{
|
||||
public short FailedImportMaxStrikes { get; init; } = -1;
|
||||
}
|
||||
@@ -4,5 +4,5 @@ namespace Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
|
||||
public interface IArrClientFactory
|
||||
{
|
||||
IArrClient GetClient(InstanceType type);
|
||||
IArrClient GetClient(InstanceType type, float instanceVersion);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
|
||||
public interface IWhisparrClient : IArrClient
|
||||
public interface IWhisparrV2Client : IArrClient
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
|
||||
public interface IWhisparrV3Client : IArrClient
|
||||
{
|
||||
}
|
||||
@@ -14,10 +14,10 @@ using Newtonsoft.Json;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Arr;
|
||||
|
||||
public class WhisparrClient : ArrClient, IWhisparrClient
|
||||
public class WhisparrV2Client : ArrClient, IWhisparrV2Client
|
||||
{
|
||||
public WhisparrClient(
|
||||
ILogger<WhisparrClient> logger,
|
||||
public WhisparrV2Client(
|
||||
ILogger<WhisparrV2Client> logger,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IStriker striker,
|
||||
IDryRunInterceptor dryRunInterceptor
|
||||
@@ -63,7 +63,7 @@ public class WhisparrClient : ArrClient, IWhisparrClient
|
||||
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/v3/command";
|
||||
|
||||
foreach (WhisparrCommand command in GetSearchCommands(items.Cast<SeriesSearchItem>().ToHashSet()))
|
||||
foreach (WhisparrV2Command command in GetSearchCommands(items.Cast<SeriesSearchItem>().ToHashSet()))
|
||||
{
|
||||
using HttpRequestMessage request = new(HttpMethod.Post, uriBuilder.Uri);
|
||||
request.Content = new StringContent(
|
||||
@@ -104,7 +104,7 @@ public class WhisparrClient : ArrClient, IWhisparrClient
|
||||
private static string GetSearchLog(
|
||||
SeriesSearchType searchType,
|
||||
Uri instanceUrl,
|
||||
WhisparrCommand command,
|
||||
WhisparrV2Command v2Command,
|
||||
bool success,
|
||||
string? logContext
|
||||
)
|
||||
@@ -114,15 +114,15 @@ public class WhisparrClient : ArrClient, IWhisparrClient
|
||||
return searchType switch
|
||||
{
|
||||
SeriesSearchType.Episode =>
|
||||
$"episodes search {status} | {instanceUrl} | {logContext ?? $"episode ids: {string.Join(',', command.EpisodeIds)}"}",
|
||||
$"episodes search {status} | {instanceUrl} | {logContext ?? $"episode ids: {string.Join(',', v2Command.EpisodeIds)}"}",
|
||||
SeriesSearchType.Season =>
|
||||
$"season search {status} | {instanceUrl} | {logContext ?? $"season: {command.SeasonNumber} series id: {command.SeriesId}"}",
|
||||
SeriesSearchType.Series => $"series search {status} | {instanceUrl} | {logContext ?? $"series id: {command.SeriesId}"}",
|
||||
$"season search {status} | {instanceUrl} | {logContext ?? $"season: {v2Command.SeasonNumber} series id: {v2Command.SeriesId}"}",
|
||||
SeriesSearchType.Series => $"series search {status} | {instanceUrl} | {logContext ?? $"series id: {v2Command.SeriesId}"}",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(searchType), searchType, null)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<string?> ComputeCommandLogContextAsync(ArrInstance arrInstance, WhisparrCommand command, SeriesSearchType searchType)
|
||||
private async Task<string?> ComputeCommandLogContextAsync(ArrInstance arrInstance, WhisparrV2Command v2Command, SeriesSearchType searchType)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -130,7 +130,7 @@ public class WhisparrClient : ArrClient, IWhisparrClient
|
||||
|
||||
if (searchType is SeriesSearchType.Episode)
|
||||
{
|
||||
var episodes = await GetEpisodesAsync(arrInstance, command.EpisodeIds);
|
||||
var episodes = await GetEpisodesAsync(arrInstance, v2Command.EpisodeIds);
|
||||
|
||||
if (episodes?.Count is null or 0)
|
||||
{
|
||||
@@ -156,7 +156,7 @@ public class WhisparrClient : ArrClient, IWhisparrClient
|
||||
series.Add(show);
|
||||
}
|
||||
|
||||
foreach (var group in command.EpisodeIds.GroupBy(id => episodes.First(x => x.Id == id).SeriesId))
|
||||
foreach (var group in v2Command.EpisodeIds.GroupBy(id => episodes.First(x => x.Id == id).SeriesId))
|
||||
{
|
||||
var show = series.First(x => x.Id == group.Key);
|
||||
var episode = episodes
|
||||
@@ -172,19 +172,19 @@ public class WhisparrClient : ArrClient, IWhisparrClient
|
||||
|
||||
if (searchType is SeriesSearchType.Season)
|
||||
{
|
||||
Series? show = await GetSeriesAsync(arrInstance, command.SeriesId.Value);
|
||||
Series? show = await GetSeriesAsync(arrInstance, v2Command.SeriesId.Value);
|
||||
|
||||
if (show is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
log.Append($"[{show.Title} season {command.SeasonNumber}]");
|
||||
log.Append($"[{show.Title} season {v2Command.SeasonNumber}]");
|
||||
}
|
||||
|
||||
if (searchType is SeriesSearchType.Series)
|
||||
{
|
||||
Series? show = await GetSeriesAsync(arrInstance, command.SeriesId.Value);
|
||||
Series? show = await GetSeriesAsync(arrInstance, v2Command.SeriesId.Value);
|
||||
|
||||
if (show is null)
|
||||
{
|
||||
@@ -233,39 +233,39 @@ public class WhisparrClient : ArrClient, IWhisparrClient
|
||||
return JsonConvert.DeserializeObject<Series>(responseContent);
|
||||
}
|
||||
|
||||
private List<WhisparrCommand> GetSearchCommands(HashSet<SeriesSearchItem> items)
|
||||
private List<WhisparrV2Command> GetSearchCommands(HashSet<SeriesSearchItem> items)
|
||||
{
|
||||
const string episodeSearch = "EpisodeSearch";
|
||||
const string seasonSearch = "SeasonSearch";
|
||||
const string seriesSearch = "SeriesSearch";
|
||||
|
||||
List<WhisparrCommand> commands = new();
|
||||
List<WhisparrV2Command> commands = new();
|
||||
|
||||
foreach (SeriesSearchItem item in items)
|
||||
{
|
||||
WhisparrCommand command = item.SearchType is SeriesSearchType.Episode
|
||||
WhisparrV2Command v2Command = item.SearchType is SeriesSearchType.Episode
|
||||
? commands.FirstOrDefault() ?? new() { Name = episodeSearch, EpisodeIds = new() }
|
||||
: new();
|
||||
|
||||
switch (item.SearchType)
|
||||
{
|
||||
case SeriesSearchType.Episode when command.EpisodeIds is null:
|
||||
command.EpisodeIds = [item.Id];
|
||||
case SeriesSearchType.Episode when v2Command.EpisodeIds is null:
|
||||
v2Command.EpisodeIds = [item.Id];
|
||||
break;
|
||||
|
||||
case SeriesSearchType.Episode when command.EpisodeIds is not null:
|
||||
command.EpisodeIds.Add(item.Id);
|
||||
case SeriesSearchType.Episode when v2Command.EpisodeIds is not null:
|
||||
v2Command.EpisodeIds.Add(item.Id);
|
||||
break;
|
||||
|
||||
case SeriesSearchType.Season:
|
||||
command.Name = seasonSearch;
|
||||
command.SeasonNumber = item.Id;
|
||||
command.SeriesId = ((SeriesSearchItem)item).SeriesId;
|
||||
v2Command.Name = seasonSearch;
|
||||
v2Command.SeasonNumber = item.Id;
|
||||
v2Command.SeriesId = ((SeriesSearchItem)item).SeriesId;
|
||||
break;
|
||||
|
||||
case SeriesSearchType.Series:
|
||||
command.Name = seriesSearch;
|
||||
command.SeriesId = item.Id;
|
||||
v2Command.Name = seriesSearch;
|
||||
v2Command.SeriesId = item.Id;
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -278,8 +278,8 @@ public class WhisparrClient : ArrClient, IWhisparrClient
|
||||
continue;
|
||||
}
|
||||
|
||||
command.SearchType = item.SearchType;
|
||||
commands.Add(command);
|
||||
v2Command.SearchType = item.SearchType;
|
||||
commands.Add(v2Command);
|
||||
}
|
||||
|
||||
return commands;
|
||||
@@ -0,0 +1,157 @@
|
||||
using System.Text;
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using Cleanuparr.Domain.Entities.Radarr;
|
||||
using Cleanuparr.Domain.Entities.Whisparr;
|
||||
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Data.Models.Arr;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Arr;
|
||||
|
||||
public class WhisparrV3Client : ArrClient, IWhisparrV3Client
|
||||
{
|
||||
public WhisparrV3Client(
|
||||
ILogger<WhisparrV3Client> logger,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IStriker striker,
|
||||
IDryRunInterceptor dryRunInterceptor
|
||||
) : base(logger, httpClientFactory, striker, dryRunInterceptor)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string GetSystemStatusUrlPath()
|
||||
{
|
||||
return "/api/v3/system/status";
|
||||
}
|
||||
|
||||
protected override string GetQueueUrlPath()
|
||||
{
|
||||
return "/api/v3/queue";
|
||||
}
|
||||
|
||||
protected override string GetQueueUrlQuery(int page)
|
||||
{
|
||||
return $"page={page}&pageSize=200&includeUnknownMovieItems=true&includeMovie=true";
|
||||
}
|
||||
|
||||
protected override string GetQueueDeleteUrlPath(long recordId)
|
||||
{
|
||||
return $"/api/v3/queue/{recordId}";
|
||||
}
|
||||
|
||||
protected override string GetQueueDeleteUrlQuery(bool removeFromClient)
|
||||
{
|
||||
string query = "blocklist=true&skipRedownload=true&changeCategory=false";
|
||||
query += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public override async Task SearchItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
|
||||
{
|
||||
if (items?.Count is null or 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<long> ids = items.Select(item => item.Id).ToList();
|
||||
|
||||
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/v3/command";
|
||||
|
||||
WhisparrV3Command command = new()
|
||||
{
|
||||
Name = "MoviesSearch",
|
||||
MovieIds = ids,
|
||||
};
|
||||
|
||||
using HttpRequestMessage request = new(HttpMethod.Post, uriBuilder.Uri);
|
||||
request.Content = new StringContent(
|
||||
JsonConvert.SerializeObject(command),
|
||||
Encoding.UTF8,
|
||||
"application/json"
|
||||
);
|
||||
SetApiKey(request, arrInstance.ApiKey);
|
||||
|
||||
string? logContext = await ComputeCommandLogContextAsync(arrInstance, command);
|
||||
|
||||
try
|
||||
{
|
||||
HttpResponseMessage? response = await _dryRunInterceptor.InterceptAsync<HttpResponseMessage>(SendRequestAsync, request);
|
||||
response?.Dispose();
|
||||
|
||||
_logger.LogInformation("{log}", GetSearchLog(arrInstance.Url, command, true, logContext));
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogError("{log}", GetSearchLog(arrInstance.Url, command, false, logContext));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static string GetSearchLog(Uri instanceUrl, WhisparrV3Command command, bool success, string? logContext)
|
||||
{
|
||||
string status = success ? "triggered" : "failed";
|
||||
string message = logContext ?? $"movie ids: {string.Join(',', command.MovieIds)}";
|
||||
|
||||
return $"movie search {status} | {instanceUrl} | {message}";
|
||||
}
|
||||
|
||||
private async Task<string?> ComputeCommandLogContextAsync(ArrInstance arrInstance, WhisparrV3Command command)
|
||||
{
|
||||
try
|
||||
{
|
||||
StringBuilder log = new();
|
||||
|
||||
foreach (long movieId in command.MovieIds)
|
||||
{
|
||||
Movie? movie = await GetMovie(arrInstance, movieId);
|
||||
|
||||
if (movie is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
log.Append($"[{movie.Title}]");
|
||||
}
|
||||
|
||||
return log.ToString();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogDebug(exception, "failed to compute log context");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<Movie?> GetMovie(ArrInstance arrInstance, long movieId)
|
||||
{
|
||||
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/v3/movie/{movieId}";
|
||||
|
||||
using HttpRequestMessage request = new(HttpMethod.Get, uriBuilder.Uri);
|
||||
SetApiKey(request, arrInstance.ApiKey);
|
||||
|
||||
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
string responseBody = await response.Content.ReadAsStringAsync();
|
||||
return JsonConvert.DeserializeObject<Movie>(responseBody);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ public sealed class DownloadHunter : IDownloadHunter
|
||||
return;
|
||||
}
|
||||
|
||||
var arrClient = _arrClientFactory.GetClient(request.InstanceType);
|
||||
var arrClient = _arrClientFactory.GetClient(request.InstanceType, request.Instance.Version);
|
||||
await arrClient.SearchItemsAsync(request.Instance, [request.SearchItem]);
|
||||
|
||||
// Prevent manual db edits
|
||||
|
||||
@@ -47,7 +47,7 @@ public sealed class QueueItemRemover : IQueueItemRemover
|
||||
{
|
||||
try
|
||||
{
|
||||
var arrClient = _arrClientFactory.GetClient(request.InstanceType);
|
||||
var arrClient = _arrClientFactory.GetClient(request.InstanceType, request.Instance.Version);
|
||||
await arrClient.DeleteQueueItemAsync(request.Instance, request.Record, request.RemoveFromClient, request.DeleteReason);
|
||||
|
||||
// Set context for EventPublisher
|
||||
@@ -56,6 +56,7 @@ public sealed class QueueItemRemover : IQueueItemRemover
|
||||
ContextProvider.Set(nameof(QueueRecord), request.Record);
|
||||
ContextProvider.Set(nameof(ArrInstance) + nameof(ArrInstance.Url), request.Instance.Url);
|
||||
ContextProvider.Set(nameof(InstanceType), request.InstanceType);
|
||||
ContextProvider.Set("version", request.Instance.Version);
|
||||
|
||||
// Use the new centralized EventPublisher method
|
||||
await _eventPublisher.PublishQueueItemDeleted(request.RemoveFromClient, request.DeleteReason);
|
||||
|
||||
@@ -96,11 +96,11 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
// wait for the downloads to appear in the arr queue
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), _timeProvider);
|
||||
|
||||
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Sonarr)), InstanceType.Sonarr, true);
|
||||
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Radarr)), InstanceType.Radarr, true);
|
||||
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Lidarr)), InstanceType.Lidarr, true);
|
||||
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Readarr)), InstanceType.Readarr, true);
|
||||
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Whisparr)), InstanceType.Whisparr, true);
|
||||
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Sonarr)), true);
|
||||
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Radarr)), true);
|
||||
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Lidarr)), true);
|
||||
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Readarr)), true);
|
||||
await ProcessArrConfigAsync(ContextProvider.Get<ArrConfig>(nameof(InstanceType.Whisparr)), true);
|
||||
|
||||
foreach (var pair in downloadServiceToDownloadsMap)
|
||||
{
|
||||
@@ -135,11 +135,11 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance)
|
||||
{
|
||||
using var _ = LogContext.PushProperty(LogProperties.Category, instanceType.ToString());
|
||||
using var _ = LogContext.PushProperty(LogProperties.Category, instance.ArrConfig.Type.ToString());
|
||||
|
||||
IArrClient arrClient = _arrClientFactory.GetClient(instanceType);
|
||||
IArrClient arrClient = _arrClientFactory.GetClient(instance.ArrConfig.Type, instance.Version);
|
||||
|
||||
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
|
||||
{
|
||||
|
||||
@@ -92,9 +92,9 @@ public abstract class GenericHandler : IHandler
|
||||
|
||||
protected abstract Task ExecuteInternalAsync();
|
||||
|
||||
protected abstract Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType);
|
||||
protected abstract Task ProcessInstanceAsync(ArrInstance instance);
|
||||
|
||||
protected async Task ProcessArrConfigAsync(ArrConfig config, InstanceType instanceType, bool throwOnFailure = false)
|
||||
protected async Task ProcessArrConfigAsync(ArrConfig config, bool throwOnFailure = false)
|
||||
{
|
||||
var enabledInstances = config.Instances
|
||||
.Where(x => x.Enabled)
|
||||
@@ -102,7 +102,7 @@ public abstract class GenericHandler : IHandler
|
||||
|
||||
if (enabledInstances.Count is 0)
|
||||
{
|
||||
_logger.LogDebug($"Skip processing {instanceType}. No enabled instances found");
|
||||
_logger.LogDebug($"Skip processing {config.Type}. No enabled instances found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,11 +110,11 @@ public abstract class GenericHandler : IHandler
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessInstanceAsync(arrInstance, instanceType);
|
||||
await ProcessInstanceAsync(arrInstance);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "failed to process {type} instance | {url}", instanceType, arrInstance.Url);
|
||||
_logger.LogError(exception, "failed to process {type} instance | {url}", config.Type, arrInstance.Url);
|
||||
|
||||
if (throwOnFailure)
|
||||
{
|
||||
@@ -140,14 +140,14 @@ public abstract class GenericHandler : IHandler
|
||||
return;
|
||||
}
|
||||
|
||||
if (instanceType is InstanceType.Sonarr or InstanceType.Whisparr)
|
||||
if (instanceType is InstanceType.Sonarr || (instanceType is InstanceType.Whisparr && instance.Version is 2))
|
||||
{
|
||||
QueueItemRemoveRequest<SeriesSearchItem> removeRequest = new()
|
||||
{
|
||||
InstanceType = instanceType,
|
||||
Instance = instance,
|
||||
Record = record,
|
||||
SearchItem = (SeriesSearchItem)GetRecordSearchItem(instanceType, record, isPack),
|
||||
SearchItem = (SeriesSearchItem)GetRecordSearchItem(instanceType, instance.Version, record, isPack),
|
||||
RemoveFromClient = removeFromClient,
|
||||
DeleteReason = deleteReason
|
||||
};
|
||||
@@ -161,7 +161,7 @@ public abstract class GenericHandler : IHandler
|
||||
InstanceType = instanceType,
|
||||
Instance = instance,
|
||||
Record = record,
|
||||
SearchItem = GetRecordSearchItem(instanceType, record, isPack),
|
||||
SearchItem = GetRecordSearchItem(instanceType, instance.Version, record, isPack),
|
||||
RemoveFromClient = removeFromClient,
|
||||
DeleteReason = deleteReason
|
||||
};
|
||||
@@ -173,7 +173,7 @@ public abstract class GenericHandler : IHandler
|
||||
await _eventPublisher.PublishAsync(EventType.DownloadMarkedForDeletion, "Download marked for deletion", EventSeverity.Important);
|
||||
}
|
||||
|
||||
protected SearchItem GetRecordSearchItem(InstanceType type, QueueRecord record, bool isPack = false)
|
||||
protected SearchItem GetRecordSearchItem(InstanceType type, float version, QueueRecord record, bool isPack = false)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
@@ -201,18 +201,22 @@ public abstract class GenericHandler : IHandler
|
||||
{
|
||||
Id = record.BookId
|
||||
},
|
||||
InstanceType.Whisparr when !isPack => new SeriesSearchItem
|
||||
InstanceType.Whisparr when version is 2 && !isPack => new SeriesSearchItem
|
||||
{
|
||||
Id = record.EpisodeId,
|
||||
SeriesId = record.SeriesId,
|
||||
SearchType = SeriesSearchType.Episode
|
||||
},
|
||||
InstanceType.Whisparr when isPack => new SeriesSearchItem
|
||||
InstanceType.Whisparr when version is 2 && isPack => new SeriesSearchItem
|
||||
{
|
||||
Id = record.SeasonNumber,
|
||||
SeriesId = record.SeriesId,
|
||||
SearchType = SeriesSearchType.Season
|
||||
},
|
||||
InstanceType.Whisparr when version is 3 => new SearchItem
|
||||
{
|
||||
Id = record.MovieId
|
||||
},
|
||||
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,42 +66,43 @@ public sealed class MalwareBlocker : GenericHandler
|
||||
|
||||
if (config.Sonarr.Enabled || config.DeleteKnownMalware)
|
||||
{
|
||||
await ProcessArrConfigAsync(sonarrConfig, InstanceType.Sonarr);
|
||||
await ProcessArrConfigAsync(sonarrConfig);
|
||||
}
|
||||
|
||||
if (config.Radarr.Enabled || config.DeleteKnownMalware)
|
||||
{
|
||||
await ProcessArrConfigAsync(radarrConfig, InstanceType.Radarr);
|
||||
await ProcessArrConfigAsync(radarrConfig);
|
||||
}
|
||||
|
||||
if (config.Lidarr.Enabled || config.DeleteKnownMalware)
|
||||
{
|
||||
await ProcessArrConfigAsync(lidarrConfig, InstanceType.Lidarr);
|
||||
await ProcessArrConfigAsync(lidarrConfig);
|
||||
}
|
||||
|
||||
if (config.Readarr.Enabled || config.DeleteKnownMalware)
|
||||
{
|
||||
await ProcessArrConfigAsync(readarrConfig, InstanceType.Readarr);
|
||||
await ProcessArrConfigAsync(readarrConfig);
|
||||
}
|
||||
|
||||
if (config.Whisparr.Enabled || config.DeleteKnownMalware)
|
||||
{
|
||||
await ProcessArrConfigAsync(whisparrConfig, InstanceType.Whisparr);
|
||||
await ProcessArrConfigAsync(whisparrConfig);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance)
|
||||
{
|
||||
List<string> ignoredDownloads = ContextProvider.Get<GeneralConfig>(nameof(GeneralConfig)).IgnoredDownloads;
|
||||
ignoredDownloads.AddRange(ContextProvider.Get<ContentBlockerConfig>().IgnoredDownloads);
|
||||
|
||||
using var _ = LogContext.PushProperty(LogProperties.Category, instanceType.ToString());
|
||||
using var _ = LogContext.PushProperty(LogProperties.Category, instance.ArrConfig.Type.ToString());
|
||||
|
||||
IArrClient arrClient = _arrClientFactory.GetClient(instanceType);
|
||||
IArrClient arrClient = _arrClientFactory.GetClient(instance.ArrConfig.Type, instance.Version);
|
||||
|
||||
// push to context
|
||||
ContextProvider.Set(nameof(ArrInstance) + nameof(ArrInstance.Url), instance.Url);
|
||||
ContextProvider.Set(nameof(InstanceType), instanceType);
|
||||
ContextProvider.Set(nameof(InstanceType), instance.ArrConfig.Type);
|
||||
ContextProvider.Set("version", instance.Version);
|
||||
|
||||
IReadOnlyList<IDownloadService> downloadServices = await GetInitializedDownloadServicesAsync();
|
||||
|
||||
@@ -205,7 +206,7 @@ public sealed class MalwareBlocker : GenericHandler
|
||||
|
||||
await PublishQueueItemRemoveRequest(
|
||||
downloadRemovalKey,
|
||||
instanceType,
|
||||
instance.ArrConfig.Type,
|
||||
instance,
|
||||
record,
|
||||
group.Count() > 1,
|
||||
|
||||
@@ -71,27 +71,28 @@ public sealed class QueueCleaner : GenericHandler
|
||||
var lidarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Lidarr));
|
||||
var readarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Readarr));
|
||||
var whisparrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Whisparr));
|
||||
|
||||
await ProcessArrConfigAsync(sonarrConfig, InstanceType.Sonarr);
|
||||
await ProcessArrConfigAsync(radarrConfig, InstanceType.Radarr);
|
||||
await ProcessArrConfigAsync(lidarrConfig, InstanceType.Lidarr);
|
||||
await ProcessArrConfigAsync(readarrConfig, InstanceType.Readarr);
|
||||
await ProcessArrConfigAsync(whisparrConfig, InstanceType.Whisparr);
|
||||
|
||||
await ProcessArrConfigAsync(sonarrConfig);
|
||||
await ProcessArrConfigAsync(radarrConfig);
|
||||
await ProcessArrConfigAsync(lidarrConfig);
|
||||
await ProcessArrConfigAsync(readarrConfig);
|
||||
await ProcessArrConfigAsync(whisparrConfig);
|
||||
}
|
||||
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance)
|
||||
{
|
||||
List<string> ignoredDownloads = ContextProvider.Get<GeneralConfig>(nameof(GeneralConfig)).IgnoredDownloads;
|
||||
QueueCleanerConfig queueCleanerConfig = ContextProvider.Get<QueueCleanerConfig>();
|
||||
ignoredDownloads.AddRange(queueCleanerConfig.IgnoredDownloads);
|
||||
|
||||
using var _ = LogContext.PushProperty(LogProperties.Category, instanceType.ToString());
|
||||
using var _ = LogContext.PushProperty(LogProperties.Category, instance.ArrConfig.Type.ToString());
|
||||
|
||||
IArrClient arrClient = _arrClientFactory.GetClient(instanceType);
|
||||
IArrClient arrClient = _arrClientFactory.GetClient(instance.ArrConfig.Type, instance.Version);
|
||||
|
||||
// push to context
|
||||
ContextProvider.Set(nameof(ArrInstance) + nameof(ArrInstance.Url), instance.Url);
|
||||
ContextProvider.Set(nameof(InstanceType), instanceType);
|
||||
ContextProvider.Set(nameof(InstanceType), instance.ArrConfig.Type);
|
||||
ContextProvider.Set("version", instance.Version);
|
||||
|
||||
IReadOnlyList<IDownloadService> downloadServices = await GetInitializedDownloadServicesAsync();
|
||||
bool hasEnabledTorrentClients = ContextProvider
|
||||
@@ -183,7 +184,7 @@ public sealed class QueueCleaner : GenericHandler
|
||||
|
||||
await PublishQueueItemRemoveRequest(
|
||||
downloadRemovalKey,
|
||||
instanceType,
|
||||
instance.ArrConfig.Type,
|
||||
instance,
|
||||
record,
|
||||
group.Count() > 1,
|
||||
@@ -203,7 +204,7 @@ public sealed class QueueCleaner : GenericHandler
|
||||
|
||||
// Failed import check
|
||||
bool shouldRemoveFromArr = await arrClient
|
||||
.ShouldRemoveFromQueue(instanceType, record, downloadCheckResult.IsPrivate, instance.ArrConfig.FailedImportMaxStrikes);
|
||||
.ShouldRemoveFromQueue(instance.ArrConfig.Type, record, downloadCheckResult.IsPrivate, instance.ArrConfig.FailedImportMaxStrikes);
|
||||
|
||||
if (shouldRemoveFromArr)
|
||||
{
|
||||
@@ -211,7 +212,7 @@ public sealed class QueueCleaner : GenericHandler
|
||||
|
||||
await PublishQueueItemRemoveRequest(
|
||||
downloadRemovalKey,
|
||||
instanceType,
|
||||
instance.ArrConfig.Type,
|
||||
instance,
|
||||
record,
|
||||
group.Count() > 1,
|
||||
|
||||
@@ -120,8 +120,9 @@ public class NotificationPublisher : INotificationPublisher
|
||||
{
|
||||
var record = ContextProvider.Get<QueueRecord>(nameof(QueueRecord));
|
||||
var instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||
var instanceVersion = (float)ContextProvider.Get<object>("version");
|
||||
var instanceUrl = ContextProvider.Get<Uri>(nameof(ArrInstance) + nameof(ArrInstance.Url));
|
||||
var imageUrl = GetImageFromContext(record, instanceType);
|
||||
var imageUrl = GetImageFromContext(record, instanceType, instanceVersion);
|
||||
|
||||
NotificationContext context = new()
|
||||
{
|
||||
@@ -153,8 +154,9 @@ public class NotificationPublisher : INotificationPublisher
|
||||
{
|
||||
var record = ContextProvider.Get<QueueRecord>(nameof(QueueRecord));
|
||||
var instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||
var instanceVersion = (float)ContextProvider.Get<object>("version");
|
||||
var instanceUrl = ContextProvider.Get<Uri>(nameof(ArrInstance) + nameof(ArrInstance.Url));
|
||||
var imageUrl = GetImageFromContext(record, instanceType);
|
||||
var imageUrl = GetImageFromContext(record, instanceType, instanceVersion);
|
||||
|
||||
return new NotificationContext
|
||||
{
|
||||
@@ -237,7 +239,7 @@ public class NotificationPublisher : INotificationPublisher
|
||||
};
|
||||
}
|
||||
|
||||
private Uri? GetImageFromContext(QueueRecord record, InstanceType instanceType)
|
||||
private Uri? GetImageFromContext(QueueRecord record, InstanceType instanceType, float version)
|
||||
{
|
||||
Uri? image = instanceType switch
|
||||
{
|
||||
@@ -245,7 +247,8 @@ public class NotificationPublisher : INotificationPublisher
|
||||
InstanceType.Radarr => record.Movie?.Images?.FirstOrDefault(x => x.CoverType == "poster")?.RemoteUrl,
|
||||
InstanceType.Lidarr => record.Album?.Images?.FirstOrDefault(x => x.CoverType == "cover")?.Url,
|
||||
InstanceType.Readarr => record.Book?.Images?.FirstOrDefault(x => x.CoverType == "cover")?.Url,
|
||||
InstanceType.Whisparr => record.Series?.Images?.FirstOrDefault(x => x.CoverType == "poster")?.RemoteUrl,
|
||||
InstanceType.Whisparr when version is 2 => record.Series?.Images?.FirstOrDefault(x => x.CoverType == "poster")?.RemoteUrl,
|
||||
InstanceType.Whisparr when version is 3 => record.Movie?.Images?.FirstOrDefault(x => x.CoverType == "poster")?.RemoteUrl ?? record.Movie?.Images?.FirstOrDefault(x => x.CoverType == "screenshot")?.RemoteUrl,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(instanceType))
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
@@ -22,7 +22,7 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
@@ -11,9 +11,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="10.0.0-rc.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" />
|
||||
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
|
||||
1109
code/backend/Cleanuparr.Persistence/Migrations/Data/20251231171212_AddWhisparrV3.Designer.cs
generated
Normal file
1109
code/backend/Cleanuparr.Persistence/Migrations/Data/20251231171212_AddWhisparrV3.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddWhisparrV3 : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<float>(
|
||||
name: "version",
|
||||
table: "arr_instances",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f);
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
UPDATE arr_instances
|
||||
SET version = CASE
|
||||
WHEN (
|
||||
SELECT type
|
||||
FROM arr_configs
|
||||
WHERE arr_configs.id = arr_instances.arr_config_id
|
||||
) = 'sonarr' THEN 4
|
||||
WHEN (
|
||||
SELECT type
|
||||
FROM arr_configs
|
||||
WHERE arr_configs.id = arr_instances.arr_config_id
|
||||
) = 'radarr' THEN 6
|
||||
WHEN (
|
||||
SELECT type
|
||||
FROM arr_configs
|
||||
WHERE arr_configs.id = arr_instances.arr_config_id
|
||||
) = 'lidarr' THEN 3
|
||||
WHEN (
|
||||
SELECT type
|
||||
FROM arr_configs
|
||||
WHERE arr_configs.id = arr_instances.arr_config_id
|
||||
) = 'readarr' THEN 0.4
|
||||
WHEN (
|
||||
SELECT type
|
||||
FROM arr_configs
|
||||
WHERE arr_configs.id = arr_instances.arr_config_id
|
||||
) = 'whisparr' THEN 2
|
||||
END;
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "version",
|
||||
table: "arr_instances");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,10 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("url");
|
||||
|
||||
b.Property<float>("Version")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("version");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_arr_instances");
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ public sealed class ArrInstance
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public float Version { get; set; }
|
||||
|
||||
public Guid ArrConfigId { get; set; }
|
||||
|
||||
public ArrConfig ArrConfig { get; set; } = null!;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -112,7 +112,6 @@
|
||||
"cli": {
|
||||
"schematicCollections": [
|
||||
"angular-eslint"
|
||||
],
|
||||
"analytics": false
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
806
code/frontend/package-lock.json
generated
806
code/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,16 +11,14 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^19.2.17",
|
||||
"@angular/cdk": "^19.2.17",
|
||||
"@angular/common": "^19.2.17",
|
||||
"@angular/compiler": "^19.2.17",
|
||||
"@angular/core": "^19.2.17",
|
||||
"@angular/forms": "^19.2.17",
|
||||
"@angular/platform-browser": "^19.2.17",
|
||||
"@angular/platform-browser-dynamic": "^19.2.17",
|
||||
"@angular/router": "^19.2.17",
|
||||
"@angular/service-worker": "^19.2.17",
|
||||
"@angular/common": "^19.2.16",
|
||||
"@angular/compiler": "^19.2.0",
|
||||
"@angular/core": "^19.2.16",
|
||||
"@angular/forms": "^19.2.16",
|
||||
"@angular/platform-browser": "^19.2.16",
|
||||
"@angular/platform-browser-dynamic": "^19.2.16",
|
||||
"@angular/router": "^19.2.16",
|
||||
"@angular/service-worker": "^19.2.16",
|
||||
"@microsoft/signalr": "^8.0.7",
|
||||
"@ngrx/signals": "^19.2.0",
|
||||
"@primeng/themes": "^19.1.3",
|
||||
@@ -32,9 +30,9 @@
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.2.17",
|
||||
"@angular/cli": "^19.2.17",
|
||||
"@angular/compiler-cli": "^19.2.17",
|
||||
"@angular-devkit/build-angular": "^19.2.12",
|
||||
"@angular/cli": "^19.2.12",
|
||||
"@angular/compiler-cli": "^19.2.16",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"angular-eslint": "19.6.0",
|
||||
"eslint": "^9.27.0",
|
||||
|
||||
@@ -216,12 +216,15 @@
|
||||
</div>
|
||||
<span class="text-xs text-color-secondary">{{event.timestamp | date: 'yyyy-MM-dd HH:mm:ss'}}</span>
|
||||
</div>
|
||||
<div class="timeline-message"
|
||||
[pTooltip]="event.message"
|
||||
<div class="timeline-message"
|
||||
[pTooltip]="event.message"
|
||||
tooltipPosition="top"
|
||||
[showDelay]="500">
|
||||
{{truncateMessage(event.message)}}
|
||||
</div>
|
||||
<div class="text-xs text-color-secondary mt-1" *ngIf="getDownloadName(event)">
|
||||
{{getDownloadName(event)}}
|
||||
</div>
|
||||
<div class="text-xs text-color-secondary mt-1" *ngIf="event.trackingId">
|
||||
Tracking: {{event.trackingId}}
|
||||
</div>
|
||||
|
||||
@@ -361,6 +361,12 @@ export class DashboardPageComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract downloadName from event data if available
|
||||
getDownloadName(event: AppEvent): string | null {
|
||||
const parsed = this.parseEventData(event.data);
|
||||
return parsed?.downloadName || null;
|
||||
}
|
||||
|
||||
// Process message to convert URLs to clickable links and handle newlines
|
||||
processManualEventMessage(message: string): string {
|
||||
if (!message) return '';
|
||||
|
||||
@@ -213,16 +213,30 @@
|
||||
|
||||
<div class="field">
|
||||
<label for="instance-apikey">API Key *</label>
|
||||
<input
|
||||
<input
|
||||
id="instance-apikey"
|
||||
type="password"
|
||||
pInputText
|
||||
formControlName="apiKey"
|
||||
type="password"
|
||||
pInputText
|
||||
formControlName="apiKey"
|
||||
placeholder="Your Lidarr API key"
|
||||
class="w-full"
|
||||
/>
|
||||
<small *ngIf="hasError(instanceForm, 'apiKey', 'required')" class="form-error-text">API key is required</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="instance-version">Version *</label>
|
||||
<p-select
|
||||
id="instance-version"
|
||||
formControlName="version"
|
||||
[options]="versionOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Select version"
|
||||
styleClass="w-full"
|
||||
></p-select>
|
||||
<small *ngIf="hasError(instanceForm, 'version', 'required')" class="form-error-text">Version is required</small>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ToastModule } from "primeng/toast";
|
||||
import { DialogModule } from "primeng/dialog";
|
||||
import { ConfirmDialogModule } from "primeng/confirmdialog";
|
||||
import { TagModule } from "primeng/tag";
|
||||
import { SelectModule } from "primeng/select";
|
||||
import { ConfirmationService } from "primeng/api";
|
||||
import { NotificationService } from "../../core/services/notification.service";
|
||||
import { LoadingErrorStateComponent } from "../../shared/components/loading-error-state/loading-error-state.component";
|
||||
@@ -37,6 +38,7 @@ import { UrlValidators } from "../../core/validators/url.validator";
|
||||
DialogModule,
|
||||
ConfirmDialogModule,
|
||||
TagModule,
|
||||
SelectModule,
|
||||
LoadingErrorStateComponent,
|
||||
],
|
||||
providers: [LidarrConfigStore, ConfirmationService],
|
||||
@@ -51,6 +53,10 @@ export class LidarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
globalForm: FormGroup;
|
||||
instanceForm: FormGroup;
|
||||
|
||||
versionOptions = [
|
||||
{ label: 'v3', value: 3 }
|
||||
];
|
||||
|
||||
// Modal state
|
||||
showInstanceModal = false;
|
||||
modalMode: 'add' | 'edit' = 'add';
|
||||
@@ -97,6 +103,7 @@ export class LidarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
name: ['', Validators.required],
|
||||
url: ['', [Validators.required, UrlValidators.httpUrl]],
|
||||
apiKey: ['', Validators.required],
|
||||
version: [3, Validators.required],
|
||||
});
|
||||
|
||||
// Load Lidarr config data
|
||||
@@ -316,7 +323,8 @@ export class LidarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
enabled: true,
|
||||
name: '',
|
||||
url: '',
|
||||
apiKey: ''
|
||||
apiKey: '',
|
||||
version: 3
|
||||
});
|
||||
this.showInstanceModal = true;
|
||||
}
|
||||
@@ -332,6 +340,7 @@ export class LidarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
name: instance.name,
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
version: instance.version,
|
||||
});
|
||||
this.showInstanceModal = true;
|
||||
}
|
||||
@@ -361,6 +370,7 @@ export class LidarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
name: this.instanceForm.get('name')?.value,
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
version: this.instanceForm.get('version')?.value,
|
||||
};
|
||||
|
||||
if (this.modalMode === 'add') {
|
||||
@@ -452,6 +462,7 @@ export class LidarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
version: this.instanceForm.get('version')?.value,
|
||||
};
|
||||
|
||||
this.lidarrStore.testInstance({ request: testRequest, instanceId: this.editingInstance?.id });
|
||||
@@ -464,6 +475,7 @@ export class LidarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
version: instance.version,
|
||||
};
|
||||
|
||||
this.lidarrStore.testInstance({ request: testRequest, instanceId: instance.id });
|
||||
|
||||
@@ -213,16 +213,30 @@
|
||||
|
||||
<div class="field">
|
||||
<label for="instance-apikey">API Key *</label>
|
||||
<input
|
||||
<input
|
||||
id="instance-apikey"
|
||||
type="password"
|
||||
pInputText
|
||||
formControlName="apiKey"
|
||||
type="password"
|
||||
pInputText
|
||||
formControlName="apiKey"
|
||||
placeholder="Your Radarr API key"
|
||||
class="w-full"
|
||||
/>
|
||||
<small *ngIf="hasError(instanceForm, 'apiKey', 'required')" class="form-error-text">API key is required</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="instance-version">Version *</label>
|
||||
<p-select
|
||||
id="instance-version"
|
||||
formControlName="version"
|
||||
[options]="versionOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Select version"
|
||||
styleClass="w-full"
|
||||
></p-select>
|
||||
<small *ngIf="hasError(instanceForm, 'version', 'required')" class="form-error-text">Version is required</small>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ToastModule } from "primeng/toast";
|
||||
import { DialogModule } from "primeng/dialog";
|
||||
import { ConfirmDialogModule } from "primeng/confirmdialog";
|
||||
import { TagModule } from "primeng/tag";
|
||||
import { SelectModule } from "primeng/select";
|
||||
import { ConfirmationService } from "primeng/api";
|
||||
import { NotificationService } from "../../core/services/notification.service";
|
||||
import { LoadingErrorStateComponent } from "../../shared/components/loading-error-state/loading-error-state.component";
|
||||
@@ -37,6 +38,7 @@ import { UrlValidators } from "../../core/validators/url.validator";
|
||||
DialogModule,
|
||||
ConfirmDialogModule,
|
||||
TagModule,
|
||||
SelectModule,
|
||||
LoadingErrorStateComponent,
|
||||
],
|
||||
providers: [RadarrConfigStore, ConfirmationService],
|
||||
@@ -51,6 +53,11 @@ export class RadarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
globalForm: FormGroup;
|
||||
instanceForm: FormGroup;
|
||||
|
||||
// Version options for Radarr (v6 only)
|
||||
versionOptions = [
|
||||
{ label: 'v6', value: 6 }
|
||||
];
|
||||
|
||||
// Modal state
|
||||
showInstanceModal = false;
|
||||
modalMode: 'add' | 'edit' = 'add';
|
||||
@@ -97,6 +104,7 @@ export class RadarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
name: ['', Validators.required],
|
||||
url: ['', [Validators.required, UrlValidators.httpUrl]],
|
||||
apiKey: ['', Validators.required],
|
||||
version: [6, Validators.required],
|
||||
});
|
||||
|
||||
// Load Radarr config data
|
||||
@@ -316,7 +324,8 @@ export class RadarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
enabled: true,
|
||||
name: '',
|
||||
url: '',
|
||||
apiKey: ''
|
||||
apiKey: '',
|
||||
version: 6
|
||||
});
|
||||
this.showInstanceModal = true;
|
||||
}
|
||||
@@ -332,6 +341,7 @@ export class RadarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
name: instance.name,
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
version: instance.version,
|
||||
});
|
||||
this.showInstanceModal = true;
|
||||
}
|
||||
@@ -361,6 +371,7 @@ export class RadarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
name: this.instanceForm.get('name')?.value,
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
version: this.instanceForm.get('version')?.value,
|
||||
};
|
||||
|
||||
if (this.modalMode === 'add') {
|
||||
@@ -452,6 +463,7 @@ export class RadarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
version: this.instanceForm.get('version')?.value,
|
||||
};
|
||||
|
||||
this.radarrStore.testInstance({ request: testRequest, instanceId: this.editingInstance?.id });
|
||||
@@ -464,6 +476,7 @@ export class RadarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
version: instance.version,
|
||||
};
|
||||
|
||||
this.radarrStore.testInstance({ request: testRequest, instanceId: instance.id });
|
||||
|
||||
@@ -213,16 +213,30 @@
|
||||
|
||||
<div class="field">
|
||||
<label for="instance-apikey">API Key *</label>
|
||||
<input
|
||||
<input
|
||||
id="instance-apikey"
|
||||
type="password"
|
||||
pInputText
|
||||
formControlName="apiKey"
|
||||
type="password"
|
||||
pInputText
|
||||
formControlName="apiKey"
|
||||
placeholder="Your Readarr API key"
|
||||
class="w-full"
|
||||
/>
|
||||
<small *ngIf="hasError(instanceForm, 'apiKey', 'required')" class="form-error-text">API key is required</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="instance-version">Version *</label>
|
||||
<p-select
|
||||
id="instance-version"
|
||||
formControlName="version"
|
||||
[options]="versionOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Select version"
|
||||
styleClass="w-full"
|
||||
></p-select>
|
||||
<small *ngIf="hasError(instanceForm, 'version', 'required')" class="form-error-text">Version is required</small>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ToastModule } from "primeng/toast";
|
||||
import { DialogModule } from "primeng/dialog";
|
||||
import { ConfirmDialogModule } from "primeng/confirmdialog";
|
||||
import { TagModule } from "primeng/tag";
|
||||
import { SelectModule } from "primeng/select";
|
||||
import { ConfirmationService } from "primeng/api";
|
||||
import { NotificationService } from "../../core/services/notification.service";
|
||||
import { LoadingErrorStateComponent } from "../../shared/components/loading-error-state/loading-error-state.component";
|
||||
@@ -37,6 +38,7 @@ import { UrlValidators } from "../../core/validators/url.validator";
|
||||
DialogModule,
|
||||
ConfirmDialogModule,
|
||||
TagModule,
|
||||
SelectModule,
|
||||
LoadingErrorStateComponent,
|
||||
],
|
||||
providers: [ReadarrConfigStore, ConfirmationService],
|
||||
@@ -51,6 +53,10 @@ export class ReadarrSettingsComponent implements OnDestroy, CanComponentDeactiva
|
||||
globalForm: FormGroup;
|
||||
instanceForm: FormGroup;
|
||||
|
||||
versionOptions = [
|
||||
{ label: 'v0.4', value: 0.4 }
|
||||
];
|
||||
|
||||
// Modal state
|
||||
showInstanceModal = false;
|
||||
modalMode: 'add' | 'edit' = 'add';
|
||||
@@ -97,6 +103,7 @@ export class ReadarrSettingsComponent implements OnDestroy, CanComponentDeactiva
|
||||
name: ['', Validators.required],
|
||||
url: ['', [Validators.required, UrlValidators.httpUrl]],
|
||||
apiKey: ['', Validators.required],
|
||||
version: [0.4, Validators.required],
|
||||
});
|
||||
|
||||
// Load Readarr config data
|
||||
@@ -303,7 +310,8 @@ export class ReadarrSettingsComponent implements OnDestroy, CanComponentDeactiva
|
||||
enabled: true,
|
||||
name: '',
|
||||
url: '',
|
||||
apiKey: ''
|
||||
apiKey: '',
|
||||
version: 0.4
|
||||
});
|
||||
this.showInstanceModal = true;
|
||||
}
|
||||
@@ -319,6 +327,7 @@ export class ReadarrSettingsComponent implements OnDestroy, CanComponentDeactiva
|
||||
name: instance.name,
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
version: instance.version,
|
||||
});
|
||||
this.showInstanceModal = true;
|
||||
}
|
||||
@@ -348,6 +357,7 @@ export class ReadarrSettingsComponent implements OnDestroy, CanComponentDeactiva
|
||||
name: this.instanceForm.get('name')?.value,
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
version: this.instanceForm.get('version')?.value,
|
||||
};
|
||||
|
||||
if (this.modalMode === 'add') {
|
||||
@@ -452,6 +462,7 @@ export class ReadarrSettingsComponent implements OnDestroy, CanComponentDeactiva
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
version: this.instanceForm.get('version')?.value,
|
||||
};
|
||||
|
||||
this.readarrStore.testInstance({ request: testRequest, instanceId: this.editingInstance?.id });
|
||||
@@ -464,6 +475,7 @@ export class ReadarrSettingsComponent implements OnDestroy, CanComponentDeactiva
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
version: instance.version,
|
||||
};
|
||||
|
||||
this.readarrStore.testInstance({ request: testRequest, instanceId: instance.id });
|
||||
|
||||
@@ -213,16 +213,30 @@
|
||||
|
||||
<div class="field">
|
||||
<label for="instance-apikey">API Key *</label>
|
||||
<input
|
||||
<input
|
||||
id="instance-apikey"
|
||||
type="password"
|
||||
pInputText
|
||||
formControlName="apiKey"
|
||||
type="password"
|
||||
pInputText
|
||||
formControlName="apiKey"
|
||||
placeholder="Your Sonarr API key"
|
||||
class="w-full"
|
||||
/>
|
||||
<small *ngIf="hasError(instanceForm, 'apiKey', 'required')" class="form-error-text">API key is required</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="instance-version">Version *</label>
|
||||
<p-select
|
||||
id="instance-version"
|
||||
formControlName="version"
|
||||
[options]="versionOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Select version"
|
||||
styleClass="w-full"
|
||||
></p-select>
|
||||
<small *ngIf="hasError(instanceForm, 'version', 'required')" class="form-error-text">Version is required</small>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ToastModule } from "primeng/toast";
|
||||
import { DialogModule } from "primeng/dialog";
|
||||
import { ConfirmDialogModule } from "primeng/confirmdialog";
|
||||
import { TagModule } from "primeng/tag";
|
||||
import { SelectModule } from "primeng/select";
|
||||
import { ConfirmationService } from "primeng/api";
|
||||
import { NotificationService } from "../../core/services/notification.service";
|
||||
import { LoadingErrorStateComponent } from "../../shared/components/loading-error-state/loading-error-state.component";
|
||||
@@ -37,6 +38,7 @@ import { UrlValidators } from "../../core/validators/url.validator";
|
||||
DialogModule,
|
||||
ConfirmDialogModule,
|
||||
TagModule,
|
||||
SelectModule,
|
||||
LoadingErrorStateComponent,
|
||||
],
|
||||
providers: [SonarrConfigStore, ConfirmationService],
|
||||
@@ -51,6 +53,10 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
globalForm: FormGroup;
|
||||
instanceForm: FormGroup;
|
||||
|
||||
versionOptions = [
|
||||
{ label: 'v4', value: 4 }
|
||||
];
|
||||
|
||||
// Modal state
|
||||
showInstanceModal = false;
|
||||
modalMode: 'add' | 'edit' = 'add';
|
||||
@@ -97,6 +103,7 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
name: ['', Validators.required],
|
||||
url: ['', [Validators.required, UrlValidators.httpUrl]],
|
||||
apiKey: ['', Validators.required],
|
||||
version: [4, Validators.required],
|
||||
});
|
||||
|
||||
// Load Sonarr config data
|
||||
@@ -316,7 +323,8 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
enabled: true,
|
||||
name: '',
|
||||
url: '',
|
||||
apiKey: ''
|
||||
apiKey: '',
|
||||
version: 4
|
||||
});
|
||||
this.showInstanceModal = true;
|
||||
}
|
||||
@@ -332,6 +340,7 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
name: instance.name,
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
version: instance.version,
|
||||
});
|
||||
this.showInstanceModal = true;
|
||||
}
|
||||
@@ -361,6 +370,7 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
name: this.instanceForm.get('name')?.value,
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
version: this.instanceForm.get('version')?.value,
|
||||
};
|
||||
|
||||
if (this.modalMode === 'add') {
|
||||
@@ -452,6 +462,7 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
version: this.instanceForm.get('version')?.value,
|
||||
};
|
||||
|
||||
this.sonarrStore.testInstance({ request: testRequest, instanceId: this.editingInstance?.id });
|
||||
@@ -464,6 +475,7 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
version: instance.version,
|
||||
};
|
||||
|
||||
this.sonarrStore.testInstance({ request: testRequest, instanceId: instance.id });
|
||||
|
||||
@@ -213,16 +213,30 @@
|
||||
|
||||
<div class="field">
|
||||
<label for="instance-apikey">API Key *</label>
|
||||
<input
|
||||
<input
|
||||
id="instance-apikey"
|
||||
type="password"
|
||||
pInputText
|
||||
formControlName="apiKey"
|
||||
type="password"
|
||||
pInputText
|
||||
formControlName="apiKey"
|
||||
placeholder="Your Whisparr API key"
|
||||
class="w-full"
|
||||
/>
|
||||
<small *ngIf="hasError(instanceForm, 'apiKey', 'required')" class="form-error-text">API key is required</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="instance-version">Version *</label>
|
||||
<p-select
|
||||
id="instance-version"
|
||||
formControlName="version"
|
||||
[options]="versionOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Select version"
|
||||
styleClass="w-full"
|
||||
></p-select>
|
||||
<small *ngIf="hasError(instanceForm, 'version', 'required')" class="form-error-text">Version is required</small>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ToastModule } from "primeng/toast";
|
||||
import { DialogModule } from "primeng/dialog";
|
||||
import { ConfirmDialogModule } from "primeng/confirmdialog";
|
||||
import { TagModule } from "primeng/tag";
|
||||
import { SelectModule } from "primeng/select";
|
||||
import { ConfirmationService } from "primeng/api";
|
||||
import { NotificationService } from "../../core/services/notification.service";
|
||||
import { LoadingErrorStateComponent } from "../../shared/components/loading-error-state/loading-error-state.component";
|
||||
@@ -37,6 +38,7 @@ import { UrlValidators } from "../../core/validators/url.validator";
|
||||
DialogModule,
|
||||
ConfirmDialogModule,
|
||||
TagModule,
|
||||
SelectModule,
|
||||
LoadingErrorStateComponent,
|
||||
],
|
||||
providers: [WhisparrConfigStore, ConfirmationService],
|
||||
@@ -51,6 +53,11 @@ export class WhisparrSettingsComponent implements OnDestroy, CanComponentDeactiv
|
||||
globalForm: FormGroup;
|
||||
instanceForm: FormGroup;
|
||||
|
||||
versionOptions = [
|
||||
{ label: 'v2', value: 2 },
|
||||
{ label: 'v3', value: 3 }
|
||||
];
|
||||
|
||||
// Modal state
|
||||
showInstanceModal = false;
|
||||
modalMode: 'add' | 'edit' = 'add';
|
||||
@@ -97,6 +104,7 @@ export class WhisparrSettingsComponent implements OnDestroy, CanComponentDeactiv
|
||||
name: ['', Validators.required],
|
||||
url: ['', [Validators.required, UrlValidators.httpUrl]],
|
||||
apiKey: ['', Validators.required],
|
||||
version: [3, Validators.required],
|
||||
});
|
||||
|
||||
// Load Whisparr config data
|
||||
@@ -311,7 +319,8 @@ export class WhisparrSettingsComponent implements OnDestroy, CanComponentDeactiv
|
||||
enabled: true,
|
||||
name: '',
|
||||
url: '',
|
||||
apiKey: ''
|
||||
apiKey: '',
|
||||
version: 3
|
||||
});
|
||||
this.showInstanceModal = true;
|
||||
}
|
||||
@@ -327,6 +336,7 @@ export class WhisparrSettingsComponent implements OnDestroy, CanComponentDeactiv
|
||||
name: instance.name,
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
version: instance.version,
|
||||
});
|
||||
this.showInstanceModal = true;
|
||||
}
|
||||
@@ -356,6 +366,7 @@ export class WhisparrSettingsComponent implements OnDestroy, CanComponentDeactiv
|
||||
name: this.instanceForm.get('name')?.value,
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
version: this.instanceForm.get('version')?.value,
|
||||
};
|
||||
|
||||
if (this.modalMode === 'add') {
|
||||
@@ -447,6 +458,7 @@ export class WhisparrSettingsComponent implements OnDestroy, CanComponentDeactiv
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
version: this.instanceForm.get('version')?.value,
|
||||
};
|
||||
|
||||
this.whisparrStore.testInstance({ request: testRequest, instanceId: this.editingInstance?.id });
|
||||
@@ -459,6 +471,7 @@ export class WhisparrSettingsComponent implements OnDestroy, CanComponentDeactiv
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
version: instance.version,
|
||||
};
|
||||
|
||||
this.whisparrStore.testInstance({ request: testRequest, instanceId: instance.id });
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface ArrInstance {
|
||||
name: string;
|
||||
url: string;
|
||||
apiKey: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -17,6 +18,7 @@ export interface CreateArrInstanceDto {
|
||||
name: string;
|
||||
url: string;
|
||||
apiKey: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,4 +27,5 @@ export interface CreateArrInstanceDto {
|
||||
export interface TestArrInstanceRequest {
|
||||
url: string;
|
||||
apiKey: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
import { Important } from '@site/src/components/documentation';
|
||||
import {
|
||||
AppCard,
|
||||
styles
|
||||
@@ -10,7 +11,11 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
# Supported Apps
|
||||
|
||||
Cleanuparr integrates with popular *arr applications and download clients for media management automation.
|
||||
Cleanuparr integrates with the servarr applications and download clients for media management automation.
|
||||
|
||||
<Important>
|
||||
Only the latest versions of the following applications are supported, unless explicitly specified otherwise.
|
||||
</Important>
|
||||
|
||||
<div className={styles.documentationPage}>
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ services:
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `PORT` | 11011 | Port for the web interface |
|
||||
| `BIND_ADDRESS` | 0.0.0.0 | IP address to bind to |
|
||||
| `BASE_PATH` | *(empty)* | Base path for reverse proxy setups ([examples](#example-configurations)) |
|
||||
| `PUID` | 1000 | User ID for file permissions |
|
||||
| `PGID` | 1000 | Group ID for file permissions |
|
||||
@@ -277,6 +278,7 @@ To run Cleanuparr as a systemd service:
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=PORT=11011
|
||||
Environment=BIND_ADDRESS=0.0.0.0
|
||||
Environment=BASE_PATH=
|
||||
|
||||
[Install]
|
||||
@@ -330,7 +332,7 @@ To change the port or base path for AUR installations, see [Port and Base Path (
|
||||
|
||||
<SectionTitle>Port and Base Path (Non-Docker)</SectionTitle>
|
||||
|
||||
For all non-Docker installations (Windows, macOS, Linux portable), you can configure the PORT and BASE_PATH by creating a configuration file.
|
||||
For all non-Docker installations (Windows, macOS, Linux portable), you can configure the PORT, BIND_ADDRESS and BASE_PATH by creating a configuration file.
|
||||
|
||||
### Initial Setup
|
||||
|
||||
@@ -347,6 +349,7 @@ For all non-Docker installations (Windows, macOS, Linux portable), you can confi
|
||||
```json
|
||||
{
|
||||
"PORT": 11011,
|
||||
"BIND_ADDRESS": "0.0.0.0",
|
||||
"BASE_PATH": ""
|
||||
}
|
||||
```
|
||||
@@ -358,11 +361,19 @@ For all non-Docker installations (Windows, macOS, Linux portable), you can confi
|
||||
**Running on port 8080:**
|
||||
```json
|
||||
{
|
||||
"PORT": 8080,
|
||||
"PORT": 8080
|
||||
}
|
||||
```
|
||||
Access via: `http://localhost:8080`
|
||||
|
||||
**Binding to localhost only:**
|
||||
```json
|
||||
{
|
||||
"BIND_ADDRESS": "127.0.0.1"
|
||||
}
|
||||
```
|
||||
Only accessible from the local machine.
|
||||
|
||||
**Running with reverse proxy at `/cleanuparr`:**
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -39,16 +39,16 @@ Download and configure the .NET SDK for FreeBSD:
|
||||
cd ~
|
||||
|
||||
# Set up variables for cleaner commands
|
||||
DOTNET_VERSION="v10.0.101-amd64-freebsd-14"
|
||||
DOTNET_VERSION="v9.0.104-amd64-freebsd-14"
|
||||
DOTNET_BASE_URL="https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download"
|
||||
|
||||
# Download .NET SDK
|
||||
wget -q "${DOTNET_BASE_URL}/${DOTNET_VERSION}/dotnet-sdk-10.0.101-freebsd-x64.tar.gz"
|
||||
wget -q "${DOTNET_BASE_URL}/${DOTNET_VERSION}/dotnet-sdk-9.0.104-freebsd-x64.tar.gz"
|
||||
|
||||
# Set up .NET environment
|
||||
export DOTNET_ROOT=$(pwd)/.dotnet
|
||||
mkdir -p "$DOTNET_ROOT"
|
||||
tar zxf dotnet-sdk-10.0.101-freebsd-x64.tar.gz -C "$DOTNET_ROOT"
|
||||
tar zxf dotnet-sdk-9.0.104-freebsd-x64.tar.gz -C "$DOTNET_ROOT"
|
||||
export PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools
|
||||
```
|
||||
</Step>
|
||||
@@ -70,7 +70,7 @@ mkdir -p /tmp/nuget
|
||||
|
||||
# Set up variables for package URLs
|
||||
NUGET_BASE_URL="${DOTNET_BASE_URL}/${DOTNET_VERSION}"
|
||||
RUNTIME_VERSION="10.0.1"
|
||||
RUNTIME_VERSION="9.0.3"
|
||||
|
||||
# Download required packages
|
||||
wget -q -P /tmp/nuget/ \
|
||||
|
||||
37
docs/package-lock.json
generated
37
docs/package-lock.json
generated
@@ -220,7 +220,6 @@
|
||||
"version": "5.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.41.0.tgz",
|
||||
"integrity": "sha512-G9I2atg1ShtFp0t7zwleP6aPS4DcZvsV4uoQOripp16aR6VJzbEnKFPLW4OFXzX7avgZSpYeBAS+Zx4FOgmpPw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "5.41.0",
|
||||
"@algolia/requester-browser-xhr": "5.41.0",
|
||||
@@ -336,7 +335,6 @@
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -2017,7 +2015,6 @@
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -2039,7 +2036,6 @@
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -2144,7 +2140,6 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
|
||||
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -2550,7 +2545,6 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
|
||||
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -3395,7 +3389,6 @@
|
||||
"version": "3.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz",
|
||||
"integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.9.2",
|
||||
"@docusaurus/logger": "3.9.2",
|
||||
@@ -4112,7 +4105,6 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz",
|
||||
"integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/mdx": "^2.0.0"
|
||||
},
|
||||
@@ -4405,7 +4397,6 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz",
|
||||
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@svgr/babel-preset": "8.1.0",
|
||||
@@ -4739,7 +4730,6 @@
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -5052,7 +5042,6 @@
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -5120,7 +5109,6 @@
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -5163,7 +5151,6 @@
|
||||
"version": "5.41.0",
|
||||
"resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.41.0.tgz",
|
||||
"integrity": "sha512-9E4b3rJmYbBkn7e3aAPt1as+VVnRhsR4qwRRgOzpeyz4PAOuwKh0HI4AN6mTrqK0S0M9fCCSTOUnuJ8gPY/tvA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@algolia/abtesting": "1.7.0",
|
||||
"@algolia/client-abtesting": "5.41.0",
|
||||
@@ -5704,7 +5691,6 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
@@ -6627,7 +6613,6 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
|
||||
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -7930,7 +7915,6 @@
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -10047,10 +10031,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-hast": {
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
|
||||
"integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
|
||||
"license": "MIT",
|
||||
"version": "13.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
|
||||
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
@@ -12147,7 +12130,6 @@
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -12628,7 +12610,6 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -13484,7 +13465,6 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
|
||||
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -14281,7 +14261,6 @@
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -14290,7 +14269,6 @@
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@@ -14341,7 +14319,6 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz",
|
||||
"integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
},
|
||||
@@ -14394,7 +14371,6 @@
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
|
||||
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.13",
|
||||
"history": "^4.9.0",
|
||||
@@ -16165,8 +16141,7 @@
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"peer": true
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
@@ -16234,7 +16209,6 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -16552,7 +16526,6 @@
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -16740,7 +16713,6 @@
|
||||
"version": "5.99.5",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.5.tgz",
|
||||
"integrity": "sha512-q+vHBa6H9qwBLUlHL4Y7L0L1/LlyBKZtS9FHNCQmtayxjI5RKC9yD8gpvLeqGv5lCQp1Re04yi0MF40pf30Pvg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.6",
|
||||
@@ -17315,7 +17287,6 @@
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
|
||||
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -4668,11 +4668,6 @@ fs-extra@^11.1.1, fs-extra@^11.2.0:
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
|
||||
fsevents@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
|
||||
@@ -5901,9 +5896,9 @@ mdast-util-phrasing@^4.0.0:
|
||||
unist-util-is "^6.0.0"
|
||||
|
||||
mdast-util-to-hast@^13.0.0:
|
||||
version "13.2.1"
|
||||
resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz"
|
||||
integrity sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==
|
||||
version "13.2.0"
|
||||
resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz"
|
||||
integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==
|
||||
dependencies:
|
||||
"@types/hast" "^3.0.0"
|
||||
"@types/mdast" "^4.0.0"
|
||||
|
||||
Reference in New Issue
Block a user