package com.sleazyweasel.applescriptifier;
import com.sleazyweasel.applescriptifier.preferences.MuseControllerPreferences;
import de.felixbruns.jotify.JotifyPool;
import de.felixbruns.jotify.exceptions.AuthenticationException;
import de.felixbruns.jotify.exceptions.ConnectionException;
import de.felixbruns.jotify.media.Playlist;
import de.felixbruns.jotify.media.PlaylistContainer;
import de.felixbruns.jotify.media.Track;
import nl.pascaldevink.jotify.gui.JotifyApplication;
import nl.pascaldevink.jotify.gui.JotifyPlayer;
import nl.pascaldevink.jotify.gui.listeners.JotifyBroadcast;
import nl.pascaldevink.jotify.gui.listeners.PlayerListener;
import javax.swing.*;
import java.awt.*;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
public class NativeSpotifySupportImpl implements NativeSpotifySupport, PlayerListener {
private static final Logger logger = Logger.getLogger(NativeSpotifySupportImpl.class.getName());
private JotifyPool jotifyPool;
private JotifyPlayer jotifyPlayer;
private double volume;
private Status playerStatus;
private Track currentTrack;
private int currentPlayerPosition;
private Playlist currentPlaylist;
private MusicPlayerInputType currentInputType = MusicPlayerInputType.NONE;
private List<MusicPlayerStateChangeListener> listeners = new ArrayList<MusicPlayerStateChangeListener>();
public NativeSpotifySupportImpl() {
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
if (jotifyPool != null) {
try {
jotifyPool.close();
} catch (ConnectionException e) {
logger.log(Level.WARNING, "Exception caught.", e);;
}
}
}
});
}
private synchronized JotifyPlayer getJotifyPlayer() {
if (jotifyPlayer == null) {
try {
jotifyPlayer = new JotifyPlayer(getJotifyPool());
JotifyBroadcast.getInstance().addPlayerListener(this);
} catch (Exception e) {
logger.log(Level.WARNING, "Exception caught.", e);;
throw new RuntimeException(e);
}
}
return jotifyPlayer;
}
private synchronized JotifyPool getJotifyPool() {
if (jotifyPool == null) {
jotifyPool = new JotifyPool(2);
}
return jotifyPool;
}
public boolean isSpotifyAuthorized() {
JotifyPool pool = getJotifyPool();
File configFile = getConfigFile();
if (!configFile.exists()) {
return false;
}
BufferedReader reader = null;
try {
pool.close();
reader = new BufferedReader(new FileReader(configFile));
String userLine = reader.readLine();
String passwordLine = reader.readLine();
String userName = userLine.substring(5);
String password = passwordLine.substring(9);
pool.login(userName, password);
} catch (Exception e) {
logger.log(Level.WARNING, "Exception caught.", e);;
return false;
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
logger.log(Level.WARNING, "Exception caught.", e);;
}
}
}
return true;
}
//todo return different stuff, depending on no connection vs. bad username/password
//todo figure out better exception handling here.
public boolean authorize(String username, char[] password) {
try {
getJotifyPool().login(username, new String(password));
saveConfig(username, password);
} catch (ConnectionException e) {
logger.log(Level.WARNING, "Exception caught.", e);;
return false;
} catch (AuthenticationException e) {
logger.log(Level.WARNING, "Exception caught.", e);;
return false;
} catch (IOException e) {
logger.log(Level.WARNING, "Exception caught.", e);;
return false;
}
return true;
}
public void saveConfig(String username, char[] password) throws IOException {
File spotifyConfigDirectory = getSpotifyConfigDirectory();
if (!spotifyConfigDirectory.exists()) {
spotifyConfigDirectory.mkdirs();
Runtime.getRuntime().exec(new String[]{"chmod", "700", spotifyConfigDirectory.getAbsolutePath()});
}
File configFile = getConfigFile();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(configFile))));
writer.write("user=" + username);
writer.newLine();
writer.write("password=" + new String(password));
writer.newLine();
writer.close();
}
private File getConfigFile() {
return new File(getSpotifyConfigDirectory(), "config");
}
private static File getSpotifyConfigDirectory() {
String userHome = System.getProperty("user.home");
return new File(userHome + "/.config/spotify");
}
@Override
public List<Playlist> getPlaylists() {
//todo figure out a way to background this task, so the user can start using the app sooner.
try {
PlaylistContainer playlistContainer = getJotifyPool().playlistContainer();
List<Playlist> playlists = playlistContainer.getPlaylists();
List<Playlist> results = new ArrayList<Playlist>(playlists.size());
for (Playlist playlist : playlists) {
Playlist reifiedPlaylist = getJotifyPool().playlist(playlist.getId(), true);
if (reifiedPlaylist.hasTracks()) {
results.add(reifiedPlaylist);
}
}
return results;
} catch (TimeoutException e) {
logger.log(Level.WARNING, "Exception caught.", e);;
throw new RuntimeException(e);
}
}
@Override
public void close() {
try {
getJotifyPool().close();
} catch (ConnectionException e) {
throw new RuntimeException(e);
} finally {
jotifyPool = null;
}
}
@Override
public void setListener(PlayerListener playbackListener) {
if (playbackListener != null) {
JotifyBroadcast.getInstance().addPlayerListener(playbackListener);
} else {
//todo add code to remove a player listener
}
}
@Override
public void play(Playlist playlist) {
this.currentPlaylist = playlist;
JotifyPlayer player = getJotifyPlayer();
List<Track> tracks = playlist.getTracks();
List<Track> browsedTracks;
try {
browsedTracks = getJotifyPool().browse(tracks);
} catch (TimeoutException e) {
logger.log(Level.WARNING, "Exception caught.", e);;
this.jotifyPool = null;
this.jotifyPlayer = null;
throw new RuntimeException(e);
} catch (Exception e) {
logger.log(Level.WARNING, "Exception caught.", e);;
//todo figure out a better way to do this!
this.jotifyPool = null;
this.jotifyPlayer = null;
throw new RuntimeException(e);
}
player.controlSelect(browsedTracks);
player.controlPlay();
}
@Override
public void next() {
getJotifyPlayer().controlNext();
}
@Override
public void play() {
getJotifyPlayer().controlPlay();
}
@Override
public void pause() {
getJotifyPlayer().controlPause();
}
@Override
public void previous() {
getJotifyPlayer().controlPrevious();
}
@Override
public void setVolume(double volume) {
this.volume = volume;
getJotifyPlayer().controlVolume((float) volume);
}
@Override
public void volumeUp() {
setVolume(Math.min(volume + .1f, 1f));
}
@Override
public void volumeDown() {
setVolume(Math.max(0, volume - .1f));
}
@Override
public void initializeFromSavedUserState(MuseControllerPreferences preferences) {
setVolume(preferences.getPreviousSpotifyVolume());
}
@Override
public void removeListener(MusicPlayerStateChangeListener listener) {
listeners.remove(listener);
}
@Override
public void playPause() {
if (Status.PLAY.equals(playerStatus)) {
pause();
} else {
play();
}
}
@Override
public void thumbsUp() {
//spotify doesn't support this idea.
}
@Override
public void thumbsDown() {
//spotify doesn't support this idea.
}
@Override
public Image image(String imageCode) {
try {
return getJotifyPool().image(imageCode);
} catch (TimeoutException e) {
return new ImageIcon(JotifyApplication.class.getResource("images/cover.png")).getImage().getScaledInstance(130, 130, Image.SCALE_SMOOTH);
}
}
@Override
public URL imageUrl(String imageCode) {
try {
return new URL("http://o.scdn.co/image/" + imageCode);
} catch (MalformedURLException e) {
logger.log(Level.WARNING, "Exception caught.", e);;
return null;
}
}
@Override
public void bounce() {
//no op for now...
}
@Override
public void activate() {
//no op?
}
@Override
public MusicPlayerState getState() {
String title = "";
String artistName = "";
String albumName = "";
String cover = "";
if (currentTrack != null) {
title = currentTrack.getTitle();
artistName = currentTrack.getArtist().getName();
albumName = currentTrack.getAlbum().getName();
//todo this is not a URL that anyone can use to get the image... figure out how to get an image URL from spotify, if it is even possible.
cover = imageUrl(currentTrack.getCover()).toString();
}
String playlistName = "";
if (currentPlaylist != null) {
playlistName = currentPlaylist.getName();
}
return new MusicPlayerState(false, title, artistName, playlistName, albumName, currentInputType,
buildStationMap(), cover, renderCurrentPosition(), isPlaying(), null, volume);
}
private String renderCurrentPosition() {
//todo prettify.
return String.valueOf(currentPlayerPosition);
}
private Map<Integer, String> buildStationMap() {
return null;
}
@Override
public void selectStation(Integer stationNumber) {
throw new UnsupportedOperationException();
}
@Override
public void askToChooseStation() {
currentInputType = MusicPlayerInputType.CHOOSE_STATION;
}
@Override
public boolean isPlaying() {
return Status.PLAY.equals(playerStatus);
}
@Override
public void addListener(MusicPlayerStateChangeListener listener) {
listeners.add(listener);
}
@Override
public void cancelStationSelection() {
currentInputType = MusicPlayerInputType.NONE;
}
@Override
public boolean isConfigured() {
return getConfigFile().exists();
}
@Override
public boolean isAuthorized() {
return isSpotifyAuthorized();
}
@Override
public void sleep() {
//no-op...no support for this.
}
private void notifyListeners() {
for (MusicPlayerStateChangeListener listener : listeners) {
listener.stateChanged(this, getState());
}
}
//************************* methods from PlayerListener
@Override
public void playerTrackChanged(Track track) {
currentTrack = track;
notifyListeners();
}
@Override
public void playerStatusChanged(Status status) {
this.playerStatus = status;
notifyListeners();
}
@Override
public void playerPositionChanged(int position) {
currentPlayerPosition = position;
notifyListeners();
}
}