fix(spotify): parse flat title/subtitle embed format for playlist tracks

Spotify's playlist embed uses {title, subtitle, audioPreview} per track
instead of {name, artists, album}. Fixes:
- _spTrackItem: reads title as trackName, subtitle as artistName fallback,
  audioPreview.url as previewUrl, isExplicit as boolean
- Route: reads entity.authors[0].name for playlist owner, entity.subtitle
  for track artist fallback, entity.title fallback for entity.name
- Playlist tracks get playlist cover art as default artwork (no per-track art)
- Removes temporary debug logging

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-04-27 18:14:02 +02:00
1 parent 363fed61a5
commit a305ec98e2
1 file changed
+33 -17
+33 -17
View File
@@ -447,24 +447,38 @@ function _spArtistName(artistsRaw) {
} }
function _spTrackItem(t, defaultAlbum, defaultArt, defaultAlbumArtist) { function _spTrackItem(t, defaultAlbum, defaultArt, defaultAlbumArtist) {
// Unwrap optional {track: ...} wrapper (playlist item format) // Unwrap optional {track: ...} wrapper (old playlist format)
let raw = t?.track || t; let raw = t?.track || t;
// Handle GraphQL-style {data: {__typename, name, artists, ...}} // Handle GraphQL-style {data: {__typename, ...}}
const d = raw?.data || raw; const d = raw?.data || raw;
const artist = _spAllArtists(d?.artists) || defaultAlbumArtist || '';
// Name: new embed uses "title", old/track embed uses "name"
const trackName = d?.name || d?.title || '';
// Artists: array format [{name}] or [{profile:{name}}], GraphQL {items:[...]},
// or flat string in "subtitle" (new playlist embed: "Myrne, Aviella")
let artist = '';
if (d?.artists) {
artist = _spAllArtists(d.artists);
} else if (typeof d?.subtitle === 'string' && d.subtitle) {
artist = d.subtitle;
}
artist = artist || defaultAlbumArtist || '';
const art = _spArtwork(d) || defaultArt || ''; const art = _spArtwork(d) || defaultArt || '';
const albumObj = d?.albumOfTrack || d?.album || {}; const albumObj = d?.albumOfTrack || d?.album || {};
const album = albumObj?.name || defaultAlbum || ''; const album = albumObj?.name || defaultAlbum || '';
const date = albumObj?.date?.isoString || albumObj?.release_date || ''; const date = albumObj?.date?.isoString || albumObj?.release_date || '';
return { return {
trackName: d?.name || '', trackName,
artistName: artist, artistName: artist,
collectionName: album, collectionName: album,
artworkUrl100: art, artworkUrl100: art,
releaseDate: date, releaseDate: date,
trackNumber: d?.trackNumber || d?.track_number, trackNumber: d?.trackNumber || d?.track_number,
trackExplicitness: d?.contentRating?.label === 'EXPLICIT' || d?.explicit ? 'explicit' : 'notExplicit', trackExplicitness: d?.contentRating?.label === 'EXPLICIT' || d?.isExplicit || d?.explicit ? 'explicit' : 'notExplicit',
previewUrl: d?.previewUrl || d?.preview_url || null, previewUrl: d?.previewUrl || d?.preview_url || d?.audioPreview?.url || null,
}; };
} }
@@ -480,13 +494,14 @@ app.post('/api/spotify/resolve', async (req, res) => {
try { try {
const { type, id } = parseSpotifyUrl(url); const { type, id } = parseSpotifyUrl(url);
const entity = await fetchSpotifyEmbed(type, id); const entity = await fetchSpotifyEmbed(type, id);
const artist = _spArtistName(entity.artists); // "subtitle" is used by the flat embed format as artist/owner string
const artist = _spArtistName(entity.artists) || (typeof entity.subtitle === 'string' ? entity.subtitle : '');
const artUrl = _spArtwork(entity); const artUrl = _spArtwork(entity);
if (type === 'track') { if (type === 'track') {
return res.json({ return res.json({
type: 'track', type: 'track',
name: entity.name, name: entity.name || entity.title || '',
artist, artist,
artworkUrl: artUrl, artworkUrl: artUrl,
tracks: [_spTrackItem(entity, entity.album?.name, artUrl, artist)], tracks: [_spTrackItem(entity, entity.album?.name, artUrl, artist)],
@@ -497,27 +512,28 @@ app.post('/api/spotify/resolve', async (req, res) => {
const trackList = entity.trackList || entity.tracks?.items || []; const trackList = entity.trackList || entity.tracks?.items || [];
return res.json({ return res.json({
type: 'album', type: 'album',
name: entity.name, name: entity.name || entity.title || '',
artist, artist,
artworkUrl: artUrl, artworkUrl: artUrl,
tracks: trackList.map(t => _spTrackItem(t, entity.name, artUrl, artist)), tracks: trackList.map(t => _spTrackItem(t, entity.name || entity.title, artUrl, artist)),
}); });
} }
if (type === 'playlist') { if (type === 'playlist') {
const trackList = entity.trackList || entity.content?.items || entity.tracks?.items || []; const trackList = entity.trackList || entity.content?.items || entity.tracks?.items || [];
const owner = entity.ownerV2?.data?.name || entity.owner?.displayName || entity.owner?.display_name || ''; const owner = entity.authors?.[0]?.name
const firstRaw = trackList[0]; || entity.ownerV2?.data?.name
console.log('[spotify/resolve] entity keys:', Object.keys(entity)); || entity.owner?.displayName
console.log('[spotify/resolve] trackList[0]:', JSON.stringify(firstRaw, null, 2).slice(0, 800)); || entity.owner?.display_name
|| (typeof entity.subtitle === 'string' ? entity.subtitle : '');
return res.json({ return res.json({
type: 'playlist', type: 'playlist',
name: entity.name, name: entity.name || entity.title || '',
artist: owner, artist: owner,
artworkUrl: artUrl, artworkUrl: artUrl,
description: entity.description || '', description: entity.description || '',
tracks: trackList.map(t => _spTrackItem(t, null, null, null)), // pass artUrl as defaultArt so tracks show the playlist cover when no per-track art exists
_debug: { entityKeys: Object.keys(entity), firstTrack: firstRaw }, tracks: trackList.map(t => _spTrackItem(t, null, artUrl, null)),
}); });
} }