/* * Copyright 2008-2013, ETH Zürich, Samuel Welten, Michael Kuhn, Tobias Langner, * Sandro Affentranger, Lukas Bossard, Michael Grob, Rahul Jain, * Dominic Langenegger, Sonia Mayor Alonso, Roger Odermatt, Tobias Schlueter, * Yannick Stucki, Sebastian Wendland, Samuel Zehnder, Samuel Zihlmann, * Samuel Zweifel * * This file is part of Jukefox. * * Jukefox is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or any later version. Jukefox is * distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * Jukefox. If not, see <http://www.gnu.org/licenses/>. */ package ch.ethz.dcg.pancho3.view.webpublisher; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.DefaultHttpClient; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import ch.ethz.dcg.jukefox.commons.utils.JoinableThread; import ch.ethz.dcg.jukefox.commons.utils.Log; import ch.ethz.dcg.jukefox.controller.player.AndroidPlayerController; import ch.ethz.dcg.jukefox.controller.player.IOnPlayerStateChangeListener; import ch.ethz.dcg.jukefox.controller.player.IOnPlaylistStateChangeListener; import ch.ethz.dcg.jukefox.manager.AndroidSettingsManager; import ch.ethz.dcg.jukefox.model.collection.BaseAlbum; import ch.ethz.dcg.jukefox.model.collection.BaseArtist; import ch.ethz.dcg.jukefox.model.collection.BaseSong; import ch.ethz.dcg.jukefox.model.collection.IReadOnlyPlaylist; import ch.ethz.dcg.jukefox.model.collection.PlaylistSong; import ch.ethz.dcg.jukefox.model.commons.EmptyPlaylistException; import ch.ethz.dcg.jukefox.model.player.PlayerState; import ch.ethz.dcg.jukefox.playmode.IPlayMode; import ch.ethz.dcg.pancho3.commons.settings.ISettingsReader; import ch.ethz.dcg.pancho3.model.JukefoxApplication; import com.googlecode.ascrblr.api.scrobbler.AudioscrobblerService; import com.googlecode.ascrblr.api.scrobbler.TrackInfo; import com.googlecode.ascrblr.api.scrobbler.TrackInfo.SourceType; import com.googlecode.ascrblr.api.util.AuthenticationException; import com.googlecode.ascrblr.api.util.ServiceException; import com.googlecode.ascrblr.api.util.SessionExpiredException; public class AudioScrobbler implements IOnPlayerStateChangeListener, IOnPlaylistStateChangeListener, OnSharedPreferenceChangeListener { public static final String ANSWER_OK = "OK"; public static final String ANSWER_BAD_SESSION = "BADSESSION"; private final static String TAG = AudioScrobbler.class.getSimpleName(); private AudioscrobblerService service; private boolean credentialsAreSet = false; // private TrackInfo track = null; private List<TrackInfo> bufferedTracks = null; /** Number of track titles to collect before sending them (0 is store all) **/ private int numTracksToBuffer = 1; private boolean paused = false; private ISettingsReader settings; private IAudioScrobblerListener listener; private AndroidPlayerController controller; /** Maps the songId to the track object **/ private HashMap<Integer, TrackInfo> tracks; public AudioScrobbler(IAudioScrobblerListener listener, AndroidPlayerController controller) { this.controller = controller; this.settings = AndroidSettingsManager.getAndroidSettingsReader(); this.listener = listener; tracks = new HashMap<Integer, TrackInfo>(); // Instantiating service (default protocol version is 1.2) service = new AudioscrobblerService("1.2"); bufferedTracks = new ArrayList<TrackInfo>(); readBufferFromFile(); readSettings(); settings.addSettingsChangeListener(this); init(); } private void init() { JoinableThread t = new JoinableThread(new Runnable() { @Override public void run() { // JukefoxApplication.getCollectionModel().getApplicationStateManager().getApplicationStateReader() // .waitForPlaybackFunctionality(); controller.addOnPlaylistStateChangeListener(AudioScrobbler.this); controller.addOnPlayerStateChangeListener(AudioScrobbler.this); BaseSong<BaseArtist, BaseAlbum> song; try { song = controller.getCurrentSong(); setTrack(song); } catch (EmptyPlaylistException e) { Log.w(TAG, e); } } }); t.start(); } public void readSettings() { numTracksToBuffer = settings.getScrobbleInterval(); paused = settings.isScrobblingPaused(); setCredentials(settings.getLastFmUserName(), settings.getLastFmPassword()); Log.v(TAG, "set scrobble credentials"); } public boolean goodConnection() { boolean ret = true; try { service.testConnection(); } catch (Exception e) { Log.w(TAG, e); ret = false; } return ret; } public boolean isInitialized() { return credentialsAreSet; } private void reHandshake() { setCredentials(settings.getLastFmUserName(), settings.getLastFmPassword()); } public void setCredentials(String username, String password) { try { // Set credentials (initializes handshake to obtain session) // Log.v("Set scrobbler credentials to", "username: " + username + // ", password: " + password); service.setCredentials(username, password); credentialsAreSet = true; if (username == null || password == null || username.length() == 0 || password.length() == 0) { credentialsAreSet = false; Log.v(TAG, "credentials unset"); return; } Log.v(TAG, "credentials set to: " + username + ", <some pwd>"); } catch (ServiceException e) { Log.w(TAG, e); credentialsAreSet = false; } catch (Exception e) { Log.w(TAG, "unable to connect!"); credentialsAreSet = false; Log.w(TAG, e); } } public synchronized void scrobbleSubmitAsync(BaseSong<BaseArtist, BaseAlbum> song) { if (!tracks.containsKey(song.getId())) { return; } final TrackInfo trackToSubmit = tracks.get(song.getId()); Thread t = new Thread(new Runnable() { @Override public void run() { scrobbleSubmit(trackToSubmit); } }); t.start(); } public synchronized void scrobbleSubmit(TrackInfo trackToSubmit) { Log.v(TAG, "Scrobble Submit: credentials set: " + "" + credentialsAreSet); bufferedTracks.add(trackToSubmit); boolean authError = false; if (credentialsAreSet && !paused && bufferedTracks.size() >= numTracksToBuffer) { Log.v(TAG, "" + bufferedTracks.size() + " " + numTracksToBuffer); List<TrackInfo> sent = new ArrayList<TrackInfo>(bufferedTracks.size()); for (TrackInfo track : bufferedTracks) { authError = serviceScrobbleSubmit(authError, track); if (service.postURL != null) { Log.v(TAG, "PostURL: " + service.postURL); // Create a new HttpClient and Post Header HttpClient httpclient = new DefaultHttpClient(); HttpPost httppost = new HttpPost(service.postURL); try { // Execute HTTP Post Request HttpResponse response = httpclient.execute(httppost); InputStream is = response.getEntity().getContent(); DataInputStream din = new DataInputStream(is); String responseState = din.readLine(); Log.v(TAG, "HttpPost Response: " + responseState); if (responseState.equals(ANSWER_OK)) { sent.add(track); } else if (responseState.equals(ANSWER_BAD_SESSION)) { setCredentials(settings.getLastFmUserName(), settings.getLastFmPassword()); authError = serviceScrobbleSubmit(authError, track); HttpPost httppost2 = new HttpPost(service.postURL); HttpResponse response2 = httpclient.execute(httppost2); InputStream is2 = response2.getEntity().getContent(); DataInputStream din2 = new DataInputStream(is2); String responseState2 = din2.readLine(); Log.v(TAG, "HttpPost Response: " + responseState2); if (responseState.equals(ANSWER_OK)) { sent.add(track); } Log.v(TAG, "PostURL: " + service.postURL); } } catch (ClientProtocolException e) { Log.w(TAG, e); } catch (IOException e) { Log.w(TAG, e); } } } for (TrackInfo ti : sent) { bufferedTracks.remove(ti); } if (authError && listener != null) { listener.onAuthenticationFailed(); } } else if (!credentialsAreSet) { broadcastScrobbleSubmit(); } } private boolean serviceScrobbleSubmit(boolean authError, TrackInfo track) { for (int i = 0; i < 2; i++) { try { // Submit the track information after the minimal amount of time // which is at least 31sec. or half the track length. if (track != null) { service.submit(track); // track = null; } break; } catch (SessionExpiredException e) { Log.w(TAG, e); reHandshake(); } catch (AuthenticationException e) { // do some error handling here Log.w(TAG, "unable to scrobble "); Log.w(TAG, e); authError = true; } catch (ServiceException e) { // do some error handling here Log.w(TAG, "unable to scrobble "); Log.w(TAG, e); } catch (Exception e) { Log.w(TAG, "unable to scrobble "); Log.w(TAG, e); } } return authError; } private void broadcastScrobbleSubmit() { Intent i = new Intent("fm.last.android.playbackcomplete"); JukefoxApplication.getAppContext().sendBroadcast(i); Log.v(TAG, "broadcasted scrobble submit intent"); } public void setTrack(BaseSong<BaseArtist, BaseAlbum> song) { String artist = song.getArtist().getName(); String album = song.getAlbum().getName(); String title = song.getTitle(); Integer id = song.getId(); Log.v(TAG, "Set Track: " + artist + " - " + title); TrackInfo track = new TrackInfo(artist, title); track.setStartTime(Long.toString(System.currentTimeMillis())); track.setSource(SourceType.E); track.setAlbum(album); tracks.put(id, track); if (!credentialsAreSet) { broadcastSetTrack(track); Log.v(TAG, "broadcasted scrobble set track intent"); } } private void broadcastSetTrack(TrackInfo track) { Intent i = new Intent("fm.last.android.metachanged"); i.putExtra("artist", track.getArtist()); i.putExtra("album", track.getAlbum()); i.putExtra("track", track.getTrack()); i.putExtra("duration", track.getLength()); JukefoxApplication.getAppContext().sendBroadcast(i); } public synchronized void scrobbleNotifyAsync(final TrackInfo track) { Thread t = new Thread(new Runnable() { @Override public void run() { scrobbleNotify(track); } }); t.start(); } public void scrobbleNotify(TrackInfo track) { if (credentialsAreSet && numTracksToBuffer <= 1 && !paused) { for (int i = 0; i < 2; i++) { try { if (track != null) { // Notify the service which track you are currently // playing... service.notifyNew(track); } break; } catch (SessionExpiredException e) { Log.w(TAG, e); reHandshake(); } catch (ServiceException e) { // do some error handling here Log.w(TAG, "unable to scrobble " + track.getArtist() + " - " + track.getTrack()); Log.w(TAG, e); } catch (IOException e) { Log.w(TAG, "unable to scrobble " + track.getArtist() + " - " + track.getTrack()); Log.w(TAG, e); } catch (Exception e) { Log.w(TAG, e); } } } } public void writeBufferToFile() { if (numTracksToBuffer > 1) { File f = JukefoxApplication.getDirectoryManager().getScrobbleBufferFile(); FileOutputStream fout = null; OutputStreamWriter outStream = null; try { fout = new FileOutputStream(f, false); outStream = new OutputStreamWriter(fout); } catch (FileNotFoundException e) { Log.w(TAG, e); } for (TrackInfo ti : bufferedTracks) { try { outStream.write(ti.getArtist() + "\n"); outStream.write(ti.getTrack() + "\n"); outStream.write("" + ti.getStartTime() + "\n"); } catch (Exception e) { Log.w(TAG, e); } } try { outStream.close(); } catch (Exception e) { } bufferedTracks.clear(); } } private void readBufferFromFile() { File f = JukefoxApplication.getDirectoryManager().getScrobbleBufferFile(); FileInputStream fin = null; if (f == null || !f.exists() || !f.canRead()) { return; } try { fin = new FileInputStream(f); } catch (FileNotFoundException e) { Log.w(TAG, e); } if (fin == null) { return; } DataInputStream din = new DataInputStream(fin); if (din == null) { return; } String artist = null; String track = null; String time = null; try { artist = din.readLine(); if (artist != null) { track = din.readLine(); time = din.readLine(); } } catch (Exception e) { Log.w(TAG, e); } while (artist != null && track != null && time != null) { try { TrackInfo ti = new TrackInfo(artist, track); ti.setStartTime(time); ti.setSource(SourceType.E); bufferedTracks.add(ti); track = din.readLine(); time = din.readLine(); artist = din.readLine(); } catch (Exception e) { Log.w(TAG, e); } } } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Context ctx = JukefoxApplication.getAppContext(); String key1 = ctx.getString(ch.ethz.dcg.pancho3.R.string.KEY_SCROBBLE_USERNAME); String key2 = ctx.getString(ch.ethz.dcg.pancho3.R.string.KEY_SCROBBLE_PWD); if (!key.equals(key1) && !key.equals(key2)) { return; } readSettings(); } @Override public void onSongCompleted(PlaylistSong<BaseArtist, BaseAlbum> song) { scrobbleSubmitAsync(song); } @Override public void onSongSkipped(PlaylistSong<BaseArtist, BaseAlbum> song, int position) { // scrobbleSubmitAsync(); // Don't scrobble if song was not completed } @Override public void onSongStarted(PlaylistSong<BaseArtist, BaseAlbum> song) { setTrack(song); scrobbleNotifyAsync(tracks.get(song.getId())); } @Override public void onPlayModeChanged(IPlayMode newPlayMode) { } @Override public void onPlaylistChanged(IReadOnlyPlaylist newPlaylist) { } public void onDestroy() { settings.removeSettingsChangeListener(this); } @Override public void onCurrentSongChanged(PlaylistSong<BaseArtist, BaseAlbum> newSong) { } @Override public void onPlayerStateChanged(PlayerState playerState) { } }