Skip to content

Commit

Permalink
add spotify lyrics support
Browse files Browse the repository at this point in the history
  • Loading branch information
topi314 committed Jan 6, 2024
1 parent de673da commit 82a41f7
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 8 deletions.
2 changes: 1 addition & 1 deletion main/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ java {

dependencies {
api "com.github.topi314.lavasearch:lavasearch:1.0.0"
api "com.github.topi314.lavalyrics:lavalyrics:98f7f59"
api "com.github.topi314.lavalyrics:lavalyrics:01bf4e7"
compileOnly "dev.arbjerg:lavaplayer:2.0.4"
implementation "org.jsoup:jsoup:1.15.3"
implementation "commons-io:commons-io:2.7"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public static JsonBrowser fetchResponseAsJson(HttpInterface httpInterface, HttpU
var data = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
log.error("Server responded with not found to '{}': {}", request.getURI(), data);
return null;
} else if (statusCode == HttpStatus.SC_NO_CONTENT) {
log.error("Server responded with not content to '{}'", request.getURI());
return null;
} else if (!HttpClientTools.isSuccessWithContent(statusCode)) {
var data = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
log.error("Server responded with an error to '{}': {}", request.getURI(), data);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.github.topi314.lavasrc.spotify;

import com.github.topi314.lavalyrics.AudioLyricsManager;
import com.github.topi314.lavalyrics.lyrics.AudioLyrics;
import com.github.topi314.lavalyrics.lyrics.BasicAudioLyrics;
import com.github.topi314.lavasearch.AudioSearchManager;
import com.github.topi314.lavasearch.result.AudioSearchResult;
import com.github.topi314.lavasearch.result.BasicAudioSearchResult;
Expand Down Expand Up @@ -30,14 +33,15 @@
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class SpotifySourceManager extends MirroringAudioSourceManager implements HttpConfigurable, AudioSearchManager {
public class SpotifySourceManager extends MirroringAudioSourceManager implements HttpConfigurable, AudioSearchManager, AudioLyricsManager {

public static final Pattern URL_PATTERN = Pattern.compile("(https?://)(www\\.)?open\\.spotify\\.com/((?<region>[a-zA-Z-]+)/)?(user/(?<user>[a-zA-Z0-9-_]+)/)?(?<type>track|album|playlist|artist)/(?<identifier>[a-zA-Z0-9-_]+)");
public static final String SEARCH_PREFIX = "spsearch:";
Expand All @@ -48,31 +52,40 @@ public class SpotifySourceManager extends MirroringAudioSourceManager implements
public static final int PLAYLIST_MAX_PAGE_ITEMS = 100;
public static final int ALBUM_MAX_PAGE_ITEMS = 50;
public static final String API_BASE = "https://api.spotify.com/v1/";
public static final String CLIENT_API_BASE = "https://spclient.wg.spotify.com/";
public static final Set<AudioSearchResult.Type> SEARCH_TYPES = Set.of(AudioSearchResult.Type.ALBUM, AudioSearchResult.Type.ARTIST, AudioSearchResult.Type.PLAYLIST, AudioSearchResult.Type.TRACK);
private static final Logger log = LoggerFactory.getLogger(SpotifySourceManager.class);

private final HttpInterfaceManager httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager();
private final String clientId;
private final String clientSecret;
private final String spDc;
private final String countryCode;
private int playlistPageLimit = 6;
private int albumPageLimit = 6;
private String token;
private Instant tokenExpire;

private String spToken;
private Instant spTokenExpire;

public SpotifySourceManager(String[] providers, String clientId, String clientSecret, String countryCode, AudioPlayerManager audioPlayerManager) {
this(clientId, clientSecret, countryCode, unused -> audioPlayerManager, new DefaultMirroringAudioTrackResolver(providers));
this(clientId, clientSecret, null, countryCode, unused -> audioPlayerManager, new DefaultMirroringAudioTrackResolver(providers));
}

public SpotifySourceManager(String[] providers, String clientId, String clientSecret, String countryCode, Function<Void, AudioPlayerManager> audioPlayerManager) {
this(clientId, clientSecret, countryCode, audioPlayerManager, new DefaultMirroringAudioTrackResolver(providers));
this(clientId, clientSecret, null, countryCode, audioPlayerManager, new DefaultMirroringAudioTrackResolver(providers));
}

public SpotifySourceManager(String clientId, String clientSecret, String countryCode, AudioPlayerManager audioPlayerManager, MirroringAudioTrackResolver mirroringAudioTrackResolver) {
this(clientId, clientSecret, countryCode, unused -> audioPlayerManager, mirroringAudioTrackResolver);
this(clientId, clientSecret, null, countryCode, unused -> audioPlayerManager, mirroringAudioTrackResolver);
}

public SpotifySourceManager(String clientId, String clientSecret, String countryCode, Function<Void, AudioPlayerManager> audioPlayerManager, MirroringAudioTrackResolver mirroringAudioTrackResolver) {
this(clientId, clientSecret, null, countryCode, audioPlayerManager, mirroringAudioTrackResolver);
}

public SpotifySourceManager(String clientId, String clientSecret, String spDc, String countryCode, Function<Void, AudioPlayerManager> audioPlayerManager, MirroringAudioTrackResolver mirroringAudioTrackResolver) {
super(audioPlayerManager, mirroringAudioTrackResolver);

if (clientId == null || clientId.isEmpty()) {
Expand All @@ -85,6 +98,8 @@ public SpotifySourceManager(String clientId, String clientSecret, String country
}
this.clientSecret = clientSecret;

this.spDc = spDc;

if (countryCode == null || countryCode.isEmpty()) {
countryCode = "US";
}
Expand All @@ -99,11 +114,78 @@ public void setAlbumPageLimit(int albumPageLimit) {
this.albumPageLimit = albumPageLimit;
}

@NotNull
@Override
public String getSourceName() {
return "spotify";
}

@Override
@Nullable
public AudioLyrics loadLyrics(@NotNull AudioTrack audioTrack) {
var spotifyTackId = "";
if (audioTrack instanceof SpotifyAudioTrack) {
spotifyTackId = audioTrack.getIdentifier();
}

if (spotifyTackId.isEmpty()) {
AudioItem item = AudioReference.NO_TRACK;
try {
if (audioTrack.getInfo().isrc != null && !audioTrack.getInfo().isrc.isEmpty()) {
item = this.getSearch("isrc:" + audioTrack.getInfo().isrc, false);
}
if (item == AudioReference.NO_TRACK) {
item = this.getSearch(String.format("%s %s", audioTrack.getInfo().title, audioTrack.getInfo().author), false);
}
} catch (IOException e) {
throw new RuntimeException(e);
}

if (item == AudioReference.NO_TRACK) {
return null;
}
if (item instanceof AudioTrack) {
spotifyTackId = ((AudioTrack) item).getIdentifier();
} else if (item instanceof AudioPlaylist) {
var playlist = (AudioPlaylist) item;
if (!playlist.getTracks().isEmpty()) {
spotifyTackId = playlist.getTracks().get(0).getIdentifier();
}
}
}

try {
return this.getLyrics(spotifyTackId);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public AudioLyrics getLyrics(String id) throws IOException {
if (this.spDc == null || this.spDc.isEmpty()) {
throw new IllegalArgumentException("Spotify spDc must be set");
}

var request = new HttpGet(CLIENT_API_BASE + "color-lyrics/v2/track/" + id + "?format=json&vocalRemoval=false");
request.addHeader("App-Platform", "WebPlayer");
request.addHeader("Authorization", "Bearer " + this.getSpToken());
var json = LavaSrcTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request);
if (json == null) {
return null;
}

var lyrics = new ArrayList<AudioLyrics.Line>();
for (var line : json.get("lyrics").get("lines").values()) {
lyrics.add(new BasicAudioLyrics.BasicLine(
Duration.ofMillis(line.get("startTimeMs").asLong(0)),
null,
line.get("words").text()
));
}

return new BasicAudioLyrics("spotify", "MusixMatch", null, lyrics);
}

@Override
public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException {
var extendedAudioTrackInfo = super.decodeTrack(input);
Expand Down Expand Up @@ -188,6 +270,22 @@ public AudioItem loadItem(String identifier, boolean preview) {
return null;
}

public void requestSpToken() throws IOException {
var request = new HttpGet("https://open.spotify.com/get_access_token?reason=transport&productType=web_player");
request.addHeader("Cookie", "sp_dc=" + this.spDc);

var json = LavaSrcTools.fetchResponseAsJson(this.httpInterfaceManager.getInterface(), request);
this.spToken = json.get("accessToken").text();
this.spTokenExpire = Instant.now().plusMillis(json.get("accessTokenExpirationTimestampMs").asLong(0));
}

public String getSpToken() throws IOException {
if (this.spToken == null || this.spTokenExpire == null || this.spTokenExpire.isBefore(Instant.now())) {
this.requestSpToken();
}
return this.spToken;
}

public void requestToken() throws IOException {
var request = new HttpPost("https://accounts.spotify.com/api/token");
request.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString((this.clientId + ":" + this.clientSecret).getBytes(StandardCharsets.UTF_8)));
Expand Down
4 changes: 2 additions & 2 deletions plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ dependencies {
implementation project(":main")
compileOnly "com.github.topi314.lavasearch:lavasearch:1.0.0"
implementation "com.github.topi314.lavasearch:lavasearch-plugin-api:1.0.0"
compileOnly "com.github.topi314.lavalyrics:lavalyrics:98f7f59"
implementation "com.github.topi314.lavalyrics:lavalyrics-plugin-api:98f7f59"
compileOnly "com.github.topi314.lavalyrics:lavalyrics:01bf4e7"
implementation "com.github.topi314.lavalyrics:lavalyrics-plugin-api:01bf4e7"
}

publishing {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.github.topi314.lavasrc.applemusic.AppleMusicSourceManager;
import com.github.topi314.lavasrc.deezer.DeezerAudioSourceManager;
import com.github.topi314.lavasrc.flowerytts.FloweryTTSSourceManager;
import com.github.topi314.lavasrc.mirror.DefaultMirroringAudioTrackResolver;
import com.github.topi314.lavasrc.spotify.SpotifySourceManager;
import com.github.topi314.lavasrc.yandexmusic.YandexMusicSourceManager;
import com.github.topi314.lavasrc.youtube.YoutubeSearchManager;
Expand Down Expand Up @@ -36,7 +37,7 @@ public LavaSrcPlugin(LavaSrcConfig pluginConfig, SourcesConfig sourcesConfig, Sp
log.info("Loading LavaSrc plugin...");

if (sourcesConfig.isSpotify()) {
this.spotify = new SpotifySourceManager(pluginConfig.getProviders(), spotifyConfig.getClientId(), spotifyConfig.getClientSecret(), spotifyConfig.getCountryCode(), unused -> manager);
this.spotify = new SpotifySourceManager(spotifyConfig.getClientId(), spotifyConfig.getClientSecret(), spotifyConfig.getSpDc(), spotifyConfig.getCountryCode(), unused -> manager, new DefaultMirroringAudioTrackResolver(pluginConfig.getProviders()));
if (spotifyConfig.getPlaylistLoadLimit() > 0) {
this.spotify.setPlaylistPageLimit(spotifyConfig.getPlaylistLoadLimit());
}
Expand Down Expand Up @@ -138,6 +139,10 @@ public SearchManager configure(@NotNull SearchManager manager) {
@NotNull
@Override
public LyricsManager configure(@NotNull LyricsManager manager) {
if (this.spotify != null) {
log.info("Registering Spotify lyrics manager...");
manager.registerLyricsManager(this.spotify);
}
if (this.deezer != null) {
log.info("Registering Deezer lyrics manager...");
manager.registerLyricsManager(this.deezer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class SpotifyConfig {

private String clientId;
private String clientSecret;
private String spDc;
private String countryCode;
private int playlistLoadLimit;
private int albumLoadLimit;
Expand All @@ -29,6 +30,14 @@ public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}

public String getSpDc() {
return this.spDc;
}

public void setSpDc(String spDc) {
this.spDc = spDc;
}

public String getCountryCode() {
return this.countryCode;
}
Expand Down

0 comments on commit 82a41f7

Please sign in to comment.