mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
Add backup API endpoints with automatic fallback
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user