package com.sleazyweasel.pandora;/* Pandoroid Radio - open source pandora.com client for android
* Copyright (C) 2011 Andrew Regner <andrew@aregner.com>
*
* This program 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 2
* of the License, or (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/* This class is designed to be used as a stand-alone Java module for interacting
* with Pandora Radio. Other then the XmlRpc class which is based on the android
* library, this class should run in any Java VM.
*/
//import java.io.Console; //Not supported by android's JVM - used for testing this class with java6 on PC/Mac
import com.sleazyweasel.applescriptifier.BadPandoraPasswordException;
import org.xmlrpc.android.XMLRPCException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.*;
public class XmlRpcPandoraRadio implements PandoraRadio {
public static final String PROTOCOL_VERSION = "34";
private static final String RPC_URL = "https://www.pandora.com/radio/xmlrpc/v" + PROTOCOL_VERSION + "?";
private static final String NON_SSL_RPC_URL = "http://www.pandora.com/radio/xmlrpc/v" + PROTOCOL_VERSION + "?";
private static final String USER_AGENT = "com.magicbos.doombox";
public static final long PLAYLIST_VALIDITY_TIME = 3600 * 3;
public static final String DEFAULT_AUDIO_FORMAT = "aacplus";
private static final ArrayList<Object> EMPTY_ARGS = new ArrayList<Object>();
private XmlRpc xmlrpc;
private XmlRpc nonSslXmlrpc;
private Blowfish blowfish_encode;
private Blowfish blowfish_decode;
private String authToken;
private String rid;
private String lid;
private String webAuthToken;
private List<Station> stations;
private long offset = 0L;
public XmlRpcPandoraRadio() {
xmlrpc = new XmlRpc(RPC_URL);
xmlrpc.addHeader("User-agent", USER_AGENT);
nonSslXmlrpc = new XmlRpc(NON_SSL_RPC_URL);
nonSslXmlrpc.addHeader("User-agent", USER_AGENT);
blowfish_encode = new Blowfish(PandoraKeys.out_key_p, PandoraKeys.out_key_s);
blowfish_decode = new Blowfish(PandoraKeys.in_key_p, PandoraKeys.in_key_s);
}
private String pad(String s, int l) {
String result = s;
while (l - s.length() > 0) {
result += '\0';
l--;
}
return result;
}
private String fromHex(String hexText) {
String decodedText = null;
String chunk = null;
if (hexText != null && hexText.length() > 0) {
int numBytes = hexText.length() / 2;
char[] rawToByte = new char[numBytes];
int offset = 0;
for (int i = 0; i < numBytes; i++) {
chunk = hexText.substring(offset, offset + 2);
offset += 2;
rawToByte[i] = (char) (Integer.parseInt(chunk, 16) & 0x000000FF);
}
decodedText = new String(rawToByte);
}
return decodedText;
}
public String pandoraEncrypt(String s) {
int length = s.length();
StringBuilder result = new StringBuilder(length * 2);
int i8 = 0;
for (int i = 0; i < length; i += 8) {
i8 = (i + 8 >= length) ? (length) : (i + 8);
String substring = s.substring(i, i8);
String padded = pad(substring, 8);
long[] blownstring = blowfish_encode.encrypt(padded.toCharArray());
for (int c = 0; c < blownstring.length; c++) {
if (blownstring[c] < 0x10)
result.append("0");
result.append(Integer.toHexString((int) blownstring[c]));
}
}
return result.toString();
}
public String pandoraDecrypt(String s) {
StringBuilder result = new StringBuilder();
int length = s.length();
int i16 = 0;
for (int i = 0; i < length; i += 16) {
i16 = (i + 16 > length) ? (length - 1) : (i + 16);
result.append(blowfish_decode.decrypt(pad(fromHex(s.substring(i, i16)), 8).toCharArray()));
}
return result.toString().trim();
}
public List<Character> pandoraDecryptToBytes(String s) {
List<Character> results = new ArrayList<Character>();
int length = s.length();
int i16 = 0;
for (int i = 0; i < length; i += 16) {
i16 = (i + 16 > length) ? (length - 1) : (i + 16);
List<Character> decrypt = blowfish_decode.decryptToBytes(pad(fromHex(s.substring(i, i16)), 8).toCharArray());
results.addAll(decrypt);
}
return results;
}
private String formatUrlArg(boolean v) {
return v ? "true" : "false";
}
private String formatUrlArg(int v) {
return String.valueOf(v);
}
private String formatUrlArg(long v) {
return String.valueOf(v);
}
private String formatUrlArg(float v) {
return String.valueOf(v);
}
private String formatUrlArg(double v) {
return String.valueOf(v);
}
private String formatUrlArg(char v) {
return String.valueOf(v);
}
private String formatUrlArg(short v) {
return String.valueOf(v);
}
private String formatUrlArg(Object v) {
return URLEncoder.encode(v.toString());
}
private String formatUrlArg(Object[] v) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < v.length; i++) {
result.append(formatUrlArg(v[i]));
if (i < v.length - 1)
result.append("%2C");
}
return result.toString();
}
private String formatUrlArg(Iterator<?> v) {
StringBuilder result = new StringBuilder();
while (v.hasNext()) {
result.append(formatUrlArg(v.next()));
if (v.hasNext())
result.append("%2C");
}
return result.toString();
}
private String formatUrlArg(Collection<?> v) {
return formatUrlArg(v.iterator());
}
public static void printXmlRpc(String xml) {
xml = xml.replace("<param>", "\n\t<param>").replace("</params>", "\n</params>");
System.err.println(xml);
}
//@SuppressWarnings("unchecked")
private Object xmlrpcCall(String method, ArrayList<Object> args, ArrayList<Object> urlArgs, boolean includeTimestamp, boolean useSsl) {
if (urlArgs == null)
urlArgs = (ArrayList<Object>) args.clone();
// args.add(0, new Long(System.currentTimeMillis() / 1000L) + 15552000);
if (includeTimestamp) {
args.add(0, (System.currentTimeMillis() / 1000L) + offset);
}
if (authToken != null)
args.add(1, authToken);
String xml = XmlRpc.makeCall(method, args);
printXmlRpc(xml);
String data = pandoraEncrypt(xml);
ArrayList<String> urlArgStrings = new ArrayList<String>();
if (rid != null) {
urlArgStrings.add("rid=" + rid);
}
if (lid != null) {
urlArgStrings.add("lid=" + lid);
}
method = method.substring(method.lastIndexOf('.') + 1);
urlArgStrings.add("method=" + method);
Iterator<Object> urlArgsIter = urlArgs.iterator();
int count = 1;
while (urlArgsIter.hasNext()) {
urlArgStrings.add("arg" + (count++) + "=" + formatUrlArg(urlArgsIter.next()));
}
StringBuilder url = new StringBuilder(useSsl ? RPC_URL : NON_SSL_RPC_URL);
Iterator<String> argIter = urlArgStrings.iterator();
while (argIter.hasNext()) {
url.append(argIter.next());
if (argIter.hasNext())
url.append("&");
}
Object result = null;
try {
XmlRpc rpc = useSsl ? xmlrpc : nonSslXmlrpc;
result = rpc.callWithBody(url.toString(), data);
} catch (XMLRPCException e) {
if (e.getMessage().contains("AUTH_INVALID_USERNAME_PASSWORD")) {
throw new BadPandoraPasswordException();
}
throw new RuntimeException("Pandora command failed.", e);
}
return result;
}
Object xmlrpcCall(String method, ArrayList<Object> args) {
return xmlrpcCall(method, args, true);
}
Object xmlrpcCall(String method, ArrayList<Object> args, boolean useSsl) {
return xmlrpcCall(method, args, null, true, useSsl);
}
private Object xmlrpcCall(String method, boolean includeTimestamp) {
EMPTY_ARGS.clear();
return xmlrpcCall(method, EMPTY_ARGS, null, includeTimestamp, true);
}
@Override
public void connect(String user, String password) {
rid = String.format("%07dP", System.currentTimeMillis() % 1000L);
authToken = null;
ArrayList<Object> args = new ArrayList<Object>();
args.add("");
args.add(user);
args.add(password);
args.add("html5tuner");
args.add("");
args.add("");
args.add("HTML5");
args.add(true);
Object result = xmlrpcCall("listener.authenticateListener", args, EMPTY_ARGS, true, true);
if (result instanceof HashMap<?, ?>) {
HashMap<String, Object> userInfo = (HashMap<String, Object>) result;
webAuthToken = (String) userInfo.get("webAuthToken");
authToken = (String) userInfo.get("authToken");
lid = (String) userInfo.get("listenerId");
}
}
@Override
public void sync() {
long currentSystemTime = System.currentTimeMillis() / 1000L;
URL url;
try {
url = new URL("http://ridetheclown.com/s2/synctime.php");
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
String timestampAsString;
InputStream inputStream = null;
try {
inputStream = url.openStream();
timestampAsString = new BufferedReader(new InputStreamReader(inputStream)).readLine();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
//ignore this...
}
}
}
// String result = (String) xmlrpcCall("misc.sync", false);
// List<Character> s = pandoraDecryptToBytes(result);
// //first 4 bytes appear to be junk?
// StringBuilder timestampAsString = new StringBuilder();
// for (int i = 4; i < s.size(); i++) {
// timestampAsString.append(s.get(i));
// }
long currentPandoraTime = Long.valueOf(timestampAsString.trim());
offset = currentPandoraTime - currentSystemTime;
}
@Override
public void disconnect() {
authToken = null;
webAuthToken = null;
if (stations != null) {
stations.clear();
stations = null;
}
}
@Override
public List<Station> getStations() {
// get stations
Object result = xmlrpcCall("station.getStations", true);
if (result instanceof Object[]) {
Object[] stationsResult = (Object[]) result;
stations = new ArrayList<Station>(stationsResult.length);
for (int s = 0; s < stationsResult.length; s++) {
stations.add(new Station((HashMap<String, Object>) stationsResult[s]));
}
Collections.sort(stations);
}
return stations;
}
@Override
public Station getStationById(long sid) {
for (Station station : stations) {
if (station.getId() == sid) {
return station;
}
}
return null;
}
@Override
public void rate(Song song, boolean rating) {
ArrayList<Object> args = new ArrayList<Object>(3);
args.add(String.valueOf(song.getStationId()));
args.add(song.getTrackToken());
args.add(rating);
xmlrpcCall("station.addFeedback", args);
}
public void bookmarkSong(Station station, Song song) {
ArrayList<Object> args = new ArrayList<Object>(2);
args.add(String.valueOf(station.getId()));
args.add(song.getTrackToken());
xmlrpcCall("station.createBookmark", args);
}
public void bookmarkArtist(Station station, Song song) {
ArrayList<Object> args = new ArrayList<Object>(1);
args.add(song.getArtistMusicId());
xmlrpcCall("station.createArtistBookmark", args);
}
public void tired(Station station, Song song) {
ArrayList<Object> args = new ArrayList<Object>(3);
args.add(song.getTrackToken());
args.add(String.valueOf(station.getId()));
xmlrpcCall("listener.addTiredSong", args);
}
@Override
public boolean isAlive() {
return authToken != null;
}
@Override
public Song[] getPlaylist(Station station, String format) {
ArrayList<Object> args = new ArrayList<Object>(7);
args.add(station.getStationId());
args.add("0");
args.add("");
args.add("");
args.add(format);
args.add("0");
args.add("0");
Object result = xmlrpcCall("playlist.getFragment", args, false);
if (result instanceof Object[]) {
Object[] fragmentsResult = (Object[]) result;
Song[] list = new Song[fragmentsResult.length];
for (int f = 0; f < fragmentsResult.length; f++) {
list[f] = new Song((HashMap<String, Object>) fragmentsResult[f], this);
}
return list;
}
return null;
}
@Override
public void tired(Song song) {
throw new UnsupportedOperationException();
}
}