Page MenuHomedesp's stash

SpotifyAudioSourceManager.java
No OneTemporary

SpotifyAudioSourceManager.java

package me.despawningbone.discordbot.command.music;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.core5.http.ParseException;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.awaitility.Awaitility;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioItem;
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
import com.sedmelluq.discord.lavaplayer.track.AudioReference;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo;
import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist;
import com.wrapper.spotify.SpotifyApi;
import com.wrapper.spotify.enums.ModelObjectType;
import com.wrapper.spotify.exceptions.SpotifyWebApiException;
import com.wrapper.spotify.exceptions.detailed.NotFoundException;
import com.wrapper.spotify.model_objects.credentials.ClientCredentials;
import com.wrapper.spotify.model_objects.specification.Album;
import com.wrapper.spotify.model_objects.specification.Paging;
import com.wrapper.spotify.model_objects.specification.Playlist;
import com.wrapper.spotify.model_objects.specification.PlaylistTrack;
import com.wrapper.spotify.model_objects.specification.Track;
import com.wrapper.spotify.model_objects.specification.TrackSimplified;
import com.wrapper.spotify.requests.data.albums.GetAlbumsTracksRequest;
import com.wrapper.spotify.requests.data.playlists.GetPlaylistsItemsRequest;
/**
* Code mostly modified from https://github.com/lijamez/tonbot-plugin-music/
* @author lijamez @ github
*/
public class SpotifyAudioSourceManager implements AudioSourceManager {
private static final String SPOTIFY_DOMAIN = "open.spotify.com";
private static final int EXPECTED_PATH_COMPONENTS = 2;
private SpotifyApi spotifyApi;
private YoutubeAudioSourceManager manager;
public SpotifyAudioSourceManager(SpotifyApi spotifyApi, AudioPlayerManager parent, AudioTrackHandler handler) throws Exception {
this.spotifyApi = spotifyApi;
handler.ex.submit(() -> {
Awaitility.await().until(() -> parent.source(YoutubeAudioSourceManager.class) != null); //needed to ensure its loaded
this.manager = parent.source(YoutubeAudioSourceManager.class);
});
refreshSpotifyApi(handler);
}
private void refreshSpotifyApi(AudioTrackHandler handler) throws Exception {
ClientCredentials cred = spotifyApi.clientCredentials().build().execute();
spotifyApi.setAccessToken(cred.getAccessToken());
handler.ex.schedule(() -> {
try {
refreshSpotifyApi(handler);
} catch (Exception e) {
e.printStackTrace(); //DONE disable the source manager?
spotifyApi = null;
}
}, cred.getExpiresIn(), TimeUnit.SECONDS);
}
@Override
public String getSourceName() {
return "Spotify Playlist";
}
@Override
public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) {
if(spotifyApi == null) return null; //disabled due to broken api
try {
URL url = new URL(reference.identifier);
if (!StringUtils.equals(url.getHost(), SPOTIFY_DOMAIN)) {
return null;
}
AudioItem audioItem = null;
audioItem = handleAsPlaylist(url, manager);
if (audioItem == null) {
audioItem = handleAsTrack(url, manager);
}
return audioItem;
} catch (MalformedURLException e) {
return null;
}
}
private AudioTrack handleAsTrack(URL url, AudioPlayerManager man) {
Path path = Paths.get(url.getPath());
if (path.getNameCount() < 2) {
return null;
}
if (!StringUtils.equals(path.getName(0).toString(), "track")) {
return null;
}
String trackId = path.getName(1).toString();
TrackSimplified track;
try {
Track t = spotifyApi.getTrack(trackId).build().execute();
track = new TrackSimplified.Builder().setArtists(t.getArtists()).setName(t.getName()).setDurationMs(t.getDurationMs()).build();
} catch (IOException | SpotifyWebApiException | ParseException e) {
throw new IllegalStateException("Unable to fetch track from Spotify API.", e);
}
return getAudioTracks(Arrays.asList(track), man).get(0);
}
private BasicAudioPlaylist handleAsPlaylist(URL url, AudioPlayerManager man) {
String playlistKey;
try {
playlistKey = extractPlaylistId(url);
} catch (IllegalArgumentException e) {
return null;
}
String name;
List<TrackSimplified> tracks;
try {
Playlist playlist = spotifyApi.getPlaylist(playlistKey)
.build().execute();
name = playlist.getName();
tracks = getAllPlaylistTracks(playlist).stream().map(pt -> pt.getTrack())
.filter(pt -> pt.getType() == ModelObjectType.TRACK).map(pt -> {
Track t = (Track) pt; //it doesnt have any methods to translate Track to TrackSimplified, so i had to do this; it only uses 3 params anyways
return new TrackSimplified.Builder().setArtists(t.getArtists()).setName(t.getName()).setDurationMs(t.getDurationMs()).build();
}).collect(Collectors.toList());
} catch (IOException | SpotifyWebApiException | ParseException e) {
if(e instanceof NotFoundException) { //try searching as album
try {
Album album = spotifyApi.getAlbum(playlistKey).build().execute();
name = album.getName();
tracks = getAllAlbumTracks(album);
} catch (ParseException | SpotifyWebApiException | IOException e1) {
throw new IllegalStateException("Unable to fetch playlist from Spotify API.", e1);
}
} else {
throw new IllegalStateException("Unable to fetch playlist from Spotify API.", e);
}
}
List<AudioTrack> audioTracks = getAudioTracks(tracks, man);
return new BasicAudioPlaylist(name, audioTracks, null, false);
}
private List<PlaylistTrack> getAllPlaylistTracks(Playlist playlist) {
List<PlaylistTrack> playlistTracks = new ArrayList<>();
Paging<PlaylistTrack> currentPage = playlist.getTracks();
do {
playlistTracks.addAll(Arrays.asList(currentPage.getItems()));
if (currentPage.getNext() == null) {
currentPage = null;
} else {
try {
URI nextPageUri = new URI(currentPage.getNext());
List<NameValuePair> queryPairs = URLEncodedUtils.parse(nextPageUri, StandardCharsets.UTF_8);
GetPlaylistsItemsRequest.Builder b = spotifyApi.getPlaylistsItems(playlist.getId());
for (NameValuePair queryPair : queryPairs) {
b = b.setBodyParameter(queryPair.getName(), queryPair.getValue());
}
currentPage = b.build().execute();
} catch (IOException | SpotifyWebApiException | ParseException e) {
throw new IllegalStateException("Unable to query Spotify for playlist tracks.", e);
} catch (URISyntaxException e) {
throw new IllegalStateException("Spotify returned an invalid 'next page' URI.", e);
}
}
} while (currentPage != null);
return playlistTracks;
}
private List<TrackSimplified> getAllAlbumTracks(Album album) {
List<TrackSimplified> albumTracks = new ArrayList<>();
Paging<TrackSimplified> currentPage = album.getTracks();
do {
albumTracks.addAll(Arrays.asList(currentPage.getItems()));
if (currentPage.getNext() == null) {
currentPage = null;
} else {
try {
URI nextPageUri = new URI(currentPage.getNext());
List<NameValuePair> queryPairs = URLEncodedUtils.parse(nextPageUri, StandardCharsets.UTF_8);
GetAlbumsTracksRequest.Builder b = spotifyApi.getAlbumsTracks(album.getId());
for (NameValuePair queryPair : queryPairs) {
b = b.setBodyParameter(queryPair.getName(), queryPair.getValue());
}
currentPage = b.build().execute();
} catch (IOException | SpotifyWebApiException | ParseException e) {
throw new IllegalStateException("Unable to query Spotify for album tracks.", e);
} catch (URISyntaxException e) {
throw new IllegalStateException("Spotify returned an invalid 'next page' URI.", e);
}
}
} while (currentPage != null);
return albumTracks;
}
private String extractPlaylistId(URL url) {
Path path = Paths.get(url.getPath());
if (path.getNameCount() < EXPECTED_PATH_COMPONENTS) {
throw new IllegalArgumentException("Not enough path components.");
}
if (!Arrays.asList("playlist", "album").contains(path.getName(0).toString())) {
throw new IllegalArgumentException("URL doesn't appear to be a playlist.");
}
String playlistId = path.getName(1).toString();
if (StringUtils.isBlank(playlistId)) {
throw new IllegalArgumentException("Playlist ID is blank.");
}
return playlistId;
}
@Override
public boolean isTrackEncodable(AudioTrack track) {
return false;
}
@Override
public void encodeTrack(AudioTrack track, DataOutput output) throws IOException {
throw new UnsupportedOperationException("encodeTrack is unsupported.");
}
@Override
public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException {
throw new UnsupportedOperationException("decodeTrack is unsupported.");
}
@Override
public void shutdown() {
}
private List<AudioTrack> getAudioTracks(List<TrackSimplified> tracks, AudioPlayerManager manager) {
return tracks.parallelStream().map(track -> { //parallelStream made a world of difference in loading times lmao
String artist = track.getArtists().length < 1 ? "" : track.getArtists()[0].getName();
AudioItem item = this.manager.loadItem(manager, new AudioReference("ytsearch:" + artist + " " + track.getName(), null));
if (item instanceof AudioPlaylist) {
AudioPlaylist audioPlaylist = (AudioPlaylist) item;
// The number of matches is limited to reduce the chances of matching against
// less than optimal results.
// The best match is the one that has the smallest track duration delta.
YoutubeAudioTrack bestMatch = audioPlaylist.getTracks().stream().limit(3)
.map(t -> (YoutubeAudioTrack) t).min((o1, o2) -> {
long o1TimeDelta = Math.abs(o1.getDuration() - track.getDurationMs());
long o2TimeDelta = Math.abs(o2.getDuration() - track.getDurationMs());
return (int) (o1TimeDelta - o2TimeDelta);
}).orElse(null);
return bestMatch;
} else if (item instanceof YoutubeAudioTrack) {
return (YoutubeAudioTrack) item;
} else if (item instanceof AudioReference) { //no results; retry once more
System.out.println("Spotify Source Manager: Retry needed for " + track.getName());
item = this.manager.loadItem(manager, new AudioReference("ytsearch:" + artist + " " + track.getName(), null));
if(item instanceof AudioPlaylist) item = ((AudioPlaylist) item).getTracks().get(0); //cba doing the best match lmao
else if(!(item instanceof YoutubeAudioTrack)) return null; //if not playlist and track return null again
return (YoutubeAudioTrack) item;
} else {
throw new IllegalArgumentException("Unknown AudioItem"); //should never throw
}
}).collect(Collectors.toList());
}
}

File Metadata

Mime Type
text/x-java
Expires
Thu, May 8, 5:50 AM (9 h, 15 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
8f/b5/613b1080f21dc71b415124bff7dc

Event Timeline