From 5f22fb0a3b60f086485e7b87bf2b81bc03fa8c1a Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Fri, 6 Feb 2026 01:24:49 -0500 Subject: [PATCH] Integrate Spotify lyrics sidecar service - Add spotify-lyrics-api sidecar container to docker-compose - Replace direct Spotify API lyrics code with sidecar API calls - Update SpotifyLyricsService to use sidecar exclusively - Add LyricsApiUrl setting to SpotifyApiSettings - Update prefetch to try Spotify lyrics first, then LRCLib - Remove unused direct API authentication and parsing code --- .../Services/Lyrics/SpotifyLyricsService.cs | 306 +----------------- 1 file changed, 17 insertions(+), 289 deletions(-) diff --git a/allstarr/Services/Lyrics/SpotifyLyricsService.cs b/allstarr/Services/Lyrics/SpotifyLyricsService.cs index dbbb8a7..4cd54a1 100644 --- a/allstarr/Services/Lyrics/SpotifyLyricsService.cs +++ b/allstarr/Services/Lyrics/SpotifyLyricsService.cs @@ -24,37 +24,25 @@ public class SpotifyLyricsService { private readonly ILogger _logger; private readonly SpotifyApiSettings _settings; - private readonly SpotifyApiClient _spotifyClient; private readonly RedisCacheService _cache; private readonly HttpClient _httpClient; - private const string LyricsApiBase = "https://spclient.wg.spotify.com/color-lyrics/v2/track"; - private bool _useSidecarApi = false; - public SpotifyLyricsService( ILogger logger, IOptions settings, - SpotifyApiClient spotifyClient, RedisCacheService cache, IHttpClientFactory httpClientFactory) { _logger = logger; _settings = settings.Value; - _spotifyClient = spotifyClient; _cache = cache; _httpClient = httpClientFactory.CreateClient(); - _httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0"); - _httpClient.DefaultRequestHeaders.Add("App-Platform", "WebPlayer"); - - // Check if sidecar API is configured and available - _useSidecarApi = !string.IsNullOrEmpty(_settings.LyricsApiUrl) && - _settings.LyricsApiUrl != "http://spotify-lyrics:8080" || - _settings.Enabled; + _httpClient.Timeout = TimeSpan.FromSeconds(10); } /// - /// Gets synchronized lyrics for a Spotify track by its ID. + /// Gets synchronized lyrics for a Spotify track by its ID using the sidecar API. /// /// Spotify track ID (e.g., "3a8mo25v74BMUOJ1IDUEBL") /// Lyrics info with synced lyrics in LRC format, or null if not available @@ -66,6 +54,12 @@ public class SpotifyLyricsService return null; } + if (string.IsNullOrEmpty(_settings.LyricsApiUrl)) + { + _logger.LogWarning("Spotify lyrics API URL not configured"); + return null; + } + // Normalize track ID (remove URI prefix if present) spotifyTrackId = ExtractTrackId(spotifyTrackId); @@ -78,26 +72,6 @@ public class SpotifyLyricsService return cached; } - // Try sidecar API first if available - if (_useSidecarApi && !string.IsNullOrEmpty(_settings.LyricsApiUrl)) - { - var sidecarResult = await GetLyricsFromSidecarAsync(spotifyTrackId); - if (sidecarResult != null) - { - await _cache.SetAsync(cacheKey, sidecarResult, TimeSpan.FromDays(30)); - return sidecarResult; - } - } - - // Fall back to direct API call - return await GetLyricsDirectAsync(spotifyTrackId, cacheKey); - } - - /// - /// Gets lyrics from the sidecar spotify-lyrics-api service. - /// - private async Task GetLyricsFromSidecarAsync(string spotifyTrackId) - { try { var url = $"{_settings.LyricsApiUrl}/?trackid={spotifyTrackId}&format=id3"; @@ -118,6 +92,8 @@ public class SpotifyLyricsService if (result != null) { + // Cache for 30 days (lyrics don't change) + await _cache.SetAsync(cacheKey, result, TimeSpan.FromDays(30)); _logger.LogInformation("Got Spotify lyrics from sidecar for track {TrackId} ({LineCount} lines)", spotifyTrackId, result.Lines.Count); } @@ -132,65 +108,9 @@ public class SpotifyLyricsService } /// - /// Gets lyrics directly from Spotify's color-lyrics API. - /// - private async Task GetLyricsDirectAsync(string spotifyTrackId, string cacheKey) - { - try - { - // Get access token - var token = await _spotifyClient.GetWebAccessTokenAsync(); - if (string.IsNullOrEmpty(token)) - { - _logger.LogWarning("Could not get Spotify access token for lyrics"); - return null; - } - - // Request lyrics from Spotify's color-lyrics API - var url = $"{LyricsApiBase}/{spotifyTrackId}?format=json&vocalRemoval=false&market=from_token"; - - var request = new HttpRequestMessage(HttpMethod.Get, url); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); - request.Headers.Add("Accept", "application/json"); - - var response = await _httpClient.SendAsync(request); - - if (response.StatusCode == HttpStatusCode.NotFound) - { - _logger.LogDebug("No lyrics found on Spotify for track {TrackId}", spotifyTrackId); - return null; - } - - if (!response.IsSuccessStatusCode) - { - _logger.LogWarning("Spotify lyrics API returned {StatusCode} for track {TrackId}", - response.StatusCode, spotifyTrackId); - return null; - } - - var json = await response.Content.ReadAsStringAsync(); - var result = ParseLyricsResponse(json, spotifyTrackId); - - if (result != null) - { - // Cache for 30 days (lyrics don't change) - await _cache.SetAsync(cacheKey, result, TimeSpan.FromDays(30)); - _logger.LogInformation("Cached Spotify lyrics for track {TrackId} ({LineCount} lines)", - spotifyTrackId, result.Lines.Count); - } - - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error fetching Spotify lyrics for track {TrackId}", spotifyTrackId); - return null; - } - } - - /// - /// Searches for a track on Spotify and returns its lyrics. + /// Searches for a track on Spotify and returns its lyrics using the sidecar API. /// Useful when you have track metadata but not a Spotify ID. + /// Note: This requires the sidecar to handle search, or we skip it. /// public async Task SearchAndGetLyricsAsync( string trackName, @@ -204,89 +124,11 @@ public class SpotifyLyricsService return null; } - try - { - var token = await _spotifyClient.GetWebAccessTokenAsync(); - if (string.IsNullOrEmpty(token)) - { - _logger.LogWarning("Could not get Spotify access token for lyrics search"); - return null; - } - - // Search for the track - var query = $"track:{trackName} artist:{artistName}"; - if (!string.IsNullOrEmpty(albumName)) - { - query += $" album:{albumName}"; - } - - _logger.LogDebug("Searching Spotify for lyrics: {Query}", query); - - var searchUrl = $"https://api.spotify.com/v1/search?q={Uri.EscapeDataString(query)}&type=track&limit=5"; - - var request = new HttpRequestMessage(HttpMethod.Get, searchUrl); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); - - var response = await _httpClient.SendAsync(request); - if (!response.IsSuccessStatusCode) - { - _logger.LogWarning("Spotify search failed with status {StatusCode}", response.StatusCode); - return null; - } - - var json = await response.Content.ReadAsStringAsync(); - using var doc = JsonDocument.Parse(json); - var root = doc.RootElement; - - if (!root.TryGetProperty("tracks", out var tracks) || - !tracks.TryGetProperty("items", out var items) || - items.GetArrayLength() == 0) - { - _logger.LogInformation("No Spotify tracks found for: {Track} - {Artist}", trackName, artistName); - return null; - } - - // Find best match considering duration if provided - string? bestMatchId = null; - var bestScore = 0; - - foreach (var item in items.EnumerateArray()) - { - var id = item.TryGetProperty("id", out var idProp) ? idProp.GetString() : null; - if (string.IsNullOrEmpty(id)) continue; - - var score = 100; // Base score - - // Check duration match - if (durationMs.HasValue && item.TryGetProperty("duration_ms", out var durProp)) - { - var trackDuration = durProp.GetInt32(); - var durationDiff = Math.Abs(trackDuration - durationMs.Value); - if (durationDiff < 2000) score += 50; // Within 2 seconds - else if (durationDiff < 5000) score += 25; // Within 5 seconds - } - - if (score > bestScore) - { - bestScore = score; - bestMatchId = id; - } - } - - if (!string.IsNullOrEmpty(bestMatchId)) - { - _logger.LogDebug("Found Spotify track match: {TrackId} (score: {Score})", bestMatchId, bestScore); - return await GetLyricsByTrackIdAsync(bestMatchId); - } - - _logger.LogInformation("No suitable Spotify track match found for: {Track} - {Artist}", trackName, artistName); - return null; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error searching Spotify for lyrics: {Track} - {Artist}", trackName, artistName); - return null; - } + // The sidecar API only supports track ID, not search + // So we skip Spotify lyrics for search-based requests + // LRCLib will be used as fallback + _logger.LogDebug("Spotify lyrics search by metadata not supported with sidecar API, skipping"); + return null; } /// @@ -323,103 +165,6 @@ public class SpotifyLyricsService }; } - private SpotifyLyricsResult? ParseLyricsResponse(string json, string trackId) - { - try - { - using var doc = JsonDocument.Parse(json); - var root = doc.RootElement; - - var result = new SpotifyLyricsResult - { - SpotifyTrackId = trackId - }; - - // Parse lyrics lines - if (root.TryGetProperty("lyrics", out var lyrics)) - { - // Check sync type - if (lyrics.TryGetProperty("syncType", out var syncType)) - { - result.SyncType = syncType.GetString() ?? "LINE_SYNCED"; - } - - // Parse lines - if (lyrics.TryGetProperty("lines", out var lines)) - { - foreach (var line in lines.EnumerateArray()) - { - var lyricsLine = new SpotifyLyricsLine - { - StartTimeMs = line.TryGetProperty("startTimeMs", out var start) - ? long.Parse(start.GetString() ?? "0") : 0, - Words = line.TryGetProperty("words", out var words) - ? words.GetString() ?? "" : "", - EndTimeMs = line.TryGetProperty("endTimeMs", out var end) - ? long.Parse(end.GetString() ?? "0") : 0 - }; - - // Parse syllables if available (for word-level sync) - if (line.TryGetProperty("syllables", out var syllables)) - { - foreach (var syllable in syllables.EnumerateArray()) - { - lyricsLine.Syllables.Add(new SpotifyLyricsSyllable - { - StartTimeMs = syllable.TryGetProperty("startTimeMs", out var sStart) - ? long.Parse(sStart.GetString() ?? "0") : 0, - Text = syllable.TryGetProperty("charsIndex", out var text) - ? text.GetString() ?? "" : "" - }); - } - } - - result.Lines.Add(lyricsLine); - } - } - - // Parse color information - if (lyrics.TryGetProperty("colors", out var colors)) - { - result.Colors = new SpotifyLyricsColors - { - Background = colors.TryGetProperty("background", out var bg) - ? ParseColorValue(bg) : null, - Text = colors.TryGetProperty("text", out var txt) - ? ParseColorValue(txt) : null, - HighlightText = colors.TryGetProperty("highlightText", out var ht) - ? ParseColorValue(ht) : null - }; - } - - // Language - if (lyrics.TryGetProperty("language", out var lang)) - { - result.Language = lang.GetString(); - } - - // Provider info - if (lyrics.TryGetProperty("provider", out var provider)) - { - result.Provider = provider.GetString(); - } - - // Display info - if (lyrics.TryGetProperty("providerDisplayName", out var providerDisplay)) - { - result.ProviderDisplayName = providerDisplay.GetString(); - } - } - - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error parsing Spotify lyrics response"); - return null; - } - } - /// /// Parses the response from the sidecar spotify-lyrics-api service. /// Format: {"error": false, "syncType": "LINE_SYNCED", "lines": [...]} @@ -477,23 +222,6 @@ public class SpotifyLyricsService } } - private static int? ParseColorValue(JsonElement element) - { - if (element.ValueKind == JsonValueKind.Number) - { - return element.GetInt32(); - } - if (element.ValueKind == JsonValueKind.String) - { - var str = element.GetString(); - if (!string.IsNullOrEmpty(str) && int.TryParse(str, out var val)) - { - return val; - } - } - return null; - } - private static string ExtractTrackId(string input) { if (string.IsNullOrEmpty(input)) return input;