Skip to content

Commit afd7923

Browse files
improve: add spotify isrc extraction & optimize metadata
The same api that returns ISRC can be used for metadata, in a single request.
1 parent 999dc9e commit afd7923

File tree

1 file changed

+137
-17
lines changed

1 file changed

+137
-17
lines changed

src/sources/spotify.js

Lines changed: 137 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,82 @@ export default class SpotifySource {
152152
return limit === 0 ? 'unlimited' : `${limit * multiplier} tracks max`
153153
}
154154

155+
_base62ToHex(id) {
156+
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
157+
let bn = 0n
158+
for (const char of id) {
159+
bn = bn * 62n + BigInt(alphabet.indexOf(char))
160+
}
161+
return bn.toString(16).padStart(32, '0')
162+
}
163+
164+
async _fetchTrackMetadata(id) {
165+
const token = this.anonymousToken || this.mobileToken || this.accessToken
166+
if (!token) return null
167+
168+
try {
169+
const hexId = this._base62ToHex(id)
170+
const url = `${SPOTIFY_CLIENT_API_URL}/metadata/4/track/${hexId}?market=from_token`
171+
const { body, statusCode } = await http1makeRequest(url, {
172+
responseType: 'buffer',
173+
headers: {
174+
Authorization: `Bearer ${token}`,
175+
'Accept': 'application/json',
176+
'App-Platform': 'WebPlayer',
177+
'Spotify-App-Version': '1.2.83.284.g147edeea'
178+
}
179+
})
180+
181+
if (statusCode !== 200 || !body) return null
182+
183+
const bodyStr = body.toString()
184+
try {
185+
return JSON.parse(bodyStr)
186+
} catch {
187+
const isrcIndex = body.indexOf('isrc')
188+
if (isrcIndex !== -1) {
189+
const bodyRange = body.subarray(isrcIndex, isrcIndex + 50).toString()
190+
const isrcMatch = bodyRange.match(/[A-Z0-9]{12}/)
191+
if (isrcMatch) return { external_id: [{ type: 'isrc', id: isrcMatch[0] }] }
192+
}
193+
}
194+
195+
return null
196+
} catch (e) {
197+
logger('debug', 'Spotify', `Exception in _fetchTrackMetadata for ${id}: ${e.message}`)
198+
return null
199+
}
200+
}
201+
202+
_buildTrackFromMetadata(data) {
203+
if (!data || !data.name) return null
204+
205+
const id = data.canonical_uri?.split(':').pop() || data.gid
206+
207+
const isExplicit = !!data.explicit
208+
const trackInfo = {
209+
identifier: id,
210+
isSeekable: true,
211+
author: data.artist?.map((a) => a.name).join(', ') || 'Unknown',
212+
length: data.duration || 0,
213+
isStream: false,
214+
position: 0,
215+
title: data.name,
216+
uri: `https://open.spotify.com/track/${id}?explicit=${isExplicit}`,
217+
artworkUrl: data.album?.cover_group?.image?.find(img => img.size === 'LARGE' || img.size === 'DEFAULT')?.file_id
218+
? `https://i.scdn.co/image/${data.album.cover_group.image.find(img => img.size === 'LARGE' || img.size === 'DEFAULT').file_id}`
219+
: null,
220+
isrc: data.external_id?.find((e) => e.type === 'isrc')?.id || null,
221+
sourceName: 'spotify'
222+
}
223+
224+
return {
225+
encoded: encodeTrack(trackInfo),
226+
info: trackInfo,
227+
pluginInfo: {}
228+
}
229+
}
230+
155231
_isTokenValid() {
156232
return (
157233
this.tokenExpiry && Date.now() < this.tokenExpiry - TOKEN_REFRESH_MARGIN
@@ -712,6 +788,20 @@ export default class SpotifySource {
712788
data.searchV2,
713789
searchType
714790
)
791+
if (results.length > 0 && searchType === 'track') {
792+
const topTrack = results[0]
793+
if (!topTrack.info.isrc) {
794+
const metadata = await this._fetchTrackMetadata(topTrack.info.identifier)
795+
if (metadata) {
796+
const isrc = metadata.external_id?.find((e) => e.type === 'isrc')?.id
797+
if (isrc) {
798+
topTrack.info.isrc = isrc
799+
topTrack.encoded = encodeTrack(topTrack.info)
800+
}
801+
}
802+
}
803+
}
804+
715805
if (results.length > 0) {
716806
return { loadType: 'search', data: results }
717807
}
@@ -733,6 +823,21 @@ export default class SpotifySource {
733823

734824
if (data && !data.error) {
735825
const results = this._processOfficialSearchResults(data, spotifyType)
826+
827+
if (results.length > 0 && spotifyType === 'track') {
828+
const topTrack = results[0]
829+
if (!topTrack.info.isrc) {
830+
const metadata = await this._fetchTrackMetadata(topTrack.info.identifier)
831+
if (metadata) {
832+
const isrc = metadata.external_id?.find((e) => e.type === 'isrc')?.id
833+
if (isrc) {
834+
topTrack.info.isrc = isrc
835+
topTrack.encoded = encodeTrack(topTrack.info)
836+
}
837+
}
838+
}
839+
}
840+
736841
if (results.length > 0) {
737842
return { loadType: 'search', data: results }
738843
}
@@ -1081,10 +1186,31 @@ export default class SpotifySource {
10811186
}
10821187
}
10831188

1189+
if (!track.info.isrc) {
1190+
const metadata = await this._fetchTrackMetadata(id)
1191+
if (metadata) {
1192+
const isrc = metadata.external_id?.find((e) => e.type === 'isrc')?.id
1193+
if (isrc) {
1194+
track.info.isrc = isrc
1195+
track.encoded = encodeTrack(track.info)
1196+
}
1197+
}
1198+
}
1199+
10841200
return {
10851201
loadType: 'track',
10861202
data: track
10871203
}
1204+
} else {
1205+
// GraphQL failed, try metadata endpoint as primary fallback
1206+
const metadata = await this._fetchTrackMetadata(id)
1207+
const track = this._buildTrackFromMetadata(metadata)
1208+
if (track) {
1209+
return {
1210+
loadType: 'track',
1211+
data: track
1212+
}
1213+
}
10881214
}
10891215
}
10901216

@@ -1109,6 +1235,17 @@ export default class SpotifySource {
11091235
}
11101236
}
11111237

