feat: convert Tidal tracks to Spotify ID immediately for lyrics

- Added SpotifyId field to Song model
- SquidWTFMetadataService now calls Odesli API when fetching track metadata
- Spotify ID is populated immediately when track is loaded, not during lyrics fetch
- GetLyrics now checks song.SpotifyId first before falling back to cache/Odesli
- Enables Spotify lyrics for all SquidWTF (Tidal) tracks automatically
- Reduces latency - conversion happens once during track load, not every lyrics request
This commit is contained in:
2026-02-07 11:57:24 -05:00
parent 2272e8d363
commit 3937e637c6
3 changed files with 49 additions and 6 deletions

View File

@@ -1165,9 +1165,15 @@ public class JellyfinController : ControllerBase
{
song = await _metadataService.GetSongAsync(provider!, externalId!);
// Try to find Spotify ID from matched tracks cache
// External tracks from playlists should have been matched and cached
if (song != null)
// Use Spotify ID from song metadata if available (populated during GetSongAsync)
if (song != null && !string.IsNullOrEmpty(song.SpotifyId))
{
spotifyTrackId = song.SpotifyId;
_logger.LogInformation("Using Spotify ID {SpotifyId} from song metadata for {Provider}/{ExternalId}",
spotifyTrackId, provider, externalId);
}
// Fallback: Try to find Spotify ID from matched tracks cache
else if (song != null)
{
spotifyTrackId = await FindSpotifyIdForExternalTrackAsync(song);
if (!string.IsNullOrEmpty(spotifyTrackId))
@@ -1177,8 +1183,7 @@ public class JellyfinController : ControllerBase
}
else
{
// If no cached Spotify ID, try to convert via Odesli/song.link
// This works for SquidWTF (Tidal), Deezer, Qobuz, etc.
// Last resort: Try to convert via Odesli/song.link
spotifyTrackId = await ConvertToSpotifyIdViaOdesliAsync(song, provider!, externalId!);
if (!string.IsNullOrEmpty(spotifyTrackId))
{

View File

@@ -44,6 +44,11 @@ public class Song
/// </summary>
public string? Isrc { get; set; }
/// <summary>
/// Spotify track ID (for lyrics and matching)
/// </summary>
public string? SpotifyId { get; set; }
/// <summary>
/// Full release date (format: YYYY-MM-DD)
/// </summary>

View File

@@ -280,7 +280,40 @@ public class SquidWTFMetadataService : IMusicMetadataService
if (!result.RootElement.TryGetProperty("data", out var track))
return null;
return ParseTidalTrackFull(track);
var song = ParseTidalTrackFull(track);
// Convert to Spotify ID via Odesli for lyrics support
if (song != null && !string.IsNullOrEmpty(externalId))
{
try
{
var tidalUrl = $"https://tidal.com/browse/track/{externalId}";
var odesliUrl = $"https://api.song.link/v1-alpha.1/links?url={Uri.EscapeDataString(tidalUrl)}&userCountry=US";
_logger.LogDebug("🔗 Converting Tidal track {ExternalId} to Spotify ID via Odesli", externalId);
var odesliResponse = await _httpClient.GetAsync(odesliUrl);
if (odesliResponse.IsSuccessStatusCode)
{
var odesliJson = await odesliResponse.Content.ReadAsStringAsync();
var odesliDoc = JsonDocument.Parse(odesliJson);
if (odesliDoc.RootElement.TryGetProperty("linksByPlatform", out var platforms) &&
platforms.TryGetProperty("spotify", out var spotifyPlatform) &&
spotifyPlatform.TryGetProperty("entityUniqueId", out var spotifyIdEl))
{
song.SpotifyId = spotifyIdEl.GetString();
_logger.LogInformation("✓ Converted squidwtf/{ExternalId} → Spotify ID {SpotifyId}", externalId, song.SpotifyId);
}
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to convert Tidal track to Spotify ID via Odesli");
}
}
return song;
}, (Song?)null);
}