66import com .google .gson .stream .JsonReader ;
77import de .labystudio .spotifyapi .model .Track ;
88import de .labystudio .spotifyapi .open .model .AccessTokenResponse ;
9+ import de .labystudio .spotifyapi .open .model .GraphQLOperation ;
10+ import de .labystudio .spotifyapi .open .model .track .Image ;
911import de .labystudio .spotifyapi .open .model .track .OpenTrack ;
12+ import de .labystudio .spotifyapi .open .model .track .TrackResponse ;
1013import de .labystudio .spotifyapi .open .totp .TOTP ;
1114import de .labystudio .spotifyapi .open .totp .gson .SecretDeserializer ;
1215import de .labystudio .spotifyapi .open .totp .gson .SecretSerializer ;
1619import javax .imageio .ImageIO ;
1720import javax .net .ssl .HttpsURLConnection ;
1821import java .awt .image .BufferedImage ;
19- import java .io .BufferedReader ;
20- import java .io .IOException ;
21- import java .io .InputStream ;
22- import java .io .InputStreamReader ;
22+ import java .io .*;
2323import java .net .HttpURLConnection ;
2424import java .net .URL ;
2525import java .nio .charset .StandardCharsets ;
26+ import java .util .List ;
2627import java .util .concurrent .Executor ;
2728import java .util .concurrent .Executors ;
2829import java .util .function .Consumer ;
@@ -43,7 +44,7 @@ public class OpenSpotifyAPI {
4344 public static final String USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537." + (int ) (Math .random () * 90 );
4445 public static final String URL_API_GEN_ACCESS_TOKEN = "https://open.spotify.com/api/token?reason=%s&productType=web-player&totp=%s&totpServer=%s&totpVer=%s" ;
4546
46- public static final String URL_API_TRACKS = "https://api.spotify.com/v1/tracks/%s " ;
47+ public static final String URL_API_GRAPHQL = "https://api-partner .spotify.com/pathfinder/ v1/query " ;
4748 public static final String URL_API_SERVER_TIME = "https://open.spotify.com/api/server-time" ;
4849
4950 private final Executor executor = Executors .newSingleThreadExecutor ();
@@ -292,12 +293,18 @@ public BufferedImage requestImage(String trackId) throws IOException {
292293 private String requestImageUrl (String trackId ) throws IOException {
293294 // Request track information
294295 OpenTrack openTrack = this .requestOpenTrack (trackId );
295- if (openTrack == null ) {
296+ if (openTrack == null || openTrack .album == null ) {
297+ return null ;
298+ }
299+
300+ // Get images from album
301+ List <Image > images = openTrack .album .getImages ();
302+ if (images == null || images .isEmpty ()) {
296303 return null ;
297304 }
298305
299- // Get largest image url
300- return openTrack . album . images .get (0 ).url ;
306+ // Get largest image url (images are sorted by size in descending order from API)
307+ return images .get (0 ).url ;
301308 }
302309
303310 /**
@@ -324,44 +331,99 @@ public OpenTrack requestOpenTrack(String trackId) throws IOException {
324331 return cachedOpenTrack ;
325332 }
326333
327- // Create REST API url
328- String url = String .format (URL_API_TRACKS , trackId );
329- OpenTrack openTrack = this .request (url , OpenTrack .class , true );
334+ // Use GraphQL API to get track information
335+ TrackResponse graphQLResponse = this .requestTrack (trackId );
336+
337+ // Extract OpenTrack from GraphQL response
338+ OpenTrack openTrack = graphQLResponse != null && graphQLResponse .data != null
339+ ? graphQLResponse .data .trackUnion
340+ : null ;
341+
342+ if (openTrack == null ) {
343+ return null ;
344+ }
330345
331346 // Cache the open track and return it
332347 this .openTrackCache .push (trackId , openTrack );
333348 return openTrack ;
334349 }
335350
336351 /**
337- * Request the open spotify api with the given url
352+ * Request track information using GraphQL API
353+ *
354+ * @param trackId The track id to lookup
355+ * @return GraphQL response
356+ * @throws IOException if the request failed
357+ */
358+ private TrackResponse requestTrack (String trackId ) throws IOException {
359+ JsonObject variables = new JsonObject ();
360+ variables .addProperty ("uri" , "spotify:track:" + trackId );
361+ return this .requestGraphQL (GraphQLOperation .GET_TRACK , variables , TrackResponse .class );
362+ }
363+
364+ /**
365+ * Request the open spotify GraphQL api with the given operation
338366 * It will try again once if it fails
339367 *
340- * @param url The url to request
341- * @param clazz The class to parse the response to
342- * @param canGenerateNewAccessToken It will try again once if it fails
343- * @param <T> The type of the response
368+ * @param operation The GraphQL operation to execute
369+ * @param variables The variables for the GraphQL query
370+ * @param clazz The class to parse the response to
371+ * @param <T> The type of the response
344372 * @return The parsed response
345373 * @throws IOException if the request failed
346374 */
347- public <T > T request (String url , Class <?> clazz , boolean canGenerateNewAccessToken ) throws IOException {
375+ public <T > T requestGraphQL (
376+ GraphQLOperation operation ,
377+ JsonObject variables ,
378+ Class <T > clazz
379+ ) throws IOException {
380+ return this .requestGraphQL (operation , variables , clazz , true );
381+ }
382+
383+ private <T > T requestGraphQL (
384+ GraphQLOperation operation ,
385+ JsonObject variables ,
386+ Class <T > clazz ,
387+ boolean canGenerateNewAccessToken
388+ ) throws IOException {
348389 // Generate access token if not present
349390 if (this .accessTokenResponse == null ) {
350391 this .accessTokenResponse = this .generateAccessToken ();
351392 }
352393
394+ // Build GraphQL query using JsonObject
395+ JsonObject query = new JsonObject ();
396+ query .addProperty ("operationName" , operation .getOperationName ());
397+ query .add ("variables" , variables );
398+
399+ JsonObject extensions = new JsonObject ();
400+ JsonObject persistedQuery = new JsonObject ();
401+ persistedQuery .addProperty ("version" , 1 );
402+ persistedQuery .addProperty ("sha256Hash" , operation .getSha256Hash ());
403+ extensions .add ("persistedQuery" , persistedQuery );
404+ query .add ("extensions" , extensions );
405+
353406 // Connect
354- HttpsURLConnection connection = (HttpsURLConnection ) new URL (url ).openConnection ();
407+ HttpsURLConnection connection = (HttpsURLConnection ) new URL (URL_API_GRAPHQL ).openConnection ();
408+ connection .setRequestMethod ("POST" );
409+ connection .setDoOutput (true );
355410 connection .addRequestProperty ("User-Agent" , USER_AGENT );
356411 connection .addRequestProperty ("referer" , "https://open.spotify.com/" );
357412 connection .addRequestProperty ("app-platform" , "WebPlayer" );
358413 connection .addRequestProperty ("origin" , "https://open.spotify.com" );
414+ connection .addRequestProperty ("content-type" , "application/json" );
359415
360416 // Add access token
361417 if (this .accessTokenResponse != null ) {
362418 connection .addRequestProperty ("authorization" , "Bearer " + this .accessTokenResponse .accessToken );
363419 }
364420
421+ // Write the query
422+ try (OutputStream os = connection .getOutputStream ()) {
423+ byte [] input = GSON .toJson (query ).getBytes (StandardCharsets .UTF_8 );
424+ os .write (input , 0 , input .length );
425+ }
426+
365427 // Access token outdated
366428 if (connection .getResponseCode () / 100 != 2 ) {
367429 // Prevent infinite loop
@@ -370,7 +432,7 @@ public <T> T request(String url, Class<?> clazz, boolean canGenerateNewAccessTok
370432 this .accessTokenResponse = this .generateAccessToken ();
371433
372434 // Try again
373- return this .request ( url , clazz , false );
435+ return this .requestGraphQL ( operation , variables , clazz , false );
374436 } else {
375437 // Request failed twice
376438 return null ;
0 commit comments