diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index bc596bfc..7b9b0857 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -237,8 +237,9 @@ impl Spirc { let ident = session.device_id().to_owned(); // Uri updated in response to issue #288 - debug!("canonical_username: {}", &session.username()); - let uri = format!("hm://remote/user/{}/", url_encode(&session.username())); + let canonical_username = &session.username(); + debug!("canonical_username: {}", canonical_username); + let uri = format!("hm://remote/user/{}/", url_encode(canonical_username)); let subscription = Box::pin( session @@ -631,11 +632,11 @@ impl SpircTask { fn handle_user_attributes_mutation(&mut self, mutation: UserAttributesMutation) { for attribute in mutation.get_fields().iter() { let key = attribute.get_name(); - if let Some(old_value) = self.session.user_attribute(key) { + if let Some(old_value) = self.session.user_data().attributes.get(key) { let new_value = match old_value.as_ref() { "0" => "1", "1" => "0", - _ => &old_value, + _ => old_value, }; self.session.set_user_attribute(key, new_value); trace!( diff --git a/core/src/session.rs b/core/src/session.rs index c1193dc3..926c4bc1 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::future::Future; use std::io; use std::pin::Pin; +use std::process::exit; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, RwLock, Weak}; use std::task::Context; @@ -42,12 +43,18 @@ pub enum SessionError { pub type UserAttributes = HashMap; +#[derive(Debug, Clone, Default)] +pub struct UserData { + pub country: String, + pub canonical_username: String, + pub attributes: UserAttributes, +} + +#[derive(Debug, Clone, Default)] struct SessionData { - country: String, time_delta: i64, - canonical_username: String, invalid: bool, - user_attributes: UserAttributes, + user_data: UserData, } struct SessionInternal { @@ -89,13 +96,7 @@ impl Session { let session = Session(Arc::new(SessionInternal { config, - data: RwLock::new(SessionData { - country: String::new(), - canonical_username: String::new(), - invalid: false, - time_delta: 0, - user_attributes: HashMap::new(), - }), + data: RwLock::new(SessionData::default()), http_client, tx_connection: sender_tx, cache: cache.map(Arc::new), @@ -118,7 +119,8 @@ impl Session { connection::authenticate(&mut transport, credentials, &session.config().device_id) .await?; info!("Authenticated as \"{}\" !", reusable_credentials.username); - session.0.data.write().unwrap().canonical_username = reusable_credentials.username.clone(); + session.0.data.write().unwrap().user_data.canonical_username = + reusable_credentials.username.clone(); if let Some(cache) = session.cache() { cache.save_credentials(&reusable_credentials); } @@ -199,6 +201,18 @@ impl Session { ); } + fn check_catalogue(attributes: &UserAttributes) { + if let Some(account_type) = attributes.get("type") { + if account_type != "premium" { + error!("librespot does not support {:?} accounts.", account_type); + info!("Please support Spotify and your artists and sign up for a premium account."); + + // TODO: logout instead of exiting + exit(1); + } + } + } + fn dispatch(&self, cmd: u8, data: Bytes) { use PacketType::*; let packet_type = FromPrimitive::from_u8(cmd); @@ -219,7 +233,7 @@ impl Session { Some(CountryCode) => { let country = String::from_utf8(data.as_ref().to_owned()).unwrap(); info!("Country: {:?}", country); - self.0.data.write().unwrap().country = country; + self.0.data.write().unwrap().user_data.country = country; } Some(StreamChunkRes) | Some(ChannelError) => { self.channel().dispatch(packet_type.unwrap(), data); @@ -266,7 +280,9 @@ impl Session { } trace!("Received product info: {:?}", user_attributes); - self.0.data.write().unwrap().user_attributes = user_attributes; + Self::check_catalogue(&user_attributes); + + self.0.data.write().unwrap().user_data.attributes = user_attributes; } Some(PongAck) | Some(SecretBlock) @@ -295,47 +311,47 @@ impl Session { &self.0.config } - pub fn username(&self) -> String { - self.0.data.read().unwrap().canonical_username.clone() - } - - pub fn country(&self) -> String { - self.0.data.read().unwrap().country.clone() + pub fn user_data(&self) -> UserData { + self.0.data.read().unwrap().user_data.clone() } pub fn device_id(&self) -> &str { &self.config().device_id } - pub fn user_attribute(&self, key: &str) -> Option { + pub fn username(&self) -> String { self.0 .data .read() .unwrap() - .user_attributes - .get(key) - .map(|value| value.to_owned()) - } - - pub fn user_attributes(&self) -> UserAttributes { - self.0.data.read().unwrap().user_attributes.clone() + .user_data + .canonical_username + .clone() } pub fn set_user_attribute(&self, key: &str, value: &str) -> Option { + let mut dummy_attributes = UserAttributes::new(); + dummy_attributes.insert(key.to_owned(), value.to_owned()); + Self::check_catalogue(&dummy_attributes); + self.0 .data .write() .unwrap() - .user_attributes + .user_data + .attributes .insert(key.to_owned(), value.to_owned()) } pub fn set_user_attributes(&self, attributes: UserAttributes) { + Self::check_catalogue(&attributes); + self.0 .data .write() .unwrap() - .user_attributes + .user_data + .attributes .extend(attributes) } diff --git a/metadata/src/audio/item.rs b/metadata/src/audio/item.rs index 09b72ebc..50aa2bf9 100644 --- a/metadata/src/audio/item.rs +++ b/metadata/src/audio/item.rs @@ -12,7 +12,7 @@ use crate::{ use super::file::AudioFiles; -use librespot_core::session::Session; +use librespot_core::session::{Session, UserData}; use librespot_core::spotify_id::{SpotifyId, SpotifyItemType}; pub type AudioItemResult = Result; @@ -43,18 +43,27 @@ impl AudioItem { pub trait InnerAudioItem { async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult; - fn allowed_in_country(restrictions: &Restrictions, country: &str) -> AudioItemAvailability { + fn allowed_for_user( + user_data: &UserData, + restrictions: &Restrictions, + ) -> AudioItemAvailability { + let country = &user_data.country; + let user_catalogue = match user_data.attributes.get("catalogue") { + Some(catalogue) => catalogue, + None => "premium", + }; + for premium_restriction in restrictions.iter().filter(|restriction| { restriction .catalogue_strs .iter() - .any(|catalogue| *catalogue == "premium") + .any(|restricted_catalogue| restricted_catalogue == user_catalogue) }) { if let Some(allowed_countries) = &premium_restriction.countries_allowed { // A restriction will specify either a whitelast *or* a blacklist, // but not both. So restrict availability if there is a whitelist // and the country isn't on it. - if allowed_countries.iter().any(|allowed| country == *allowed) { + if allowed_countries.iter().any(|allowed| country == allowed) { return Ok(()); } else { return Err(UnavailabilityReason::NotWhitelisted); @@ -64,7 +73,7 @@ pub trait InnerAudioItem { if let Some(forbidden_countries) = &premium_restriction.countries_forbidden { if forbidden_countries .iter() - .any(|forbidden| country == *forbidden) + .any(|forbidden| country == forbidden) { return Err(UnavailabilityReason::Blacklisted); } else { @@ -92,13 +101,13 @@ pub trait InnerAudioItem { Ok(()) } - fn available_in_country( + fn available_for_user( + user_data: &UserData, availability: &Availabilities, restrictions: &Restrictions, - country: &str, ) -> AudioItemAvailability { Self::available(availability)?; - Self::allowed_in_country(restrictions, country)?; + Self::allowed_for_user(user_data, restrictions)?; Ok(()) } } diff --git a/metadata/src/availability.rs b/metadata/src/availability.rs index c40427cb..eb2b5fdd 100644 --- a/metadata/src/availability.rs +++ b/metadata/src/availability.rs @@ -33,6 +33,8 @@ pub enum UnavailabilityReason { Blacklisted, #[error("available date is in the future")] Embargo, + #[error("required data was not present")] + NoData, #[error("whitelist present and country not on it")] NotWhitelisted, } diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index 30c89f19..7032999b 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -67,10 +67,10 @@ impl Deref for Episodes { impl InnerAudioItem for Episode { async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult { let episode = Self::get(session, id).await?; - let availability = Self::available_in_country( + let availability = Self::available_for_user( + &session.user_data(), &episode.availability, &episode.restrictions, - &session.country(), ); Ok(AudioItem { diff --git a/metadata/src/track.rs b/metadata/src/track.rs index 8e7f6702..d0639c82 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -81,7 +81,11 @@ impl InnerAudioItem for Track { let availability = if Local::now() < track.earliest_live_timestamp.as_utc() { Err(UnavailabilityReason::Embargo) } else { - Self::available_in_country(&track.availability, &track.restrictions, &session.country()) + Self::available_for_user( + &session.user_data(), + &track.availability, + &track.restrictions, + ) }; Ok(AudioItem {