From 8b21e9880f11cadf29a216f010eedba172c0ec7f Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Fri, 30 Jan 2026 12:12:55 -0500 Subject: [PATCH] Add backup API endpoints with automatic fallback --- README.md | 2 +- .../SquidWTF/SquidWTFDownloadService.cs | 178 +++++++++++++----- .../SquidWTF/SquidWTFMetadataService.cs | 55 +++++- 3 files changed, 180 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index e5eef8b..a2862f1 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ Choose your preferred provider via the `MUSIC_SERVICE` environment variable. Add - **Subsonic**: Navidrome or other Subsonic-compatible server - Credentials for at least one music provider (IF NOT USING SQUIDWTF): - **Deezer**: ARL token from browser cookies - - **Qobuz**: User ID + User Auth Token from browser localStorage ([see Wiki guide](https://github.com/V1ck3s/allstarr/wiki/Getting-Qobuz-Credentials-(User-ID-&-Token))) + - **Qobuz**: User ID + User Auth Token from browser localStorage ([see Wiki guide](https://github.com/V1ck3s/octo-fiesta/wiki/Getting-Qobuz-Credentials-(User-ID-&-Token))) - Docker and Docker Compose (recommended) **or** [.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) for manual installation ## Configuration diff --git a/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs b/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs index 1b3d325..2b0c897 100644 --- a/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs +++ b/allstarr/Services/SquidWTF/SquidWTFDownloadService.cs @@ -26,7 +26,20 @@ public class SquidWTFDownloadService : BaseDownloadService private DateTime _lastRequestTime = DateTime.MinValue; private readonly int _minRequestIntervalMs = 200; - private const string SquidWTFApiBase = "https://triton.squid.wtf"; + // 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; protected override string ProviderName => "squidwtf"; @@ -43,7 +56,44 @@ public class SquidWTFDownloadService : BaseDownloadService { _httpClient = httpClientFactory.CreateClient(); _squidwtfSettings = SquidWTFSettings.Value; + _currentApiBase = DecodeEndpoint(PrimaryEndpoint); } + + private string DecodeEndpoint(string base64) + { + var bytes = Convert.FromBase64String(base64); + return Encoding.UTF8.GetString(bytes); + } + + 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 @@ -51,14 +101,25 @@ public class SquidWTFDownloadService : BaseDownloadService { try { - // Test connectivity to triton.squid.wtf - var response = await _httpClient.GetAsync("https://triton.squid.wtf/"); + var response = await _httpClient.GetAsync(_currentApiBase); Console.WriteLine($"Response code from is available async: {response.IsSuccessStatusCode}"); - return 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"); + Logger.LogWarning(ex, "SquidWTF service not available, trying backup"); + + if (await TryNextEndpointAsync()) + { + return await IsAvailableAsync(); + } + return false; } } @@ -147,56 +208,69 @@ public class SquidWTFDownloadService : BaseDownloadService _ => "LOSSLESS" // Default to lossless }; - // Use the triton.squid.wtf endpoint to get track download info - var url = $"https://triton.squid.wtf/track/?id={trackId}&quality={quality}"; + var url = $"{_currentApiBase}track/?id={trackId}&quality={quality}"; Console.WriteLine($"%%%%%%%%%%%%%%%%%%% URL For downloads??: {url}"); - 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 triton.squid.wtf"); - } - - // 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" - }; + 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; + } }); } diff --git a/allstarr/Services/SquidWTF/SquidWTFMetadataService.cs b/allstarr/Services/SquidWTF/SquidWTFMetadataService.cs index a897665..3151323 100644 --- a/allstarr/Services/SquidWTF/SquidWTFMetadataService.cs +++ b/allstarr/Services/SquidWTF/SquidWTFMetadataService.cs @@ -21,7 +21,21 @@ public class SquidWTFMetadataService : IMusicMetadataService private readonly SubsonicSettings _settings; private readonly ILogger _logger; private readonly RedisCacheService _cache; - private const string BaseUrl = "https://triton.squid.wtf"; + + // 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; public SquidWTFMetadataService( IHttpClientFactory httpClientFactory, @@ -34,17 +48,54 @@ public class SquidWTFMetadataService : IMusicMetadataService _settings = settings.Value; _logger = logger; _cache = cache; + _currentApiBase = DecodeEndpoint(PrimaryEndpoint); // 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 DecodeEndpoint(string base64) + { + var bytes = Convert.FromBase64String(base64); + return Encoding.UTF8.GetString(bytes); + } + + 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 { - var url = $"{BaseUrl}/search/?s={Uri.EscapeDataString(query)}"; + var url = $"{_currentApiBase}/search/?s={Uri.EscapeDataString(query)}"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode)