Add backup API endpoints with automatic fallback

This commit is contained in:
2026-01-30 12:12:55 -05:00
parent 6b745be835
commit 8b21e9880f
3 changed files with 180 additions and 55 deletions

View File

@@ -140,7 +140,7 @@ Choose your preferred provider via the `MUSIC_SERVICE` environment variable. Add
- **Subsonic**: Navidrome or other Subsonic-compatible server - **Subsonic**: Navidrome or other Subsonic-compatible server
- Credentials for at least one music provider (IF NOT USING SQUIDWTF): - Credentials for at least one music provider (IF NOT USING SQUIDWTF):
- **Deezer**: ARL token from browser cookies - **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 - Docker and Docker Compose (recommended) **or** [.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) for manual installation
## Configuration ## Configuration

View File

@@ -26,7 +26,20 @@ public class SquidWTFDownloadService : BaseDownloadService
private DateTime _lastRequestTime = DateTime.MinValue; private DateTime _lastRequestTime = DateTime.MinValue;
private readonly int _minRequestIntervalMs = 200; 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"; protected override string ProviderName => "squidwtf";
@@ -43,7 +56,44 @@ public class SquidWTFDownloadService : BaseDownloadService
{ {
_httpClient = httpClientFactory.CreateClient(); _httpClient = httpClientFactory.CreateClient();
_squidwtfSettings = SquidWTFSettings.Value; _squidwtfSettings = SquidWTFSettings.Value;
_currentApiBase = DecodeEndpoint(PrimaryEndpoint);
} }
private string DecodeEndpoint(string base64)
{
var bytes = Convert.FromBase64String(base64);
return Encoding.UTF8.GetString(bytes);
}
private async Task<bool> 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 #region BaseDownloadService Implementation
@@ -51,14 +101,25 @@ public class SquidWTFDownloadService : BaseDownloadService
{ {
try try
{ {
// Test connectivity to triton.squid.wtf var response = await _httpClient.GetAsync(_currentApiBase);
var response = await _httpClient.GetAsync("https://triton.squid.wtf/");
Console.WriteLine($"Response code from is available async: {response.IsSuccessStatusCode}"); 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) 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; return false;
} }
} }
@@ -147,56 +208,69 @@ public class SquidWTFDownloadService : BaseDownloadService
_ => "LOSSLESS" // Default to lossless _ => "LOSSLESS" // Default to lossless
}; };
// Use the triton.squid.wtf endpoint to get track download info var url = $"{_currentApiBase}track/?id={trackId}&quality={quality}";
var url = $"https://triton.squid.wtf/track/?id={trackId}&quality={quality}";
Console.WriteLine($"%%%%%%%%%%%%%%%%%%% URL For downloads??: {url}"); Console.WriteLine($"%%%%%%%%%%%%%%%%%%% URL For downloads??: {url}");
var response = await _httpClient.GetAsync(url, cancellationToken); try
response.EnsureSuccessStatusCode(); {
var response = await _httpClient.GetAsync(url, cancellationToken);
var json = await response.Content.ReadAsStringAsync(cancellationToken); response.EnsureSuccessStatusCode();
var doc = JsonDocument.Parse(json);
var json = await response.Content.ReadAsStringAsync(cancellationToken);
if (!doc.RootElement.TryGetProperty("data", out var data)) var doc = JsonDocument.Parse(json);
{
throw new Exception("Invalid response from triton.squid.wtf"); 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"); // Get the manifest (base64 encoded JSON containing the actual CDN URL)
var manifestBase64 = data.GetProperty("manifest").GetString()
// Decode the manifest ?? throw new Exception("No manifest in response");
var manifestJson = Encoding.UTF8.GetString(Convert.FromBase64String(manifestBase64));
var manifest = JsonDocument.Parse(manifestJson); // Decode the manifest
var manifestJson = Encoding.UTF8.GetString(Convert.FromBase64String(manifestBase64));
// Extract the download URL from the manifest var manifest = JsonDocument.Parse(manifestJson);
if (!manifest.RootElement.TryGetProperty("urls", out var urls) || urls.GetArrayLength() == 0)
{ // Extract the download URL from the manifest
throw new Exception("No download URLs in 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 downloadUrl = urls[0].GetString()
var mimeType = manifest.RootElement.TryGetProperty("mimeType", out var mimeTypeEl) ?? throw new Exception("Download URL is null");
? mimeTypeEl.GetString()
: "audio/flac"; var mimeType = manifest.RootElement.TryGetProperty("mimeType", out var mimeTypeEl)
? mimeTypeEl.GetString()
var audioQuality = data.TryGetProperty("audioQuality", out var audioQualityEl) : "audio/flac";
? audioQualityEl.GetString()
: "LOSSLESS"; var audioQuality = data.TryGetProperty("audioQuality", out var audioQualityEl)
? audioQualityEl.GetString()
Logger.LogDebug("Decoded manifest - URL: {Url}, MIME: {MimeType}, Quality: {Quality}", : "LOSSLESS";
downloadUrl, mimeType, audioQuality);
Logger.LogDebug("Decoded manifest - URL: {Url}, MIME: {MimeType}, Quality: {Quality}",
return new DownloadResult downloadUrl, mimeType, audioQuality);
{
DownloadUrl = downloadUrl, return new DownloadResult
MimeType = mimeType ?? "audio/flac", {
AudioQuality = audioQuality ?? "LOSSLESS" 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;
}
}); });
} }

View File

@@ -21,7 +21,21 @@ public class SquidWTFMetadataService : IMusicMetadataService
private readonly SubsonicSettings _settings; private readonly SubsonicSettings _settings;
private readonly ILogger<SquidWTFMetadataService> _logger; private readonly ILogger<SquidWTFMetadataService> _logger;
private readonly RedisCacheService _cache; 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( public SquidWTFMetadataService(
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
@@ -34,17 +48,54 @@ public class SquidWTFMetadataService : IMusicMetadataService
_settings = settings.Value; _settings = settings.Value;
_logger = logger; _logger = logger;
_cache = cache; _cache = cache;
_currentApiBase = DecodeEndpoint(PrimaryEndpoint);
// Set up default headers // Set up default headers
_httpClient.DefaultRequestHeaders.Add("User-Agent", _httpClient.DefaultRequestHeaders.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0"); "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<bool> 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<List<Song>> SearchSongsAsync(string query, int limit = 20) public async Task<List<Song>> SearchSongsAsync(string query, int limit = 20)
{ {
try try
{ {
var url = $"{BaseUrl}/search/?s={Uri.EscapeDataString(query)}"; var url = $"{_currentApiBase}/search/?s={Uri.EscapeDataString(query)}";
var response = await _httpClient.GetAsync(url); var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)