mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-11-01 20:15:26 -05:00
451 lines
17 KiB
C#
451 lines
17 KiB
C#
using Cleanuparr.Api.Extensions;
|
|
using Cleanuparr.Api.Features.Arr.Contracts.Requests;
|
|
using Cleanuparr.Domain.Enums;
|
|
using Cleanuparr.Infrastructure.Events.Interfaces;
|
|
using Cleanuparr.Api.Features.Arr.Contracts.Responses;
|
|
using Cleanuparr.Infrastructure.Health;
|
|
using Cleanuparr.Persistence;
|
|
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
|
using Cleanuparr.Shared.Helpers;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Storage;
|
|
|
|
namespace Cleanuparr.Api.Features.Arr.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/configuration")]
|
|
[Authorize]
|
|
public sealed class ArrConfigController : ControllerBase
|
|
{
|
|
private readonly ILogger<ArrConfigController> _logger;
|
|
private readonly DataContext _dataContext;
|
|
private readonly EventsContext _eventsContext;
|
|
private readonly IInstanceHealthChecker _healthChecker;
|
|
private readonly IEventPublisher _eventPublisher;
|
|
|
|
/// <summary>
|
|
/// Creates the controller with its injected dependencies.
|
|
/// </summary>
|
|
public ArrConfigController(
|
|
ILogger<ArrConfigController> logger,
|
|
DataContext dataContext,
|
|
EventsContext eventsContext,
|
|
IInstanceHealthChecker healthChecker,
|
|
IEventPublisher eventPublisher)
|
|
{
|
|
_logger = logger;
|
|
_dataContext = dataContext;
|
|
_eventsContext = eventsContext;
|
|
_healthChecker = healthChecker;
|
|
_eventPublisher = eventPublisher;
|
|
}
|
|
|
|
[HttpGet("sonarr")]
|
|
public Task<IActionResult> GetSonarrConfig() => GetArrConfig(InstanceType.Sonarr);
|
|
|
|
[HttpGet("radarr")]
|
|
public Task<IActionResult> GetRadarrConfig() => GetArrConfig(InstanceType.Radarr);
|
|
|
|
[HttpGet("lidarr")]
|
|
public Task<IActionResult> GetLidarrConfig() => GetArrConfig(InstanceType.Lidarr);
|
|
|
|
[HttpGet("readarr")]
|
|
public Task<IActionResult> GetReadarrConfig() => GetArrConfig(InstanceType.Readarr);
|
|
|
|
[HttpGet("whisparr")]
|
|
public Task<IActionResult> GetWhisparrConfig() => GetArrConfig(InstanceType.Whisparr);
|
|
|
|
[HttpGet("sportarr")]
|
|
public Task<IActionResult> GetSportarrConfig() => GetArrConfig(InstanceType.Sportarr);
|
|
[HttpGet("lazylibrarian")]
|
|
public Task<IActionResult> GetLazyLibrarianConfig() => GetArrConfig(InstanceType.LazyLibrarian);
|
|
|
|
[HttpPut("sonarr")]
|
|
public Task<IActionResult> UpdateSonarrConfig([FromBody] UpdateArrConfigRequest request)
|
|
=> UpdateArrConfig(InstanceType.Sonarr, request);
|
|
|
|
[HttpPut("radarr")]
|
|
public Task<IActionResult> UpdateRadarrConfig([FromBody] UpdateArrConfigRequest request)
|
|
=> UpdateArrConfig(InstanceType.Radarr, request);
|
|
|
|
[HttpPut("lidarr")]
|
|
public Task<IActionResult> UpdateLidarrConfig([FromBody] UpdateArrConfigRequest request)
|
|
=> UpdateArrConfig(InstanceType.Lidarr, request);
|
|
|
|
[HttpPut("readarr")]
|
|
public Task<IActionResult> UpdateReadarrConfig([FromBody] UpdateArrConfigRequest request)
|
|
=> UpdateArrConfig(InstanceType.Readarr, request);
|
|
|
|
[HttpPut("whisparr")]
|
|
public Task<IActionResult> UpdateWhisparrConfig([FromBody] UpdateArrConfigRequest request)
|
|
=> UpdateArrConfig(InstanceType.Whisparr, request);
|
|
|
|
[HttpPut("sportarr")]
|
|
public Task<IActionResult> UpdateSportarrConfig([FromBody] UpdateArrConfigRequest request)
|
|
=> UpdateArrConfig(InstanceType.Sportarr, request);
|
|
[HttpPut("lazylibrarian")]
|
|
public Task<IActionResult> UpdateLazyLibrarianConfig([FromBody] UpdateArrConfigRequest request)
|
|
=> UpdateArrConfig(InstanceType.LazyLibrarian, request);
|
|
|
|
[HttpPost("sonarr/instances")]
|
|
public Task<IActionResult> CreateSonarrInstance([FromBody] ArrInstanceRequest request)
|
|
=> CreateArrInstance(InstanceType.Sonarr, request);
|
|
|
|
[HttpPut("sonarr/instances/{id}")]
|
|
public Task<IActionResult> UpdateSonarrInstance(Guid id, [FromBody] ArrInstanceRequest request)
|
|
=> UpdateArrInstance(InstanceType.Sonarr, id, request);
|
|
|
|
[HttpDelete("sonarr/instances/{id}")]
|
|
public Task<IActionResult> DeleteSonarrInstance(Guid id)
|
|
=> DeleteArrInstance(InstanceType.Sonarr, id);
|
|
|
|
[HttpPost("radarr/instances")]
|
|
public Task<IActionResult> CreateRadarrInstance([FromBody] ArrInstanceRequest request)
|
|
=> CreateArrInstance(InstanceType.Radarr, request);
|
|
|
|
[HttpPut("radarr/instances/{id}")]
|
|
public Task<IActionResult> UpdateRadarrInstance(Guid id, [FromBody] ArrInstanceRequest request)
|
|
=> UpdateArrInstance(InstanceType.Radarr, id, request);
|
|
|
|
[HttpDelete("radarr/instances/{id}")]
|
|
public Task<IActionResult> DeleteRadarrInstance(Guid id)
|
|
=> DeleteArrInstance(InstanceType.Radarr, id);
|
|
|
|
[HttpPost("lidarr/instances")]
|
|
public Task<IActionResult> CreateLidarrInstance([FromBody] ArrInstanceRequest request)
|
|
=> CreateArrInstance(InstanceType.Lidarr, request);
|
|
|
|
[HttpPut("lidarr/instances/{id}")]
|
|
public Task<IActionResult> UpdateLidarrInstance(Guid id, [FromBody] ArrInstanceRequest request)
|
|
=> UpdateArrInstance(InstanceType.Lidarr, id, request);
|
|
|
|
[HttpDelete("lidarr/instances/{id}")]
|
|
public Task<IActionResult> DeleteLidarrInstance(Guid id)
|
|
=> DeleteArrInstance(InstanceType.Lidarr, id);
|
|
|
|
[HttpPost("readarr/instances")]
|
|
public Task<IActionResult> CreateReadarrInstance([FromBody] ArrInstanceRequest request)
|
|
=> CreateArrInstance(InstanceType.Readarr, request);
|
|
|
|
[HttpPut("readarr/instances/{id}")]
|
|
public Task<IActionResult> UpdateReadarrInstance(Guid id, [FromBody] ArrInstanceRequest request)
|
|
=> UpdateArrInstance(InstanceType.Readarr, id, request);
|
|
|
|
[HttpDelete("readarr/instances/{id}")]
|
|
public Task<IActionResult> DeleteReadarrInstance(Guid id)
|
|
=> DeleteArrInstance(InstanceType.Readarr, id);
|
|
|
|
[HttpPost("whisparr/instances")]
|
|
public Task<IActionResult> CreateWhisparrInstance([FromBody] ArrInstanceRequest request)
|
|
=> CreateArrInstance(InstanceType.Whisparr, request);
|
|
|
|
[HttpPut("whisparr/instances/{id}")]
|
|
public Task<IActionResult> UpdateWhisparrInstance(Guid id, [FromBody] ArrInstanceRequest request)
|
|
=> UpdateArrInstance(InstanceType.Whisparr, id, request);
|
|
|
|
[HttpDelete("whisparr/instances/{id}")]
|
|
public Task<IActionResult> DeleteWhisparrInstance(Guid id)
|
|
=> DeleteArrInstance(InstanceType.Whisparr, id);
|
|
|
|
[HttpPost("sportarr/instances")]
|
|
public Task<IActionResult> CreateSportarrInstance([FromBody] ArrInstanceRequest request)
|
|
=> CreateArrInstance(InstanceType.Sportarr, request);
|
|
|
|
[HttpPut("sportarr/instances/{id}")]
|
|
public Task<IActionResult> UpdateSportarrInstance(Guid id, [FromBody] ArrInstanceRequest request)
|
|
=> UpdateArrInstance(InstanceType.Sportarr, id, request);
|
|
|
|
[HttpDelete("sportarr/instances/{id}")]
|
|
public Task<IActionResult> DeleteSportarrInstance(Guid id)
|
|
=> DeleteArrInstance(InstanceType.Sportarr, id);
|
|
[HttpPost("lazylibrarian/instances")]
|
|
public Task<IActionResult> CreateLazyLibrarianInstance([FromBody] ArrInstanceRequest request)
|
|
=> CreateArrInstance(InstanceType.LazyLibrarian, request);
|
|
|
|
[HttpPut("lazylibrarian/instances/{id}")]
|
|
public Task<IActionResult> UpdateLazyLibrarianInstance(Guid id, [FromBody] ArrInstanceRequest request)
|
|
=> UpdateArrInstance(InstanceType.LazyLibrarian, id, request);
|
|
|
|
[HttpDelete("lazylibrarian/instances/{id}")]
|
|
public Task<IActionResult> DeleteLazyLibrarianInstance(Guid id)
|
|
=> DeleteArrInstance(InstanceType.LazyLibrarian, id);
|
|
|
|
[HttpPost("sonarr/instances/test")]
|
|
public Task<IActionResult> TestSonarrInstance([FromBody] TestArrInstanceRequest request)
|
|
=> TestArrInstance(InstanceType.Sonarr, request);
|
|
|
|
[HttpPost("radarr/instances/test")]
|
|
public Task<IActionResult> TestRadarrInstance([FromBody] TestArrInstanceRequest request)
|
|
=> TestArrInstance(InstanceType.Radarr, request);
|
|
|
|
[HttpPost("lidarr/instances/test")]
|
|
public Task<IActionResult> TestLidarrInstance([FromBody] TestArrInstanceRequest request)
|
|
=> TestArrInstance(InstanceType.Lidarr, request);
|
|
|
|
[HttpPost("readarr/instances/test")]
|
|
public Task<IActionResult> TestReadarrInstance([FromBody] TestArrInstanceRequest request)
|
|
=> TestArrInstance(InstanceType.Readarr, request);
|
|
|
|
[HttpPost("whisparr/instances/test")]
|
|
public Task<IActionResult> TestWhisparrInstance([FromBody] TestArrInstanceRequest request)
|
|
=> TestArrInstance(InstanceType.Whisparr, request);
|
|
|
|
[HttpPost("sportarr/instances/test")]
|
|
public Task<IActionResult> TestSportarrInstance([FromBody] TestArrInstanceRequest request)
|
|
=> TestArrInstance(InstanceType.Sportarr, request);
|
|
[HttpPost("lazylibrarian/instances/test")]
|
|
public Task<IActionResult> TestLazyLibrarianInstance([FromBody] TestArrInstanceRequest request)
|
|
=> TestArrInstance(InstanceType.LazyLibrarian, request);
|
|
|
|
private async Task<IActionResult> GetArrConfig(InstanceType type)
|
|
{
|
|
await DataContext.Lock.WaitAsync();
|
|
try
|
|
{
|
|
var config = await _dataContext.ArrConfigs
|
|
.Include(x => x.Instances)
|
|
.AsNoTracking()
|
|
.FirstAsync(x => x.Type == type);
|
|
|
|
config.Instances = config.Instances
|
|
.OrderBy(i => i.Name)
|
|
.ToList();
|
|
|
|
return Ok(ToDto(config));
|
|
}
|
|
finally
|
|
{
|
|
DataContext.Lock.Release();
|
|
}
|
|
}
|
|
|
|
private async Task<IActionResult> UpdateArrConfig(InstanceType type, UpdateArrConfigRequest request)
|
|
{
|
|
await DataContext.Lock.WaitAsync();
|
|
try
|
|
{
|
|
var config = await _dataContext.ArrConfigs
|
|
.FirstAsync(x => x.Type == type);
|
|
|
|
config.FailedImportMaxStrikes = request.FailedImportMaxStrikes;
|
|
config.Validate();
|
|
|
|
await _dataContext.SaveChangesAsync();
|
|
|
|
return Ok(new { Message = $"{type} configuration updated successfully" });
|
|
}
|
|
finally
|
|
{
|
|
DataContext.Lock.Release();
|
|
}
|
|
}
|
|
|
|
private async Task<IActionResult> CreateArrInstance(InstanceType type, ArrInstanceRequest request)
|
|
{
|
|
await DataContext.Lock.WaitAsync();
|
|
try
|
|
{
|
|
var config = await _dataContext.ArrConfigs
|
|
.FirstAsync(x => x.Type == type);
|
|
|
|
var instance = request.ToEntity(config.Id);
|
|
await _dataContext.ArrInstances.AddAsync(instance);
|
|
await _dataContext.SaveChangesAsync();
|
|
|
|
return CreatedAtAction(GetConfigActionName(type), new { id = instance.Id }, ToDto(instance));
|
|
}
|
|
finally
|
|
{
|
|
DataContext.Lock.Release();
|
|
}
|
|
}
|
|
|
|
private async Task<IActionResult> UpdateArrInstance(InstanceType type, Guid id, ArrInstanceRequest request)
|
|
{
|
|
await DataContext.Lock.WaitAsync();
|
|
try
|
|
{
|
|
var config = await _dataContext.ArrConfigs
|
|
.Include(c => c.Instances)
|
|
.FirstAsync(x => x.Type == type);
|
|
|
|
var instance = config.Instances.FirstOrDefault(i => i.Id == id);
|
|
if (instance is null)
|
|
{
|
|
return this.ProblemResult(StatusCodes.Status404NotFound, $"{type} instance with ID {id} not found");
|
|
}
|
|
|
|
request.ApplyTo(instance);
|
|
|
|
await _dataContext.SaveChangesAsync();
|
|
|
|
return Ok(ToDto(instance));
|
|
}
|
|
finally
|
|
{
|
|
DataContext.Lock.Release();
|
|
}
|
|
}
|
|
|
|
private async Task<IActionResult> DeleteArrInstance(InstanceType type, Guid id)
|
|
{
|
|
await DataContext.Lock.WaitAsync();
|
|
try
|
|
{
|
|
var config = await _dataContext.ArrConfigs
|
|
.Include(c => c.Instances)
|
|
.FirstAsync(x => x.Type == type);
|
|
|
|
var instance = config.Instances.FirstOrDefault(i => i.Id == id);
|
|
if (instance is null)
|
|
{
|
|
return this.ProblemResult(StatusCodes.Status404NotFound, $"{type} instance with ID {id} not found");
|
|
}
|
|
|
|
await using IDbContextTransaction transaction = await _dataContext.Database.BeginTransactionAsync();
|
|
|
|
try
|
|
{
|
|
config.Instances.Remove(instance);
|
|
await _dataContext.SaveChangesAsync();
|
|
|
|
await DeleteSeekerEventsForInstanceAsync(id);
|
|
|
|
await transaction.CommitAsync();
|
|
}
|
|
catch
|
|
{
|
|
await transaction.RollbackAsync();
|
|
throw;
|
|
}
|
|
|
|
await FailStrandedSearchEventsForInstanceAsync(id);
|
|
|
|
return NoContent();
|
|
}
|
|
finally
|
|
{
|
|
DataContext.Lock.Release();
|
|
}
|
|
}
|
|
|
|
private async Task DeleteSeekerEventsForInstanceAsync(Guid arrInstanceId, CancellationToken cancellationToken = default)
|
|
{
|
|
await using IDbContextTransaction transaction = await _eventsContext.Database.BeginTransactionAsync(cancellationToken);
|
|
|
|
try
|
|
{
|
|
|
|
await _eventsContext.CustomFormatScoreEntries
|
|
.Where(e => e.ArrInstanceId == arrInstanceId)
|
|
.ExecuteDeleteAsync(cancellationToken);
|
|
|
|
await _eventsContext.CustomFormatScoreHistory
|
|
.Where(e => e.ArrInstanceId == arrInstanceId)
|
|
.ExecuteDeleteAsync(cancellationToken);
|
|
|
|
await _eventsContext.SeekerHistory
|
|
.Where(e => e.ArrInstanceId == arrInstanceId)
|
|
.ExecuteDeleteAsync(cancellationToken);
|
|
|
|
await _eventsContext.SeekerCycles
|
|
.Where(e => e.ArrInstanceId == arrInstanceId)
|
|
.ExecuteDeleteAsync(cancellationToken);
|
|
|
|
await _eventsContext.SearchQueue
|
|
.Where(e => e.ArrInstanceId == arrInstanceId)
|
|
.ExecuteDeleteAsync(cancellationToken);
|
|
|
|
await _eventsContext.SeekerCommandTrackers
|
|
.Where(e => e.ArrInstanceId == arrInstanceId)
|
|
.ExecuteDeleteAsync(cancellationToken);
|
|
|
|
await transaction.CommitAsync(cancellationToken);
|
|
}
|
|
catch
|
|
{
|
|
await transaction.RollbackAsync(cancellationToken);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private async Task FailStrandedSearchEventsForInstanceAsync(Guid arrInstanceId)
|
|
{
|
|
try
|
|
{
|
|
await _eventPublisher.FailStrandedSearchEvents(arrInstanceId);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_logger.LogError(exception,
|
|
"Failed to mark the search events of instance {InstanceId} as failed, the seeker command monitor will retry",
|
|
arrInstanceId);
|
|
}
|
|
}
|
|
|
|
private async Task<IActionResult> TestArrInstance(InstanceType type, TestArrInstanceRequest request)
|
|
{
|
|
try
|
|
{
|
|
string? resolvedApiKey = null;
|
|
|
|
if (request.ApiKey.IsPlaceholder() && request.InstanceId.HasValue)
|
|
{
|
|
var existingInstance = await _dataContext.ArrInstances
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(i => i.Id == request.InstanceId.Value);
|
|
|
|
if (existingInstance is null)
|
|
{
|
|
return this.ProblemResult(StatusCodes.Status404NotFound, $"Instance with ID {request.InstanceId.Value} not found");
|
|
}
|
|
|
|
resolvedApiKey = existingInstance.ApiKey;
|
|
}
|
|
|
|
var testInstance = request.ToTestInstance(resolvedApiKey);
|
|
await _healthChecker.CheckAsync(type, testInstance);
|
|
|
|
return Ok(new { Message = $"Connection to {type} instance successful" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to test {Type} instance connection", type);
|
|
return this.ProblemResult(StatusCodes.Status400BadRequest, $"Connection failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private static ArrConfigResponse ToDto(ArrConfig config) => new()
|
|
{
|
|
Id = config.Id,
|
|
Type = config.Type,
|
|
FailedImportMaxStrikes = config.FailedImportMaxStrikes,
|
|
Instances = config.Instances.Select(ToDto).ToList(),
|
|
};
|
|
|
|
private static ArrInstanceResponse ToDto(ArrInstance instance) => new()
|
|
{
|
|
Id = instance.Id,
|
|
Enabled = instance.Enabled,
|
|
Version = instance.Version,
|
|
Name = instance.Name,
|
|
Url = instance.Url.ToString(),
|
|
ApiKey = instance.ApiKey,
|
|
ExternalUrl = instance.ExternalUrl?.ToString(),
|
|
};
|
|
|
|
private static string GetConfigActionName(InstanceType type) => type switch
|
|
{
|
|
InstanceType.Sonarr => nameof(GetSonarrConfig),
|
|
InstanceType.Radarr => nameof(GetRadarrConfig),
|
|
InstanceType.Lidarr => nameof(GetLidarrConfig),
|
|
InstanceType.Readarr => nameof(GetReadarrConfig),
|
|
InstanceType.Whisparr => nameof(GetWhisparrConfig),
|
|
InstanceType.Sportarr => nameof(GetSportarrConfig),
|
|
InstanceType.LazyLibrarian => nameof(GetLazyLibrarianConfig),
|
|
_ => nameof(GetSonarrConfig),
|
|
};
|
|
}
|