mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-01-02 10:57:52 -05:00
Compare commits
5 Commits
main
...
add_whispa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
530ddf01f0 | ||
|
|
e465582a01 | ||
|
|
01238759b6 | ||
|
|
acb98db17f | ||
|
|
8c16a8b9dd |
@@ -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" });
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using Cleanuparr.Infrastructure.Features.Arr;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.Arr;
|
||||
|
||||
public class WhisparrV2ClientTests
|
||||
{
|
||||
private readonly Mock<ILogger<WhisparrV2Client>> _loggerMock;
|
||||
private readonly Mock<IHttpClientFactory> _httpClientFactoryMock;
|
||||
private readonly Mock<IStriker> _strikerMock;
|
||||
private readonly Mock<IDryRunInterceptor> _dryRunInterceptorMock;
|
||||
private readonly Mock<HttpMessageHandler> _httpMessageHandlerMock;
|
||||
private readonly WhisparrV2Client _client;
|
||||
|
||||
public WhisparrV2ClientTests()
|
||||
{
|
||||
_loggerMock = new Mock<ILogger<WhisparrV2Client>>();
|
||||
_httpClientFactoryMock = new Mock<IHttpClientFactory>();
|
||||
_strikerMock = new Mock<IStriker>();
|
||||
_dryRunInterceptorMock = new Mock<IDryRunInterceptor>();
|
||||
_httpMessageHandlerMock = new Mock<HttpMessageHandler>();
|
||||
|
||||
var httpClient = new HttpClient(_httpMessageHandlerMock.Object);
|
||||
_httpClientFactoryMock.Setup(x => x.CreateClient(It.IsAny<string>())).Returns(httpClient);
|
||||
|
||||
_client = new WhisparrV2Client(
|
||||
_loggerMock.Object,
|
||||
_httpClientFactoryMock.Object,
|
||||
_strikerMock.Object,
|
||||
_dryRunInterceptorMock.Object
|
||||
);
|
||||
}
|
||||
|
||||
#region IsRecordValid Tests
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenEpisodeIdIsZero_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Episode",
|
||||
DownloadId = "abc123",
|
||||
Protocol = "torrent",
|
||||
EpisodeId = 0,
|
||||
SeriesId = 1
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
_loggerMock.Verify(
|
||||
x => x.Log(
|
||||
LogLevel.Debug,
|
||||
It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("episode id and/or series id missing")),
|
||||
It.IsAny<Exception?>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenSeriesIdIsZero_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Episode",
|
||||
DownloadId = "abc123",
|
||||
Protocol = "torrent",
|
||||
EpisodeId = 1,
|
||||
SeriesId = 0
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenBothIdsAreZero_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Episode",
|
||||
DownloadId = "abc123",
|
||||
Protocol = "torrent",
|
||||
EpisodeId = 0,
|
||||
SeriesId = 0
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenBothIdsAreSet_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Episode",
|
||||
DownloadId = "abc123",
|
||||
Protocol = "torrent",
|
||||
EpisodeId = 42,
|
||||
SeriesId = 10
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenDownloadIdIsNull_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Episode",
|
||||
DownloadId = null!,
|
||||
Protocol = "torrent",
|
||||
EpisodeId = 42,
|
||||
SeriesId = 10
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenDownloadIdIsEmpty_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Episode",
|
||||
DownloadId = "",
|
||||
Protocol = "torrent",
|
||||
EpisodeId = 42,
|
||||
SeriesId = 10
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
using Cleanuparr.Infrastructure.Features.Arr;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.Arr;
|
||||
|
||||
public class WhisparrV3ClientTests
|
||||
{
|
||||
private readonly Mock<ILogger<WhisparrV3Client>> _loggerMock;
|
||||
private readonly Mock<IHttpClientFactory> _httpClientFactoryMock;
|
||||
private readonly Mock<IStriker> _strikerMock;
|
||||
private readonly Mock<IDryRunInterceptor> _dryRunInterceptorMock;
|
||||
private readonly Mock<HttpMessageHandler> _httpMessageHandlerMock;
|
||||
private readonly WhisparrV3Client _client;
|
||||
|
||||
public WhisparrV3ClientTests()
|
||||
{
|
||||
_loggerMock = new Mock<ILogger<WhisparrV3Client>>();
|
||||
_httpClientFactoryMock = new Mock<IHttpClientFactory>();
|
||||
_strikerMock = new Mock<IStriker>();
|
||||
_dryRunInterceptorMock = new Mock<IDryRunInterceptor>();
|
||||
_httpMessageHandlerMock = new Mock<HttpMessageHandler>();
|
||||
|
||||
var httpClient = new HttpClient(_httpMessageHandlerMock.Object);
|
||||
_httpClientFactoryMock.Setup(x => x.CreateClient(It.IsAny<string>())).Returns(httpClient);
|
||||
|
||||
_client = new WhisparrV3Client(
|
||||
_loggerMock.Object,
|
||||
_httpClientFactoryMock.Object,
|
||||
_strikerMock.Object,
|
||||
_dryRunInterceptorMock.Object
|
||||
);
|
||||
}
|
||||
|
||||
#region IsRecordValid Tests
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenMovieIdIsZero_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Movie",
|
||||
DownloadId = "abc123",
|
||||
Protocol = "torrent",
|
||||
MovieId = 0
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
_loggerMock.Verify(
|
||||
x => x.Log(
|
||||
LogLevel.Debug,
|
||||
It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("movie id missing")),
|
||||
It.IsAny<Exception?>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenMovieIdIsSet_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Movie",
|
||||
DownloadId = "abc123",
|
||||
Protocol = "torrent",
|
||||
MovieId = 42
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenDownloadIdIsNull_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Movie",
|
||||
DownloadId = null!,
|
||||
Protocol = "torrent",
|
||||
MovieId = 42
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRecordValid_WhenDownloadIdIsEmpty_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var record = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
Title = "Test Movie",
|
||||
DownloadId = "",
|
||||
Protocol = "torrent",
|
||||
MovieId = 42
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _client.IsRecordValid(record);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -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
|
||||
@@ -1040,5 +1040,240 @@ public class QueueCleanerTests : IDisposable
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PublishQueueItemRemoveRequest_ForWhisparrV2_PublishesSeriesSearchItemRequest()
|
||||
{
|
||||
// Arrange - test that Whisparr v2 uses SeriesSearchItem
|
||||
var whisparrInstance = TestDataContextFactory.AddWhisparrInstance(_fixture.DataContext, version: 2);
|
||||
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Whisparr, 2f))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
DownloadId = "whisparr-v2-download-id",
|
||||
Title = "Whisparr V2 Download",
|
||||
Protocol = "torrent",
|
||||
SeriesId = 10,
|
||||
EpisodeId = 100
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
.Setup(x => x.Iterate(
|
||||
It.IsAny<IArrClient>(),
|
||||
It.IsAny<ArrInstance>(),
|
||||
It.IsAny<Func<IReadOnlyList<QueueRecord>, Task>>()
|
||||
))
|
||||
.Returns(async (IArrClient client, ArrInstance instance, Func<IReadOnlyList<QueueRecord>, Task> callback) =>
|
||||
{
|
||||
await callback([queueRecord]);
|
||||
});
|
||||
|
||||
var mockDownloadService = _fixture.CreateMockDownloadService();
|
||||
mockDownloadService
|
||||
.Setup(x => x.ShouldRemoveFromArrQueueAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<List<string>>()
|
||||
))
|
||||
.ReturnsAsync(new DownloadCheckResult
|
||||
{
|
||||
Found = true,
|
||||
ShouldRemove = true,
|
||||
IsPrivate = false,
|
||||
DeleteFromClient = true,
|
||||
DeleteReason = DeleteReason.Stalled
|
||||
});
|
||||
|
||||
_fixture.DownloadServiceFactory
|
||||
.Setup(x => x.GetDownloadService(It.IsAny<DownloadClientConfig>()))
|
||||
.Returns(mockDownloadService.Object);
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert - should publish QueueItemRemoveRequest<SeriesSearchItem>
|
||||
_fixture.MessageBus.Verify(
|
||||
x => x.Publish(
|
||||
It.Is<QueueItemRemoveRequest<SeriesSearchItem>>(r =>
|
||||
r.InstanceType == InstanceType.Whisparr &&
|
||||
r.SearchItem.Id == 100 && // EpisodeId
|
||||
r.SearchItem.SeriesId == 10 &&
|
||||
r.SearchItem.SearchType == SeriesSearchType.Episode &&
|
||||
r.DeleteReason == DeleteReason.Stalled
|
||||
),
|
||||
It.IsAny<CancellationToken>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PublishQueueItemRemoveRequest_ForWhisparrV3_PublishesSearchItemRequest()
|
||||
{
|
||||
// Arrange - test that Whisparr v3 uses SearchItem
|
||||
var whisparrInstance = TestDataContextFactory.AddWhisparrInstance(_fixture.DataContext, version: 3);
|
||||
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Whisparr, 3f))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
DownloadId = "whisparr-v3-download-id",
|
||||
Title = "Whisparr V3 Download",
|
||||
Protocol = "torrent",
|
||||
MovieId = 42
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
.Setup(x => x.Iterate(
|
||||
It.IsAny<IArrClient>(),
|
||||
It.IsAny<ArrInstance>(),
|
||||
It.IsAny<Func<IReadOnlyList<QueueRecord>, Task>>()
|
||||
))
|
||||
.Returns(async (IArrClient client, ArrInstance instance, Func<IReadOnlyList<QueueRecord>, Task> callback) =>
|
||||
{
|
||||
await callback([queueRecord]);
|
||||
});
|
||||
|
||||
var mockDownloadService = _fixture.CreateMockDownloadService();
|
||||
mockDownloadService
|
||||
.Setup(x => x.ShouldRemoveFromArrQueueAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<List<string>>()
|
||||
))
|
||||
.ReturnsAsync(new DownloadCheckResult
|
||||
{
|
||||
Found = true,
|
||||
ShouldRemove = true,
|
||||
IsPrivate = false,
|
||||
DeleteFromClient = true,
|
||||
DeleteReason = DeleteReason.Stalled
|
||||
});
|
||||
|
||||
_fixture.DownloadServiceFactory
|
||||
.Setup(x => x.GetDownloadService(It.IsAny<DownloadClientConfig>()))
|
||||
.Returns(mockDownloadService.Object);
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert - should publish QueueItemRemoveRequest<SearchItem> with MovieId
|
||||
_fixture.MessageBus.Verify(
|
||||
x => x.Publish(
|
||||
It.Is<QueueItemRemoveRequest<SearchItem>>(r =>
|
||||
r.InstanceType == InstanceType.Whisparr &&
|
||||
r.SearchItem.Id == 42 && // MovieId
|
||||
r.DeleteReason == DeleteReason.Stalled
|
||||
),
|
||||
It.IsAny<CancellationToken>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PublishQueueItemRemoveRequest_ForWhisparrV2Pack_PublishesSeasonSearchItemRequest()
|
||||
{
|
||||
// Arrange - test that Whisparr v2 pack (multiple records with same download ID) uses SeriesSearchItem with Season search type
|
||||
var whisparrInstance = TestDataContextFactory.AddWhisparrInstance(_fixture.DataContext, version: 2);
|
||||
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
||||
|
||||
var mockArrClient = new Mock<IArrClient>();
|
||||
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.Setup(x => x.GetClient(InstanceType.Whisparr, 2f))
|
||||
.Returns(mockArrClient.Object);
|
||||
|
||||
// Create multiple records with same download ID to simulate a pack (season pack)
|
||||
var record1 = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
DownloadId = "whisparr-v2-pack-download-id",
|
||||
Title = "Whisparr V2 Season Pack - Episode 1",
|
||||
Protocol = "torrent",
|
||||
SeriesId = 10,
|
||||
EpisodeId = 100,
|
||||
SeasonNumber = 3
|
||||
};
|
||||
var record2 = new QueueRecord
|
||||
{
|
||||
Id = 2,
|
||||
DownloadId = "whisparr-v2-pack-download-id",
|
||||
Title = "Whisparr V2 Season Pack - Episode 2",
|
||||
Protocol = "torrent",
|
||||
SeriesId = 10,
|
||||
EpisodeId = 101,
|
||||
SeasonNumber = 3
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
.Setup(x => x.Iterate(
|
||||
It.IsAny<IArrClient>(),
|
||||
It.IsAny<ArrInstance>(),
|
||||
It.IsAny<Func<IReadOnlyList<QueueRecord>, Task>>()
|
||||
))
|
||||
.Returns(async (IArrClient client, ArrInstance instance, Func<IReadOnlyList<QueueRecord>, Task> callback) =>
|
||||
{
|
||||
await callback([record1, record2]);
|
||||
});
|
||||
|
||||
var mockDownloadService = _fixture.CreateMockDownloadService();
|
||||
mockDownloadService
|
||||
.Setup(x => x.ShouldRemoveFromArrQueueAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<List<string>>()
|
||||
))
|
||||
.ReturnsAsync(new DownloadCheckResult
|
||||
{
|
||||
Found = true,
|
||||
ShouldRemove = true,
|
||||
IsPrivate = false,
|
||||
DeleteFromClient = true,
|
||||
DeleteReason = DeleteReason.Stalled
|
||||
});
|
||||
|
||||
_fixture.DownloadServiceFactory
|
||||
.Setup(x => x.GetDownloadService(It.IsAny<DownloadClientConfig>()))
|
||||
.Returns(mockDownloadService.Object);
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert - should publish QueueItemRemoveRequest<SeriesSearchItem> with Season search type
|
||||
// because multiple records with the same download ID indicate a pack
|
||||
_fixture.MessageBus.Verify(
|
||||
x => x.Publish(
|
||||
It.Is<QueueItemRemoveRequest<SeriesSearchItem>>(r =>
|
||||
r.InstanceType == InstanceType.Whisparr &&
|
||||
r.SearchItem.Id == 3 && // SeasonNumber
|
||||
r.SearchItem.SeriesId == 10 &&
|
||||
r.SearchItem.SearchType == SeriesSearchType.Season &&
|
||||
r.DeleteReason == DeleteReason.Stalled
|
||||
),
|
||||
It.IsAny<CancellationToken>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -197,16 +197,17 @@ public static class TestDataContextFactory
|
||||
/// <summary>
|
||||
/// Adds an enabled Whisparr instance to the context
|
||||
/// </summary>
|
||||
public static ArrInstance AddWhisparrInstance(DataContext context, string url = "http://whisparr:6969", bool enabled = true)
|
||||
public static ArrInstance AddWhisparrInstance(DataContext context, string url = "http://whisparr:6969", bool enabled = true, float version = 2)
|
||||
{
|
||||
var arrConfig = context.ArrConfigs.First(x => x.Type == InstanceType.Whisparr);
|
||||
var instance = new ArrInstance
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Test Whisparr",
|
||||
Name = $"Test Whisparr v{version}",
|
||||
Url = new Uri(url),
|
||||
ApiKey = "test-api-key",
|
||||
Enabled = enabled,
|
||||
Version = version,
|
||||
ArrConfigId = arrConfig.Id,
|
||||
ArrConfig = arrConfig
|
||||
};
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
};
|
||||
|
||||
|
||||
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!;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user