/src/com/sleazyweasel/pandora/JsonPandoraRadio.java
Java | 311 lines | 270 code | 39 blank | 2 comment | 27 complexity | d3dcdd0b1237e75a8a6419802f537c37 MD5 | raw file
- package com.sleazyweasel.pandora;
- import com.google.gson.*;
- import com.sleazyweasel.applescriptifier.BadPandoraPasswordException;
- import de.felixbruns.jotify.util.Hex;
- import javax.crypto.Cipher;
- import javax.crypto.spec.SecretKeySpec;
- import java.io.*;
- import java.net.HttpURLConnection;
- import java.net.URL;
- import java.net.URLEncoder;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.logging.Logger;
- public class JsonPandoraRadio implements PandoraRadio {
- private static final Logger logger = Logger.getLogger(JsonPandoraRadio.class.getName());
- private static final String BLOWFISH_ECB_PKCS5_PADDING = "Blowfish/ECB/PKCS5Padding";
- private Long syncTime;
- private Long clientStartTime;
- private Integer partnerId;
- private String partnerAuthToken;
- private String userAuthToken;
- private Long userId;
- private String user;
- private String password;
- private PandoraAuthConfiguration authConfiguration = PandoraAuthConfiguration.PANDORAONE_CONFIG;
- private List<Station> stations;
- @Override
- public void connect(String user, String password) throws BadPandoraPasswordException {
- clientStartTime = System.currentTimeMillis() / 1000L;
- partnerLogin();
- userLogin(user, password);
- this.user = user;
- this.password = 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);
- String encryptedUserLoginData = encrypt(userLoginData);
- String urlEncodedPartnerAuthToken = urlEncode(partnerAuthToken);
- String userLoginUrl = String.format(authConfiguration.getBaseUrl() + "method=auth.userLogin&auth_token=%s&partner_id=%d", urlEncodedPartnerAuthToken, partnerId);
- JsonObject jsonElement = doPost(userLoginUrl, encryptedUserLoginData).getAsJsonObject();
- String loginStatus = jsonElement.get("stat").getAsString();
- if ("ok".equals(loginStatus)) {
- JsonObject userLoginResult = jsonElement.get("result").getAsJsonObject();
- userAuthToken = userLoginResult.get("userAuthToken").getAsString();
- userId = userLoginResult.get("userId").getAsLong();
- } else {
- throw new BadPandoraPasswordException();
- }
- }
- 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();
- JsonObject asJsonObject = partnerLoginData.getAsJsonObject();
- checkForError(asJsonObject, "Failed at Partner Login");
- JsonObject result = asJsonObject.getAsJsonObject("result");
- String encryptedSyncTime = result.get("syncTime").getAsString();
- 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() {
- JsonObject result = doStandardCall("user.getStationList", new HashMap<String, Object>(), false);
- checkForError(result, "Failed to get Stations");
- JsonArray stationArray = result.get("result").getAsJsonObject().getAsJsonArray("stations");
- stations = new ArrayList<Station>();
- for (JsonElement jsonStationElement : stationArray) {
- JsonObject jsonStation = jsonStationElement.getAsJsonObject();
- String stationId = jsonStation.get("stationId").getAsString();
- String stationIdToken = jsonStation.get("stationToken").getAsString();
- boolean isQuickMix = jsonStation.getAsJsonPrimitive("isQuickMix").getAsBoolean();
- String stationName = jsonStation.get("stationName").getAsString();
- stations.add(new Station(stationId, stationIdToken, false, isQuickMix, stationName));
- }
- return stations;
- }
- private JsonObject doStandardCall(String method, Map<String, Object> postData, boolean useSsl) {
- 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);
- logger.fine("url = " + url);
- postData.put("userAuthToken", userAuthToken);
- postData.put("syncTime", getPandoraTime());
- String jsonData = new Gson().toJson(postData);
- logger.fine("jsonData = " + jsonData);
- return doPost(url, encrypt(jsonData)).getAsJsonObject();
- }
- @Override
- public Station getStationById(long sid) {
- if (stations == null) {
- getStations();
- }
- for (Station station : stations) {
- if (sid == station.getId()) {
- return station;
- }
- }
- return null;
- }
- @Override
- public void rate(Song song, boolean rating) {
- String method = "station.addFeedback";
- Map<String, Object> data = new HashMap<String, Object>();
- data.put("trackToken", song.getTrackToken());
- data.put("isPositive", rating);
- JsonObject ratingResult = doStandardCall(method, data, false);
- checkForError(ratingResult, "failed to rate song");
- }
- @Override
- public void tired(Song song) {
- String method = "user.sleepSong";
- Map<String, Object> data = new HashMap<String, Object>();
- data.put("trackToken", song.getTrackToken());
- JsonObject ratingResult = doStandardCall(method, data, false);
- checkForError(ratingResult, "failed to sleep song");
- }
- @Override
- public boolean isAlive() {
- return userAuthToken != null;
- }
- @Override
- public Song[] getPlaylist(Station station, String format) {
- Map<String, Object> data = new HashMap<String, Object>();
- data.put("stationToken", station.getStationIdToken());
- data.put("additionalAudioUrl", "HTTP_192_MP3,HTTP_128_MP3");
- JsonObject songResult = doStandardCall("station.getPlaylist", data, true);
- try {
- checkForError(songResult, "Failed to get playlist from station");
- } catch (RuntimeException e) {
- String errorCode = songResult.get("code").getAsString();
- if ("1003".equals(errorCode) && authConfiguration == PandoraAuthConfiguration.PANDORAONE_CONFIG) {
- authConfiguration = PandoraAuthConfiguration.ANDROID_CONFIG;
- reLogin();
- return getPlaylist(station, format);
- } else {
- throw e;
- }
- }
- JsonArray songsArray = songResult.get("result").getAsJsonObject().get("items").getAsJsonArray();
- List<Song> results = new ArrayList<Song>();
- for (JsonElement songElement : songsArray) {
- JsonObject songData = songElement.getAsJsonObject();
- //it is completely retarded that pandora leaves this up to the client. Come on, Pandora! Use your brains!
- if (songData.get("adToken") != null) {
- continue;
- }
- String album = songData.get("albumName").getAsString();
- String artist = songData.get("artistName").getAsString();
- JsonElement additionalAudioUrlElement = songData.get("additionalAudioUrl");
- String additionalAudioUrl = additionalAudioUrlElement != null ? additionalAudioUrlElement.getAsString() : null;
- JsonObject audioUrlMap = songData.get("audioUrlMap").getAsJsonObject();
- JsonObject highQuality = audioUrlMap.get("highQuality").getAsJsonObject();
- String audioUrl = highQuality.get("audioUrl").getAsString();
- logger.fine("audioUrl = " + audioUrl);
- logger.fine("additionalAudioUrl = " + additionalAudioUrl);
- String title = songData.get("songName").getAsString();
- String albumDetailUrl = songData.get("albumDetailUrl").getAsString();
- String artRadio = songData.get("albumArtUrl").getAsString();
- String trackToken = songData.get("trackToken").getAsString();
- Integer rating = songData.get("songRating").getAsInt();
- if (audioUrl != null && authConfiguration == PandoraAuthConfiguration.PANDORAONE_CONFIG) {
- results.add(new Song(album, artist, audioUrl, station.getStationId(), title, albumDetailUrl, artRadio, trackToken, rating));
- } else if (additionalAudioUrl != null) {
- results.add(new Song(album, artist, additionalAudioUrl, station.getStationId(), title, albumDetailUrl, artRadio, trackToken, rating));
- }
- }
- return results.toArray(new Song[results.size()]);
- }
- private void reLogin() {
- partnerLogin();
- userLogin(user, password);
- }
- private void checkForError(JsonObject songResult, String errorMessage) {
- String stat = songResult.get("stat").getAsString();
- if (!"ok".equals(stat)) {
- throw new RuntimeException(errorMessage);
- }
- }
- private JsonElement doPartnerLogin() {
- String partnerLoginUrl = authConfiguration.getBaseUrl() + "method=auth.partnerLogin";
- Map<String, Object> data = new HashMap<String, Object>();
- data.put("username", authConfiguration.getUserName());
- data.put("password", authConfiguration.getPassword());
- data.put("deviceModel", authConfiguration.getDeviceModel());
- data.put("version", "5");
- data.put("includeUrls", true);
- String stringData = new Gson().toJson(data);
- return doPost(partnerLoginUrl, stringData);
- }
- private static JsonElement doPost(String urlInput, String stringData) {
- try {
- URL url = new URL(urlInput);
- HttpURLConnection urlConnection = (HttpURLConnection) 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(), "UTF-8"));
- String line;
- while ((line = reader.readLine()) != null) {
- logger.fine("response = " + 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(HttpURLConnection 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 String encrypt(String input) {
- try {
- Cipher encryptionCipher = Cipher.getInstance(BLOWFISH_ECB_PKCS5_PADDING);
- encryptionCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(authConfiguration.getEncryptionKey().getBytes(), "Blowfish"));
- byte[] bytes = encryptionCipher.doFinal(input.getBytes());
- return Hex.toHex(bytes);
- } catch (Exception e) {
- throw new RuntimeException("Failed to properly encrypt data", e);
- }
- }
- private String decrypt(String input) {
- byte[] result;
- try {
- Cipher decryptionCipher = Cipher.getInstance(BLOWFISH_ECB_PKCS5_PADDING);
- decryptionCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(authConfiguration.getDecriptionKey().getBytes(), "Blowfish"));
- 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);
- }
- }