Skip to content

Commit acefb0d

Browse files
committed
implement open spotify api to fetch the track cover image, version 1.0.6
1 parent 37d96da commit acefb0d

File tree

12 files changed

+347
-4
lines changed

12 files changed

+347
-4
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ repositories {
1414
}
1515
1616
dependencies {
17-
implementation 'com.github.LabyStudio:java-spotify-api:1.0.5:all'
17+
implementation 'com.github.LabyStudio:java-spotify-api:1.0.6:all'
1818
}
1919
```
2020

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ plugins {
44
}
55

66
group 'de.labystudio'
7-
version '1.0.5'
7+
version '1.0.6'
88

99
compileJava {
1010
sourceCompatibility = '1.8'
@@ -18,6 +18,7 @@ repositories {
1818
dependencies {
1919
implementation group: 'net.java.dev.jna', name: 'jna', version: '4.5.0'
2020
implementation group: 'net.java.dev.jna', name: 'jna-platform', version: '4.5.0'
21+
implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.6'
2122
}
2223

2324
java {
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package de.labystudio.spotifyapi.open;
2+
3+
import com.google.gson.Gson;
4+
import com.google.gson.stream.JsonReader;
5+
import de.labystudio.spotifyapi.model.Track;
6+
import de.labystudio.spotifyapi.open.model.AccessTokenResponse;
7+
import de.labystudio.spotifyapi.open.model.track.OpenTrack;
8+
9+
import javax.imageio.ImageIO;
10+
import javax.net.ssl.HttpsURLConnection;
11+
import java.awt.image.BufferedImage;
12+
import java.io.IOException;
13+
import java.io.InputStreamReader;
14+
import java.net.URL;
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
import java.util.Map;
18+
import java.util.concurrent.ConcurrentHashMap;
19+
import java.util.concurrent.Executor;
20+
import java.util.concurrent.Executors;
21+
import java.util.function.Consumer;
22+
23+
/**
24+
* OpenSpotify REST API.
25+
* Implements the functionality to request the image of a Spotify track.
26+
*
27+
* @author LabyStudio
28+
*/
29+
public class OpenSpotifyAPI {
30+
31+
private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/71.0.3578.98";
32+
33+
private static final String URL_API_GEN_ACCESS_TOKEN = "https://open.spotify.com/get_access_token?reason=transport&productType=web_player";
34+
private static final String URL_API_TRACKS = "https://api.spotify.com/v1/tracks/%s";
35+
36+
private final Executor executor = Executors.newSingleThreadExecutor();
37+
38+
private final Map<Track, BufferedImage> imageCache = new ConcurrentHashMap<>();
39+
private final List<Track> cacheQueue = new ArrayList<>();
40+
private int cacheSize = 10;
41+
42+
private AccessTokenResponse accessTokenResponse;
43+
44+
public OpenSpotifyAPI() {
45+
this.generateAccessTokenAsync(accessTokenResponse -> this.accessTokenResponse = accessTokenResponse);
46+
}
47+
48+
/**
49+
* Generate an access token asynchronously for the open spotify api
50+
*/
51+
private void generateAccessTokenAsync(Consumer<AccessTokenResponse> callback) {
52+
this.executor.execute(() -> {
53+
try {
54+
// Generate access token
55+
callback.accept(this.generateAccessToken());
56+
} catch (Exception error) {
57+
error.printStackTrace();
58+
}
59+
});
60+
}
61+
62+
/**
63+
* Generate an access token for the open spotify api
64+
*/
65+
private AccessTokenResponse generateAccessToken() throws IOException {
66+
// Open connection
67+
HttpsURLConnection connection = (HttpsURLConnection) new URL(URL_API_GEN_ACCESS_TOKEN).openConnection();
68+
connection.addRequestProperty("User-Agent", USER_AGENT);
69+
connection.addRequestProperty("referer", "https://open.spotify.com/");
70+
connection.addRequestProperty("app-platform", "WebPlayer");
71+
72+
// Read response
73+
JsonReader reader = new JsonReader(new InputStreamReader(connection.getInputStream()));
74+
return new Gson().fromJson(reader, AccessTokenResponse.class);
75+
}
76+
77+
/**
78+
* Request the cover image of the given track asynchronously.
79+
* If the track is already in the cache, it will be returned.
80+
*
81+
* @param track The track to lookup
82+
* @param callback Response with the buffered image track. It won't be called on an error.
83+
*/
84+
public void requestImageAsync(Track track, Consumer<BufferedImage> callback) {
85+
this.executor.execute(() -> {
86+
try {
87+
BufferedImage image = this.requestImage(track);
88+
if (image != null) {
89+
callback.accept(image);
90+
}
91+
} catch (Exception error) {
92+
error.printStackTrace();
93+
}
94+
});
95+
}
96+
97+
/**
98+
* Request the cover image of the given track synchronously.
99+
* If the track is already in the cache, it will be returned.
100+
*
101+
* @param track The track to lookup
102+
* @return The buffered image of the track or null if it failed
103+
* @throws IOException if the request failed
104+
*/
105+
public BufferedImage requestImage(Track track) throws IOException {
106+
return this.requestImage(track, true);
107+
}
108+
109+
/**
110+
* Request the cover image of the given track.
111+
* If the track is already in the cache, it will be returned.
112+
*
113+
* @param track The track to lookup
114+
* @param canGenerateNewAccessToken It will try again once if it fails
115+
* @return Buffered image track.
116+
* @throws IOException if the request failed
117+
*/
118+
private BufferedImage requestImage(Track track, boolean canGenerateNewAccessToken) throws IOException {
119+
BufferedImage cachedImage = this.imageCache.get(track);
120+
if (cachedImage != null) {
121+
return cachedImage;
122+
}
123+
124+
// Create REST API url
125+
String url = String.format(URL_API_TRACKS, track.getId());
126+
127+
// Connect
128+
HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
129+
connection.addRequestProperty("User-Agent", USER_AGENT);
130+
connection.addRequestProperty("referer", "https://open.spotify.com/");
131+
connection.addRequestProperty("app-platform", "WebPlayer");
132+
connection.addRequestProperty("origin", "https://open.spotify.com");
133+
connection.addRequestProperty("authorization", "Bearer " + this.accessTokenResponse.accessToken);
134+
135+
// Access token outdated
136+
if (connection.getResponseCode() / 100 != 2) {
137+
// Prevent infinite loop
138+
if (canGenerateNewAccessToken) {
139+
// Generate new access token
140+
this.accessTokenResponse = this.generateAccessToken();
141+
142+
// Try again
143+
return this.requestImage(track, false);
144+
} else {
145+
// Request failed twice
146+
return null;
147+
}
148+
}
149+
150+
// Read response
151+
JsonReader reader = new JsonReader(new InputStreamReader(connection.getInputStream()));
152+
OpenTrack openTrack = new Gson().fromJson(reader, OpenTrack.class);
153+
154+
// Get largest image url
155+
String imageUrl = openTrack.album.images.get(0).url;
156+
157+
// Download cover image
158+
if (imageUrl != null) {
159+
BufferedImage image = ImageIO.read(new URL(imageUrl));
160+
if (image == null) {
161+
throw new IOException("Could not load image: " + imageUrl);
162+
}
163+
164+
// Remove image from cache if cache is full
165+
if (this.cacheQueue.size() > this.cacheSize) {
166+
Track element = this.cacheQueue.remove(0);
167+
this.imageCache.remove(element);
168+
}
169+
170+
// Add new image to cache
171+
this.imageCache.put(track, image);
172+
this.cacheQueue.add(track);
173+
174+
return image;
175+
}
176+
177+
return null;
178+
}
179+
180+
/**
181+
* Set the maximal amount of images to cache.
182+
* Default is 10.
183+
*
184+
* @param cacheSize The maximal amount of images to cache
185+
*/
186+
public void setCacheSize(int cacheSize) {
187+
this.cacheSize = cacheSize;
188+
}
189+
190+
/**
191+
* Clear the cache of images.
192+
*/
193+
public void clearCache() {
194+
this.imageCache.clear();
195+
this.cacheQueue.clear();
196+
}
197+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package de.labystudio.spotifyapi.open.model;
2+
3+
public class AccessTokenResponse {
4+
public String accessToken;
5+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
package de.labystudio.spotifyapi.open.model.track;
3+
4+
import com.google.gson.annotations.SerializedName;
5+
6+
import java.util.List;
7+
8+
public class Album {
9+
10+
@SerializedName("album_type")
11+
public String albumType;
12+
13+
public List<Artist> artists = null;
14+
15+
@SerializedName("available_markets")
16+
public List<Object> availableMarkets = null;
17+
18+
@SerializedName("external_urls")
19+
public ExternalUrls externalUrls;
20+
21+
public String href;
22+
public String id;
23+
public List<Image> images = null;
24+
public String name;
25+
26+
@SerializedName("release_date")
27+
public String releaseDate;
28+
29+
@SerializedName("release_date_precision")
30+
public String releaseDatePrecision;
31+
32+
@SerializedName("total_tracks")
33+
public Integer totalTracks;
34+
35+
public String type;
36+
public String uri;
37+
38+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
package de.labystudio.spotifyapi.open.model.track;
3+
4+
import com.google.gson.annotations.SerializedName;
5+
6+
public class Artist {
7+
8+
@SerializedName("external_urls")
9+
public ExternalUrls externalUrls;
10+
public String href;
11+
public String id;
12+
public String name;
13+
public String type;
14+
public String uri;
15+
16+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
package de.labystudio.spotifyapi.open.model.track;
3+
4+
public class ExternalIds {
5+
6+
public String isrc;
7+
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
package de.labystudio.spotifyapi.open.model.track;
3+
4+
public class ExternalUrls {
5+
6+
public String spotify;
7+
8+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
package de.labystudio.spotifyapi.open.model.track;
3+
4+
public class Image {
5+
6+
public Integer height;
7+
public String url;
8+
public Integer width;
9+
10+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
2+
package de.labystudio.spotifyapi.open.model.track;
3+
4+
import com.google.gson.annotations.SerializedName;
5+
6+
import java.util.List;
7+
8+
public class OpenTrack {
9+
10+
public Album album;
11+
12+
public List<Artist> artists = null;
13+
14+
@SerializedName("available_markets")
15+
public List<Object> availableMarkets = null;
16+
17+
@SerializedName("disc_number")
18+
public Integer discNumber;
19+
20+
@SerializedName("duration_ms")
21+
public Integer durationMs;
22+
23+
public Boolean explicit;
24+
25+
@SerializedName("external_ids")
26+
public ExternalIds externalIds;
27+
28+
@SerializedName("external_urls")
29+
public ExternalUrls externalUrls;
30+
31+
public String href;
32+
33+
public String id;
34+
35+
@SerializedName("is_local")
36+
public Boolean isLocal;
37+
38+
public String name;
39+
40+
public Integer popularity;
41+
42+
@SerializedName("preview_url")
43+
public Object previewUrl;
44+
45+
@SerializedName("track_number")
46+
public Integer trackNumber;
47+
48+
public String type;
49+
50+
public String uri;
51+
52+
}

0 commit comments

Comments
 (0)