mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-10 16:08:39 -05:00
Add Spotify direct API integration for lyrics, ISRC matching, and playlist ordering
Features: - SpotifyApiClient: Direct Spotify API client using sp_dc session cookie - SpotifyLyricsService: Fetch synced lyrics from Spotify's color-lyrics API - SpotifyPlaylistFetcher: Get playlists with correct track ordering and ISRC codes - SpotifyTrackMatchingService: ISRC-based exact track matching for external providers Improvements: - Lyrics endpoint now prioritizes: 1) Jellyfin embedded, 2) Spotify synced, 3) LRCLIB - Fixed playback progress reporting - removed incorrect body wrapping for Jellyfin API - Added SpotifyApiSettings configuration model Security: - Session cookie and client ID properly masked in startup logs - All credentials read from environment variables only
This commit is contained in:
66
allstarr/Models/Settings/SpotifyApiSettings.cs
Normal file
66
allstarr/Models/Settings/SpotifyApiSettings.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
namespace allstarr.Models.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for direct Spotify API access.
|
||||
/// This enables fetching playlist data directly from Spotify rather than relying on the Jellyfin plugin.
|
||||
///
|
||||
/// Benefits over Jellyfin plugin approach:
|
||||
/// - Track ordering is preserved (critical for playlists like Release Radar)
|
||||
/// - ISRC codes available for exact matching
|
||||
/// - Real-time data without waiting for plugin sync
|
||||
/// - Full track metadata (duration, release date, etc.)
|
||||
/// </summary>
|
||||
public class SpotifyApiSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable direct Spotify API integration.
|
||||
/// When enabled, playlists will be fetched directly from Spotify instead of the Jellyfin plugin.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Spotify Client ID from https://developer.spotify.com/dashboard
|
||||
/// Used for OAuth token refresh and API access.
|
||||
/// </summary>
|
||||
public string ClientId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Spotify Client Secret from https://developer.spotify.com/dashboard
|
||||
/// Optional - only needed for certain OAuth flows.
|
||||
/// </summary>
|
||||
public string ClientSecret { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Spotify session cookie (sp_dc).
|
||||
/// Required for accessing editorial/personalized playlists like Release Radar and Discover Weekly.
|
||||
/// These playlists are not available via the official API.
|
||||
///
|
||||
/// To get this cookie:
|
||||
/// 1. Log into open.spotify.com in your browser
|
||||
/// 2. Open DevTools (F12) > Application > Cookies > https://open.spotify.com
|
||||
/// 3. Copy the value of the "sp_dc" cookie
|
||||
///
|
||||
/// Note: This cookie expires periodically and will need to be refreshed.
|
||||
/// </summary>
|
||||
public string SessionCookie { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Cache duration in minutes for playlist data.
|
||||
/// Playlists like Release Radar only update weekly, so caching is beneficial.
|
||||
/// Default: 60 minutes
|
||||
/// </summary>
|
||||
public int CacheDurationMinutes { get; set; } = 60;
|
||||
|
||||
/// <summary>
|
||||
/// Rate limit delay between Spotify API requests in milliseconds.
|
||||
/// Default: 100ms (Spotify allows ~100 requests per minute)
|
||||
/// </summary>
|
||||
public int RateLimitDelayMs { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to prefer ISRC matching over fuzzy title/artist matching when ISRC is available.
|
||||
/// ISRC provides exact track identification across services.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool PreferIsrcMatching { get; set; } = true;
|
||||
}
|
||||
231
allstarr/Models/Spotify/SpotifyPlaylistTrack.cs
Normal file
231
allstarr/Models/Spotify/SpotifyPlaylistTrack.cs
Normal file
@@ -0,0 +1,231 @@
|
||||
using allstarr.Models.Domain;
|
||||
|
||||
namespace allstarr.Models.Spotify;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a track from a Spotify playlist with full metadata including position.
|
||||
/// This model preserves track ordering which is critical for playlists like Release Radar.
|
||||
/// </summary>
|
||||
public class SpotifyPlaylistTrack
|
||||
{
|
||||
/// <summary>
|
||||
/// Spotify track ID (e.g., "3a8mo25v74BMUOJ1IDUEBL")
|
||||
/// </summary>
|
||||
public string SpotifyId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Track's position in the playlist (0-based index).
|
||||
/// This is critical for maintaining correct playlist order.
|
||||
/// </summary>
|
||||
public int Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Track title
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Album name
|
||||
/// </summary>
|
||||
public string Album { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Album Spotify ID
|
||||
/// </summary>
|
||||
public string AlbumId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// List of artist names
|
||||
/// </summary>
|
||||
public List<string> Artists { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of artist Spotify IDs
|
||||
/// </summary>
|
||||
public List<string> ArtistIds { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// ISRC (International Standard Recording Code) for exact track identification.
|
||||
/// This enables precise matching across different streaming services.
|
||||
/// </summary>
|
||||
public string? Isrc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Track duration in milliseconds
|
||||
/// </summary>
|
||||
public int DurationMs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the track contains explicit content
|
||||
/// </summary>
|
||||
public bool Explicit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Track's popularity score (0-100)
|
||||
/// </summary>
|
||||
public int Popularity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Preview URL for 30-second audio clip (may be null)
|
||||
/// </summary>
|
||||
public string? PreviewUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Album artwork URL (largest available)
|
||||
/// </summary>
|
||||
public string? AlbumArtUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Release date of the album (format varies: YYYY, YYYY-MM, or YYYY-MM-DD)
|
||||
/// </summary>
|
||||
public string? ReleaseDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When this track was added to the playlist
|
||||
/// </summary>
|
||||
public DateTime? AddedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Disc number within the album
|
||||
/// </summary>
|
||||
public int DiscNumber { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Track number within the disc
|
||||
/// </summary>
|
||||
public int TrackNumber { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Primary (first) artist name
|
||||
/// </summary>
|
||||
public string PrimaryArtist => Artists.FirstOrDefault() ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// All artists as a comma-separated string
|
||||
/// </summary>
|
||||
public string AllArtists => string.Join(", ", Artists);
|
||||
|
||||
/// <summary>
|
||||
/// Track duration as TimeSpan
|
||||
/// </summary>
|
||||
public TimeSpan Duration => TimeSpan.FromMilliseconds(DurationMs);
|
||||
|
||||
/// <summary>
|
||||
/// Converts to the legacy MissingTrack format for compatibility with existing matching logic.
|
||||
/// </summary>
|
||||
public MissingTrack ToMissingTrack() => new()
|
||||
{
|
||||
SpotifyId = SpotifyId,
|
||||
Title = Title,
|
||||
Album = Album,
|
||||
Artists = Artists
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Spotify playlist with its tracks in order.
|
||||
/// </summary>
|
||||
public class SpotifyPlaylist
|
||||
{
|
||||
/// <summary>
|
||||
/// Spotify playlist ID
|
||||
/// </summary>
|
||||
public string SpotifyId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Playlist name
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Playlist description
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Playlist owner's display name
|
||||
/// </summary>
|
||||
public string? OwnerName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Playlist owner's Spotify ID
|
||||
/// </summary>
|
||||
public string? OwnerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of tracks in the playlist
|
||||
/// </summary>
|
||||
public int TotalTracks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Playlist cover image URL
|
||||
/// </summary>
|
||||
public string? ImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is a collaborative playlist
|
||||
/// </summary>
|
||||
public bool Collaborative { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this playlist is public
|
||||
/// </summary>
|
||||
public bool Public { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tracks in the playlist, ordered by position
|
||||
/// </summary>
|
||||
public List<SpotifyPlaylistTrack> Tracks { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// When this data was fetched from Spotify
|
||||
/// </summary>
|
||||
public DateTime FetchedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot ID for change detection (Spotify's playlist version identifier)
|
||||
/// </summary>
|
||||
public string? SnapshotId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Spotify track that has been matched to an external provider track.
|
||||
/// Preserves position for correct playlist ordering.
|
||||
/// </summary>
|
||||
public class MatchedTrack
|
||||
{
|
||||
/// <summary>
|
||||
/// Position in the original Spotify playlist (0-based)
|
||||
/// </summary>
|
||||
public int Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Original Spotify track ID
|
||||
/// </summary>
|
||||
public string SpotifyId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Original Spotify track title (for debugging/logging)
|
||||
/// </summary>
|
||||
public string SpotifyTitle { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Original Spotify artist (for debugging/logging)
|
||||
/// </summary>
|
||||
public string SpotifyArtist { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// ISRC used for matching (if available)
|
||||
/// </summary>
|
||||
public string? Isrc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How the match was made: "isrc" or "fuzzy"
|
||||
/// </summary>
|
||||
public string MatchType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The matched song from the external provider
|
||||
/// </summary>
|
||||
public Song MatchedSong { get; set; } = null!;
|
||||
}
|
||||
Reference in New Issue
Block a user