Skip to content

Commit a601887

Browse files
committed
use spotify client id & secret for searching
1 parent 8004212 commit a601887

File tree

7 files changed

+98
-54
lines changed

7 files changed

+98
-54
lines changed

README.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,16 @@ plugins:
120120
yandexmusic: false # Enable Yandex Music lyrics source
121121
vkmusic: false # Enable Vk Music lyrics source
122122
spotify:
123-
clientId: "your client id"
124-
clientSecret: "your client secret"
123+
# clientId & clientSecret are required for using spsearch
124+
# clientId: "your client id"
125+
# clientSecret: "your client secret"
125126
# spDc: "your sp dc cookie" # the sp dc cookie used for accessing the spotify lyrics api
126127
countryCode: "US" # the country code you want to use for filtering the artists top tracks. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
127128
playlistLoadLimit: 6 # The number of pages at 100 tracks each
128129
albumLoadLimit: 6 # The number of pages at 50 tracks each
129130
resolveArtistsInSearch: true # Whether to resolve artists in track search results (can be slow)
130131
localFiles: false # Enable local files support with Spotify playlists. Please note `uri` & `isrc` will be `null` & `identifier` will be `"local"`
132+
preferAnonymousToken: true # Whether to use the anonymous token for resolving tracks, artists and albums. Playlists are always resolved with the anonymous token to support autogenerated playlists.
131133
applemusic:
132134
countryCode: "US" # the country code you want to use for filtering the artists top tracks and language. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
133135
mediaAPIToken: "your apple music api token" # apple music api token

application.example.yml

+4-2
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,16 @@ plugins:
2424
yandexmusic: false # Enable Yandex Music lyrics source
2525
vkmusic: false # Enable Vk Music lyrics source
2626
spotify:
27-
clientId: "your client id"
28-
clientSecret: "your client secret"
27+
# clientId & clientSecret are required for using spsearch
28+
# clientId: "your client id"
29+
# clientSecret: "your client secret"
2930
# spDc: "your sp dc cookie" # the sp dc cookie used for accessing the spotify lyrics api
3031
countryCode: "US" # the country code you want to use for filtering the artists top tracks. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
3132
playlistLoadLimit: 6 # The number of pages at 100 tracks each
3233
albumLoadLimit: 6 # The number of pages at 50 tracks each
3334
resolveArtistsInSearch: true # Whether to resolve artists in track search results (can be slow)
3435
localFiles: false # Enable local files support with Spotify playlists. Please note `uri` & `isrc` will be `null` & `identifier` will be `"local"`
36+
preferAnonymousToken: true # Whether to use the anonymous token for resolving tracks, artists and albums. Playlists are always resolved with the anonymous token to support autogenerated playlists.
3537
applemusic:
3638
countryCode: "US" # the country code you want to use for filtering the artists top tracks and language. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
3739
mediaAPIToken: "your apple music api token" # apple music api token

main/src/main/java/com/github/topi314/lavasrc/spotify/SpotifySourceManager.java

+28-17
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,13 @@ public class SpotifySourceManager extends MirroringAudioSourceManager implements
5656
private static final Logger log = LoggerFactory.getLogger(SpotifySourceManager.class);
5757

5858
private final HttpInterfaceManager httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager();
59-
private SpotifyTokenTracker tokenTracker;
59+
private final SpotifyTokenTracker tokenTracker;
6060
private final String countryCode;
6161
private int playlistPageLimit = 6;
6262
private int albumPageLimit = 6;
6363
private boolean localFiles;
6464
private boolean resolveArtistsInSearch = true;
65+
private boolean preferAnonymousToken = false;
6566

