PageRenderTime 74ms CodeModel.GetById 44ms RepoModel.GetById 1ms app.codeStats 0ms

/src/com/sleazyweasel/pandora/JsonPandoraRadio.java

https://github.com/jkwatson/Muse-Controller
Java | 311 lines | 270 code | 39 blank | 2 comment | 27 complexity | d3dcdd0b1237e75a8a6419802f537c37 MD5 | raw file
  1. package com.sleazyweasel.pandora;
  2. import com.google.gson.*;
  3. import com.sleazyweasel.applescriptifier.BadPandoraPasswordException;
  4. import de.felixbruns.jotify.util.Hex;
  5. import javax.crypto.Cipher;
  6. import javax.crypto.spec.SecretKeySpec;
  7. import java.io.*;
  8. import java.net.HttpURLConnection;
  9. import java.net.URL;
  10. import java.net.URLEncoder;
  11. import java.util.ArrayList;
  12. import java.util.HashMap;
  13. import java.util.List;
  14. import java.util.Map;
  15. import java.util.logging.Logger;
  16. public class JsonPandoraRadio implements PandoraRadio {
  17. private static final Logger logger = Logger.getLogger(JsonPandoraRadio.class.getName());
  18. private static final String BLOWFISH_ECB_PKCS5_PADDING = "Blowfish/ECB/PKCS5Padding";
  19. private Long syncTime;
  20. private Long clientStartTime;
  21. private Integer partnerId;
  22. private String partnerAuthToken;
  23. private String userAuthToken;
  24. private Long userId;
  25. private String user;
  26. private String password;
  27. private PandoraAuthConfiguration authConfiguration = PandoraAuthConfiguration.PANDORAONE_CONFIG;
  28. private List<Station> stations;
  29. @Override
  30. public void connect(String user, String password) throws BadPandoraPasswordException {
  31. clientStartTime = System.currentTimeMillis() / 1000L;
  32. partnerLogin();
  33. userLogin(user, password);
  34. this.user = user;
  35. this.password = password;
  36. }
  37. private void userLogin(String user, String password) {
  38. Map<String, Object> userLoginInputs = new HashMap<String, Object>();
  39. userLoginInputs.put("loginType", "user");
  40. userLoginInputs.put("username", user);
  41. userLoginInputs.put("password", password);
  42. userLoginInputs.put("partnerAuthToken", partnerAuthToken);
  43. userLoginInputs.put("syncTime", getPandoraTime());
  44. String userLoginData = new Gson().toJson(userLoginInputs);
  45. String encryptedUserLoginData = encrypt(userLoginData);
  46. String urlEncodedPartnerAuthToken = urlEncode(partnerAuthToken);
  47. String userLoginUrl = String.format(authConfiguration.getBaseUrl() + "method=auth.userLogin&auth_token=%s&partner_id=%d", urlEncodedPartnerAuthToken, partnerId);
  48. JsonObject jsonElement = doPost(userLoginUrl, encryptedUserLoginData).getAsJsonObject();
  49. String loginStatus = jsonElement.get("stat").getAsString();
  50. if ("ok".equals(loginStatus)) {
  51. JsonObject userLoginResult = jsonElement.get("result").getAsJsonObject();
  52. userAuthToken = userLoginResult.get("userAuthToken").getAsString();
  53. userId = userLoginResult.get("userId").getAsLong();
  54. } else {
  55. throw new BadPandoraPasswordException();
  56. }
  57. }
  58. private String urlEncode(String f) {
  59. try {
  60. return URLEncoder.encode(f, "ISO-8859-1");
  61. } catch (UnsupportedEncodingException e) {
  62. throw new RuntimeException("This better not happen, because ISO-8859-1 is a valid encoding", e);
  63. }
  64. }
  65. private long getPandoraTime() {
  66. return syncTime + ((System.currentTimeMillis() / 1000) - clientStartTime);
  67. }
  68. private void partnerLogin() {
  69. JsonElement partnerLoginData = doPartnerLogin();
  70. JsonObject asJsonObject = partnerLoginData.getAsJsonObject();
  71. checkForError(asJsonObject, "Failed at Partner Login");
  72. JsonObject result = asJsonObject.getAsJsonObject("result");
  73. String encryptedSyncTime = result.get("syncTime").getAsString();
  74. partnerAuthToken = result.get("partnerAuthToken").getAsString();
  75. syncTime = Long.valueOf(decrypt(encryptedSyncTime));
  76. partnerId = result.get("partnerId").getAsInt();
  77. }
  78. @Override
  79. public void sync() {
  80. //don't think we need to do this, since it's a part of the core json APIs.
  81. }
  82. @Override
  83. public void disconnect() {
  84. syncTime = null;
  85. clientStartTime = null;
  86. partnerId = null;
  87. partnerAuthToken = null;
  88. userAuthToken = null;
  89. stations = null;
  90. }
  91. @Override
  92. public List<Station> getStations() {
  93. JsonObject result = doStandardCall("user.getStationList", new HashMap<String, Object>(), false);
  94. checkForError(result, "Failed to get Stations");
  95. JsonArray stationArray = result.get("result").getAsJsonObject().getAsJsonArray("stations");
  96. stations = new ArrayList<Station>();
  97. for (JsonElement jsonStationElement : stationArray) {
  98. JsonObject jsonStation = jsonStationElement.getAsJsonObject();
  99. String stationId = jsonStation.get("stationId").getAsString();
  100. String stationIdToken = jsonStation.get("stationToken").getAsString();
  101. boolean isQuickMix = jsonStation.getAsJsonPrimitive("isQuickMix").getAsBoolean();
  102. String stationName = jsonStation.get("stationName").getAsString();
  103. stations.add(new Station(stationId, stationIdToken, false, isQuickMix, stationName));
  104. }
  105. return stations;
  106. }
  107. private JsonObject doStandardCall(String method, Map<String, Object> postData, boolean useSsl) {
  108. String url = String.format((useSsl ? authConfiguration.getBaseUrl() : authConfiguration.getNonTlsBaseUrl()) + "method=%s&auth_token=%s&partner_id=%d&user_id=%s", method, urlEncode(userAuthToken), partnerId, userId);
  109. logger.fine("url = " + url);
  110. postData.put("userAuthToken", userAuthToken);
  111. postData.put("syncTime", getPandoraTime());
  112. String jsonData = new Gson().toJson(postData);
  113. logger.fine("jsonData = " + jsonData);
  114. return doPost(url, encrypt(jsonData)).getAsJsonObject();
  115. }
  116. @Override
  117. public Station getStationById(long sid) {
  118. if (stations == null) {
  119. getStations();
  120. }
  121. for (Station station : stations) {
  122. if (sid == station.getId()) {
  123. return station;
  124. }
  125. }
  126. return null;
  127. }
  128. @Override
  129. public void rate(Song song, boolean rating) {
  130. String method = "station.addFeedback";
  131. Map<String, Object> data = new HashMap<String, Object>();
  132. data.put("trackToken", song.getTrackToken());
  133. data.put("isPositive", rating);
  134. JsonObject ratingResult = doStandardCall(method, data, false);
  135. checkForError(ratingResult, "failed to rate song");
  136. }
  137. @Override
  138. public void tired(Song song) {
  139. String method = "user.sleepSong";
  140. Map<String, Object> data = new HashMap<String, Object>();
  141. data.put("trackToken", song.getTrackToken());
  142. JsonObject ratingResult = doStandardCall(method, data, false);
  143. checkForError(ratingResult, "failed to sleep song");
  144. }
  145. @Override
  146. public boolean isAlive() {
  147. return userAuthToken != null;
  148. }
  149. @Override
  150. public Song[] getPlaylist(Station station, String format) {
  151. Map<String, Object> data = new HashMap<String, Object>();
  152. data.put("stationToken", station.getStationIdToken());
  153. data.put("additionalAudioUrl", "HTTP_192_MP3,HTTP_128_MP3");
  154. JsonObject songResult = doStandardCall("station.getPlaylist", data, true);
  155. try {
  156. checkForError(songResult, "Failed to get playlist from station");
  157. } catch (RuntimeException e) {
  158. String errorCode = songResult.get("code").getAsString();
  159. if ("1003".equals(errorCode) && authConfiguration == PandoraAuthConfiguration.PANDORAONE_CONFIG) {
  160. authConfiguration = PandoraAuthConfiguration.ANDROID_CONFIG;
  161. reLogin();
  162. return getPlaylist(station, format);
  163. } else {
  164. throw e;
  165. }
  166. }
  167. JsonArray songsArray = songResult.get("result").getAsJsonObject().get("items").getAsJsonArray();
  168. List<Song> results = new ArrayList<Song>();
  169. for (JsonElement songElement : songsArray) {
  170. JsonObject songData = songElement.getAsJsonObject();
  171. //it is completely retarded that pandora leaves this up to the client. Come on, Pandora! Use your brains!
  172. if (songData.get("adToken") != null) {
  173. continue;
  174. }
  175. String album = songData.get("albumName").getAsString();
  176. String artist = songData.get("artistName").getAsString();
  177. JsonElement additionalAudioUrlElement = songData.get("additionalAudioUrl");
  178. String additionalAudioUrl = additionalAudioUrlElement != null ? additionalAudioUrlElement.getAsString() : null;
  179. JsonObject audioUrlMap = songData.get("audioUrlMap").getAsJsonObject();
  180. JsonObject highQuality = audioUrlMap.get("highQuality").getAsJsonObject();
  181. String audioUrl = highQuality.get("audioUrl").getAsString();
  182. logger.fine("audioUrl = " + audioUrl);
  183. logger.fine("additionalAudioUrl = " + additionalAudioUrl);
  184. String title = songData.get("songName").getAsString();
  185. String albumDetailUrl = songData.get("albumDetailUrl").getAsString();
  186. String artRadio = songData.get("albumArtUrl").getAsString();
  187. String trackToken = songData.get("trackToken").getAsString();
  188. Integer rating = songData.get("songRating").getAsInt();
  189. if (audioUrl != null && authConfiguration == PandoraAuthConfiguration.PANDORAONE_CONFIG) {
  190. results.add(new Song(album, artist, audioUrl, station.getStationId(), title, albumDetailUrl, artRadio, trackToken, rating));
  191. } else if (additionalAudioUrl != null) {
  192. results.add(new Song(album, artist, additionalAudioUrl, station.getStationId(), title, albumDetailUrl, artRadio, trackToken, rating));
  193. }
  194. }
  195. return results.toArray(new Song[results.size()]);
  196. }
  197. private void reLogin() {
  198. partnerLogin();
  199. userLogin(user, password);
  200. }
  201. private void checkForError(JsonObject songResult, String errorMessage) {
  202. String stat = songResult.get("stat").getAsString();
  203. if (!"ok".equals(stat)) {
  204. throw new RuntimeException(errorMessage);
  205. }
  206. }
  207. private JsonElement doPartnerLogin() {
  208. String partnerLoginUrl = authConfiguration.getBaseUrl() + "method=auth.partnerLogin";
  209. Map<String, Object> data = new HashMap<String, Object>();
  210. data.put("username", authConfiguration.getUserName());
  211. data.put("password", authConfiguration.getPassword());
  212. data.put("deviceModel", authConfiguration.getDeviceModel());
  213. data.put("version", "5");
  214. data.put("includeUrls", true);
  215. String stringData = new Gson().toJson(data);
  216. return doPost(partnerLoginUrl, stringData);
  217. }
  218. private static JsonElement doPost(String urlInput, String stringData) {
  219. try {
  220. URL url = new URL(urlInput);
  221. HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
  222. urlConnection.setRequestMethod("POST");
  223. urlConnection.setDoOutput(true);
  224. urlConnection.setDoInput(true);
  225. setRequestHeaders(urlConnection);
  226. urlConnection.setRequestProperty("Content-length", String.valueOf(stringData.length()));
  227. urlConnection.connect();
  228. DataOutputStream out = new DataOutputStream(urlConnection.getOutputStream());
  229. out.writeBytes(stringData);
  230. out.flush();
  231. out.close();
  232. BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), "UTF-8"));
  233. String line;
  234. while ((line = reader.readLine()) != null) {
  235. logger.fine("response = " + line);
  236. JsonParser parser = new JsonParser();
  237. return parser.parse(line);
  238. }
  239. } catch (IOException e) {
  240. throw new RuntimeException("Failed to connect to Pandora", e);
  241. }
  242. throw new RuntimeException("Failed to get a response from Pandora");
  243. }
  244. private static void setRequestHeaders(HttpURLConnection conn) {
  245. conn.setRequestProperty("User-Agent", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)");
  246. conn.setRequestProperty("Content-Type", "text/plain");
  247. conn.setRequestProperty("Accept", "*/*");
  248. }
  249. private String encrypt(String input) {
  250. try {
  251. Cipher encryptionCipher = Cipher.getInstance(BLOWFISH_ECB_PKCS5_PADDING);
  252. encryptionCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(authConfiguration.getEncryptionKey().getBytes(), "Blowfish"));
  253. byte[] bytes = encryptionCipher.doFinal(input.getBytes());
  254. return Hex.toHex(bytes);
  255. } catch (Exception e) {
  256. throw new RuntimeException("Failed to properly encrypt data", e);
  257. }
  258. }
  259. private String decrypt(String input) {
  260. byte[] result;
  261. try {
  262. Cipher decryptionCipher = Cipher.getInstance(BLOWFISH_ECB_PKCS5_PADDING);
  263. decryptionCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(authConfiguration.getDecriptionKey().getBytes(), "Blowfish"));
  264. result = decryptionCipher.doFinal(Hex.toBytes(input));
  265. } catch (Exception e) {
  266. throw new RuntimeException("Failed to properly decrypt data", e);
  267. }
  268. byte[] chopped = new byte[result.length - 4];
  269. System.arraycopy(result, 4, chopped, 0, chopped.length);
  270. return new String(chopped);
  271. }
  272. }