pub mod accessory;
pub mod ad;
pub mod auth;
pub mod bookmark;
mod crypt;
pub mod device;
pub mod errors;
pub mod music;
pub mod station;
pub mod test;
pub mod track;
pub mod user;
use std::collections::HashMap;
use std::fmt::Debug;
use reqwest;
use serde::{Deserialize, Serialize};
use serde_json;
use crate::errors::Error;
use crate::json::auth::{PartnerLogin, PartnerLoginResponse};
use crate::json::errors::{JsonError, JsonErrorKind};
#[derive(Debug, Clone)]
pub struct PandoraSession {
client: reqwest::blocking::Client,
endpoint_url: url::Url,
tokens: SessionTokens,
json: serde_json::value::Value,
args: std::collections::BTreeMap<String, String>,
encrypted: bool,
}
impl PandoraSession {
pub fn new<T: ToEncryptionTokens, E: ToEndpoint>(
client: Option<reqwest::blocking::Client>,
to_encryption_tokens: &T,
to_endpoint: &E,
) -> Self {
Self {
client: client.unwrap_or_else(reqwest::blocking::Client::new),
endpoint_url: to_endpoint.to_endpoint_url(),
tokens: SessionTokens::new(to_encryption_tokens),
json: serde_json::value::Value::Object(serde_json::map::Map::new()),
args: std::collections::BTreeMap::new(),
encrypted: false,
}
}
pub fn copy_session(&self) -> Self {
Self {
client: self.client.clone(),
endpoint_url: self.endpoint_url.clone(),
tokens: self.tokens.clone(),
json: serde_json::value::Value::Object(serde_json::map::Map::new()),
args: std::collections::BTreeMap::new(),
encrypted: false,
}
}
pub fn http_client(&self) -> &reqwest::blocking::Client {
&self.client
}
pub fn endpoint<E: ToEndpoint>(&mut self, to_endpoint: E) -> &mut Self {
self.endpoint_url = to_endpoint.to_endpoint_url();
self
}
pub fn endpoint_mut<E: ToEndpoint>(&mut self) -> &mut url::Url {
&mut self.endpoint_url
}
pub fn update_partner_tokens<T: ToPartnerTokens>(&mut self, to_partner_tokens: &T) {
self.tokens.update_partner_tokens(to_partner_tokens);
}
pub fn update_user_tokens<T: ToUserTokens>(&mut self, to_user_tokens: &T) {
self.tokens.update_user_tokens(to_user_tokens);
}
pub fn session_tokens(&self) -> &SessionTokens {
&self.tokens
}
pub fn session_tokens_mut(&mut self) -> &mut SessionTokens {
&mut self.tokens
}
pub fn json(&mut self, json: serde_json::value::Value) -> &mut Self {
self.json = json;
self
}
pub fn json_mut(&mut self) -> &mut serde_json::value::Value {
&mut self.json
}
pub fn arg(&mut self, key: &str, value: &str) -> &mut Self {
self.args.insert(key.to_string(), value.to_string());
self
}
pub fn encrypted(&mut self) -> &mut Self {
self.encrypted = true;
self
}
fn add_session_tokens_to_args(&mut self) {
if let Some(auth_token) = self
.tokens
.user_token
.clone()
.or_else(|| self.tokens.partner_token.clone())
{
self.arg("auth_token", &auth_token);
}
if let Some(partner_id) = self.tokens.partner_id.clone() {
self.arg("partner_id", &partner_id);
}
if let Some(user_id) = self.tokens.user_id.clone() {
self.arg("user_id", &user_id);
}
}
fn add_session_tokens_to_json(&mut self) {
let json_obj = self
.json
.as_object_mut()
.expect("Programming Error accessing API request json for modification.");
if let Some(partner_auth_token) = self.tokens.partner_token.clone() {
json_obj.insert(
"partnerAuthToken".to_string(),
serde_json::Value::String(partner_auth_token),
);
}
if let Some(user_auth_token) = self.tokens.user_token.clone() {
json_obj.insert(
"userAuthToken".to_string(),
serde_json::Value::String(user_auth_token),
);
}
if let Some(sync_time) = self.tokens.sync_time {
json_obj.insert("syncTime".to_string(), serde_json::Value::from(sync_time));
}
}
pub fn build(&mut self) -> reqwest::blocking::RequestBuilder {
self.add_session_tokens_to_args();
let mut url: url::Url = self.endpoint_url.clone();
url.query_pairs_mut().extend_pairs(&self.args);
self.add_session_tokens_to_json();
let mut body: String = self.json.to_string();
if self.encrypted {
body = self.tokens.encrypt(&body);
}
self.client.post(url).body(body)
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PandoraResponse<T> {
pub stat: PandoraStatus,
pub result: Option<T>,
pub message: Option<String>,
pub code: Option<u32>,
}
impl<T: serde::de::DeserializeOwned> Into<std::result::Result<T, JsonError>>
for PandoraResponse<T>
{
fn into(self) -> std::result::Result<T, JsonError> {
match self {
PandoraResponse {
stat: PandoraStatus::Ok,
result: Some(result),
..
} => Ok(result),
PandoraResponse {
stat: PandoraStatus::Ok,
result: None,
..
} => {
let default_value = serde_json::json!({});
let deser = serde_json::from_value(default_value);
deser.map_err(|_| JsonError::new(None, Some(String::from("Invalid JSON content."))))
}
PandoraResponse { code, message, .. } => Err(JsonError::new(code, message)),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PandoraStatus {
Ok,
Fail,
}
pub trait PandoraApiRequest: serde::ser::Serialize {
type Response: Debug + serde::de::DeserializeOwned;
type Error: Debug + From<serde_json::error::Error> + From<reqwest::Error> + From<JsonError>;
fn get_method(&self) -> String;
fn get_json(&self) -> std::result::Result<serde_json::value::Value, Self::Error> {
serde_json::to_value(self).map_err(Self::Error::from)
}
fn encrypt_request(&self) -> bool {
false
}
fn request(
&self,
session: &mut PandoraSession,
) -> std::result::Result<reqwest::blocking::RequestBuilder, Self::Error> {
let mut tmp_session = session.clone();
tmp_session
.arg("method", &self.get_method())
.json(self.get_json()?);
if self.encrypt_request() {
tmp_session.encrypted();
}
Ok(tmp_session.build())
}
fn response(
&self,
session: &mut PandoraSession,
) -> std::result::Result<Self::Response, Self::Error> {
let response = self.request(session)?.send().map_err(Self::Error::from)?;
response.error_for_status_ref().map_err(Self::Error::from)?;
let response_obj: PandoraResponse<Self::Response> = if cfg!(test) {
let response_body = response.text()?;
if cfg!(test) {
}
serde_json::from_slice(response_body.as_bytes())?
} else {
response.json()?
};
if cfg!(test) {
}
let result: std::result::Result<Self::Response, JsonError> = response_obj.into();
match result {
Err(JsonError {
kind: JsonErrorKind::InvalidAuthToken,
message,
}) => {
session.session_tokens_mut().clear_partner_tokens();
session.session_tokens_mut().clear_user_tokens();
Err(JsonError {
kind: JsonErrorKind::InvalidAuthToken,
message,
})
},
Err(JsonError {
kind: JsonErrorKind::InsufficientConnectivity,
message,
}) => {
session.session_tokens_mut().clear_partner_tokens();
session.session_tokens_mut().clear_user_tokens();
Err(JsonError {
kind: JsonErrorKind::InsufficientConnectivity,
message,
})
}
res => res,
}
.map_err(Self::Error::from)
}
}
pub trait ToEndpoint: serde::ser::Serialize {
fn to_endpoint(&self) -> String;
fn to_endpoint_url(&self) -> url::Url {
url::Url::parse(&self.to_endpoint()).expect("Error parsing Pandora endpoint url.")
}
}
impl ToEndpoint for String {
fn to_endpoint(&self) -> String {
self.clone()
}
}
pub trait ToEncryptionTokens {
fn to_encrypt_key(&self) -> String;
fn encrypt(&self, data: &str) -> String {
crypt::encrypt(&self.to_encrypt_key(), data)
}
fn to_decrypt_key(&self) -> String;
fn decrypt(&self, hex_data: &str) -> Vec<u8> {
crypt::decrypt(&self.to_decrypt_key(), hex_data)
}
}
pub trait ToPartnerTokens {
fn to_partner_id(&self) -> Option<String>;
fn to_partner_token(&self) -> Option<String>;
fn to_sync_time(&self) -> Option<String>;
}
pub trait ToUserTokens {
fn to_user_id(&self) -> Option<String>;
fn to_user_token(&self) -> Option<String>;
}
pub trait ToStationToken: serde::ser::Serialize {
fn to_station_token(&self) -> String;
}
impl ToStationToken for String {
fn to_station_token(&self) -> String {
self.clone()
}
}
impl ToStationToken for &str {
fn to_station_token(&self) -> String {
(*self).to_string()
}
}
pub trait ToTrackingToken: serde::ser::Serialize {
fn to_ad_tracking_tokens(&self) -> String;
}
impl ToTrackingToken for String {
fn to_ad_tracking_tokens(&self) -> String {
self.clone()
}
}
impl ToTrackingToken for &str {
fn to_ad_tracking_tokens(&self) -> String {
(*self).to_string()
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Partner {
pub username: String,
pub password: String,
pub device_model: String,
pub version: String,
#[serde(skip)]
pub encrypt_password: String,
#[serde(skip)]
pub decrypt_password: String,
#[serde(skip)]
pub endpoint_host: String,
}
impl Partner {
pub fn new_android() -> Self {
Self {
username: "android".to_string(),
password: "AC7IBG09A3DTSYM4R41UJWL07VLN8JI7".to_string(),
device_model: "android-generic".to_string(),
version: "5".to_string(),
decrypt_password: "R=U!LH$O2B#".to_string(),
encrypt_password: "6#26FRL$ZWD".to_string(),
endpoint_host: "tuner.pandora.com".to_string(),
}
}
pub fn new_ios() -> Self {
Self {
username: "iphone".to_string(),
password: "P2E4FC0EAD3*878N92B2CDp34I0B1@388137C".to_string(),
device_model: "IP01".to_string(),
version: "5".to_string(),
decrypt_password: "20zE1E47BE57$51".to_string(),
encrypt_password: "721^26xE22776".to_string(),
endpoint_host: "tuner.pandora.com".to_string(),
}
}
pub fn new_palm() -> Self {
Self {
username: "palm".to_string(),
password: "IUC7IBG09A3JTSYM4N11UJWL07VLH8JP0".to_string(),
device_model: "pre".to_string(),
version: "5".to_string(),
decrypt_password: "E#U$MY$O2B=".to_string(),
encrypt_password: "%526CBL$ZU3".to_string(),
endpoint_host: "tuner.pandora.com".to_string(),
}
}
pub fn new_windows_mobile() -> Self {
Self {
username: "winmo".to_string(),
password: "ED227E10a628EB0E8Pm825Dw7114AC39".to_string(),
device_model: "VERIZON_MOTOQ9C".to_string(),
version: "5".to_string(),
decrypt_password: "7D671jt0C5E5d251".to_string(),
encrypt_password: "v93C8C2s12E0EBD".to_string(),
endpoint_host: "tuner.pandora.com".to_string(),
}
}
pub fn new_desktop_air() -> Self {
Self {
username: "pandora one".to_string(),
password: "TVCKIBGS9AO9TSYLNNFUML0743LH82D".to_string(),
device_model: "D01".to_string(),
version: "5".to_string(),
decrypt_password: "U#IO$RZPAB%VX2".to_string(),
encrypt_password: "2%3WCL*JU$MP]4".to_string(),
endpoint_host: "internal-tuner.pandora.com".to_string(),
}
}
pub fn new_vista_widget() -> Self {
Self {
username: "windowsgadget".to_string(),
password: "EVCCIBGS9AOJTSYMNNFUML07VLH8JYP0".to_string(),
device_model: "WG01".to_string(),
version: "5".to_string(),
decrypt_password: "E#IO$MYZOAB%FVR2".to_string(),
encrypt_password: "%22CML*ZU$8YXP[1".to_string(),
endpoint_host: "internal-tuner.pandora.com".to_string(),
}
}
pub fn init_session(&self) -> PandoraSession {
PandoraSession::new(None, self, self)
}
pub fn to_partner_login(&self) -> PartnerLogin {
PartnerLogin {
username: self.username.clone(),
password: self.password.clone(),
device_model: self.device_model.clone(),
version: self.version.clone(),
optional: HashMap::new(),
}
}
pub fn login(&self, session: &mut PandoraSession) -> Result<PartnerLoginResponse, Error> {
let response = self.to_partner_login().response(session)?;
session.update_partner_tokens(&response);
Ok(response)
}
}
impl Default for Partner {
fn default() -> Self {
Self::new_android()
}
}
impl ToEncryptionTokens for Partner {
fn to_encrypt_key(&self) -> String {
self.encrypt_password.clone()
}
fn to_decrypt_key(&self) -> String {
self.decrypt_password.clone()
}
}
impl ToEndpoint for Partner {
fn to_endpoint(&self) -> String {
format!("https://{}/services/json", self.endpoint_host)
}
}
#[derive(Debug, Clone)]
pub struct SessionTokens {
pub encrypt_key: String,
pub decrypt_key: String,
pub partner_id: Option<String>,
pub partner_token: Option<String>,
sync_time: Option<u64>,
local_time_base: Option<std::time::Instant>,
pub user_id: Option<String>,
pub user_token: Option<String>,
}
impl SessionTokens {
pub fn new<T: ToEncryptionTokens>(to_encryption_tokens: &T) -> Self {
Self {
encrypt_key: to_encryption_tokens.to_encrypt_key(),
decrypt_key: to_encryption_tokens.to_decrypt_key(),
partner_id: None,
partner_token: None,
sync_time: None,
local_time_base: None,
user_id: None,
user_token: None,
}
}
pub fn update_partner_tokens<T: ToPartnerTokens>(&mut self, to_partner_tokens: &T) {
self.partner_id = to_partner_tokens.to_partner_id();
self.partner_token = to_partner_tokens.to_partner_token();
if let Some(sync_time) = to_partner_tokens.to_sync_time() {
let sync_time_bytes: Vec<u8> =
self.decrypt(&sync_time).iter().skip(4).cloned().collect();
let sync_time_str = std::str::from_utf8(&sync_time_bytes).unwrap_or("0");
self.set_sync_time(sync_time_str.parse::<u64>().unwrap_or(0));
}
}
pub fn update_user_tokens<T: ToUserTokens>(&mut self, to_user_tokens: &T) {
self.user_id = to_user_tokens.to_user_id();
self.user_token = to_user_tokens.to_user_token();
}
pub fn set_sync_time(&mut self, sync_time: u64) {
self.local_time_base = Some(std::time::Instant::now());
self.sync_time = Some(sync_time);
}
pub fn clear_sync_time(&mut self) {
self.local_time_base = None;
self.sync_time = None;
}
pub fn get_sync_time(&self) -> Option<u64> {
self.sync_time
.and_then(|st| self.local_time_base.map(|ltb| ltb.elapsed().as_secs() + st))
}
pub fn clear_partner_tokens(&mut self) {
self.partner_id = None;
self.partner_token = None;
self.clear_sync_time();
}
pub fn clear_user_tokens(&mut self) {
self.user_id = None;
self.user_token = None;
}
}
impl ToEncryptionTokens for SessionTokens {
fn to_encrypt_key(&self) -> String {
self.encrypt_key.clone()
}
fn to_decrypt_key(&self) -> String {
self.decrypt_key.clone()
}
}
impl<T: ToEncryptionTokens> From<&T> for SessionTokens {
fn from(tokens: &T) -> Self {
Self::new(tokens)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Timestamp {
timezone_offset: u32,
time: i64,
year: u32,
month: u8,
day: u8,
hours: u8,
minutes: u8,
seconds: u8,
date: u8,
}
impl Into<chrono::DateTime<chrono::Utc>> for Timestamp {
fn into(self) -> chrono::DateTime<chrono::Utc> {
let naive_dt = chrono::NaiveDateTime::from_timestamp(self.time, 0);
chrono::DateTime::<chrono::Utc>::from_utc(naive_dt, chrono::Utc)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::errors::Error;
use crate::json::auth::user_login;
pub fn session_login(partner: &Partner) -> Result<PandoraSession, Error> {
let mut session = partner.init_session();
let _partner_login = partner.login(&mut session)?;
let test_username_raw = include_str!("../../test_username.txt");
let test_username = test_username_raw.trim();
let test_password_raw = include_str!("../../test_password.txt");
let test_password = test_password_raw.trim();
let user_login = user_login(&mut session, &test_username, &test_password)?;
session.update_user_tokens(&user_login);
Ok(session)
}
#[test]
fn partner_test() {
let partner = Partner::default();
let mut session = partner.init_session();
let partner_login = partner
.login(&mut session)
.expect("Failed while performing partner login");
session.update_partner_tokens(&partner_login);
}
}