6667
public SpotifySourceManager(String[] providers, String clientId, String clientSecret, String countryCode, AudioPlayerManager audioPlayerManager) {
6768
this(clientId, clientSecret, null, countryCode, unused -> audioPlayerManager, new DefaultMirroringAudioTrackResolver(providers));
@@ -80,6 +81,10 @@ public SpotifySourceManager(String clientId, String clientSecret, String country
8081
}
8182

8283
public SpotifySourceManager(String clientId, String clientSecret, String spDc, String countryCode, Function<Void, AudioPlayerManager> audioPlayerManager, MirroringAudioTrackResolver mirroringAudioTrackResolver) {
84+
this(clientId, clientSecret, false, spDc, countryCode, audioPlayerManager, mirroringAudioTrackResolver);
85+
}
86+
87+
public SpotifySourceManager(String clientId, String clientSecret, boolean preferAnonymousToken, String spDc, String countryCode, Function<Void, AudioPlayerManager> audioPlayerManager, MirroringAudioTrackResolver mirroringAudioTrackResolver) {
8388
super(audioPlayerManager, mirroringAudioTrackResolver);
8489

8590
this.tokenTracker = new SpotifyTokenTracker(this, clientId, clientSecret, spDc);
@@ -88,6 +93,7 @@ public SpotifySourceManager(String clientId, String clientSecret, String spDc, S
8893
countryCode = "US";
8994
}
9095
this.countryCode = countryCode;
96+
this.preferAnonymousToken = preferAnonymousToken;
9197
}
9298

9399
public void setPlaylistPageLimit(int playlistPageLimit) {
@@ -114,6 +120,10 @@ public void setSpDc(String spDc) {
114120
this.tokenTracker.setSpDc(spDc);
115121
}
116122

123+
public void setPreferAnonymousToken(boolean preferAnonymousToken) {
124+
this.preferAnonymousToken = preferAnonymousToken;
125+
}
126+
117127
@NotNull
118128
@Override
119129
public String getSourceName() {
@@ -270,9 +280,10 @@ public AudioItem loadItem(String identifier, boolean preview) {
270280
return null;
271281
}
272282

273-
public JsonBrowser getJson(String uri) throws IOException {
283+
public JsonBrowser getJson(String uri, boolean anonymous, boolean preferAnonymous) throws IOException {
274284
var request = new HttpGet(uri);
275-
request.addHeader("Authorization", "Bearer " + this.tokenTracker.getAccessToken());
285+
var accessToken = anonymous ? this.tokenTracker.getAnonymousAccessToken() : this.tokenTracker.getAccessToken(preferAnonymous);
286+
request.addHeader("Authorization", "Bearer " + accessToken);
276287
return LavaSrcTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request);
277288
}
278289

@@ -281,7 +292,7 @@ private AudioSearchResult getAutocomplete(String query, Set<AudioSearchResult.Ty
281292
types = SEARCH_TYPES;
282293
}
283294
var url = API_BASE + "search?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&type=" + types.stream().map(AudioSearchResult.Type::getName).collect(Collectors.joining(","));
284-
var json = this.getJson(url);
295+
var json = this.getJson(url, false, false);
285296
if (json == null) {
286297
return AudioSearchResult.EMPTY;
287298
}
@@ -331,14 +342,14 @@ private AudioSearchResult getAutocomplete(String query, Set<AudioSearchResult.Ty
331342
}
332343

333344
public AudioItem getSearch(String query, boolean preview) throws IOException {
334-
var json = this.getJson(API_BASE + "search?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&type=track");
345+
var json = this.getJson(API_BASE + "search?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&type=track", false, false);
335346
if (json == null || json.get("tracks").get("items").values().isEmpty()) {
336347
return AudioReference.NO_TRACK;
337348
}
338349

339350
if (this.resolveArtistsInSearch) {
340351
var artistIds = json.get("tracks").get("items").values().stream().map(track -> track.get("artists").index(0).get("id").text()).collect(Collectors.joining(","));
341-
var artistJson = this.getJson(API_BASE + "artists?ids=" + artistIds);
352+
var artistJson = this.getJson(API_BASE + "artists?ids=" + artistIds, false, false);
342353
if (artistJson != null) {
343354
for (var artist : artistJson.get("artists").values()) {
344355
for (var track : json.get("tracks").get("items").values()) {
@@ -354,7 +365,7 @@ public AudioItem getSearch(String query, boolean preview) throws IOException {
354365
}
355366

356367
public AudioItem getRecommendations(String query, boolean preview) throws IOException {
357-
var json = this.getJson(API_BASE + "recommendations?" + query);
368+
var json = this.getJson(API_BASE + "recommendations?" + query, false, false);
358369
if (json == null || json.get("tracks").values().isEmpty()) {
359370
return AudioReference.NO_TRACK;
360371
}
@@ -363,12 +374,12 @@ public AudioItem getRecommendations(String query, boolean preview) throws IOExce
363374
}
364375

365376
public AudioItem getAlbum(String id, boolean preview) throws IOException {
366-
var json = this.getJson(API_BASE + "albums/" + id);
377+
var json = this.getJson(API_BASE + "albums/" + id, false, this.preferAnonymousToken);
367378
if (json == null) {
368379
return AudioReference.NO_TRACK;
369380
}
370381

371-
var artistJson = this.getJson(API_BASE + "artists/" + json.get("artists").index(0).get("id").text());
382+
var artistJson = this.getJson(API_BASE + "artists/" + json.get("artists").index(0).get("id").text(), false, this.preferAnonymousToken);
372383
if (artistJson == null) {
373384
artistJson = JsonBrowser.newMap();
374385
}
@@ -379,10 +390,10 @@ public AudioItem getAlbum(String id, boolean preview) throws IOException {
379390
var offset = 0;
380391
var pages = 0;
381392
do {
382-
page = this.getJson(API_BASE + "albums/" + id + "/tracks?limit=" + ALBUM_MAX_PAGE_ITEMS + "&offset=" + offset);
393+
page = this.getJson(API_BASE + "albums/" + id + "/tracks?limit=" + ALBUM_MAX_PAGE_ITEMS + "&offset=" + offset, false, this.preferAnonymousToken);
383394
offset += ALBUM_MAX_PAGE_ITEMS;
384395

385-
var tracksPage = this.getJson(API_BASE + "tracks/?ids=" + page.get("items").values().stream().map(track -> track.get("id").text()).collect(Collectors.joining(",")));
396+
var tracksPage = this.getJson(API_BASE + "tracks/?ids=" + page.get("items").values().stream().map(track -> track.get("id").text()).collect(Collectors.joining(",")), false, this.preferAnonymousToken);
386397

387398
for (var track : tracksPage.get("tracks").values()) {
388399
var albumJson = JsonBrowser.newMap();
@@ -407,7 +418,7 @@ public AudioItem getAlbum(String id, boolean preview) throws IOException {
407418
}
408419

409420
public AudioItem getPlaylist(String id, boolean preview) throws IOException {
410-
var json = this.getJson(API_BASE + "playlists/" + id);
421+
var json = this.getJson(API_BASE + "playlists/" + id, true, false);
411422
if (json == null) {
412423
return AudioReference.NO_TRACK;
413424
}
@@ -417,7 +428,7 @@ public AudioItem getPlaylist(String id, boolean preview) throws IOException {
417428
var offset = 0;
418429
var pages = 0;
419430
do {
420-
page = this.getJson(API_BASE + "playlists/" + id + "/tracks?limit=" + PLAYLIST_MAX_PAGE_ITEMS + "&offset=" + offset);
431+
page = this.getJson(API_BASE + "playlists/" + id + "/tracks?limit=" + PLAYLIST_MAX_PAGE_ITEMS + "&offset=" + offset, true, false);
421432
offset += PLAYLIST_MAX_PAGE_ITEMS;
422433

423434
for (var value : page.get("items").values()) {
@@ -440,12 +451,12 @@ public AudioItem getPlaylist(String id, boolean preview) throws IOException {
440451
}
441452

442453
public AudioItem getArtist(String id, boolean preview) throws IOException {
443-
var json = this.getJson(API_BASE + "artists/" + id);
454+
var json = this.getJson(API_BASE + "artists/" + id, false, this.preferAnonymousToken);
444455
if (json == null) {
445456
return AudioReference.NO_TRACK;
446457
}
447458

448-
var tracksJson = this.getJson(API_BASE + "artists/" + id + "/top-tracks?market=" + this.countryCode);
459+
var tracksJson = this.getJson(API_BASE + "artists/" + id + "/top-tracks?market=" + this.countryCode, false, this.preferAnonymousToken);
449460
if (tracksJson == null || tracksJson.get("tracks").values().isEmpty()) {
450461
return AudioReference.NO_TRACK;
451462
}
@@ -458,12 +469,12 @@ public AudioItem getArtist(String id, boolean preview) throws IOException {
458469
}
459470

460471
public AudioItem getTrack(String id, boolean preview) throws IOException {
461-
var json = this.getJson(API_BASE + "tracks/" + id);
472+
var json = this.getJson(API_BASE + "tracks/" + id, false, this.preferAnonymousToken);
462473
if (json == null) {
463474
return AudioReference.NO_TRACK;
464475
}
465476

466-
var artistJson = this.getJson(API_BASE + "artists/" + json.get("artists").index(0).get("id").text());
477+
var artistJson = this.getJson(API_BASE + "artists/" + json.get("artists").index(0).get("id").text(), false, this.preferAnonymousToken);
467478
if (artistJson != null) {
468479
json.get("artists").index(0).put("images", artistJson.get("images"));
469480
}

main/src/main/java/com/github/topi314/lavasrc/spotify/SpotifyTokenTracker.java

+48-32
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import org.apache.http.client.methods.CloseableHttpResponse;
66
import org.apache.http.client.methods.HttpGet;
77
import org.apache.http.client.methods.HttpPost;
8-
import org.apache.http.client.methods.HttpUriRequest;
98
import org.apache.http.impl.client.CloseableHttpClient;
109
import org.apache.http.impl.client.HttpClients;
1110
import org.apache.http.message.BasicNameValuePair;
@@ -43,6 +42,9 @@ public class SpotifyTokenTracker {
4342
private String accessToken;
4443
private Instant expires;
4544

45+
private String anonymousAccessToken;
46+
private Instant anonymousExpires;
47+
4648
private String spDc;
4749
private String accountToken;
4850
private Instant accountTokenExpire;
@@ -54,8 +56,6 @@ public SpotifyTokenTracker(SpotifySourceManager source, String clientId, String
5456

5557
if (!hasValidCredentials()) {
5658
log.debug("Missing/invalid credentials, falling back to public token.");
57-
} else {
58-
log.debug("Valid credentials found, ready to request access token.");
5959
}
6060

6161
this.spDc = spDc;
@@ -72,7 +72,14 @@ public void setClientIDS(String clientId, String clientSecret) {
7272
this.expires = null;
7373
}
7474

75-
public String getAccessToken() throws IOException {
75+
private boolean hasValidCredentials() {
76+
return clientId != null && !clientId.isEmpty() && clientSecret != null && !clientSecret.isEmpty();
77+
}
78+
79+
public String getAccessToken(boolean preferAnonymousToken) throws IOException {
80+
if (preferAnonymousToken || !hasValidCredentials()) {
81+
return this.getAnonymousAccessToken();
82+
}
7683
if (this.accessToken == null || this.expires == null || this.expires.isBefore(Instant.now())) {
7784
synchronized (this) {
7885
if (accessToken == null || this.expires == null || this.expires.isBefore(Instant.now())) {
@@ -85,40 +92,48 @@ public String getAccessToken() throws IOException {
8592
}
8693

8794
private void refreshAccessToken() throws IOException {
88-
boolean usePublicToken = !hasValidCredentials();
89-
HttpUriRequest request;
90-
91-
if (!usePublicToken) {
92-
request = new HttpPost("https://accounts.spotify.com/api/token");
93-
request.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString((this.clientId + ":" + this.clientSecret).getBytes(StandardCharsets.UTF_8)));
94-
((HttpPost) request).setEntity(new UrlEncodedFormEntity(List.of(new BasicNameValuePair("grant_type", "client_credentials")), StandardCharsets.UTF_8));
95-
} else {
96-
request = new HttpGet(generateGetAccessTokenURL());
97-
}
95+
var request = new HttpPost("https://accounts.spotify.com/api/token");
96+
request.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString((this.clientId + ":" + this.clientSecret).getBytes(StandardCharsets.UTF_8)));
97+
request.setEntity(new UrlEncodedFormEntity(List.of(new BasicNameValuePair("grant_type", "client_credentials")), StandardCharsets.UTF_8));
9898

99-
try {
100-
var json = LavaSrcTools.fetchResponseAsJson(sourceManager.getHttpInterface(), request);
101-
if (!json.get("error").isNull()) {
102-
String error = json.get("error").text();
103-
log.error("Error while fetching access token: {}", error);
104-
throw new RuntimeException("Error while fetching access token: " + error);
105-
}
99+
var json = LavaSrcTools.fetchResponseAsJson(sourceManager.getHttpInterface(), request);
100+
if (json == null) {
101+
throw new RuntimeException("No response from Spotify API");
102+
}
103+
if (!json.get("error").isNull()) {
104+
var error = json.get("error").text();
105+
throw new RuntimeException("Error while fetching access token: " + error);
106+
}
107+
accessToken = json.get("access_token").text();
108+
expires = Instant.now().plusSeconds(json.get("expires_in").asLong(0));
109+
}
106110

107-
if (!usePublicToken) {
108-
accessToken = json.get("access_token").text();
109-
expires = Instant.now().plusSeconds(json.get("expires_in").asLong(0));
110-
} else {
111-
accessToken = json.get("accessToken").text();
112-
expires = Instant.ofEpochMilli(json.get("accessTokenExpirationTimestampMs").asLong(0));
111+
public String getAnonymousAccessToken() throws IOException {
112+
if (this.anonymousAccessToken == null || this.anonymousExpires == null || this.anonymousExpires.isBefore(Instant.now())) {
113+
synchronized (this) {
114+
if (this.anonymousAccessToken == null || this.anonymousExpires == null || this.anonymousExpires.isBefore(Instant.now())) {
115+
log.debug("Anonymous access token is invalid or expired, refreshing token...");
116+
this.refreshAnonymousAccessToken();
117+
}
113118
}
114-
} catch (IOException e) {
115-
log.error("Access token refreshing failed.", e);
116-
throw new RuntimeException("Access token refreshing failed", e);
117119
}
120+
return this.anonymousAccessToken;
118121
}
119122

120-
private boolean hasValidCredentials() {
121-
return clientId != null && !clientId.isEmpty() && clientSecret != null && !clientSecret.isEmpty();
123+
private void refreshAnonymousAccessToken() throws IOException {
124+
var request = new HttpGet(generateGetAccessTokenURL());
125+
126+
var json = LavaSrcTools.fetchResponseAsJson(sourceManager.getHttpInterface(), request);
127+
if (json == null) {
128+
throw new RuntimeException("No response from Spotify API");
129+
}
130+
if (!json.get("error").isNull()) {
131+
var error = json.get("error").text();
132+
throw new RuntimeException("Error while fetching access token: " + error);
133+
}
134+
135+
anonymousAccessToken = json.get("accessToken").text();
136+
anonymousExpires = Instant.ofEpochMilli(json.get("accessTokenExpirationTimestampMs").asLong(0));
122137
}
123138

124139
public void setSpDc(String spDc) {
@@ -292,4 +307,5 @@ private static byte[] hexStringToByteArray(String s) {
292307
}
293308
return data;
294309
}
310+
295311
}

plugin/src/main/java/com/github/topi314/lavasrc/plugin/LavaSrcPlugin.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public LavaSrcPlugin(
6868
this.lyricsSourcesConfig = lyricsSourcesConfig;
6969

7070
if (sourcesConfig.isSpotify() || lyricsSourcesConfig.isSpotify()) {
71-
this.spotify = new SpotifySourceManager(spotifyConfig.getClientId(), spotifyConfig.getClientSecret(), spotifyConfig.getSpDc(), spotifyConfig.getCountryCode(), unused -> manager, new DefaultMirroringAudioTrackResolver(pluginConfig.getProviders()));
71+
this.spotify = new SpotifySourceManager(spotifyConfig.getClientId(), spotifyConfig.getClientSecret(), spotifyConfig.isPreferAnonymousToken(), spotifyConfig.getSpDc(), spotifyConfig.getCountryCode(), unused -> manager, new DefaultMirroringAudioTrackResolver(pluginConfig.getProviders()));
7272
if (spotifyConfig.getPlaylistLoadLimit() > 0) {
7373
this.spotify.setPlaylistPageLimit(spotifyConfig.getPlaylistLoadLimit());
7474
}
@@ -273,6 +273,9 @@ public void updateConfig(@RequestBody Config config) {
273273
if (spotifyConfig.getClientId() != null && spotifyConfig.getClientSecret() != null) {
274274
this.spotify.setClientIDSecret(spotifyConfig.getClientId(), spotifyConfig.getClientSecret());
275275
}
276+
if (spotifyConfig.getPreferAnonymousToken() != null) {
277+
this.spotify.setPreferAnonymousToken(spotifyConfig.getPreferAnonymousToken());
278+
}
276279
}
277280

278281
var appleMusicConfig = config.getAppleMusic();

plugin/src/main/java/com/github/topi314/lavasrc/plugin/config/SpotifyConfig.java

+9
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public class SpotifyConfig {
1515
private int albumLoadLimit = 6;
1616
private boolean resolveArtistsInSearch = true;
1717
private boolean localFiles = false;
18+
private boolean preferAnonymousToken = false;
1819

1920
public String getClientId() {
2021
return this.clientId;
@@ -79,4 +80,12 @@ public boolean isLocalFiles() {
7980
public void setLocalFiles(boolean localFiles) {
8081
this.localFiles = localFiles;
8182
}
83+
84+
public boolean isPreferAnonymousToken() {
85+
return this.preferAnonymousToken;
86+
}
87+
88+
public void setPreferAnonymousToken(boolean preferAnonymousToken) {
89+
this.preferAnonymousToken = preferAnonymousToken;
90+
}
8291
}

protocol/src/commonMain/kotlin/com/github/topi314/lavasrc/protocol/Config.kt

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ data class SpotifyConfig(
1818
val clientId: String? = null,
1919
val clientSecret: String? = null,
2020
val spDc: String? = null,
21+
val preferAnonymousToken: Boolean? = null,
2122
)
2223

2324
@Serializable

0 commit comments

Comments
 (0)