diff --git a/allstarr/Program.cs b/allstarr/Program.cs index e703f2e..a8371e4 100644 --- a/allstarr/Program.cs +++ b/allstarr/Program.cs @@ -11,9 +11,30 @@ using allstarr.Services.Common; using allstarr.Services.Lyrics; using allstarr.Middleware; using allstarr.Filters; +using Microsoft.Extensions.Http; +using System.Text; var builder = WebApplication.CreateBuilder(args); +// Decode SquidWTF API base URLs once at startup +var squidWtfApiUrls = DecodeSquidWtfUrls(); +static List DecodeSquidWtfUrls() +{ + var encodedUrls = new[] + { + "aHR0cHM6Ly90cml0b24uc3F1aWQud3Rm", // triton + "aHR0cHM6Ly93b2xmLnFxZGwuc2l0ZQ==", // wolf + "aHR0cDovL2h1bmQucXFkbC5zaXRl", // hund + "aHR0cHM6Ly9tYXVzLnFxZGwuc2l0ZQ==", // maus + "aHR0cHM6Ly92b2dlbC5xcWRsLnNpdGU=", // vogel + "aHR0cHM6Ly9rYXR6ZS5xcWRsLnNpdGU=" // katze + }; + + return encodedUrls + .Select(encoded => Encoding.UTF8.GetString(Convert.FromBase64String(encoded))) + .ToList(); +} + // Determine backend type FIRST var backendType = builder.Configuration.GetValue("Backend:Type"); @@ -55,6 +76,17 @@ builder.Services.AddControllers() }); builder.Services.AddHttpClient(); +builder.Services.ConfigureAll(options => +{ + options.HttpMessageHandlerBuilderActions.Add(builder => + { + builder.PrimaryHandler = new HttpClientHandler + { + AllowAutoRedirect = true, + MaxAutomaticRedirections = 5 + }; + }); +}); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddHttpContextAccessor(); @@ -151,9 +183,26 @@ else if (musicService == MusicService.Deezer) } else if (musicService == MusicService.SquidWTF) { - // SquidWTF services - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + // SquidWTF services - pass decoded URLs with fallback support + builder.Services.AddSingleton(sp => + new SquidWTFMetadataService( + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService>(), + sp.GetRequiredService>(), + sp.GetRequiredService(), + squidWtfApiUrls)); + builder.Services.AddSingleton(sp => + new SquidWTFDownloadService( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService>(), + sp, + sp.GetRequiredService>(), + squidWtfApiUrls)); } // Startup validation - register validators based on backend @@ -168,7 +217,11 @@ else builder.Services.AddSingleton(); builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +builder.Services.AddSingleton(sp => + new SquidWTFStartupValidator( + sp.GetRequiredService>(), + sp.GetRequiredService().CreateClient(), + squidWtfApiUrls)); // Register orchestrator as hosted service builder.Services.AddHostedService(); diff --git a/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs b/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs index b4ca25b..1e9e010 100644 --- a/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs +++ b/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs @@ -26,20 +26,8 @@ public class SquidWTFDownloadService : BaseDownloadService private DateTime _lastRequestTime = DateTime.MinValue; private readonly int _minRequestIntervalMs = 200; - // Primary and backup endpoints (base64 encoded to avoid detection) - private const string PrimaryEndpoint = "aHR0cHM6Ly90cml0b24uc3F1aWQud3RmLw=="; // triton.squid.wtf - - private static readonly string[] BackupEndpoints = new[] - { - "aHR0cHM6Ly93b2xmLnFxZGwuc2l0ZS8=", // wolf - "aHR0cHM6Ly9tYXVzLnFxZGwuc2l0ZS8=", // maus - "aHR0cHM6Ly92b2dlbC5xcWRsLnNpdGUv", // vogel - "aHR0cHM6Ly9rYXR6ZS5xcWRsLnNpdGUv", // katze - "aHR0cHM6Ly9odW5kLnFxZGwuc2l0ZS8=" // hund - }; - - private string _currentApiBase; - private int _currentEndpointIndex = -1; + private readonly List _apiUrls; + private int _currentUrlIndex = 0; protected override string ProviderName => "squidwtf"; @@ -51,77 +39,49 @@ public class SquidWTFDownloadService : BaseDownloadService IOptions subsonicSettings, IOptions SquidWTFSettings, IServiceProvider serviceProvider, - ILogger logger) + ILogger logger, + List apiUrls) : base(configuration, localLibraryService, metadataService, subsonicSettings.Value, serviceProvider, logger) { _httpClient = httpClientFactory.CreateClient(); _squidwtfSettings = SquidWTFSettings.Value; - _currentApiBase = DecodeEndpoint(PrimaryEndpoint); + _apiUrls = apiUrls; + } + + private async Task TryWithFallbackAsync(Func> action) + { + for (int attempt = 0; attempt < _apiUrls.Count; attempt++) + { + try + { + var baseUrl = _apiUrls[_currentUrlIndex]; + return await action(baseUrl); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Request failed with endpoint {Endpoint}, trying next...", _apiUrls[_currentUrlIndex]); + _currentUrlIndex = (_currentUrlIndex + 1) % _apiUrls.Count; + + if (attempt == _apiUrls.Count - 1) + { + Logger.LogError("All SquidWTF endpoints failed"); + throw; + } + } + } + throw new Exception("All SquidWTF endpoints failed"); } - private string DecodeEndpoint(string base64) - { - var bytes = Convert.FromBase64String(base64); - return Encoding.UTF8.GetString(bytes).TrimEnd('/'); - } - - private async Task TryNextEndpointAsync() - { - _currentEndpointIndex++; - - if (_currentEndpointIndex >= BackupEndpoints.Length) - { - Logger.LogError("All backup endpoints exhausted"); - return false; - } - - _currentApiBase = DecodeEndpoint(BackupEndpoints[_currentEndpointIndex]); - Logger.LogInformation("Switching to backup endpoint {Index}", _currentEndpointIndex + 1); - - try - { - var response = await _httpClient.GetAsync(_currentApiBase); - if (response.IsSuccessStatusCode) - { - Logger.LogInformation("Backup endpoint {Index} is available", _currentEndpointIndex + 1); - return true; - } - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Backup endpoint {Index} failed", _currentEndpointIndex + 1); - } - - return await TryNextEndpointAsync(); - } - #region BaseDownloadService Implementation public override async Task IsAvailableAsync() { - try + return await TryWithFallbackAsync(async (baseUrl) => { - var response = await _httpClient.GetAsync(_currentApiBase); + var response = await _httpClient.GetAsync(baseUrl); Console.WriteLine($"Response code from is available async: {response.IsSuccessStatusCode}"); - - if (!response.IsSuccessStatusCode && await TryNextEndpointAsync()) - { - response = await _httpClient.GetAsync(_currentApiBase); - } - - return response.IsSuccessStatusCode; - } - catch (Exception ex) - { - Logger.LogWarning(ex, "SquidWTF service not available, trying backup"); - - if (await TryNextEndpointAsync()) - { - return await IsAvailableAsync(); - } - - return false; - } + return response.IsSuccessStatusCode; + }); } protected override string? ExtractExternalIdFromAlbumId(string albumId) @@ -197,80 +157,69 @@ public class SquidWTFDownloadService : BaseDownloadService { return await QueueRequestAsync(async () => { - // Map quality settings to Tidal's quality levels - var quality = _squidwtfSettings.Quality?.ToUpperInvariant() switch + return await TryWithFallbackAsync(async (baseUrl) => { - "FLAC" => "LOSSLESS", - "HI_RES" => "HI_RES_LOSSLESS", - "LOSSLESS" => "LOSSLESS", - "HIGH" => "HIGH", - "LOW" => "LOW", - _ => "LOSSLESS" // Default to lossless - }; - - var url = $"{_currentApiBase}/track?id={trackId}&quality={quality}"; + // Map quality settings to Tidal's quality levels + var quality = _squidwtfSettings.Quality?.ToUpperInvariant() switch + { + "FLAC" => "LOSSLESS", + "HI_RES" => "HI_RES_LOSSLESS", + "LOSSLESS" => "LOSSLESS", + "HIGH" => "HIGH", + "LOW" => "LOW", + _ => "LOSSLESS" // Default to lossless + }; + + var url = $"{baseUrl}/track/?id={trackId}&quality={quality}"; - Console.WriteLine($"%%%%%%%%%%%%%%%%%%% URL For downloads??: {url}"); + Console.WriteLine($"%%%%%%%%%%%%%%%%%%% URL For downloads??: {url}"); - try - { - var response = await _httpClient.GetAsync(url, cancellationToken); - response.EnsureSuccessStatusCode(); - - var json = await response.Content.ReadAsStringAsync(cancellationToken); - var doc = JsonDocument.Parse(json); - - if (!doc.RootElement.TryGetProperty("data", out var data)) - { - throw new Exception("Invalid response from API"); - } - - // Get the manifest (base64 encoded JSON containing the actual CDN URL) - var manifestBase64 = data.GetProperty("manifest").GetString() - ?? throw new Exception("No manifest in response"); - - // Decode the manifest - var manifestJson = Encoding.UTF8.GetString(Convert.FromBase64String(manifestBase64)); - var manifest = JsonDocument.Parse(manifestJson); - - // Extract the download URL from the manifest - if (!manifest.RootElement.TryGetProperty("urls", out var urls) || urls.GetArrayLength() == 0) - { - throw new Exception("No download URLs in manifest"); - } - - var downloadUrl = urls[0].GetString() - ?? throw new Exception("Download URL is null"); - - var mimeType = manifest.RootElement.TryGetProperty("mimeType", out var mimeTypeEl) - ? mimeTypeEl.GetString() - : "audio/flac"; - - var audioQuality = data.TryGetProperty("audioQuality", out var audioQualityEl) - ? audioQualityEl.GetString() - : "LOSSLESS"; - - Logger.LogDebug("Decoded manifest - URL: {Url}, MIME: {MimeType}, Quality: {Quality}", - downloadUrl, mimeType, audioQuality); - - return new DownloadResult - { - DownloadUrl = downloadUrl, - MimeType = mimeType ?? "audio/flac", - AudioQuality = audioQuality ?? "LOSSLESS" - }; - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Failed to get track info, trying backup endpoint"); - - if (await TryNextEndpointAsync()) - { - return await GetTrackDownloadInfoAsync(trackId, cancellationToken); - } - - throw; - } + var response = await _httpClient.GetAsync(url, cancellationToken); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(cancellationToken); + var doc = JsonDocument.Parse(json); + + if (!doc.RootElement.TryGetProperty("data", out var data)) + { + throw new Exception("Invalid response from API"); + } + + // Get the manifest (base64 encoded JSON containing the actual CDN URL) + var manifestBase64 = data.GetProperty("manifest").GetString() + ?? throw new Exception("No manifest in response"); + + // Decode the manifest + var manifestJson = Encoding.UTF8.GetString(Convert.FromBase64String(manifestBase64)); + var manifest = JsonDocument.Parse(manifestJson); + + // Extract the download URL from the manifest + if (!manifest.RootElement.TryGetProperty("urls", out var urls) || urls.GetArrayLength() == 0) + { + throw new Exception("No download URLs in manifest"); + } + + var downloadUrl = urls[0].GetString() + ?? throw new Exception("Download URL is null"); + + var mimeType = manifest.RootElement.TryGetProperty("mimeType", out var mimeTypeEl) + ? mimeTypeEl.GetString() + : "audio/flac"; + + var audioQuality = data.TryGetProperty("audioQuality", out var audioQualityEl) + ? audioQualityEl.GetString() + : "LOSSLESS"; + + Logger.LogDebug("Decoded manifest - URL: {Url}, MIME: {MimeType}, Quality: {Quality}", + downloadUrl, mimeType, audioQuality); + + return new DownloadResult + { + DownloadUrl = downloadUrl, + MimeType = mimeType ?? "audio/flac", + AudioQuality = audioQuality ?? "LOSSLESS" + }; + }); }); } diff --git a/allstarr/Services/SquidWTF/SquidWTFMetadataService.cs b/allstarr/Services/SquidWTF/SquidWTFMetadataService.cs index c5e80cb..e1e545e 100644 --- a/allstarr/Services/SquidWTF/SquidWTFMetadataService.cs +++ b/allstarr/Services/SquidWTF/SquidWTFMetadataService.cs @@ -21,89 +21,63 @@ public class SquidWTFMetadataService : IMusicMetadataService private readonly SubsonicSettings _settings; private readonly ILogger _logger; private readonly RedisCacheService _cache; - - // Primary and backup endpoints (base64 encoded to avoid detection) - private const string PrimaryEndpoint = "aHR0cHM6Ly90cml0b24uc3F1aWQud3RmLw=="; // triton.squid.wtf - - private static readonly string[] BackupEndpoints = new[] - { - "aHR0cHM6Ly93b2xmLnFxZGwuc2l0ZS8=", // wolf - "aHR0cHM6Ly9tYXVzLnFxZGwuc2l0ZS8=", // maus - "aHR0cHM6Ly92b2dlbC5xcWRsLnNpdGUv", // vogel - "aHR0cHM6Ly9rYXR6ZS5xcWRsLnNpdGUv", // katze - "aHR0cHM6Ly9odW5kLnFxZGwuc2l0ZS8=" // hund - }; - - private string _currentApiBase; - private int _currentEndpointIndex = -1; + private readonly List _apiUrls; + private int _currentUrlIndex = 0; public SquidWTFMetadataService( IHttpClientFactory httpClientFactory, IOptions settings, IOptions squidwtfSettings, ILogger logger, - RedisCacheService cache) + RedisCacheService cache, + List apiUrls) { _httpClient = httpClientFactory.CreateClient(); _settings = settings.Value; _logger = logger; _cache = cache; - _currentApiBase = DecodeEndpoint(PrimaryEndpoint); + _apiUrls = apiUrls; // Set up default headers _httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0"); } + + private string GetCurrentBaseUrl() => _apiUrls[_currentUrlIndex]; + + private async Task TryWithFallbackAsync(Func> action, T defaultValue) + { + for (int attempt = 0; attempt < _apiUrls.Count; attempt++) + { + try + { + var baseUrl = _apiUrls[_currentUrlIndex]; + return await action(baseUrl); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Request failed with endpoint {Endpoint}, trying next...", _apiUrls[_currentUrlIndex]); + _currentUrlIndex = (_currentUrlIndex + 1) % _apiUrls.Count; + + if (attempt == _apiUrls.Count - 1) + { + _logger.LogError("All SquidWTF endpoints failed"); + return defaultValue; + } + } + } + return defaultValue; + } - private string DecodeEndpoint(string base64) - { - var bytes = Convert.FromBase64String(base64); - return Encoding.UTF8.GetString(bytes).TrimEnd('/'); - } - - private async Task TryNextEndpointAsync() - { - _currentEndpointIndex++; - - if (_currentEndpointIndex >= BackupEndpoints.Length) - { - _logger.LogError("All backup endpoints exhausted"); - return false; - } - - _currentApiBase = DecodeEndpoint(BackupEndpoints[_currentEndpointIndex]); - _logger.LogInformation("Switching to backup endpoint {Index}", _currentEndpointIndex + 1); - - try - { - var response = await _httpClient.GetAsync(_currentApiBase); - if (response.IsSuccessStatusCode) - { - _logger.LogInformation("Backup endpoint {Index} is available", _currentEndpointIndex + 1); - return true; - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Backup endpoint {Index} failed", _currentEndpointIndex + 1); - } - - return await TryNextEndpointAsync(); - } - public async Task> SearchSongsAsync(string query, int limit = 20) { - try + return await TryWithFallbackAsync(async (baseUrl) => { - var url = $"{_currentApiBase}/search?s={Uri.EscapeDataString(query)}"; + var url = $"{baseUrl}/search/?s={Uri.EscapeDataString(query)}"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { - if (await TryNextEndpointAsync()) - { - return await SearchSongsAsync(query, limit); - } return new List(); } @@ -125,31 +99,18 @@ public class SquidWTFMetadataService : IMusicMetadataService } } return songs; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to search songs for query: {Query}", query); - if (await TryNextEndpointAsync()) - { - return await SearchSongsAsync(query, limit); - } - return new List(); - } + }, new List()); } public async Task> SearchAlbumsAsync(string query, int limit = 20) { - try + return await TryWithFallbackAsync(async (baseUrl) => { - var url = $"{_currentApiBase}/search?al={Uri.EscapeDataString(query)}"; + var url = $"{baseUrl}/search/?al={Uri.EscapeDataString(query)}"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { - if (await TryNextEndpointAsync()) - { - return await SearchAlbumsAsync(query, limit); - } return new List(); } @@ -172,31 +133,18 @@ public class SquidWTFMetadataService : IMusicMetadataService } return albums; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to search albums for query: {Query}", query); - if (await TryNextEndpointAsync()) - { - return await SearchAlbumsAsync(query, limit); - } - return new List(); - } + }, new List()); } public async Task> SearchArtistsAsync(string query, int limit = 20) { - try + return await TryWithFallbackAsync(async (baseUrl) => { - var url = $"{_currentApiBase}/search?a={Uri.EscapeDataString(query)}"; + var url = $"{baseUrl}/search/?a={Uri.EscapeDataString(query)}"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { - if (await TryNextEndpointAsync()) - { - return await SearchArtistsAsync(query, limit); - } return new List(); } @@ -219,32 +167,16 @@ public class SquidWTFMetadataService : IMusicMetadataService } return artists; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to search artists for query: {Query}", query); - if (await TryNextEndpointAsync()) - { - return await SearchArtistsAsync(query, limit); - } - return new List(); - } + }, new List()); } public async Task> SearchPlaylistsAsync(string query, int limit = 20) { - try + return await TryWithFallbackAsync(async (baseUrl) => { - var url = $"{_currentApiBase}/search?p={Uri.EscapeDataString(query)}"; + var url = $"{baseUrl}/search/?p={Uri.EscapeDataString(query)}"; var response = await _httpClient.GetAsync(url); - if (!response.IsSuccessStatusCode) - { - if (await TryNextEndpointAsync()) - { - return await SearchPlaylistsAsync(query, limit); - } - return new List(); - } + if (!response.IsSuccessStatusCode) return new List(); var json = await response.Content.ReadAsStringAsync(); var result = JsonDocument.Parse(json); @@ -260,16 +192,7 @@ public class SquidWTFMetadataService : IMusicMetadataService } } return playlists; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to search playlists for query: {Query}", query); - if (await TryNextEndpointAsync()) - { - return await SearchPlaylistsAsync(query, limit); - } - return new List(); - } + }, new List()); } public async Task SearchAllAsync(string query, int songLimit = 20, int albumLimit = 20, int artistLimit = 20) @@ -295,10 +218,9 @@ public class SquidWTFMetadataService : IMusicMetadataService { if (externalProvider != "squidwtf") return null; - try + return await TryWithFallbackAsync(async (baseUrl) => { - // Use the /info endpoint for full track metadata - var url = $"{_currentApiBase}/info?id={externalId}"; + var url = $"{baseUrl}/info/?id={externalId}"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) return null; @@ -310,12 +232,7 @@ public class SquidWTFMetadataService : IMusicMetadataService return null; return ParseTidalTrackFull(track); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "GetSongAsync Exception"); - return null; - } + }, (Song?)null); } public async Task GetAlbumAsync(string externalProvider, string externalId) @@ -327,10 +244,9 @@ public class SquidWTFMetadataService : IMusicMetadataService var cached = await _cache.GetAsync(cacheKey); if (cached != null) return cached; - try + return await TryWithFallbackAsync(async (baseUrl) => { - // Use the /info endpoint for full track metadata - var url = $"{_currentApiBase}/album?id={externalId}"; + var url = $"{baseUrl}/album/?id={externalId}"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) return null; @@ -364,12 +280,7 @@ public class SquidWTFMetadataService : IMusicMetadataService await _cache.SetAsync(cacheKey, album, TimeSpan.FromHours(24)); return album; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "GetAlbumAsync Exception"); - return null; - } + }, (Album?)null); } public async Task GetArtistAsync(string externalProvider, string externalId) @@ -387,10 +298,9 @@ public class SquidWTFMetadataService : IMusicMetadataService return cached; } - try + return await TryWithFallbackAsync(async (baseUrl) => { - // Use the /info endpoint for full track metadata - var url = $"{_currentApiBase}/artist?f={externalId}"; + var url = $"{baseUrl}/artist/?f={externalId}"; _logger.LogInformation("Fetching artist from {Url}", url); var response = await _httpClient.GetAsync(url); @@ -451,25 +361,18 @@ public class SquidWTFMetadataService : IMusicMetadataService await _cache.SetAsync(cacheKey, artist, TimeSpan.FromHours(24)); return artist; - - } - catch (Exception ex) - { - _logger.LogWarning(ex, "GetArtistAsync Exception."); - return null; - } + }, (Artist?)null); } public async Task> GetArtistAlbumsAsync(string externalProvider, string externalId) { - - try + if (externalProvider != "squidwtf") return new List(); + + return await TryWithFallbackAsync(async (baseUrl) => { - if (externalProvider != "squidwtf") return new List(); - _logger.LogInformation("GetArtistAlbumsAsync called for SquidWTF artist {ExternalId}", externalId); - var url = $"{_currentApiBase}/artist?f={externalId}"; + var url = $"{baseUrl}/artist/?f={externalId}"; _logger.LogInformation("Fetching artist albums from URL: {Url}", url); var response = await _httpClient.GetAsync(url); @@ -503,21 +406,16 @@ public class SquidWTFMetadataService : IMusicMetadataService } return albums; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to get SquidWTF artist albums for {ExternalId}", externalId); - return new List(); - } + }, new List()); } public async Task GetPlaylistAsync(string externalProvider, string externalId) { if (externalProvider != "squidwtf") return null; - try + return await TryWithFallbackAsync(async (baseUrl) => { - var url = $"{_currentApiBase}/playlist?id={externalId}"; + var url = $"{baseUrl}/playlist/?id={externalId}"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) return null; @@ -527,21 +425,16 @@ public class SquidWTFMetadataService : IMusicMetadataService if (playlistElement.TryGetProperty("error", out _)) return null; return ParseTidalPlaylist(playlistElement); - } - catch - { - return null; - } - + }, (ExternalPlaylist?)null); } public async Task> GetPlaylistTracksAsync(string externalProvider, string externalId) { if (externalProvider != "squidwtf") return new List(); - try + return await TryWithFallbackAsync(async (baseUrl) => { - var url = $"{_currentApiBase}/playlist?id={externalId}"; + var url = $"{baseUrl}/playlist/?id={externalId}"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) return new List(); @@ -592,12 +485,7 @@ public class SquidWTFMetadataService : IMusicMetadataService } } return songs; - } - catch - { - return new List(); - } - + }, new List()); } // --- Parser functions start here --- diff --git a/allstarr/Services/SquidWTF/SquidWTFStartupValidator.cs b/allstarr/Services/SquidWTF/SquidWTFStartupValidator.cs index feeb2c1..0207104 100644 --- a/allstarr/Services/SquidWTF/SquidWTFStartupValidator.cs +++ b/allstarr/Services/SquidWTF/SquidWTFStartupValidator.cs @@ -12,25 +12,40 @@ namespace allstarr.Services.SquidWTF; public class SquidWTFStartupValidator : BaseStartupValidator { private readonly SquidWTFSettings _settings; - - // Primary endpoint (base64 encoded to avoid detection) - private const string PrimaryEndpoint = "aHR0cHM6Ly90cml0b24uc3F1aWQud3RmLw=="; // triton.squid.wtf - private readonly string _apiBase; + private readonly List _apiUrls; + private int _currentUrlIndex = 0; public override string ServiceName => "SquidWTF"; - public SquidWTFStartupValidator(IOptions settings, HttpClient httpClient) + public SquidWTFStartupValidator(IOptions settings, HttpClient httpClient, List apiUrls) : base(httpClient) { _settings = settings.Value; - _apiBase = DecodeEndpoint(PrimaryEndpoint); + _apiUrls = apiUrls; } - - private string DecodeEndpoint(string base64) - { - var bytes = Convert.FromBase64String(base64); - return Encoding.UTF8.GetString(bytes).TrimEnd('/'); - } + + private async Task TryWithFallbackAsync(Func> action, T defaultValue) + { + for (int attempt = 0; attempt < _apiUrls.Count; attempt++) + { + try + { + var baseUrl = _apiUrls[_currentUrlIndex]; + return await action(baseUrl); + } + catch + { + WriteDetail($"Endpoint {_apiUrls[_currentUrlIndex]} failed, trying next..."); + _currentUrlIndex = (_currentUrlIndex + 1) % _apiUrls.Count; + + if (attempt == _apiUrls.Count - 1) + { + return defaultValue; + } + } + } + return defaultValue; + } public override async Task ValidateAsync(CancellationToken cancellationToken) { @@ -48,54 +63,36 @@ public class SquidWTFStartupValidator : BaseStartupValidator WriteStatus("SquidWTF Quality", quality, ConsoleColor.Cyan); - // Test connectivity - try + // Test connectivity with fallback + var result = await TryWithFallbackAsync(async (baseUrl) => { - var response = await _httpClient.GetAsync(_apiBase, cancellationToken); + var response = await _httpClient.GetAsync(baseUrl, cancellationToken); if (response.IsSuccessStatusCode) { - WriteStatus("SquidWTF API", "REACHABLE", ConsoleColor.Green); + WriteStatus("SquidWTF API", $"REACHABLE ({baseUrl})", ConsoleColor.Green); WriteDetail("No authentication required - powered by Tidal"); // Try a test search to verify functionality - await ValidateSearchFunctionality(cancellationToken); + await ValidateSearchFunctionality(baseUrl, cancellationToken); return ValidationResult.Success("SquidWTF validation completed"); } else { - WriteStatus("SquidWTF API", $"HTTP {(int)response.StatusCode}", ConsoleColor.Yellow); - WriteDetail("Service may be temporarily unavailable"); - return ValidationResult.Failure($"{response.StatusCode}", "SquidWTF returned code"); + throw new HttpRequestException($"HTTP {(int)response.StatusCode}"); } - } - catch (TaskCanceledException) - { - WriteStatus("SquidWTF API", "TIMEOUT", ConsoleColor.Yellow); - WriteDetail("Could not reach service within timeout period"); - return ValidationResult.Failure("-1", "SquidWTF connection timeout"); - } - catch (HttpRequestException ex) - { - WriteStatus("SquidWTF API", "UNREACHABLE", ConsoleColor.Red); - WriteDetail(ex.Message); - return ValidationResult.Failure("-1", $"Cannot connect to SquidWTF: {ex.Message}"); - } - catch (Exception ex) - { - WriteStatus("SquidWTF API", "ERROR", ConsoleColor.Red); - WriteDetail(ex.Message); - return ValidationResult.Failure("-1", $"Validation error: {ex.Message}"); - } + }, ValidationResult.Failure("-1", "All SquidWTF endpoints failed")); + + return result; } - private async Task ValidateSearchFunctionality(CancellationToken cancellationToken) + private async Task ValidateSearchFunctionality(string baseUrl, CancellationToken cancellationToken) { try { // Test search with a simple query - var searchUrl = $"{_apiBase}/search?s=Taylor%20Swift"; + var searchUrl = $"{baseUrl}/search/?s=Taylor%20Swift"; var searchResponse = await _httpClient.GetAsync(searchUrl, cancellationToken); if (searchResponse.IsSuccessStatusCode)