/* Copyright (c) 2007 Roland Sch�r
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.rolandschaer.ascrblr.radio;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import ch.rolandschaer.ascrblr.HttpRequest;
import ch.rolandschaer.ascrblr.Service;
import ch.rolandschaer.ascrblr.Session;
import ch.rolandschaer.ascrblr.HttpRequest.RequestType;
import ch.rolandschaer.ascrblr.util.MD5Util;
import ch.rolandschaer.ascrblr.util.ResponseUtil;
import ch.rolandschaer.ascrblr.util.ServiceException;
public class RadioService extends Service {
/** Base URL for radio service */
private static final String SERVICE_BASEURL = "http://ws.audioscrobbler.com/";
/** Version string */
private static final String VERSION = "1.1.1";
/** Response type */
//TODO: Find out what responses are possible here...
public enum ResponseType {
FAILED, OK
}
/** Control command */
public enum Command {
SKIP { public String toString(){return "skip"; } },
BAN { public String toString(){return "ban"; } },
LOVE { public String toString(){return "love"; } }
}
/** Authentication token */
private AuthToken authToken;
/** Session token */
private SessionToken sessionToken;
/** Indicates if radio is already tuned to a station */
private boolean isTunedIn = false;
/**
* Default constructor.
*/
public RadioService() {}
/**
* Gets the current tuned in MP3 stream from the service.
*
* @return BufferedInputStream of the MP3 file
* @throws IOException
* @throws ServiceException
*/
public BufferedInputStream getMp3Stream() throws IOException, ServiceException {
if(!isTunedIn) {
throw new IllegalStateException("Method tuneInStation() has to be called first");
}
URL url = new URL(sessionToken.getStreamingUrl());
HttpRequest request = requestFactory.getRequest(RequestType.STREAM, url);
if(connectTimeout>0) {
request.setConnectTimeout(connectTimeout);
}
if(readTimeout>0) {
request.setReadTimeout(readTimeout);
}
// Execute the request
request.execute();
// Handle response
return new BufferedInputStream(request.getResponseStream());
}
/**
* Sets the service credentials used for authentication.
* By calling this method a request of type HANDSHAKE is
* done to obtain a valid session.
*
* @param username
* Audioscrobbler user name
* @param password
* Audioscrobbler password
* @throws IOException
* @throws ServiceException
*/
public void setCredentials(String username, String password) throws IOException, ServiceException {
this.authToken = new AuthToken(username, password);
handshake();
}
/**
* Tunes in a specific radio station.
*
* @param stationUrl
* Last.fm Station URL in form of lastfm://<type>/<descriptor>
* @throws IOException
* @throws ServiceException
*/
public void tuneInStation(StationUrl stationUrl) throws IOException, ServiceException {
doRequest(
RequestType.TUNEIN,
SERVICE_BASEURL +
"radio/" +
"adjust.php" +
"?session=" + sessionToken.getSessionId() +
"&url=" + stationUrl.toString() +
"&debug=0"
);
isTunedIn = true;
}
/*
* (non-Javadoc)
* @see ch.rolandschaer.ascrblr.Service#getVersion()
*/
public String getVersion() {
return VERSION;
}
/**
* Returns a <code>MetaData</code> object of the currently playing
* track.
*
* @return <code>MetaData</code>object of the currently playing track.
* @throws IOException
* @throws ServiceException
*/
public MetaData getMetaData() throws IOException, ServiceException {
if(!isTunedIn) {
throw new IllegalStateException("Method tuneInStation() has to be called first");
}
InputStream resultStream = null;
MetaData data = null;
try {
URL url = new URL(
SERVICE_BASEURL +
"radio/np.php" +
"?session=" +sessionToken.getSessionId() +
"&debug=0"
);
HttpRequest request = requestFactory.getRequest(RequestType.QUERY, url);
if(connectTimeout>0) {
request.setConnectTimeout(connectTimeout);
}
if(readTimeout>0) {
request.setReadTimeout(readTimeout);
}
// Execute the request
request.execute();
// Handle response
resultStream = request.getResponseStream();
HashMap<String,String> parameter = ResponseUtil.getParameterMap(resultStream);
if( !parameter.containsKey("streaming")
|| "false".equals(parameter.get("streaming")) ) {
throw new IllegalStateException("Method getMp3Stream() has to be called first.");
}
logger.info("Received track data " + parameter);
data = new MetaData(parameter);
return data;
} finally {
if (resultStream != null) {
resultStream.close();
}
}
}
/**
* Executes a command to control the <code>BufferedInputStream</code>.
*
* @return Returns <code>true</code> if the command was successful,
* <code>false</code> otherwise
* @throws IOException
* @throws ServiceException
*/
public void executeCommand(Command cmd) throws IOException, ServiceException {
doRequest(
RequestType.NOTIFY,
SERVICE_BASEURL +
"radio/" +
"control.php" +
"?session=" + sessionToken.getSessionId() +
"&command=" + cmd.toString() +
"&debug=0"
);
}
/**
* Handles handshake with the Webserver.
*
* @throws IOException
* @throws ServiceException
*/
private void handshake() throws IOException, ServiceException {
isTunedIn = false;
doRequest(
RequestType.HANDSHAKE,
SERVICE_BASEURL + "radio/handshake.php" +
"?version=" + VERSION +
"&platform=" +
"&username=" + authToken.getUsername() +
"&passwordmd5=" + authToken.getAuthToken() +
"&debug=0" +
"&partner="
);
}
/*
* (non-Javadoc)
* @see ch.rolandschaer.ascrblr.Service#handleResponse(java.io.InputStream)
*/
protected void handleResponse(InputStream responseStream) throws IOException, ServiceException {
HashMap<String,String> parameter = ResponseUtil.getParameterMap(responseStream);
if( (parameter.containsKey("session") && !parameter.get("session").equals("FAILED"))
|| (parameter.containsKey("response") && parameter.get("response").equals("OK") ) ) {
//Handle handshake response
if(parameter.containsKey("session") && parameter.get("session").length()==32){
sessionToken = new SessionToken(parameter.get("session"),
parameter.get("stream_url"),
parameter.get("base_url"),
parameter.get("base_path")
);
}
} else {
handleErrorResponse(parameter);
}
}
/**
* Handles error response.
*
* @param response
* Web server response in form of a <code>HashMap</code>. The
* response is sent in form of a parameter list e.g.
* session=failed divided by Unix new lines.
*
* @throws ServiceException
*/
private void handleErrorResponse(HashMap<String,String> response) throws ServiceException {
String responseType = response.get("session");
logger.info("Handling error response " + response);
if( responseType.startsWith(ResponseType.FAILED.toString()) ) {
throw new ServiceException(response.get("msg"));
}
}
/**
* Encapsulates session data provided by the service
*/
public static class SessionToken implements Session {
/** Session identifier */
private String sessionId;
/** Streaming URL */
private String streamingUrl;
/** Service base URL */
private String baseUrl;
/** Service base path */
private String basePath;
/**
* Session token which holds relevant session info.
*
* @param sessionId
* Session identifier given by the service
* @param streamingUrl
* Streaming URL
* @param baseUrl
* Service base URL
* @param basePath
* Service base path
*/
public SessionToken(String sessionId, String streamingUrl, String baseUrl, String basePath) {
this.sessionId = sessionId;
this.streamingUrl = streamingUrl;
this.baseUrl = baseUrl;
this.basePath = basePath;
}
public String getSessionId() {
return sessionId;
}
public String getStreamingUrl() {
return streamingUrl;
}
public String getBaseUrl() {
return baseUrl;
}
public String getBasePath() {
return basePath;
}
}
/**
* Encapsulates authentication token which is used during handshake.
*/
public static class AuthToken {
private String username;
private String passwordHash;
/**
* Authentication token needed to autenticate on the service.
*
* @param username
* last.fm username
* @param password
* last.fmm password
* @throws ServiceException
*/
public AuthToken(String username, String password) throws ServiceException {
this.username = username;
this.passwordHash = MD5Util.getMD5(password);
}
public String getUsername() { return username; }
/**
* Authentication token used by the service. The token is built as
* follows: <code>md5(password)
*
* @param timestamp Unix time stamp
* @return
* @throws ServiceException
*/
public String getAuthToken() throws ServiceException {
return passwordHash;
}
}
}