mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
feat: Odesli/song.link conversion for Spotify lyrics on external tracks
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -87,6 +87,7 @@ redis-data/
|
|||||||
apis/steering/
|
apis/steering/
|
||||||
apis/api-calls/*.json
|
apis/api-calls/*.json
|
||||||
!apis/api-calls/jellyfin-openapi-stable.json
|
!apis/api-calls/jellyfin-openapi-stable.json
|
||||||
|
apis/temp.json
|
||||||
|
|
||||||
# Log files for debugging
|
# Log files for debugging
|
||||||
apis/api-calls/*.log
|
apis/api-calls/*.log
|
||||||
|
|||||||
@@ -1158,9 +1158,20 @@ public class JellyfinController : ControllerBase
|
|||||||
spotifyTrackId = await FindSpotifyIdForExternalTrackAsync(song);
|
spotifyTrackId = await FindSpotifyIdForExternalTrackAsync(song);
|
||||||
if (!string.IsNullOrEmpty(spotifyTrackId))
|
if (!string.IsNullOrEmpty(spotifyTrackId))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Found Spotify ID {SpotifyId} for external track {Provider}/{ExternalId}",
|
_logger.LogInformation("Found Spotify ID {SpotifyId} for external track {Provider}/{ExternalId} from cache",
|
||||||
spotifyTrackId, provider, externalId);
|
spotifyTrackId, provider, externalId);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If no cached Spotify ID, try to convert via Odesli/song.link
|
||||||
|
// This works for SquidWTF (Tidal), Deezer, Qobuz, etc.
|
||||||
|
spotifyTrackId = await ConvertToSpotifyIdViaOdesliAsync(song, provider!, externalId!);
|
||||||
|
if (!string.IsNullOrEmpty(spotifyTrackId))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Converted {Provider}/{ExternalId} to Spotify ID {SpotifyId} via Odesli",
|
||||||
|
provider, externalId, spotifyTrackId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1377,6 +1388,12 @@ public class JellyfinController : ControllerBase
|
|||||||
if (song != null)
|
if (song != null)
|
||||||
{
|
{
|
||||||
spotifyTrackId = await FindSpotifyIdForExternalTrackAsync(song);
|
spotifyTrackId = await FindSpotifyIdForExternalTrackAsync(song);
|
||||||
|
|
||||||
|
// If no cached Spotify ID, try Odesli conversion
|
||||||
|
if (string.IsNullOrEmpty(spotifyTrackId))
|
||||||
|
{
|
||||||
|
spotifyTrackId = await ConvertToSpotifyIdViaOdesliAsync(song, provider, externalId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -4356,6 +4373,103 @@ public class JellyfinController : ControllerBase
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an external track URL (Tidal/Deezer/Qobuz) to a Spotify track ID using Odesli/song.link API.
|
||||||
|
/// This enables Spotify lyrics for external tracks that aren't in injected playlists.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<string?> ConvertToSpotifyIdViaOdesliAsync(Song song, string provider, string externalId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Build the source URL based on provider
|
||||||
|
string? sourceUrl = null;
|
||||||
|
|
||||||
|
switch (provider.ToLowerInvariant())
|
||||||
|
{
|
||||||
|
case "squidwtf":
|
||||||
|
// SquidWTF uses Tidal IDs
|
||||||
|
sourceUrl = $"https://tidal.com/browse/track/{externalId}";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "deezer":
|
||||||
|
sourceUrl = $"https://www.deezer.com/track/{externalId}";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "qobuz":
|
||||||
|
sourceUrl = $"https://www.qobuz.com/us-en/album/-/-/{externalId}";
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
_logger.LogDebug("Provider {Provider} not supported for Odesli conversion", provider);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache first (cache for 30 days since these mappings don't change)
|
||||||
|
var cacheKey = $"odesli:{provider}:{externalId}";
|
||||||
|
var cachedSpotifyId = await _cache.GetStringAsync(cacheKey);
|
||||||
|
if (!string.IsNullOrEmpty(cachedSpotifyId))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Returning cached Odesli conversion: {Provider}/{ExternalId} → {SpotifyId}",
|
||||||
|
provider, externalId, cachedSpotifyId);
|
||||||
|
return cachedSpotifyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Odesli API
|
||||||
|
var odesliUrl = $"https://api.song.link/v1-alpha.1/links?url={Uri.EscapeDataString(sourceUrl)}&userCountry=US";
|
||||||
|
|
||||||
|
_logger.LogDebug("Calling Odesli API: {Url}", odesliUrl);
|
||||||
|
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
httpClient.Timeout = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
var response = await httpClient.GetAsync(odesliUrl);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Odesli API returned {StatusCode} for {Provider}/{ExternalId}",
|
||||||
|
response.StatusCode, provider, externalId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
// Extract Spotify URL from linksByPlatform.spotify.url
|
||||||
|
if (root.TryGetProperty("linksByPlatform", out var platforms) &&
|
||||||
|
platforms.TryGetProperty("spotify", out var spotify) &&
|
||||||
|
spotify.TryGetProperty("url", out var spotifyUrlEl))
|
||||||
|
{
|
||||||
|
var spotifyUrl = spotifyUrlEl.GetString();
|
||||||
|
if (!string.IsNullOrEmpty(spotifyUrl))
|
||||||
|
{
|
||||||
|
// Extract Spotify ID from URL: https://open.spotify.com/track/{id}
|
||||||
|
var match = System.Text.RegularExpressions.Regex.Match(spotifyUrl, @"spotify\.com/track/([a-zA-Z0-9]+)");
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
var spotifyId = match.Groups[1].Value;
|
||||||
|
|
||||||
|
// Cache the result (30 days)
|
||||||
|
await _cache.SetStringAsync(cacheKey, spotifyId, TimeSpan.FromDays(30));
|
||||||
|
|
||||||
|
_logger.LogInformation("✓ Odesli converted {Provider}/{ExternalId} → Spotify ID {SpotifyId}",
|
||||||
|
provider, externalId, spotifyId);
|
||||||
|
|
||||||
|
return spotifyId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("No Spotify link found in Odesli response for {Provider}/{ExternalId}", provider, externalId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Error converting {Provider}/{ExternalId} via Odesli", provider, externalId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// force rebuild Sun Jan 25 13:22:47 EST 2026
|
// force rebuild Sun Jan 25 13:22:47 EST 2026
|
||||||
|
|||||||
Reference in New Issue
Block a user