11package de .labystudio .spotifyapi .open ;
22
33import com .google .gson .Gson ;
4+ import com .google .gson .JsonObject ;
45import com .google .gson .stream .JsonReader ;
56import de .labystudio .spotifyapi .model .Track ;
67import de .labystudio .spotifyapi .open .model .AccessTokenResponse ;
78import de .labystudio .spotifyapi .open .model .track .OpenTrack ;
9+ import de .labystudio .spotifyapi .open .util .TOTP ;
810
911import javax .imageio .ImageIO ;
1012import javax .net .ssl .HttpsURLConnection ;
1113import java .awt .image .BufferedImage ;
14+ import java .io .BufferedReader ;
1215import java .io .IOException ;
16+ import java .io .InputStream ;
1317import java .io .InputStreamReader ;
18+ import java .net .HttpURLConnection ;
1419import java .net .URL ;
1520import java .nio .charset .StandardCharsets ;
1621import java .util .concurrent .Executor ;
@@ -27,10 +32,13 @@ public class OpenSpotifyAPI {
2732
2833 private static final Gson GSON = new Gson ();
2934
30- private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/71.0.3578.98" ;
35+ private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/71.0.3578." + ( int ) ( Math . random () * 90 ) ;
3136
32- private static final String URL_API_GEN_ACCESS_TOKEN = "https://open.spotify.com/get_access_token?reason=transport &productType=web_player " ;
37+ private static final String URL_API_GEN_ACCESS_TOKEN = "https://open.spotify.com/get_access_token?reason=%s &productType=web-player&totp=%s&totpVer=5 " ;
3338 private static final String URL_API_TRACKS = "https://api.spotify.com/v1/tracks/%s" ;
39+ private static final String URL_API_SERVER_TIME = "https://open.spotify.com/server-time" ;
40+
41+ public static final int [] TOTP_SECRET = {12 , 56 , 76 , 33 , 88 , 44 , 88 , 33 , 78 , 78 , 11 , 66 , 22 , 22 , 55 , 69 , 54 };
3442
3543 private final Executor executor = Executors .newSingleThreadExecutor ();
3644
@@ -53,16 +61,86 @@ private void generateAccessTokenAsync(Consumer<AccessTokenResponse> callback) {
5361 });
5462 }
5563
64+ public long requestServerTime () throws IOException {
65+ // Get server time
66+ URL url = new URL (URL_API_SERVER_TIME );
67+ HttpURLConnection conn = (HttpURLConnection ) url .openConnection ();
68+ conn .setRequestMethod ("GET" );
69+ conn .setRequestProperty ("Host" , "open.spotify.com" );
70+ conn .setRequestProperty ("User-Agent" , "Mozilla/5.0" );
71+ conn .setRequestProperty ("accept" , "*/*" );
72+
73+ BufferedReader reader = new BufferedReader (new InputStreamReader (conn .getInputStream ()));
74+ String response = reader .readLine ();
75+ reader .close ();
76+
77+ JsonObject obj = GSON .fromJson (response , JsonObject .class );
78+ return obj .get ("serverTime" ).getAsLong ();
79+ }
80+
81+ public String generateTotp (long serverTime ) {
82+ StringBuilder xorResults = new StringBuilder ();
83+ for (int i = 0 ; i < TOTP_SECRET .length ; i ++) {
84+ int result = TOTP_SECRET [i ] ^ (i % 33 + 9 );
85+ xorResults .append (result );
86+ }
87+ StringBuilder hexResult = new StringBuilder ();
88+ for (int i = 0 ; i < xorResults .length (); i ++) {
89+ hexResult .append (String .format ("%02x" , (int ) xorResults .charAt (i )));
90+ }
91+ byte [] byteArray = new byte [hexResult .length () / 2 ];
92+ for (int i = 0 ; i < hexResult .length (); i += 2 ) {
93+ int byteValue = Integer .parseInt (hexResult .substring (i , i + 2 ), 16 );
94+ byteArray [i / 2 ] = (byte ) byteValue ;
95+ }
96+ return TOTP .generateOtp (byteArray , serverTime );
97+ }
98+
5699 /**
57100 * Generate an access token for the open spotify api
58101 */
59102 private AccessTokenResponse generateAccessToken () throws IOException {
103+ long serverTime = this .requestServerTime ();
104+ String totp = this .generateTotp (serverTime );
105+
106+ AccessTokenResponse response = this .getToken ("transport" , totp );
107+
108+ if (!this .hasValidAccessToken (response )) {
109+ response = this .getToken ("init" , totp );
110+ }
111+
112+ if (!this .hasValidAccessToken (response )) {
113+ throw new IOException ("Could not generate access token" );
114+ }
115+
116+ return response ;
117+ }
118+
119+ private boolean hasValidAccessToken (AccessTokenResponse response ) {
120+ return response != null && response .accessToken != null && !response .accessToken .isEmpty ();
121+ }
122+
123+ /**
124+ * Retrieve access token using totp
125+ */
126+ private AccessTokenResponse getToken (String mode , String totp ) throws IOException {
60127 // Open connection
61- HttpsURLConnection connection = (HttpsURLConnection ) new URL (URL_API_GEN_ACCESS_TOKEN ).openConnection ();
128+ String url = String .format (URL_API_GEN_ACCESS_TOKEN , mode , totp );
129+ HttpsURLConnection connection = (HttpsURLConnection ) new URL (url ).openConnection ();
62130 connection .addRequestProperty ("User-Agent" , USER_AGENT );
63131 connection .addRequestProperty ("referer" , "https://open.spotify.com/" );
64132 connection .addRequestProperty ("app-platform" , "WebPlayer" );
65133
134+ int code = connection .getResponseCode ();
135+ if (code != HttpURLConnection .HTTP_OK ) {
136+ InputStream errorStream = connection .getErrorStream ();
137+ if (errorStream != null ) {
138+ JsonReader reader = new JsonReader (new InputStreamReader (errorStream ));
139+ JsonObject response = GSON .fromJson (reader , JsonObject .class );
140+ throw new IOException ("Could not retrieve access token: " + response .toString ());
141+ }
142+ }
143+
66144 // Read response
67145 JsonReader reader = new JsonReader (new InputStreamReader (connection .getInputStream ()));
68146 return GSON .fromJson (reader , AccessTokenResponse .class );
0 commit comments