mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
refactor: organize services by provider and standardize settings pattern
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
using octo_fiesta.Services;
|
using octo_fiesta.Services;
|
||||||
|
using octo_fiesta.Services.Deezer;
|
||||||
|
using octo_fiesta.Services.Local;
|
||||||
using octo_fiesta.Models;
|
using octo_fiesta.Models;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -68,6 +70,13 @@ public class DeezerDownloadServiceTests : IDisposable
|
|||||||
{
|
{
|
||||||
DownloadMode = downloadMode
|
DownloadMode = downloadMode
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var deezerSettings = Options.Create(new DeezerSettings
|
||||||
|
{
|
||||||
|
Arl = arl,
|
||||||
|
ArlFallback = null,
|
||||||
|
Quality = null
|
||||||
|
});
|
||||||
|
|
||||||
return new DeezerDownloadService(
|
return new DeezerDownloadService(
|
||||||
_httpClientFactoryMock.Object,
|
_httpClientFactoryMock.Object,
|
||||||
@@ -75,6 +84,7 @@ public class DeezerDownloadServiceTests : IDisposable
|
|||||||
_localLibraryServiceMock.Object,
|
_localLibraryServiceMock.Object,
|
||||||
_metadataServiceMock.Object,
|
_metadataServiceMock.Object,
|
||||||
subsonicSettings,
|
subsonicSettings,
|
||||||
|
deezerSettings,
|
||||||
_loggerMock.Object);
|
_loggerMock.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using octo_fiesta.Services;
|
using octo_fiesta.Services.Deezer;
|
||||||
using octo_fiesta.Models;
|
using octo_fiesta.Models;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Moq.Protected;
|
using Moq.Protected;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using octo_fiesta.Services;
|
using octo_fiesta.Services.Local;
|
||||||
using octo_fiesta.Models;
|
using octo_fiesta.Models;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Text.Json;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using octo_fiesta.Models;
|
using octo_fiesta.Models;
|
||||||
using octo_fiesta.Services;
|
using octo_fiesta.Services;
|
||||||
|
using octo_fiesta.Services.Local;
|
||||||
|
|
||||||
namespace octo_fiesta.Controllers;
|
namespace octo_fiesta.Controllers;
|
||||||
|
|
||||||
|
|||||||
25
octo-fiesta/Models/DeezerSettings.cs
Normal file
25
octo-fiesta/Models/DeezerSettings.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
namespace octo_fiesta.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration for the Deezer downloader and metadata service
|
||||||
|
/// </summary>
|
||||||
|
public class DeezerSettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Deezer ARL token (required for downloading)
|
||||||
|
/// Obtained from browser cookies after logging into deezer.com
|
||||||
|
/// </summary>
|
||||||
|
public string? Arl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fallback ARL token (optional)
|
||||||
|
/// Used if the primary ARL token fails
|
||||||
|
/// </summary>
|
||||||
|
public string? ArlFallback { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Preferred audio quality: FLAC, MP3_320, MP3_128
|
||||||
|
/// If not specified or unavailable, the highest available quality will be used.
|
||||||
|
/// </summary>
|
||||||
|
public string? Quality { get; set; }
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
using octo_fiesta.Models;
|
using octo_fiesta.Models;
|
||||||
using octo_fiesta.Services;
|
using octo_fiesta.Services;
|
||||||
|
using octo_fiesta.Services.Deezer;
|
||||||
|
using octo_fiesta.Services.Qobuz;
|
||||||
|
using octo_fiesta.Services.Local;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -13,6 +16,8 @@ builder.Services.AddSwaggerGen();
|
|||||||
// Configuration
|
// Configuration
|
||||||
builder.Services.Configure<SubsonicSettings>(
|
builder.Services.Configure<SubsonicSettings>(
|
||||||
builder.Configuration.GetSection("Subsonic"));
|
builder.Configuration.GetSection("Subsonic"));
|
||||||
|
builder.Services.Configure<DeezerSettings>(
|
||||||
|
builder.Configuration.GetSection("Deezer"));
|
||||||
builder.Services.Configure<QobuzSettings>(
|
builder.Services.Configure<QobuzSettings>(
|
||||||
builder.Configuration.GetSection("Qobuz"));
|
builder.Configuration.GetSection("Qobuz"));
|
||||||
|
|
||||||
|
|||||||
@@ -5,26 +5,13 @@ using Org.BouncyCastle.Crypto.Engines;
|
|||||||
using Org.BouncyCastle.Crypto.Modes;
|
using Org.BouncyCastle.Crypto.Modes;
|
||||||
using Org.BouncyCastle.Crypto.Parameters;
|
using Org.BouncyCastle.Crypto.Parameters;
|
||||||
using octo_fiesta.Models;
|
using octo_fiesta.Models;
|
||||||
|
using octo_fiesta.Services;
|
||||||
|
using octo_fiesta.Services.Local;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using TagLib;
|
using TagLib;
|
||||||
using IOFile = System.IO.File;
|
using IOFile = System.IO.File;
|
||||||
|
|
||||||
namespace octo_fiesta.Services;
|
namespace octo_fiesta.Services.Deezer;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configuration for the Deezer downloader
|
|
||||||
/// </summary>
|
|
||||||
public class DeezerDownloaderSettings
|
|
||||||
{
|
|
||||||
public string? Arl { get; set; }
|
|
||||||
public string? ArlFallback { get; set; }
|
|
||||||
public string DownloadPath { get; set; } = "./downloads";
|
|
||||||
/// <summary>
|
|
||||||
/// Preferred audio quality: FLAC, MP3_320, MP3_128
|
|
||||||
/// If not specified or unavailable, the highest available quality will be used.
|
|
||||||
/// </summary>
|
|
||||||
public string? Quality { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// C# port of the DeezerDownloader JavaScript
|
/// C# port of the DeezerDownloader JavaScript
|
||||||
@@ -66,6 +53,7 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
ILocalLibraryService localLibraryService,
|
ILocalLibraryService localLibraryService,
|
||||||
IMusicMetadataService metadataService,
|
IMusicMetadataService metadataService,
|
||||||
IOptions<SubsonicSettings> subsonicSettings,
|
IOptions<SubsonicSettings> subsonicSettings,
|
||||||
|
IOptions<DeezerSettings> deezerSettings,
|
||||||
ILogger<DeezerDownloadService> logger)
|
ILogger<DeezerDownloadService> logger)
|
||||||
{
|
{
|
||||||
_httpClient = httpClientFactory.CreateClient();
|
_httpClient = httpClientFactory.CreateClient();
|
||||||
@@ -75,10 +63,11 @@ public class DeezerDownloadService : IDownloadService
|
|||||||
_subsonicSettings = subsonicSettings.Value;
|
_subsonicSettings = subsonicSettings.Value;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
|
var deezer = deezerSettings.Value;
|
||||||
_downloadPath = configuration["Library:DownloadPath"] ?? "./downloads";
|
_downloadPath = configuration["Library:DownloadPath"] ?? "./downloads";
|
||||||
_arl = configuration["Deezer:Arl"];
|
_arl = deezer.Arl;
|
||||||
_arlFallback = configuration["Deezer:ArlFallback"];
|
_arlFallback = deezer.ArlFallback;
|
||||||
_preferredQuality = configuration["Deezer:Quality"];
|
_preferredQuality = deezer.Quality;
|
||||||
|
|
||||||
if (!Directory.Exists(_downloadPath))
|
if (!Directory.Exists(_downloadPath))
|
||||||
{
|
{
|
||||||
@@ -2,7 +2,7 @@ using octo_fiesta.Models;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace octo_fiesta.Services;
|
namespace octo_fiesta.Services.Deezer;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Metadata service implementation using the Deezer API (free, no key required)
|
/// Metadata service implementation using the Deezer API (free, no key required)
|
||||||
186
octo-fiesta/Services/Deezer/DeezerStartupValidator.cs
Normal file
186
octo-fiesta/Services/Deezer/DeezerStartupValidator.cs
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using octo_fiesta.Models;
|
||||||
|
|
||||||
|
namespace octo_fiesta.Services.Deezer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates Deezer ARL credentials at startup
|
||||||
|
/// </summary>
|
||||||
|
public class DeezerStartupValidator
|
||||||
|
{
|
||||||
|
private readonly DeezerSettings _settings;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
public DeezerStartupValidator(IOptions<DeezerSettings> settings, HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_settings = settings.Value;
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ValidateAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var arl = _settings.Arl;
|
||||||
|
var arlFallback = _settings.ArlFallback;
|
||||||
|
var quality = _settings.Quality;
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(arl))
|
||||||
|
{
|
||||||
|
WriteStatus("Deezer ARL", "NOT CONFIGURED", ConsoleColor.Red);
|
||||||
|
WriteDetail("Set the Deezer__Arl environment variable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteStatus("Deezer ARL", MaskSecret(arl), ConsoleColor.Cyan);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(arlFallback))
|
||||||
|
{
|
||||||
|
WriteStatus("Deezer ARL Fallback", MaskSecret(arlFallback), ConsoleColor.Cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteStatus("Deezer Quality", string.IsNullOrWhiteSpace(quality) ? "auto (highest available)" : quality, ConsoleColor.Cyan);
|
||||||
|
|
||||||
|
// Validate ARL by calling Deezer API
|
||||||
|
await ValidateArlTokenAsync(arl, "primary", cancellationToken);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(arlFallback))
|
||||||
|
{
|
||||||
|
await ValidateArlTokenAsync(arlFallback, "fallback", cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ValidateArlTokenAsync(string arl, string label, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var fieldName = $"Deezer ARL ({label})";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post,
|
||||||
|
"https://www.deezer.com/ajax/gw-light.php?method=deezer.getUserData&input=3&api_version=1.0&api_token=null");
|
||||||
|
|
||||||
|
request.Headers.Add("Cookie", $"arl={arl}");
|
||||||
|
request.Content = new StringContent("{}", Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
WriteStatus(fieldName, $"HTTP {(int)response.StatusCode}", ConsoleColor.Red);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
var doc = JsonDocument.Parse(json);
|
||||||
|
|
||||||
|
if (doc.RootElement.TryGetProperty("results", out var results) &&
|
||||||
|
results.TryGetProperty("USER", out var user))
|
||||||
|
{
|
||||||
|
if (user.TryGetProperty("USER_ID", out var userId))
|
||||||
|
{
|
||||||
|
var userIdValue = userId.ValueKind == JsonValueKind.Number
|
||||||
|
? userId.GetInt64()
|
||||||
|
: long.TryParse(userId.GetString(), out var parsed) ? parsed : 0;
|
||||||
|
|
||||||
|
if (userIdValue > 0)
|
||||||
|
{
|
||||||
|
// BLOG_NAME is the username displayed on Deezer
|
||||||
|
var userName = user.TryGetProperty("BLOG_NAME", out var blogName) && blogName.GetString() is string bn && !string.IsNullOrEmpty(bn)
|
||||||
|
? bn
|
||||||
|
: user.TryGetProperty("NAME", out var name) && name.GetString() is string n && !string.IsNullOrEmpty(n)
|
||||||
|
? n
|
||||||
|
: "Unknown";
|
||||||
|
|
||||||
|
var offerName = GetOfferName(user);
|
||||||
|
|
||||||
|
WriteStatus(fieldName, "VALID", ConsoleColor.Green);
|
||||||
|
WriteDetail($"Logged in as {userName} ({offerName})");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteStatus(fieldName, "INVALID", ConsoleColor.Red);
|
||||||
|
WriteDetail("Token is expired or invalid");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WriteStatus(fieldName, "INVALID", ConsoleColor.Red);
|
||||||
|
WriteDetail("Unexpected response from Deezer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
WriteStatus(fieldName, "TIMEOUT", ConsoleColor.Yellow);
|
||||||
|
WriteDetail("Could not reach Deezer 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 static string GetOfferName(JsonElement user)
|
||||||
|
{
|
||||||
|
if (!user.TryGetProperty("OPTIONS", out var options))
|
||||||
|
{
|
||||||
|
return "Free";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check actual streaming capabilities, not just license_token presence
|
||||||
|
var hasLossless = options.TryGetProperty("web_lossless", out var webLossless) && webLossless.GetBoolean();
|
||||||
|
var hasHq = options.TryGetProperty("web_hq", out var webHq) && webHq.GetBoolean();
|
||||||
|
|
||||||
|
if (hasLossless)
|
||||||
|
{
|
||||||
|
return "Premium+ (Lossless)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasHq)
|
||||||
|
{
|
||||||
|
return "Premium (HQ)";
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
46
octo-fiesta/Services/Local/ILocalLibraryService.cs
Normal file
46
octo-fiesta/Services/Local/ILocalLibraryService.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using octo_fiesta.Models;
|
||||||
|
|
||||||
|
namespace octo_fiesta.Services.Local;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for local music library management
|
||||||
|
/// </summary>
|
||||||
|
public interface ILocalLibraryService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if an external song already exists locally
|
||||||
|
/// </summary>
|
||||||
|
Task<string?> GetLocalPathForExternalSongAsync(string externalProvider, string externalId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a downloaded song in the local library
|
||||||
|
/// </summary>
|
||||||
|
Task RegisterDownloadedSongAsync(Song song, string localPath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the mapping between external ID and local ID
|
||||||
|
/// </summary>
|
||||||
|
Task<string?> GetLocalIdForExternalSongAsync(string externalProvider, string externalId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a song ID to determine if it is external or local
|
||||||
|
/// </summary>
|
||||||
|
(bool isExternal, string? provider, string? externalId) ParseSongId(string songId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses an external ID to extract the provider, type and ID
|
||||||
|
/// Format: ext-{provider}-{type}-{id} (e.g., ext-deezer-artist-259, ext-deezer-album-96126, ext-deezer-song-12345)
|
||||||
|
/// Also supports legacy format: ext-{provider}-{id} (assumes song type)
|
||||||
|
/// </summary>
|
||||||
|
(bool isExternal, string? provider, string? type, string? externalId) ParseExternalId(string id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triggers a Subsonic library scan
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> TriggerLibraryScanAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current scan status
|
||||||
|
/// </summary>
|
||||||
|
Task<ScanStatus?> GetScanStatusAsync();
|
||||||
|
}
|
||||||
@@ -1,58 +1,15 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Xml.Linq;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Extensions.Options;
|
using octo_fiesta.Models;
|
||||||
using octo_fiesta.Models;
|
using octo_fiesta.Services;
|
||||||
|
|
||||||
namespace octo_fiesta.Services;
|
namespace octo_fiesta.Services.Local;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interface for local music library management
|
/// Local library service implementation
|
||||||
/// </summary>
|
/// Uses a simple JSON file to store mappings (can be replaced with a database)
|
||||||
public interface ILocalLibraryService
|
/// </summary>
|
||||||
{
|
public class LocalLibraryService : ILocalLibraryService
|
||||||
/// <summary>
|
|
||||||
/// Checks if an external song already exists locally
|
|
||||||
/// </summary>
|
|
||||||
Task<string?> GetLocalPathForExternalSongAsync(string externalProvider, string externalId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registers a downloaded song in the local library
|
|
||||||
/// </summary>
|
|
||||||
Task RegisterDownloadedSongAsync(Song song, string localPath);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the mapping between external ID and local ID
|
|
||||||
/// </summary>
|
|
||||||
Task<string?> GetLocalIdForExternalSongAsync(string externalProvider, string externalId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses a song ID to determine if it is external or local
|
|
||||||
/// </summary>
|
|
||||||
(bool isExternal, string? provider, string? externalId) ParseSongId(string songId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses an external ID to extract the provider, type and ID
|
|
||||||
/// Format: ext-{provider}-{type}-{id} (e.g., ext-deezer-artist-259, ext-deezer-album-96126, ext-deezer-song-12345)
|
|
||||||
/// Also supports legacy format: ext-{provider}-{id} (assumes song type)
|
|
||||||
/// </summary>
|
|
||||||
(bool isExternal, string? provider, string? type, string? externalId) ParseExternalId(string id);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Triggers a Subsonic library scan
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> TriggerLibraryScanAsync();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the current scan status
|
|
||||||
/// </summary>
|
|
||||||
Task<ScanStatus?> GetScanStatusAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Local library service implementation
|
|
||||||
/// Uses a simple JSON file to store mappings (can be replaced with a database)
|
|
||||||
/// </summary>
|
|
||||||
public class LocalLibraryService : ILocalLibraryService
|
|
||||||
{
|
{
|
||||||
private readonly string _mappingFilePath;
|
private readonly string _mappingFilePath;
|
||||||
private readonly string _downloadDirectory;
|
private readonly string _downloadDirectory;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace octo_fiesta.Services;
|
namespace octo_fiesta.Services.Qobuz;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Service to dynamically extract Qobuz App ID and secrets from the Qobuz web player
|
/// Service to dynamically extract Qobuz App ID and secrets from the Qobuz web player
|
||||||
@@ -2,10 +2,13 @@ using System.Security.Cryptography;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using octo_fiesta.Models;
|
using octo_fiesta.Models;
|
||||||
|
using octo_fiesta.Services;
|
||||||
|
using octo_fiesta.Services.Deezer;
|
||||||
|
using octo_fiesta.Services.Local;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using IOFile = System.IO.File;
|
using IOFile = System.IO.File;
|
||||||
|
|
||||||
namespace octo_fiesta.Services;
|
namespace octo_fiesta.Services.Qobuz;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Download service implementation for Qobuz
|
/// Download service implementation for Qobuz
|
||||||
@@ -2,7 +2,7 @@ using octo_fiesta.Models;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace octo_fiesta.Services;
|
namespace octo_fiesta.Services.Qobuz;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Metadata service implementation using the Qobuz API
|
/// Metadata service implementation using the Qobuz API
|
||||||
158
octo-fiesta/Services/Qobuz/QobuzStartupValidator.cs
Normal file
158
octo-fiesta/Services/Qobuz/QobuzStartupValidator.cs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using octo_fiesta.Models;
|
||||||
|
|
||||||
|
namespace octo_fiesta.Services.Qobuz;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates Qobuz credentials at startup
|
||||||
|
/// </summary>
|
||||||
|
public class QobuzStartupValidator
|
||||||
|
{
|
||||||
|
private readonly IOptions<QobuzSettings> _qobuzSettings;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
public QobuzStartupValidator(IOptions<QobuzSettings> qobuzSettings, HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_qobuzSettings = qobuzSettings;
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ValidateAsync(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 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,7 +1,7 @@
|
|||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using octo_fiesta.Models;
|
using octo_fiesta.Models;
|
||||||
|
using octo_fiesta.Services.Deezer;
|
||||||
|
using octo_fiesta.Services.Qobuz;
|
||||||
|
|
||||||
namespace octo_fiesta.Services;
|
namespace octo_fiesta.Services;
|
||||||
|
|
||||||
@@ -14,16 +14,19 @@ public class StartupValidationService : IHostedService
|
|||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly IOptions<SubsonicSettings> _subsonicSettings;
|
private readonly IOptions<SubsonicSettings> _subsonicSettings;
|
||||||
|
private readonly IOptions<DeezerSettings> _deezerSettings;
|
||||||
private readonly IOptions<QobuzSettings> _qobuzSettings;
|
private readonly IOptions<QobuzSettings> _qobuzSettings;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
public StartupValidationService(
|
public StartupValidationService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IOptions<SubsonicSettings> subsonicSettings,
|
IOptions<SubsonicSettings> subsonicSettings,
|
||||||
|
IOptions<DeezerSettings> deezerSettings,
|
||||||
IOptions<QobuzSettings> qobuzSettings)
|
IOptions<QobuzSettings> qobuzSettings)
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_subsonicSettings = subsonicSettings;
|
_subsonicSettings = subsonicSettings;
|
||||||
|
_deezerSettings = deezerSettings;
|
||||||
_qobuzSettings = qobuzSettings;
|
_qobuzSettings = qobuzSettings;
|
||||||
// Create a dedicated HttpClient without logging to keep startup output clean
|
// Create a dedicated HttpClient without logging to keep startup output clean
|
||||||
_httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
|
_httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
|
||||||
@@ -43,11 +46,13 @@ public class StartupValidationService : IHostedService
|
|||||||
var musicService = _subsonicSettings.Value.MusicService;
|
var musicService = _subsonicSettings.Value.MusicService;
|
||||||
if (musicService == MusicService.Qobuz)
|
if (musicService == MusicService.Qobuz)
|
||||||
{
|
{
|
||||||
await ValidateQobuzAsync(cancellationToken);
|
var qobuzValidator = new QobuzStartupValidator(_qobuzSettings, _httpClient);
|
||||||
|
await qobuzValidator.ValidateAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await ValidateDeezerArlAsync(cancellationToken);
|
var deezerValidator = new DeezerStartupValidator(_deezerSettings, _httpClient);
|
||||||
|
await deezerValidator.ValidateAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
@@ -121,244 +126,6 @@ public class StartupValidationService : IHostedService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ValidateDeezerArlAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var arl = _configuration["Deezer:Arl"];
|
|
||||||
var arlFallback = _configuration["Deezer:ArlFallback"];
|
|
||||||
var quality = _configuration["Deezer:Quality"];
|
|
||||||
|
|
||||||
Console.WriteLine();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(arl))
|
|
||||||
{
|
|
||||||
WriteStatus("Deezer ARL", "NOT CONFIGURED", ConsoleColor.Red);
|
|
||||||
WriteDetail("Set the Deezer__Arl environment variable");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteStatus("Deezer ARL", MaskSecret(arl), ConsoleColor.Cyan);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(arlFallback))
|
|
||||||
{
|
|
||||||
WriteStatus("Deezer ARL Fallback", MaskSecret(arlFallback), ConsoleColor.Cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteStatus("Deezer Quality", string.IsNullOrWhiteSpace(quality) ? "auto (highest available)" : quality, ConsoleColor.Cyan);
|
|
||||||
|
|
||||||
// Validate ARL by calling Deezer API
|
|
||||||
await ValidateArlTokenAsync(arl, "primary", cancellationToken);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(arlFallback))
|
|
||||||
{
|
|
||||||
await ValidateArlTokenAsync(arlFallback, "fallback", cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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})";
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Post,
|
|
||||||
"https://www.deezer.com/ajax/gw-light.php?method=deezer.getUserData&input=3&api_version=1.0&api_token=null");
|
|
||||||
|
|
||||||
request.Headers.Add("Cookie", $"arl={arl}");
|
|
||||||
request.Content = new StringContent("{}", Encoding.UTF8, "application/json");
|
|
||||||
|
|
||||||
var response = await _httpClient.SendAsync(request, cancellationToken);
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
WriteStatus(fieldName, $"HTTP {(int)response.StatusCode}", ConsoleColor.Red);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var json = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
||||||
var doc = JsonDocument.Parse(json);
|
|
||||||
|
|
||||||
if (doc.RootElement.TryGetProperty("results", out var results) &&
|
|
||||||
results.TryGetProperty("USER", out var user))
|
|
||||||
{
|
|
||||||
if (user.TryGetProperty("USER_ID", out var userId))
|
|
||||||
{
|
|
||||||
var userIdValue = userId.ValueKind == JsonValueKind.Number
|
|
||||||
? userId.GetInt64()
|
|
||||||
: long.TryParse(userId.GetString(), out var parsed) ? parsed : 0;
|
|
||||||
|
|
||||||
if (userIdValue > 0)
|
|
||||||
{
|
|
||||||
// BLOG_NAME is the username displayed on Deezer
|
|
||||||
var userName = user.TryGetProperty("BLOG_NAME", out var blogName) && blogName.GetString() is string bn && !string.IsNullOrEmpty(bn)
|
|
||||||
? bn
|
|
||||||
: user.TryGetProperty("NAME", out var name) && name.GetString() is string n && !string.IsNullOrEmpty(n)
|
|
||||||
? n
|
|
||||||
: "Unknown";
|
|
||||||
|
|
||||||
var offerName = GetOfferName(user);
|
|
||||||
|
|
||||||
WriteStatus(fieldName, "VALID", ConsoleColor.Green);
|
|
||||||
WriteDetail($"Logged in as {userName} ({offerName})");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteStatus(fieldName, "INVALID", ConsoleColor.Red);
|
|
||||||
WriteDetail("Token is expired or invalid");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WriteStatus(fieldName, "INVALID", ConsoleColor.Red);
|
|
||||||
WriteDetail("Unexpected response from Deezer");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
|
||||||
WriteStatus(fieldName, "TIMEOUT", ConsoleColor.Yellow);
|
|
||||||
WriteDetail("Could not reach Deezer 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 static string GetOfferName(JsonElement user)
|
|
||||||
{
|
|
||||||
if (!user.TryGetProperty("OPTIONS", out var options))
|
|
||||||
{
|
|
||||||
return "Free";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check actual streaming capabilities, not just license_token presence
|
|
||||||
var hasLossless = options.TryGetProperty("web_lossless", out var webLossless) && webLossless.GetBoolean();
|
|
||||||
var hasHq = options.TryGetProperty("web_hq", out var webHq) && webHq.GetBoolean();
|
|
||||||
|
|
||||||
if (hasLossless)
|
|
||||||
{
|
|
||||||
return "Premium+ (Lossless)";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasHq)
|
|
||||||
{
|
|
||||||
return "Premium (HQ)";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Free";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteStatus(string label, string value, ConsoleColor valueColor)
|
private static void WriteStatus(string label, string value, ConsoleColor valueColor)
|
||||||
{
|
{
|
||||||
Console.Write($" {label}: ");
|
Console.Write($" {label}: ");
|
||||||
@@ -375,23 +142,4 @@ public class StartupValidationService : IHostedService
|
|||||||
Console.WriteLine($" -> {message}");
|
Console.WriteLine($" -> {message}");
|
||||||
Console.ForegroundColor = originalColor;
|
Console.ForegroundColor = originalColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Masks a secret string, showing only the first 4 characters followed by asterisks.
|
|
||||||
/// </summary>
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user