1238+
if (!track.info.isrc) {
1239+
const metadata = await this._fetchTrackMetadata(id)
1240+
if (metadata) {
1241+
const isrc = metadata.external_id?.find((e) => e.type === 'isrc')?.id
1242+
if (isrc) {
1243+
track.info.isrc = isrc
1244+
track.encoded = encodeTrack(track.info)
1245+
}
1246+
}
1247+
}
1248+
11121249
return { loadType: 'track', data: track }
11131250
}
11141251
}
@@ -1397,23 +1534,6 @@ export default class SpotifySource {
13971534
}
13981535

13991536
async getTrackUrl(decodedTrack) {
1400-
if (!decodedTrack.isrc && this.accessToken) {
1401-
try {
1402-
const trackData = await this._apiRequest(
1403-
`/tracks/${decodedTrack.identifier}?market=${this.market}`
1404-
)
1405-
if (trackData?.external_ids?.isrc) {
1406-
decodedTrack.isrc = trackData.external_ids.isrc
1407-
}
1408-
} catch (e) {
1409-
logger(
1410-
'debug',
1411-
'Spotify',
1412-
`Failed to fetch ISRC for ${decodedTrack.identifier} via API: ${e.message}`
1413-
)
1414-
}
1415-
}
1416-
14171537
let isExplicit = false
14181538
if (decodedTrack.uri) {
14191539
try {

0 commit comments

Comments
 (0)