Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

WIP on the JSON API pandora player.

  • Loading branch information...
commit 13ceab026d7e95fdf273c032eae4ec32970b2e33 1 parent b55a2cc
@jkwatson authored
View
3  src/com/sleazyweasel/applescriptifier/JavaPandoraPlayer.java
@@ -4,6 +4,7 @@
import com.sleazyweasel.pandora.PandoraRadio;
import com.sleazyweasel.pandora.Song;
import com.sleazyweasel.pandora.Station;
+import com.sleazyweasel.pandora.XmlRpcPandoraRadio;
import javazoom.jlgui.basicplayer.*;
import java.io.*;
@@ -98,7 +99,7 @@ public void activate() {
}
player = new BasicPlayer();
player.addBasicPlayerListener(this);
- pandoraRadio = new PandoraRadio();
+ pandoraRadio = new XmlRpcPandoraRadio();
System.out.println("player.getStatus() = " + player.getStatus());
try {
LoginInfo loginInfo = getLogin();
View
248 src/com/sleazyweasel/pandora/JsonPandoraRadio.java
@@ -0,0 +1,248 @@
+package com.sleazyweasel.pandora;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.sleazyweasel.applescriptifier.BadPandoraPasswordException;
+import de.felixbruns.jotify.util.Hex;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.net.ssl.HttpsURLConnection;
+import java.io.*;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class JsonPandoraRadio implements PandoraRadio {
+ private static final String ANDROID_DECRYPTION_KEY = "R=U!LH$O2B#";
+ private static final String ANDROID_ENCRYPTION_KEY = "6#26FRL$ZWD";
+ private static final String BLOWFISH_ECB_PKCS5_PADDING = "Blowfish/ECB/PKCS5Padding";
+ private static final String BASE_URL = "https://tuner.pandora.com/services/json/?";
+ private static final String ANDROID_PARTNER_PASSWORD = "AC7IBG09A3DTSYM4R41UJWL07VLN8JI7";
+
+ private Long syncTime;
+ private Long clientStartTime;
+ private Integer partnerId;
+ private String partnerAuthToken;
+ private String userAuthToken;
+
+ private List<Station> stations;
+
+ @Override
+ public void connect(String user, String password) throws BadPandoraPasswordException {
+ clientStartTime = System.currentTimeMillis() / 1000L;
+// System.out.println("clientStartTime = " + clientStartTime); partnerLogin();
+ partnerLogin();
+ userLogin(user, password);
+ }
+
+ private void userLogin(String user, String password) {
+ Map<String, Object> userLoginInputs = new HashMap<String, Object>();
+ userLoginInputs.put("loginType", "user");
+ userLoginInputs.put("username", user);
+ userLoginInputs.put("password", password);
+ userLoginInputs.put("partnerAuthToken", partnerAuthToken);
+ userLoginInputs.put("syncTime", getPandoraTime());
+ String userLoginData = new Gson().toJson(userLoginInputs);
+// System.out.println("userLoginData = " + userLoginData);
+ String encryptedUserLoginData = encrypt(userLoginData);
+// System.out.println("encryptedUserLoginData = " + encryptedUserLoginData);
+ String urlEncodedPartnerAuthToken = urlEncode(partnerAuthToken);
+
+ String userLoginUrl = String.format(BASE_URL + "method=auth.userLogin&auth_token=%s&partner_id=%d", urlEncodedPartnerAuthToken, partnerId);
+// System.out.println("userLoginUrl = " + userLoginUrl);
+ JsonObject jsonElement = doPost(userLoginUrl, encryptedUserLoginData).getAsJsonObject();
+ String loginStatus = jsonElement.get("stat").getAsString();
+// System.out.println("loginStatus = " + loginStatus);
+ if (loginStatus.equals("ok")) {
+ JsonObject userLoginResult = jsonElement.get("result").getAsJsonObject();
+ userAuthToken = userLoginResult.get("userAuthToken").getAsString();
+// System.out.println("userAuthToken.getAsString() = " + userAuthToken.getAsString());
+ }
+ else {
+ throw new BadPandoraPasswordException();
+// System.out.println("loginStatus = " + loginStatus);
+ }
+ }
+
+ private String urlEncode(String f) {
+ try {
+ return URLEncoder.encode(f, "ISO-8859-1");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("This better not happen, because ISO-8859-1 is a valid encoding", e);
+ }
+ }
+
+ private long getPandoraTime() {
+ return syncTime + ((System.currentTimeMillis() / 1000) - clientStartTime);
+ }
+
+ private void partnerLogin() {
+ JsonElement partnerLoginData = doPartnerLogin();
+// System.out.println("parse array = " + partnerLoginData.isJsonArray());
+// System.out.println("parse object= " + partnerLoginData.isJsonObject());
+ JsonObject asJsonObject = partnerLoginData.getAsJsonObject();
+ JsonElement stat = asJsonObject.get("stat");
+// System.out.println("stat.getAsString() = " + stat.getAsString());
+ JsonObject result = asJsonObject.getAsJsonObject("result");
+ String encryptedSyncTime = result.get("syncTime").getAsString();
+// System.out.println("syncTime = " + encryptedSyncTime);
+ partnerAuthToken = result.get("partnerAuthToken").getAsString();
+ syncTime = Long.valueOf(decrypt(encryptedSyncTime));
+ partnerId = result.get("partnerId").getAsInt();
+ }
+
+ @Override
+ public void sync() {
+ //don't think we need to do this, since it's a part of the core json APIs.
+ }
+
+ @Override
+ public void disconnect() {
+ syncTime = null;
+ clientStartTime = null;
+ partnerId = null;
+ partnerAuthToken = null;
+ userAuthToken = null;
+ stations = null;
+ }
+
+ @Override
+ public List<Station> getStations() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Station getStationById(long sid) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void rate(Song song, boolean rating) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isAlive() {
+ throw new UnsupportedOperationException();
+ }
+
+ private static JsonElement doPartnerLogin() {
+ String partnerLoginUrl = BASE_URL + "method=auth.partnerLogin";
+ Map<String, Object> data = new HashMap<String, Object>();
+ data.put("username", "android");
+ data.put("password", ANDROID_PARTNER_PASSWORD);
+ data.put("deviceModel", "android-generic");
+ data.put("version", "5");
+ data.put("includeUrls", true);
+ String stringData = new Gson().toJson(data);
+// System.out.println("stringData = " + stringData);
+
+ return doPost(partnerLoginUrl, stringData);
+ }
+
+ private static JsonElement doPost(String urlInput, String stringData) {
+ try {
+ URL url = new URL(urlInput);
+ HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
+ urlConnection.setRequestMethod("POST");
+ urlConnection.setDoOutput(true);
+ urlConnection.setDoInput(true);
+
+ setRequestHeaders(urlConnection);
+
+ urlConnection.setRequestProperty("Content-length", String.valueOf(stringData.length()));
+ urlConnection.connect();
+ DataOutputStream out = new DataOutputStream(urlConnection.getOutputStream());
+
+ out.writeBytes(stringData);
+ out.flush();
+ out.close();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ // System.out.println("line = " + line);
+ JsonParser parser = new JsonParser();
+ return parser.parse(line);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to connect to Pandora", e);
+ }
+ throw new RuntimeException("Failed to get a response from Pandora");
+ }
+
+
+ private static void setRequestHeaders(HttpsURLConnection conn) {
+ conn.setRequestProperty("User-Agent", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)");
+ conn.setRequestProperty("Content-Type", "text/plain");
+ conn.setRequestProperty("Accept", "*/*");
+ }
+
+ private static String encrypt(String input) {
+ try {
+ Cipher encryptionCipher = Cipher.getInstance(BLOWFISH_ECB_PKCS5_PADDING);
+ encryptionCipher.init(Cipher.ENCRYPT_MODE, new Key() {
+ @Override
+ public String getAlgorithm() {
+ return "Blowfish";
+ }
+
+ @Override
+ public String getFormat() {
+ return "RAW";
+ }
+
+ @Override
+ public byte[] getEncoded() {
+ return ANDROID_ENCRYPTION_KEY.getBytes();
+ }
+ });
+ byte[] bytes = encryptionCipher.doFinal(input.getBytes());
+ return Hex.toHex(bytes);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to properly encrypt data", e);
+ }
+ }
+
+ private static String decrypt(String input) {
+ byte[] result = new byte[0];
+ try {
+ Cipher decryptionCipher = Cipher.getInstance(BLOWFISH_ECB_PKCS5_PADDING);
+
+ decryptionCipher.init(Cipher.DECRYPT_MODE, new Key() {
+ @Override
+ public String getAlgorithm() {
+ return "Blowfish";
+ }
+
+ @Override
+ public String getFormat() {
+ return "RAW";
+ }
+
+ @Override
+ public byte[] getEncoded() {
+ return ANDROID_DECRYPTION_KEY.getBytes();
+ }
+ });
+
+ result = decryptionCipher.doFinal(Hex.toBytes(input));
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to properly decrypt data", e);
+ }
+
+ byte[] chopped = new byte[result.length - 4];
+ System.arraycopy(result, 4, chopped, 0, chopped.length);
+
+ return new String(chopped);
+ }
+}
View
399 src/com/sleazyweasel/pandora/PandoraRadio.java
@@ -1,400 +1,21 @@
-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
+package com.sleazyweasel.pandora;
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.URLConnection;
-import java.net.URLEncoder;
-import java.util.*;
-
-
-public class 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 ArrayList<Station> stations;
- private long offset = 0L;
-
- public PandoraRadio() {
- 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);
- }
-
- 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");
- }
- }
-
- 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;
- }
-
- public void disconnect() {
- authToken = null;
- webAuthToken = null;
-
- if (stations != null) {
- stations.clear();
- stations = null;
- }
- }
-
- public ArrayList<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], this));
- }
- Collections.sort(stations);
- }
-
- return stations;
- }
-
- public Station getStationById(long sid) {
- Iterator<Station> stationIter = stations.iterator();
- Station station = null;
- while (stationIter.hasNext()) {
- station = stationIter.next();
- if (station.getId() == sid) {
- return station;
- }
- }
- return null;
- }
-
- 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);
- }
+import java.util.List;
- public void bookmarkSong(Station station, Song song) {
- ArrayList<Object> args = new ArrayList<Object>(2);
- args.add(String.valueOf(station.getId()));
- args.add(song.getTrackToken());
+public interface PandoraRadio {
+ void connect(String user, String password) throws BadPandoraPasswordException;
- xmlrpcCall("station.createBookmark", args);
- }
+ void sync();
- public void bookmarkArtist(Station station, Song song) {
- ArrayList<Object> args = new ArrayList<Object>(1);
- args.add(song.getArtistMusicId());
+ void disconnect();
- xmlrpcCall("station.createArtistBookmark", args);
- }
+ List<Station> getStations();
- 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);
- }
+ Station getStationById(long sid);
- public boolean isAlive() {
- return authToken != null;
- }
+ void rate(Song song, boolean rating);
+ boolean isAlive();
}
View
4 src/com/sleazyweasel/pandora/Song.java
@@ -39,7 +39,7 @@
private boolean finished;
private long playlistTime;
- public Song(Map<String, Object> data, PandoraRadio pandoraRadio) {
+ public Song(Map<String, Object> data, XmlRpcPandoraRadio pandoraRadio) {
try {
album = (String) data.get("albumTitle");
artist = (String) data.get("artistSummary");
@@ -91,7 +91,7 @@ public Song(Song copy, Integer newRating) {
}
public boolean isStillValid() {
- return ((System.currentTimeMillis() / 1000L) - playlistTime) < PandoraRadio.PLAYLIST_VALIDITY_TIME;
+ return ((System.currentTimeMillis() / 1000L) - playlistTime) < XmlRpcPandoraRadio.PLAYLIST_VALIDITY_TIME;
}
public String getTrackToken() {
View
6 src/com/sleazyweasel/pandora/Station.java
@@ -32,9 +32,9 @@
transient private Song[] currentPlaylist;
transient private boolean useQuickMix;
- transient private PandoraRadio pandora;
+ transient private XmlRpcPandoraRadio pandora;
- public Station(HashMap<String, Object> d, PandoraRadio instance) {
+ public Station(HashMap<String, Object> d, XmlRpcPandoraRadio instance) {
id = (String) d.get("stationId");
idToken = (String) d.get("stationIdToken");
isCreator = (Boolean) d.get("isCreator");
@@ -46,7 +46,7 @@ public Station(HashMap<String, Object> d, PandoraRadio instance) {
}
public Song[] getPlaylist(boolean forceDownload) {
- return getPlaylist(PandoraRadio.DEFAULT_AUDIO_FORMAT, forceDownload);
+ return getPlaylist(XmlRpcPandoraRadio.DEFAULT_AUDIO_FORMAT, forceDownload);
}
public Song[] getPlaylist(String format, boolean forceDownload) {
View
406 src/com/sleazyweasel/pandora/XmlRpcPandoraRadio.java
@@ -0,0 +1,406 @@
+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], this));
+ }
+ Collections.sort(stations);
+ }
+
+ return stations;
+ }
+
+ @Override
+ public Station getStationById(long sid) {
+ Iterator<Station> stationIter = stations.iterator();
+ Station station = null;
+ while (stationIter.hasNext()) {
+ station = stationIter.next();
+ 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;
+ }
+
+}
Please sign in to comment.
Something went wrong with that request. Please try again.