/*
* Copyright (c) 2010, the Last.fm Java Project and Committers
* All rights reserved.
*
* Redistribution and use of this software in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above
* copyright notice, this list of conditions and the
* following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
* TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package de.umass.lastfm.scrobble;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.util.Collection;
import java.util.Collections;
import de.umass.lastfm.Caller;
import de.umass.lastfm.Session;
import static de.umass.util.StringUtilities.encode;
import static de.umass.util.StringUtilities.md5;
/**
* This class manages communication with the server for scrobbling songs. You can retrieve an instance of this class by calling
* {@link #newScrobbler(String, String, String) newScrobbler}.<br/>
* It contains methods to perform the handshake, notify Last.fm about a now playing song and submitting songs to a musical profile, aka
* scrobbling songs.<br/>
* See <a href="http://www.last.fm/api/submissions">http://www.last.fm/api/submissions</a> for a deeper explanation of the protocol and
* various guidelines on how to use the scrobbling service, since this class does not cover error handling or caching.<br/>
* All methods in this class, which are communicating with the server, return an instance of {@link ResponseStatus} which contains
* information if the operation was successful or not.<br/>
* This class respects the <code>proxy</code> property in the {@link Caller} class in all its HTTP calls. If you need the
* <code>Scrobbler</code> to use a Proxy server, set it with {@link Caller#setProxy(java.net.Proxy)}.
*
* @author Janni Kovacs
* @see de.umass.lastfm.Track#scrobble(ScrobbleData, de.umass.lastfm.Session)
* @see de.umass.lastfm.Track#scrobble(String, String, int, de.umass.lastfm.Session)
* @see de.umass.lastfm.Track#scrobble(java.util.List, de.umass.lastfm.Session)
* @deprecated The 1.2.x scrobble protocol has now been deprecated in favour of the 2.0 protocol which is part of the Last.fm web services
* API.
*/
@Deprecated
public class Scrobbler {
private static final String DEFAULT_HANDSHAKE_URL = "http://post.audioscrobbler.com/";
private String handshakeUrl = DEFAULT_HANDSHAKE_URL;
private final String clientId, clientVersion;
private final String user;
private String sessionId;
private String nowPlayingUrl;
private String submissionUrl;
private Scrobbler(String clientId, String clientVersion, String user) {
this.clientId = clientId;
this.clientVersion = clientVersion;
this.user = user;
}
/**
* Sets the URL to use to perform a handshake. Use this method to redirect your scrobbles to another service, like Libre.fm.
*
* @param handshakeUrl The new handshake url.
*/
public void setHandshakeURL(String handshakeUrl) {
this.handshakeUrl = handshakeUrl;
}
/**
* Creates a new <code>Scrobbler</code> instance bound to the specified <code>user</code>.
*
* @param clientId The client id (or "tst")
* @param clientVersion The client version (or "1.0")
* @param user The last.fm user
* @return a new <code>Scrobbler</code> instance
*/
public static Scrobbler newScrobbler(String clientId, String clientVersion, String user) {
return new Scrobbler(clientId, clientVersion, user);
}
/**
* Performs a standard handshake with the user's password.
*
* @param password The user's password
* @return the status of the operation
* @throws IOException on I/O errors
*/
public ResponseStatus handshake(String password) throws IOException {
long time = System.currentTimeMillis() / 1000;
String auth = md5(md5(password) + time);
String url = String.format("%s?hs=true&p=1.2.1&c=%s&v=%s&u=%s&t=%s&a=%s", handshakeUrl, clientId,
clientVersion, user, time, auth);
return performHandshake(url);
}
/**
* Performs a web-service handshake.
*
* @param session An authenticated Session.
* @return the status of the operation
* @throws IOException on I/O errors
* @see de.umass.lastfm.Authenticator
*/
public ResponseStatus handshake(Session session) throws IOException {
long time = System.currentTimeMillis() / 1000;
String auth = md5(session.getSecret() + time);
String url = String
.format("%s?hs=true&p=1.2.1&c=%s&v=%s&u=%s&t=%s&a=%s&api_key=%s&sk=%s", handshakeUrl, clientId,
clientVersion, user, time, auth, session.getApiKey(), session.getKey());
return performHandshake(url);
}
/**
* Internally performs the handshake operation by calling the given <code>url</code> and examining the response.
*
* @param url The URL to call
* @return the status of the operation
* @throws IOException on I/O errors
*/
private ResponseStatus performHandshake(String url) throws IOException {
HttpURLConnection connection = Caller.getInstance().openConnection(url);
InputStream is = connection.getInputStream();
BufferedReader r = new BufferedReader(new InputStreamReader(is));
String status = r.readLine();
int statusCode = ResponseStatus.codeForStatus(status);
ResponseStatus responseStatus;
if (statusCode == ResponseStatus.OK) {
this.sessionId = r.readLine();
this.nowPlayingUrl = r.readLine();
this.submissionUrl = r.readLine();
responseStatus = new ResponseStatus(statusCode);
} else if (statusCode == ResponseStatus.FAILED) {
responseStatus = new ResponseStatus(statusCode, status.substring(status.indexOf(' ') + 1));
} else {
return new ResponseStatus(statusCode);
}
r.close();
return responseStatus;
}
/**
* Submits 'now playing' information. This does not affect the musical profile of the user.
*
* @param artist The artist's name
* @param track The track's title
* @return the status of the operation
* @throws IOException on I/O errors
*/
public ResponseStatus nowPlaying(String artist, String track) throws IOException {
return nowPlaying(artist, track, null, -1, -1);
}
/**
* Submits 'now playing' information. This does not affect the musical profile of the user.
*
* @param artist The artist's name
* @param track The track's title
* @param album The album or <code>null</code>
* @param length The length of the track in seconds
* @param tracknumber The position of the track in the album or -1
* @return the status of the operation
* @throws IOException on I/O errors
*/
public ResponseStatus nowPlaying(String artist, String track, String album, int length, int tracknumber) throws
IOException {
if (sessionId == null)
throw new IllegalStateException("Perform successful handshake first.");
String b = album != null ? encode(album) : "";
String l = length == -1 ? "" : String.valueOf(length);
String n = tracknumber == -1 ? "" : String.valueOf(tracknumber);
String body = String
.format("s=%s&a=%s&t=%s&b=%s&l=%s&n=%s&m=", sessionId, encode(artist), encode(track), b, l, n);
if (Caller.getInstance().isDebugMode())
System.out.println("now playing: " + body);
HttpURLConnection urlConnection = Caller.getInstance().openConnection(nowPlayingUrl);
urlConnection.setRequestMethod("POST");
urlConnection.setDoOutput(true);
OutputStream outputStream = urlConnection.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
writer.write(body);
writer.close();
InputStream is = urlConnection.getInputStream();
BufferedReader r = new BufferedReader(new InputStreamReader(is));
String status = r.readLine();
r.close();
return new ResponseStatus(ResponseStatus.codeForStatus(status));
}
/**
* Scrobbles a song.
*
* @param artist The artist's name
* @param track The track's title
* @param album The album or <code>null</code>
* @param length The length of the track in seconds
* @param tracknumber The position of the track in the album or -1
* @param source The source of the track
* @param startTime The time the track started playing in UNIX timestamp format and UTC time zone
* @return the status of the operation
* @throws IOException on I/O errors
*/
public ResponseStatus submit(String artist, String track, String album, int length, int tracknumber, Source source,
long startTime) throws IOException {
return submit(new SubmissionData(artist, track, album, length, tracknumber, source, startTime));
}
/**
* Scrobbles a song.
*
* @param data Contains song information
* @return the status of the operation
* @throws IOException on I/O errors
*/
public ResponseStatus submit(SubmissionData data) throws IOException {
return submit(Collections.singletonList(data));
}
/**
* Scrobbles up to 50 songs at once. Song info is contained in the <code>Collection</code> passed. Songs must be in
* chronological order of their play, that means the track first in the list has been played before the track second
* in the list and so on.
*
* @param data A list of song infos
* @return the status of the operation
* @throws IOException on I/O errors
* @throws IllegalArgumentException if data contains more than 50 entries
*/
public ResponseStatus submit(Collection<SubmissionData> data) throws IOException {
if (sessionId == null)
throw new IllegalStateException("Perform successful handshake first.");
if (data.size() > 50)
throw new IllegalArgumentException("Max 50 submissions at once");
StringBuilder builder = new StringBuilder(data.size() * 100);
int index = 0;
for (SubmissionData submissionData : data) {
builder.append(submissionData.toString(sessionId, index));
builder.append('\n');
index++;
}
String body = builder.toString();
if (Caller.getInstance().isDebugMode())
System.out.println("submit: " + body);
HttpURLConnection urlConnection = Caller.getInstance().openConnection(submissionUrl);
urlConnection.setRequestMethod("POST");
urlConnection.setDoOutput(true);
OutputStream outputStream = urlConnection.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
writer.write(body);
writer.close();
InputStream is = urlConnection.getInputStream();
BufferedReader r = new BufferedReader(new InputStreamReader(is));
String status = r.readLine();
r.close();
int statusCode = ResponseStatus.codeForStatus(status);
if (statusCode == ResponseStatus.FAILED) {
return new ResponseStatus(statusCode, status.substring(status.indexOf(' ') + 1));
}
return new ResponseStatus(statusCode);
}
}