@@ -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 - Z 0 - 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