package net.roarsoftware.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 net.roarsoftware.lastfm.Caller;
import net.roarsoftware.lastfm.Session;
import static net.roarsoftware.util.StringUtilities.encode;
import static net.roarsoftware.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
*/
public class Scrobbler {
private static final String HANDSHAKE_URL = "http://post.audioscrobbler.com/";
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;
}
/**
* 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", HANDSHAKE_URL, 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 net.roarsoftware.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", HANDSHAKE_URL, 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);
//Proxy proxy = Caller.getInstance().getProxy();
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);
}
}
|