use std::collections::HashMap;
use std::convert::TryFrom;
use pandora_api_derive::PandoraJsonRequest;
use serde::{Deserialize, Serialize};
use crate::errors::Error;
use crate::json::{PandoraJsonApiRequest, PandoraSession, Timestamp};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum UserGender {
Male,
Female,
}
impl std::fmt::Display for UserGender {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UserGender::Male => write!(f, "Male"),
UserGender::Female => write!(f, "Female"),
}
}
}
impl TryFrom<&str> for UserGender {
type Error = Error;
fn try_from(fmt: &str) -> std::result::Result<Self, Self::Error> {
match fmt {
"Male" => Ok(UserGender::Male),
"Female" => Ok(UserGender::Female),
x => Err(Self::Error::InvalidUserGender(x.to_string())),
}
}
}
impl TryFrom<String> for UserGender {
type Error = Error;
fn try_from(fmt: String) -> std::result::Result<Self, Self::Error> {
Self::try_from(fmt.as_str())
}
}
pub struct AccountMessageDismissedUnsupported {}
pub struct AcknowledgeSubscriptionExpirationUnsupported {}
pub struct AssociateDeviceUnsupported {}
pub struct AuthorizeFacebookUnsupported {}
#[derive(Debug, Clone, Default, Serialize, PandoraJsonRequest)]
#[pandora_request(encrypted = true)]
#[serde(rename_all = "camelCase")]
pub struct CanSubscribe {
#[serde(flatten)]
pub optional: HashMap<String, serde_json::value::Value>,
}
impl CanSubscribe {
pub fn new() -> Self {
Self::default()
}
pub fn and_string_option(mut self, option: &str, value: &str) -> Self {
self.optional
.insert(option.to_string(), serde_json::value::Value::from(value));
self
}
pub fn iap_vendor(self, value: &str) -> Self {
self.and_string_option("iapVendor", value)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CanSubscribeResponse {
pub can_subscribe: bool,
pub is_subscriber: bool,
}
pub async fn can_subscribe(session: &mut PandoraSession) -> Result<CanSubscribeResponse, Error> {
CanSubscribe::new().response(session).await
}
#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
#[pandora_request(encrypted = true)]
#[serde(rename_all = "camelCase")]
pub struct ChangeSettings {
pub current_username: String,
pub current_password: String,
#[serde(flatten)]
pub optional: HashMap<String, serde_json::value::Value>,
}
impl ChangeSettings {
pub fn new(current_username: &str, current_password: &str) -> Self {
Self {
current_username: current_username.to_string(),
current_password: current_password.to_string(),
optional: HashMap::new(),
}
}
pub fn and_boolean_option(mut self, option: &str, value: bool) -> Self {
self.optional
.insert(option.to_string(), serde_json::value::Value::from(value));
self
}
pub fn and_string_option(mut self, option: &str, value: &str) -> Self {
self.optional
.insert(option.to_string(), serde_json::value::Value::from(value));
self
}
pub fn and_number_option(mut self, option: &str, value: u32) -> Self {
self.optional
.insert(option.to_string(), serde_json::value::Value::from(value));
self
}
pub fn user_initiated_change(self, value: bool) -> Self {
self.and_boolean_option("userInitiatedChange", value)
}
pub fn include_facebook(self, value: bool) -> Self {
self.and_boolean_option("includeFacebook", value)
}
pub fn gender(self, value: UserGender) -> Self {
self.and_string_option("gender", &value.to_string())
}
pub fn birth_year(self, value: u32) -> Self {
self.and_number_option("birthYear", value)
}
pub fn zip_code(self, value: &str) -> Self {
self.and_string_option("zipCode", value)
}
pub fn is_profile_private(self, value: bool) -> Self {
self.and_boolean_option("isProfilePrivate", value)
}
pub fn enable_comments(self, value: bool) -> Self {
self.and_boolean_option("enableComments", value)
}
pub fn email_opt_in(self, value: bool) -> Self {
self.and_boolean_option("emailOptIn", value)
}
pub fn email_comments(self, value: bool) -> Self {
self.and_boolean_option("emailComments", value)
}
pub fn email_new_followers(self, value: bool) -> Self {
self.and_boolean_option("emailNewFollowers", value)
}
pub fn is_explicit_content_filter_enabled(self, value: bool) -> Self {
self.and_boolean_option("isExplicitContentFilterEnabled", value)
}
pub fn is_explicit_content_filter_pin_protected(self, value: bool) -> Self {
self.and_boolean_option("isExplicitContentFilterPINProtected", value)
}
pub fn new_username(self, value: &str) -> Self {
self.and_string_option("newUsername", value)
}
pub fn new_password(self, value: &str) -> Self {
self.and_string_option("newPassword", value)
}
pub fn facebook_auto_share_enabled(self, value: bool) -> Self {
self.and_boolean_option("facebookAutoShareEnabled", value)
}
pub fn auto_share_track_play(self, value: bool) -> Self {
self.and_boolean_option("autoShareTrackPlay", value)
}
pub fn auto_share_track_likes(self, value: bool) -> Self {
self.and_boolean_option("autoShareTrackLikes", value)
}
pub fn auto_share_follows(self, value: bool) -> Self {
self.and_boolean_option("autoShareFollows", value)
}
pub fn facebook_setting_checksum(self, value: bool) -> Self {
self.and_boolean_option("facebookSettingChecksum", value)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChangeSettingsResponse {
#[serde(flatten)]
pub optional: HashMap<String, serde_json::value::Value>,
}
pub async fn change_settings(
session: &mut PandoraSession,
username: &str,
password: &str,
) -> Result<ChangeSettingsResponse, Error> {
ChangeSettings::new(username, password)
.response(session)
.await
}
#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
#[pandora_request(encrypted = true)]
#[serde(rename_all = "camelCase")]
pub struct CreateUser {
pub username: String,
pub password: String,
pub gender: UserGender,
pub birth_year: u32,
pub zip_code: String,
pub email_opt_in: bool,
pub country_code: String,
pub account_type: String,
pub registered_type: String,
pub include_pandora_one_info: bool,
pub include_account_message: bool,
pub return_collect_track_lifetime_stats: bool,
pub return_is_subscriber: bool,
pub xplatform_ad_capable: bool,
pub include_facebook: bool,
pub include_googleplay: bool,
pub include_show_user_recommendations: bool,
pub include_advertiser_attributes: bool,
}
impl CreateUser {
pub fn new(
username: &str,
password: &str,
gender: UserGender,
birth_year: u32,
zip_code: &str,
country_code: &str,
) -> Self {
Self {
username: username.to_string(),
password: password.to_string(),
gender,
birth_year,
zip_code: zip_code.to_string(),
country_code: country_code.to_string(),
email_opt_in: false,
account_type: "registered".to_string(),
registered_type: "user".to_string(),
include_pandora_one_info: false,
include_account_message: false,
return_collect_track_lifetime_stats: false,
return_is_subscriber: false,
xplatform_ad_capable: false,
include_facebook: false,
include_googleplay: false,
include_show_user_recommendations: false,
include_advertiser_attributes: false,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateUserResponse {
#[serde(flatten)]
pub optional: HashMap<String, serde_json::value::Value>,
}
pub async fn create_user(
session: &mut PandoraSession,
username: &str,
password: &str,
gender: UserGender,
birth_year: u32,
zip_code: &str,
country_code: &str,
) -> Result<CreateUserResponse, Error> {
CreateUser::new(
username,
password,
gender,
birth_year,
zip_code,
country_code,
)
.response(session)
.await
}
pub struct DisconnectFacebookUnsupported {}
#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
#[serde(rename_all = "camelCase")]
pub struct EmailPassword {
pub username: String,
}
impl<TS: ToString> From<&TS> for EmailPassword {
fn from(username: &TS) -> Self {
Self {
username: username.to_string(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EmailPasswordResponse {
#[serde(flatten)]
pub optional: HashMap<String, serde_json::value::Value>,
}
pub async fn email_password(
session: &mut PandoraSession,
username: &str,
) -> Result<EmailPasswordResponse, Error> {
EmailPassword::from(&username).response(session).await
}
pub struct FacebookAuthFailedUnsupported {}
#[derive(Debug, Clone, Default, Serialize, PandoraJsonRequest)]
#[pandora_request(encrypted = true)]
#[serde(rename_all = "camelCase")]
pub struct GetBookmarks {}
impl GetBookmarks {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetBookmarksResponse {
pub artists: Vec<ArtistBookmark>,
pub songs: Vec<SongBookmark>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtistBookmark {
pub bookmark_token: String,
pub music_token: String,
pub artist_name: String,
pub art_url: String,
pub date_created: Timestamp,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SongBookmark {
pub bookmark_token: String,
pub music_token: String,
pub song_name: String,
pub artist_name: String,
pub album_name: String,
pub art_url: String,
pub sample_url: String,
pub sample_gain: String,
pub date_created: Timestamp,
}
pub async fn get_bookmarks(session: &mut PandoraSession) -> Result<GetBookmarksResponse, Error> {
GetBookmarks::new().response(session).await
}
pub struct GetFacebookInfoUnsupported {}
#[derive(Debug, Clone, Default, Serialize, PandoraJsonRequest)]
#[pandora_request(encrypted = true)]
#[serde(rename_all = "camelCase")]
pub struct GetSettings {
#[serde(flatten)]
pub optional: HashMap<String, serde_json::value::Value>,
}
impl GetSettings {
pub fn new() -> Self {
Self::default()
}
pub fn and_boolean_option(mut self, option: &str, value: bool) -> Self {
self.optional
.insert(option.to_string(), serde_json::value::Value::from(value));
self
}
pub fn include_facebook(self, value: bool) -> Self {
self.and_boolean_option("includeFacebook", value)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetSettingsResponse {
#[serde(flatten)]
pub optional: HashMap<String, serde_json::value::Value>,
}
pub async fn get_settings(session: &mut PandoraSession) -> Result<GetSettingsResponse, Error> {
GetSettings::new()
.include_facebook(false)
.response(session)
.await
}
#[derive(Debug, Clone, Default, Serialize, PandoraJsonRequest)]
#[serde(rename_all = "camelCase")]
pub struct GetStationListChecksum {}
impl GetStationListChecksum {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetStationListChecksumResponse {
pub checksum: String,
}
#[derive(Debug, Clone, Default, Serialize, PandoraJsonRequest)]
#[pandora_request(encrypted = true)]
#[serde(rename_all = "camelCase")]
pub struct GetStationList {
#[serde(flatten)]
pub optional: HashMap<String, serde_json::value::Value>,
}
impl GetStationList {
pub fn new() -> Self {
Self::default()
}
pub fn and_boolean_option(mut self, option: &str, value: bool) -> Self {
self.optional
.insert(option.to_string(), serde_json::value::Value::from(value));
self
}
pub fn and_string_option(mut self, option: &str, value: &str) -> Self {
self.optional
.insert(option.to_string(), serde_json::value::Value::from(value));
self
}
pub fn include_station_art_url(self, value: bool) -> Self {
self.and_boolean_option("includeStationArtUrl", value)
}
pub fn station_art_size(self, value: &str) -> Self {
self.and_string_option("stationArtSize", value)
}
pub fn include_ad_attributes(self, value: bool) -> Self {
self.and_boolean_option("includeAdAttributes", value)
}
pub fn include_station_seeds(self, value: bool) -> Self {
self.and_boolean_option("includeStationSeeds", value)
}
pub fn include_shuffle_instead_of_quick_mix(self, value: bool) -> Self {
self.and_boolean_option("includeShuffleInsteadOfQuickMix", value)
}
pub fn include_recommendations(self, value: bool) -> Self {
self.and_boolean_option("includeRecommendations", value)
}
pub fn include_explanations(self, value: bool) -> Self {
self.and_boolean_option("includeExplanations", value)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetStationListResponse {
pub stations: Vec<Station>,
pub checksum: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Station {
pub station_id: String,
pub station_token: String,
pub station_name: String,
pub station_detail_url: String,
#[serde(default)]
pub quick_mix_station_ids: Vec<String>,
pub is_quick_mix: bool,
pub suppress_video_ads: bool,
pub is_shared: bool,
pub requires_clean_ads: bool,
pub allow_rename: bool,
pub allow_add_music: bool,
pub allow_delete: bool,
pub allow_edit_description: bool,
pub date_created: Timestamp,
#[serde(flatten)]
pub optional: HashMap<String, serde_json::value::Value>,
}
pub async fn get_station_list(
session: &mut PandoraSession,
) -> Result<GetStationListResponse, Error> {
GetStationList::new()
.include_station_art_url(false)
.include_ad_attributes(false)
.include_station_seeds(false)
.include_shuffle_instead_of_quick_mix(false)
.include_recommendations(false)
.include_explanations(false)
.response(session)
.await
}
#[derive(Debug, Clone, Default, Serialize, PandoraJsonRequest)]
#[pandora_request(encrypted = true)]
#[serde(rename_all = "camelCase")]
pub struct GetUsageInfo {}
impl GetUsageInfo {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetUsageInfoResponse {
pub account_monthly_listening: u32,
pub device_monthly_listening: u32,
pub monthly_cap_hours: u32,
pub monthly_cap_warning_percent: u32,
pub monthly_cap_warning_repeat_percent: u32,
pub is_monthly_payer: bool,
pub is_capped: bool,
pub listening_timestamp: Option<u32>,
}
pub async fn get_usage_info(session: &mut PandoraSession) -> Result<GetUsageInfoResponse, Error> {
GetUsageInfo {}.response(session).await
}
pub struct PurchaseAmazonPayToPlayUnsupported {}
pub struct PurchaseAmazonSubscriptionUnsupported {}
pub struct PurchaseGooglePayToPlayUnsupported {}
pub struct PurchaseGoogleSubscriptionUnsupported {}
pub struct PurchaseItunesSubscriptionUnsupported {}
pub struct SetAwareOfProfileUnsupported {}
pub struct SetExplicitContentFilterUnsupported {}
#[derive(Debug, Clone, Default, Serialize, PandoraJsonRequest)]
#[serde(rename_all = "camelCase")]
pub struct SetQuickMix {
pub quick_mix_station_ids: Vec<String>,
}
impl SetQuickMix {
pub fn new() -> Self {
Self::default()
}
pub fn add_station(&mut self, station_id: &str) {
self.quick_mix_station_ids.push(station_id.to_string());
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetQuickMixResponse {
#[serde(flatten)]
pub optional: HashMap<String, serde_json::value::Value>,
}
#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
#[serde(rename_all = "camelCase")]
pub struct SleepSong {
pub track_token: String,
}
impl<TS: ToString> From<&TS> for SleepSong {
fn from(track_token: &TS) -> Self {
Self {
track_token: track_token.to_string(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SleepSongResponse {
#[serde(flatten)]
pub optional: HashMap<String, serde_json::value::Value>,
}
#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
#[serde(rename_all = "camelCase")]
pub struct StartComplimentaryTrial {
pub complimentary_sponsor: String,
}
impl<TS: ToString> From<&TS> for StartComplimentaryTrial {
fn from(complimentary_sponsor: &TS) -> Self {
Self {
complimentary_sponsor: complimentary_sponsor.to_string(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StartComplimentaryTrialResponse {
#[serde(flatten)]
pub optional: HashMap<String, serde_json::value::Value>,
}
#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
#[pandora_request(encrypted = true)]
#[serde(rename_all = "camelCase")]
pub struct ValidateUsername {
pub username: String,
}
impl<TS: ToString> From<&TS> for ValidateUsername {
fn from(username: &TS) -> Self {
Self {
username: username.to_string(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ValidateUsernameResponse {
pub is_valid: bool,
pub is_unique: Option<bool>,
}
pub async fn validate_username(
session: &mut PandoraSession,
username: &str,
) -> Result<ValidateUsernameResponse, Error> {
ValidateUsername {
username: username.to_string(),
}
.response(session)
.await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::errors;
use crate::json::{errors::JsonErrorKind, tests::session_login, Partner};
#[tokio::test]
async fn user_test() {
let partner = Partner::default();
let mut session = session_login(&partner)
.await
.expect("Failed initializing login session");
let _can_subscribe = can_subscribe(&mut session)
.await
.expect("Failed submitting subscription information request");
let _get_settings = get_settings(&mut session)
.await
.expect("Failed submitting settings info request");
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 _change_settings = change_settings(&mut session, &test_username, &test_password)
.await
.expect("Failed submitting settings change request");
}
#[tokio::test]
#[should_panic(expected = "Invalid country code.")]
async fn create_user_test() {
let partner = Partner::default();
let mut session = partner.init_session();
let partner_login = partner
.login(&mut session)
.await
.expect("Failed completing partner login");
session.update_partner_tokens(&partner_login);
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 test_gender = UserGender::Male;
let test_birth = 1970u32;
let test_zip = "90210";
let test_cc = "US";
match create_user(
&mut session,
&test_username,
&test_password,
test_gender,
test_birth,
test_zip,
test_cc,
)
.await
{
Ok(cu) => log::debug!("User successfully created? {:?}", cu),
Err(errors::Error::PandoraJsonRequestError(e))
if e.kind() == JsonErrorKind::InvalidCountryCode =>
{
panic!("Invalid country code.")
}
Err(e) => panic!("Unexpected request error: {:?}", e),
}
}
}