mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
feat: add pluggable music service architecture with Qobuz support
This commit is contained in:
@@ -7,21 +7,24 @@ namespace octo_fiesta.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Hosted service that validates configuration at startup and logs the results.
|
||||
/// Checks connectivity to Subsonic server and validates Deezer ARL token.
|
||||
/// Checks connectivity to Subsonic server and validates music service credentials (Deezer or Qobuz).
|
||||
/// Uses a dedicated HttpClient without logging to keep console output clean.
|
||||
/// </summary>
|
||||
public class StartupValidationService : IHostedService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IOptions<SubsonicSettings> _subsonicSettings;
|
||||
private readonly IOptions<QobuzSettings> _qobuzSettings;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public StartupValidationService(
|
||||
IConfiguration configuration,
|
||||
IOptions<SubsonicSettings> subsonicSettings)
|
||||
IOptions<SubsonicSettings> subsonicSettings,
|
||||
IOptions<QobuzSettings> qobuzSettings)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_subsonicSettings = subsonicSettings;
|
||||
_qobuzSettings = qobuzSettings;
|
||||
// Create a dedicated HttpClient without logging to keep startup output clean
|
||||
_httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
|
||||
}
|
||||
@@ -35,7 +38,17 @@ public class StartupValidationService : IHostedService
|
||||
Console.WriteLine();
|
||||
|
||||
await ValidateSubsonicAsync(cancellationToken);
|
||||
await ValidateDeezerArlAsync(cancellationToken);
|
||||
|
||||
// Validate music service credentials based on configured service
|
||||
var musicService = _subsonicSettings.Value.MusicService;
|
||||
if (musicService == MusicService.Qobuz)
|
||||
{
|
||||
await ValidateQobuzAsync(cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ValidateDeezerArlAsync(cancellationToken);
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("========================================");
|
||||
@@ -141,6 +154,112 @@ public class StartupValidationService : IHostedService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateQobuzAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var userAuthToken = _qobuzSettings.Value.UserAuthToken;
|
||||
var userId = _qobuzSettings.Value.UserId;
|
||||
var quality = _qobuzSettings.Value.Quality;
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userAuthToken))
|
||||
{
|
||||
WriteStatus("Qobuz UserAuthToken", "NOT CONFIGURED", ConsoleColor.Red);
|
||||
WriteDetail("Set the Qobuz__UserAuthToken environment variable");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
WriteStatus("Qobuz UserId", "NOT CONFIGURED", ConsoleColor.Red);
|
||||
WriteDetail("Set the Qobuz__UserId environment variable");
|
||||
return;
|
||||
}
|
||||
|
||||
WriteStatus("Qobuz UserAuthToken", MaskSecret(userAuthToken), ConsoleColor.Cyan);
|
||||
WriteStatus("Qobuz UserId", userId, ConsoleColor.Cyan);
|
||||
WriteStatus("Qobuz Quality", quality ?? "auto (highest available)", ConsoleColor.Cyan);
|
||||
|
||||
// Validate token by calling Qobuz API
|
||||
await ValidateQobuzTokenAsync(userAuthToken, userId, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ValidateQobuzTokenAsync(string userAuthToken, string userId, CancellationToken cancellationToken)
|
||||
{
|
||||
const string fieldName = "Qobuz credentials";
|
||||
|
||||
try
|
||||
{
|
||||
// First, get the app ID from bundle service (simple check)
|
||||
var bundleUrl = "https://play.qobuz.com/login";
|
||||
var bundleResponse = await _httpClient.GetAsync(bundleUrl, cancellationToken);
|
||||
|
||||
if (!bundleResponse.IsSuccessStatusCode)
|
||||
{
|
||||
WriteStatus(fieldName, "UNABLE TO VERIFY", ConsoleColor.Yellow);
|
||||
WriteDetail("Could not fetch Qobuz app configuration");
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to validate with a simple API call
|
||||
// We'll use the user favorites endpoint which requires authentication
|
||||
var appId = "798273057"; // Fallback app ID
|
||||
var apiUrl = $"https://www.qobuz.com/api.json/0.2/favorite/getUserFavorites?user_id={userId}&app_id={appId}";
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, apiUrl);
|
||||
request.Headers.Add("X-App-Id", appId);
|
||||
request.Headers.Add("X-User-Auth-Token", userAuthToken);
|
||||
request.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0");
|
||||
|
||||
var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
// 401 means invalid token, other errors might be network issues
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
WriteStatus(fieldName, "INVALID", ConsoleColor.Red);
|
||||
WriteDetail("Token is expired or invalid");
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteStatus(fieldName, $"HTTP {(int)response.StatusCode}", ConsoleColor.Yellow);
|
||||
WriteDetail("Unable to verify credentials");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
// If we got a successful response, credentials are valid
|
||||
if (!string.IsNullOrEmpty(json) && !json.Contains("\"error\""))
|
||||
{
|
||||
WriteStatus(fieldName, "VALID", ConsoleColor.Green);
|
||||
WriteDetail($"User ID: {userId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteStatus(fieldName, "INVALID", ConsoleColor.Red);
|
||||
WriteDetail("Unexpected response from Qobuz");
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
WriteStatus(fieldName, "TIMEOUT", ConsoleColor.Yellow);
|
||||
WriteDetail("Could not reach Qobuz within 10 seconds");
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
WriteStatus(fieldName, "UNREACHABLE", ConsoleColor.Yellow);
|
||||
WriteDetail(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteStatus(fieldName, "ERROR", ConsoleColor.Red);
|
||||
WriteDetail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateArlTokenAsync(string arl, string label, CancellationToken cancellationToken)
|
||||
{
|
||||
var fieldName = $"Deezer ARL ({label})";
|
||||
|
||||
Reference in New Issue
Block a user