mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
refactor: implement unified startup validator architecture with IStartupValidator interface
This commit is contained in:
@@ -2,24 +2,26 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using octo_fiesta.Models;
|
||||
using octo_fiesta.Services.Validation;
|
||||
|
||||
namespace octo_fiesta.Services.Deezer;
|
||||
|
||||
/// <summary>
|
||||
/// Validates Deezer ARL credentials at startup
|
||||
/// </summary>
|
||||
public class DeezerStartupValidator
|
||||
public class DeezerStartupValidator : BaseStartupValidator
|
||||
{
|
||||
private readonly DeezerSettings _settings;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public override string ServiceName => "Deezer";
|
||||
|
||||
public DeezerStartupValidator(IOptions<DeezerSettings> settings, HttpClient httpClient)
|
||||
: base(httpClient)
|
||||
{
|
||||
_settings = settings.Value;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task ValidateAsync(CancellationToken cancellationToken)
|
||||
public override async Task<ValidationResult> ValidateAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var arl = _settings.Arl;
|
||||
var arlFallback = _settings.ArlFallback;
|
||||
@@ -31,7 +33,7 @@ public class DeezerStartupValidator
|
||||
{
|
||||
WriteStatus("Deezer ARL", "NOT CONFIGURED", ConsoleColor.Red);
|
||||
WriteDetail("Set the Deezer__Arl environment variable");
|
||||
return;
|
||||
return ValidationResult.NotConfigured("Deezer ARL not configured");
|
||||
}
|
||||
|
||||
WriteStatus("Deezer ARL", MaskSecret(arl), ConsoleColor.Cyan);
|
||||
@@ -50,6 +52,8 @@ public class DeezerStartupValidator
|
||||
{
|
||||
await ValidateArlTokenAsync(arlFallback, "fallback", cancellationToken);
|
||||
}
|
||||
|
||||
return ValidationResult.Success("Deezer validation completed");
|
||||
}
|
||||
|
||||
private async Task ValidateArlTokenAsync(string arl, string label, CancellationToken cancellationToken)
|
||||
@@ -150,37 +154,4 @@ public class DeezerStartupValidator
|
||||
|
||||
return "Free";
|
||||
}
|
||||
|
||||
private static void WriteStatus(string label, string value, ConsoleColor valueColor)
|
||||
{
|
||||
Console.Write($" {label}: ");
|
||||
var originalColor = Console.ForegroundColor;
|
||||
Console.ForegroundColor = valueColor;
|
||||
Console.WriteLine(value);
|
||||
Console.ForegroundColor = originalColor;
|
||||
}
|
||||
|
||||
private static void WriteDetail(string message)
|
||||
{
|
||||
var originalColor = Console.ForegroundColor;
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($" -> {message}");
|
||||
Console.ForegroundColor = originalColor;
|
||||
}
|
||||
|
||||
private static string MaskSecret(string secret)
|
||||
{
|
||||
if (string.IsNullOrEmpty(secret))
|
||||
{
|
||||
return "(empty)";
|
||||
}
|
||||
|
||||
const int visibleChars = 4;
|
||||
if (secret.Length <= visibleChars)
|
||||
{
|
||||
return new string('*', secret.Length);
|
||||
}
|
||||
|
||||
return secret[..visibleChars] + new string('*', Math.Min(secret.Length - visibleChars, 8));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using octo_fiesta.Models;
|
||||
using octo_fiesta.Services.Validation;
|
||||
|
||||
namespace octo_fiesta.Services.Qobuz;
|
||||
|
||||
/// <summary>
|
||||
/// Validates Qobuz credentials at startup
|
||||
/// </summary>
|
||||
public class QobuzStartupValidator
|
||||
public class QobuzStartupValidator : BaseStartupValidator
|
||||
{
|
||||
private readonly IOptions<QobuzSettings> _qobuzSettings;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public override string ServiceName => "Qobuz";
|
||||
|
||||
public QobuzStartupValidator(IOptions<QobuzSettings> qobuzSettings, HttpClient httpClient)
|
||||
: base(httpClient)
|
||||
{
|
||||
_qobuzSettings = qobuzSettings;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task ValidateAsync(CancellationToken cancellationToken)
|
||||
public override async Task<ValidationResult> ValidateAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var userAuthToken = _qobuzSettings.Value.UserAuthToken;
|
||||
var userId = _qobuzSettings.Value.UserId;
|
||||
@@ -29,14 +31,14 @@ public class QobuzStartupValidator
|
||||
{
|
||||
WriteStatus("Qobuz UserAuthToken", "NOT CONFIGURED", ConsoleColor.Red);
|
||||
WriteDetail("Set the Qobuz__UserAuthToken environment variable");
|
||||
return;
|
||||
return ValidationResult.NotConfigured("Qobuz UserAuthToken not configured");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
WriteStatus("Qobuz UserId", "NOT CONFIGURED", ConsoleColor.Red);
|
||||
WriteDetail("Set the Qobuz__UserId environment variable");
|
||||
return;
|
||||
return ValidationResult.NotConfigured("Qobuz UserId not configured");
|
||||
}
|
||||
|
||||
WriteStatus("Qobuz UserAuthToken", MaskSecret(userAuthToken), ConsoleColor.Cyan);
|
||||
@@ -45,6 +47,8 @@ public class QobuzStartupValidator
|
||||
|
||||
// Validate token by calling Qobuz API
|
||||
await ValidateQobuzTokenAsync(userAuthToken, userId, cancellationToken);
|
||||
|
||||
return ValidationResult.Success("Qobuz validation completed");
|
||||
}
|
||||
|
||||
private async Task ValidateQobuzTokenAsync(string userAuthToken, string userId, CancellationToken cancellationToken)
|
||||
@@ -122,37 +126,4 @@ public class QobuzStartupValidator
|
||||
WriteDetail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteStatus(string label, string value, ConsoleColor valueColor)
|
||||
{
|
||||
Console.Write($" {label}: ");
|
||||
var originalColor = Console.ForegroundColor;
|
||||
Console.ForegroundColor = valueColor;
|
||||
Console.WriteLine(value);
|
||||
Console.ForegroundColor = originalColor;
|
||||
}
|
||||
|
||||
private static void WriteDetail(string message)
|
||||
{
|
||||
var originalColor = Console.ForegroundColor;
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($" -> {message}");
|
||||
Console.ForegroundColor = originalColor;
|
||||
}
|
||||
|
||||
private static string MaskSecret(string secret)
|
||||
{
|
||||
if (string.IsNullOrEmpty(secret))
|
||||
{
|
||||
return "(empty)";
|
||||
}
|
||||
|
||||
const int visibleChars = 4;
|
||||
if (secret.Length <= visibleChars)
|
||||
{
|
||||
return new string('*', secret.Length);
|
||||
}
|
||||
|
||||
return secret[..visibleChars] + new string('*', Math.Min(secret.Length - visibleChars, 8));
|
||||
}
|
||||
}
|
||||
|
||||
95
octo-fiesta/Services/Validation/BaseStartupValidator.cs
Normal file
95
octo-fiesta/Services/Validation/BaseStartupValidator.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
namespace octo_fiesta.Services.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for startup validators providing common functionality
|
||||
/// </summary>
|
||||
public abstract class BaseStartupValidator : IStartupValidator
|
||||
{
|
||||
protected readonly HttpClient _httpClient;
|
||||
|
||||
protected BaseStartupValidator(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the service being validated
|
||||
/// </summary>
|
||||
public abstract string ServiceName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Validates the service configuration and connectivity
|
||||
/// </summary>
|
||||
public abstract Task<ValidationResult> ValidateAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a status line to the console with colored output
|
||||
/// </summary>
|
||||
protected static void WriteStatus(string label, string value, ConsoleColor valueColor)
|
||||
{
|
||||
Console.Write($" {label}: ");
|
||||
var originalColor = Console.ForegroundColor;
|
||||
Console.ForegroundColor = valueColor;
|
||||
Console.WriteLine(value);
|
||||
Console.ForegroundColor = originalColor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a detail line to the console in dark gray
|
||||
/// </summary>
|
||||
protected static void WriteDetail(string message)
|
||||
{
|
||||
var originalColor = Console.ForegroundColor;
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine($" -> {message}");
|
||||
Console.ForegroundColor = originalColor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Masks a secret string for display, showing only the first few characters
|
||||
/// </summary>
|
||||
protected static string MaskSecret(string secret)
|
||||
{
|
||||
if (string.IsNullOrEmpty(secret))
|
||||
{
|
||||
return "(empty)";
|
||||
}
|
||||
|
||||
const int visibleChars = 4;
|
||||
if (secret.Length <= visibleChars)
|
||||
{
|
||||
return new string('*', secret.Length);
|
||||
}
|
||||
|
||||
return secret[..visibleChars] + new string('*', Math.Min(secret.Length - visibleChars, 8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles common HTTP exceptions and returns appropriate validation result
|
||||
/// </summary>
|
||||
protected static ValidationResult HandleException(Exception ex, string fieldName)
|
||||
{
|
||||
return ex switch
|
||||
{
|
||||
TaskCanceledException => ValidationResult.Failure("TIMEOUT",
|
||||
"Could not reach service within timeout period", ConsoleColor.Yellow),
|
||||
|
||||
HttpRequestException httpEx => ValidationResult.Failure("UNREACHABLE",
|
||||
httpEx.Message, ConsoleColor.Yellow),
|
||||
|
||||
_ => ValidationResult.Failure("ERROR", ex.Message, ConsoleColor.Red)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes validation result to console
|
||||
/// </summary>
|
||||
protected void WriteValidationResult(string fieldName, ValidationResult result)
|
||||
{
|
||||
WriteStatus(fieldName, result.Status, result.StatusColor);
|
||||
if (!string.IsNullOrEmpty(result.Details))
|
||||
{
|
||||
WriteDetail(result.Details);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
octo-fiesta/Services/Validation/IStartupValidator.cs
Normal file
19
octo-fiesta/Services/Validation/IStartupValidator.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace octo_fiesta.Services.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for service startup validators
|
||||
/// </summary>
|
||||
public interface IStartupValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the service being validated
|
||||
/// </summary>
|
||||
string ServiceName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Validates the service configuration and connectivity
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Validation result containing status and details</returns>
|
||||
Task<ValidationResult> ValidateAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using octo_fiesta.Models;
|
||||
|
||||
namespace octo_fiesta.Services.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Orchestrates startup validation for all configured services.
|
||||
/// This replaces the old StartupValidationService with a more extensible architecture.
|
||||
/// </summary>
|
||||
public class StartupValidationOrchestrator : IHostedService
|
||||
{
|
||||
private readonly IEnumerable<IStartupValidator> _validators;
|
||||
private readonly IOptions<SubsonicSettings> _subsonicSettings;
|
||||
|
||||
public StartupValidationOrchestrator(
|
||||
IEnumerable<IStartupValidator> validators,
|
||||
IOptions<SubsonicSettings> subsonicSettings)
|
||||
{
|
||||
_validators = validators;
|
||||
_subsonicSettings = subsonicSettings;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("========================================");
|
||||
Console.WriteLine(" octo-fiesta starting up... ");
|
||||
Console.WriteLine("========================================");
|
||||
Console.WriteLine();
|
||||
|
||||
// Run all validators
|
||||
foreach (var validator in _validators)
|
||||
{
|
||||
try
|
||||
{
|
||||
await validator.ValidateAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error validating {validator.ServiceName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("========================================");
|
||||
Console.WriteLine(" Startup validation complete ");
|
||||
Console.WriteLine("========================================");
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
87
octo-fiesta/Services/Validation/SubsonicStartupValidator.cs
Normal file
87
octo-fiesta/Services/Validation/SubsonicStartupValidator.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using octo_fiesta.Models;
|
||||
|
||||
namespace octo_fiesta.Services.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Validates Subsonic server connectivity at startup
|
||||
/// </summary>
|
||||
public class SubsonicStartupValidator : BaseStartupValidator
|
||||
{
|
||||
private readonly IOptions<SubsonicSettings> _subsonicSettings;
|
||||
|
||||
public override string ServiceName => "Subsonic";
|
||||
|
||||
public SubsonicStartupValidator(IOptions<SubsonicSettings> subsonicSettings, HttpClient httpClient)
|
||||
: base(httpClient)
|
||||
{
|
||||
_subsonicSettings = subsonicSettings;
|
||||
}
|
||||
|
||||
public override async Task<ValidationResult> ValidateAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var subsonicUrl = _subsonicSettings.Value.Url;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(subsonicUrl))
|
||||
{
|
||||
WriteStatus("Subsonic URL", "NOT CONFIGURED", ConsoleColor.Red);
|
||||
WriteDetail("Set the Subsonic__Url environment variable");
|
||||
return ValidationResult.NotConfigured("Subsonic URL not configured");
|
||||
}
|
||||
|
||||
WriteStatus("Subsonic URL", subsonicUrl, ConsoleColor.Cyan);
|
||||
|
||||
try
|
||||
{
|
||||
var pingUrl = $"{subsonicUrl.TrimEnd('/')}/rest/ping.view?v=1.16.1&c=octo-fiesta&f=json";
|
||||
var response = await _httpClient.GetAsync(pingUrl, cancellationToken);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var content = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
if (content.Contains("\"status\":\"ok\"") || content.Contains("status=\"ok\""))
|
||||
{
|
||||
WriteStatus("Subsonic server", "OK", ConsoleColor.Green);
|
||||
return ValidationResult.Success("Subsonic server is accessible");
|
||||
}
|
||||
else if (content.Contains("\"status\":\"failed\"") || content.Contains("status=\"failed\""))
|
||||
{
|
||||
WriteStatus("Subsonic server", "REACHABLE", ConsoleColor.Yellow);
|
||||
WriteDetail("Authentication may be required for some operations");
|
||||
return ValidationResult.Success("Subsonic server is reachable");
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteStatus("Subsonic server", "REACHABLE", ConsoleColor.Yellow);
|
||||
WriteDetail("Unexpected response format");
|
||||
return ValidationResult.Success("Subsonic server is reachable");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteStatus("Subsonic server", $"HTTP {(int)response.StatusCode}", ConsoleColor.Red);
|
||||
return ValidationResult.Failure($"HTTP {(int)response.StatusCode}",
|
||||
"Subsonic server returned an error", ConsoleColor.Red);
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
WriteStatus("Subsonic server", "TIMEOUT", ConsoleColor.Red);
|
||||
WriteDetail("Could not reach server within 10 seconds");
|
||||
return ValidationResult.Failure("TIMEOUT", "Could not reach server within timeout period", ConsoleColor.Red);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
WriteStatus("Subsonic server", "UNREACHABLE", ConsoleColor.Red);
|
||||
WriteDetail(ex.Message);
|
||||
return ValidationResult.Failure("UNREACHABLE", ex.Message, ConsoleColor.Red);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteStatus("Subsonic server", "ERROR", ConsoleColor.Red);
|
||||
WriteDetail(ex.Message);
|
||||
return ValidationResult.Failure("ERROR", ex.Message, ConsoleColor.Red);
|
||||
}
|
||||
}
|
||||
}
|
||||
69
octo-fiesta/Services/Validation/ValidationResult.cs
Normal file
69
octo-fiesta/Services/Validation/ValidationResult.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
namespace octo_fiesta.Services.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Result of a startup validation operation
|
||||
/// </summary>
|
||||
public class ValidationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether the validation was successful
|
||||
/// </summary>
|
||||
public bool IsValid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Short status message (e.g., "VALID", "INVALID", "TIMEOUT", "NOT CONFIGURED")
|
||||
/// </summary>
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Detailed information about the validation result
|
||||
/// </summary>
|
||||
public string? Details { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Color to use when displaying the status in console
|
||||
/// </summary>
|
||||
public ConsoleColor StatusColor { get; set; } = ConsoleColor.White;
|
||||
|
||||
/// <summary>
|
||||
/// Additional metadata about the validation
|
||||
/// </summary>
|
||||
public Dictionary<string, object> Metadata { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a successful validation result
|
||||
/// </summary>
|
||||
public static ValidationResult Success(string details, Dictionary<string, object>? metadata = null)
|
||||
{
|
||||
return new ValidationResult
|
||||
{
|
||||
IsValid = true,
|
||||
Status = "VALID",
|
||||
StatusColor = ConsoleColor.Green,
|
||||
Details = details,
|
||||
Metadata = metadata ?? new()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed validation result
|
||||
/// </summary>
|
||||
public static ValidationResult Failure(string status, string details, ConsoleColor color = ConsoleColor.Red)
|
||||
{
|
||||
return new ValidationResult
|
||||
{
|
||||
IsValid = false,
|
||||
Status = status,
|
||||
StatusColor = color,
|
||||
Details = details
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a not configured validation result
|
||||
/// </summary>
|
||||
public static ValidationResult NotConfigured(string details)
|
||||
{
|
||||
return Failure("NOT CONFIGURED", details, ConsoleColor.Red);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user