Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ https://github.com/librespot-org/librespot
- [core] Cache resolved access points during runtime (breaking)
- [core] `FileId` is moved out of `SpotifyId`. For now it will be re-exported.
- [core] Report actual platform data on login
- [core] Support `Session` authentication with a Spotify access token
- [core] `Credentials.username` is now an `Option` (breaking)
- [main] `autoplay {on|off}` now acts as an override. If unspecified, `librespot`
now follows the setting in the Connect client that controls it. (breaking)
- [metadata] Most metadata is now retrieved with the `spclient` (breaking)
Expand Down Expand Up @@ -95,6 +97,7 @@ https://github.com/librespot-org/librespot
- [main] Add an event worker thread that runs async to the main thread(s) but
sync to itself to prevent potential data races for event consumers
- [metadata] All metadata fields in the protobufs are now exposed (breaking)
- [oauth] Standalone module to obtain Spotify access token using OAuth authorization code flow.
- [playback] Explicit tracks are skipped if the controlling Connect client has
disabled such content. Applications that use librespot as a library without
Connect should use the 'filter-explicit-content' user attribute in the session.
Expand Down
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ version = "0.5.0-dev"
path = "protocol"
version = "0.5.0-dev"

[dependencies.librespot-oauth]
path = "oauth"
version = "0.5.0-dev"

[dependencies]
data-encoding = "2.5"
env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] }
Expand Down
4 changes: 4 additions & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ license = "MIT"
repository = "https://github.com/librespot-org/librespot"
edition = "2021"

[dependencies.librespot-oauth]
path = "../oauth"
version = "0.5.0-dev"

[dependencies.librespot-protocol]
path = "../protocol"
version = "0.5.0-dev"
Expand Down
22 changes: 15 additions & 7 deletions core/src/authentication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ impl From<AuthenticationError> for Error {
/// The credentials are used to log into the Spotify API.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct Credentials {
pub username: String,
pub username: Option<String>,

#[serde(serialize_with = "serialize_protobuf_enum")]
#[serde(deserialize_with = "deserialize_protobuf_enum")]
Expand All @@ -50,19 +50,27 @@ impl Credentials {
///
/// let creds = Credentials::with_password("my account", "my password");
/// ```
pub fn with_password(username: impl Into<String>, password: impl Into<String>) -> Credentials {
Credentials {
username: username.into(),
pub fn with_password(username: impl Into<String>, password: impl Into<String>) -> Self {
Self {
username: Some(username.into()),
auth_type: AuthenticationType::AUTHENTICATION_USER_PASS,
auth_data: password.into().into_bytes(),
}
}

pub fn with_access_token(token: impl Into<String>) -> Self {
Self {
username: None,
auth_type: AuthenticationType::AUTHENTICATION_SPOTIFY_TOKEN,
auth_data: token.into().into_bytes(),
}
}

pub fn with_blob(
username: impl Into<String>,
encrypted_blob: impl AsRef<[u8]>,
device_id: impl AsRef<[u8]>,
) -> Result<Credentials, Error> {
) -> Result<Self, Error> {
fn read_u8<R: Read>(stream: &mut R) -> io::Result<u8> {
let mut data = [0u8];
stream.read_exact(&mut data)?;
Expand Down Expand Up @@ -136,8 +144,8 @@ impl Credentials {
read_u8(&mut cursor)?;
let auth_data = read_bytes(&mut cursor)?;

Ok(Credentials {
username,
Ok(Self {
username: Some(username),
auth_type,
auth_data,
})
Expand Down
13 changes: 8 additions & 5 deletions core/src/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,12 @@ pub async fn authenticate(
};

let mut packet = ClientResponseEncrypted::new();
packet
.login_credentials
.mut_or_insert_default()
.set_username(credentials.username);
if let Some(username) = credentials.username {
packet
.login_credentials
.mut_or_insert_default()
.set_username(username);
}
packet
.login_credentials
.mut_or_insert_default()
Expand Down Expand Up @@ -133,6 +135,7 @@ pub async fn authenticate(
let cmd = PacketType::Login;
let data = packet.write_to_bytes()?;

debug!("Authenticating with AP using {:?}", credentials.auth_type);
transport.send((cmd as u8, data)).await?;
let (cmd, data) = transport
.next()
Expand All @@ -144,7 +147,7 @@ pub async fn authenticate(
let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?;

let reusable_credentials = Credentials {
username: welcome_data.canonical_username().to_owned(),
username: Some(welcome_data.canonical_username().to_owned()),
auth_type: welcome_data.reusable_auth_credentials_type(),
auth_data: welcome_data.reusable_auth_credentials().to_owned(),
};
Expand Down
21 changes: 21 additions & 0 deletions core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ use tokio::sync::{
};
use url::ParseError;

use librespot_oauth::OAuthError;

#[cfg(feature = "with-dns-sd")]
use dns_sd::DNSError;

Expand Down Expand Up @@ -287,6 +289,25 @@ impl fmt::Display for Error {
}
}

impl From<OAuthError> for Error {
fn from(err: OAuthError) -> Self {
use OAuthError::*;
match err {
AuthCodeBadUri { .. }
| AuthCodeNotFound { .. }
| AuthCodeListenerRead
| AuthCodeListenerParse => Error::unavailable(err),
AuthCodeStdinRead
| AuthCodeListenerBind { .. }
| AuthCodeListenerTerminated
| AuthCodeListenerWrite
| Recv
| ExchangeCode { .. } => Error::internal(err),
_ => Error::failed_precondition(err),
}
}
}

impl From<DecodeError> for Error {
fn from(err: DecodeError) -> Self {
Self::new(ErrorKind::FailedPrecondition, err)
Expand Down
76 changes: 61 additions & 15 deletions core/src/session.rs
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use byteorder::{BigEndian, ByteOrder};
use bytes::Bytes;
use futures_core::TryStream;
use futures_util::{future, ready, StreamExt, TryStreamExt};
use librespot_protocol::authentication::AuthenticationType;
use num_traits::FromPrimitive;
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
Expand All @@ -22,13 +23,13 @@ use tokio::{sync::mpsc, time::Instant};
use tokio_stream::wrappers::UnboundedReceiverStream;

use crate::{
apresolve::ApResolver,
apresolve::{ApResolver, SocketAddress},
audio_key::AudioKeyManager,
authentication::Credentials,
cache::Cache,
channel::ChannelManager,
config::SessionConfig,
connection::{self, AuthenticationError},
connection::{self, AuthenticationError, Transport},
http_client::HttpClient,
mercury::MercuryManager,
packet::PacketType,
Expand Down Expand Up @@ -77,6 +78,7 @@ struct SessionData {
client_brand_name: String,
client_model_name: String,
connection_id: String,
auth_data: Vec<u8>,
time_delta: i64,
invalid: bool,
user_data: UserData,
Expand Down Expand Up @@ -140,6 +142,46 @@ impl Session {
}))
}

async fn connect_inner(
&self,
access_point: SocketAddress,
credentials: Credentials,
) -> Result<(Credentials, Transport), Error> {
let mut transport = connection::connect(
&access_point.0,
access_point.1,
self.config().proxy.as_ref(),
)
.await?;
let mut reusable_credentials = connection::authenticate(
&mut transport,
credentials.clone(),
&self.config().device_id,
)
.await?;

// Might be able to remove this once keymaster is replaced with login5.
if credentials.auth_type == AuthenticationType::AUTHENTICATION_SPOTIFY_TOKEN {
trace!(
"Reconnect using stored credentials as token authed sessions cannot use keymaster."
);
transport = connection::connect(
&access_point.0,
access_point.1,
self.config().proxy.as_ref(),
)
.await?;
reusable_credentials = connection::authenticate(
&mut transport,
reusable_credentials.clone(),
&self.config().device_id,
)
.await?;
}

Ok((reusable_credentials, transport))
}

pub async fn connect(
&self,
credentials: Credentials,
Expand All @@ -148,17 +190,8 @@ impl Session {
let (reusable_credentials, transport) = loop {
let ap = self.apresolver().resolve("accesspoint").await?;
info!("Connecting to AP \"{}:{}\"", ap.0, ap.1);
let mut transport =
connection::connect(&ap.0, ap.1, self.config().proxy.as_ref()).await?;

match connection::authenticate(
&mut transport,
credentials.clone(),
&self.config().device_id,
)
.await
{
Ok(creds) => break (creds, transport),
match self.connect_inner(ap, credentials.clone()).await {
Ok(ct) => break ct,
Err(e) => {
if let Some(AuthenticationError::LoginFailed(ErrorCode::TryAnotherAP)) =
e.error.downcast_ref::<AuthenticationError>()
Expand All @@ -172,8 +205,13 @@ impl Session {
}
};

info!("Authenticated as \"{}\" !", reusable_credentials.username);
self.set_username(&reusable_credentials.username);
let username = reusable_credentials
.username
.as_ref()
.map_or("UNKNOWN", |s| s.as_str());
info!("Authenticated as '{username}' !");
self.set_username(username);
self.set_auth_data(&reusable_credentials.auth_data);
if let Some(cache) = self.cache() {
if store_credentials {
let cred_changed = cache
Expand Down Expand Up @@ -471,6 +509,14 @@ impl Session {
username.clone_into(&mut self.0.data.write().user_data.canonical_username);
}

pub fn auth_data(&self) -> Vec<u8> {
self.0.data.read().auth_data.clone()
}

pub fn set_auth_data(&self, auth_data: &[u8]) {
self.0.data.write().auth_data = auth_data.to_owned();
}

pub fn country(&self) -> String {
self.0.data.read().user_data.country.clone()
}
Expand Down
37 changes: 24 additions & 13 deletions examples/get_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,34 @@ const SCOPES: &str =

#[tokio::main]
async fn main() {
let session_config = SessionConfig::default();
let mut builder = env_logger::Builder::new();
builder.parse_filters("librespot=trace");
builder.init();

let mut session_config = SessionConfig::default();

let args: Vec<_> = env::args().collect();
if args.len() != 3 {
eprintln!("Usage: {} USERNAME PASSWORD", args[0]);
if args.len() == 3 {
// Only special client IDs have sufficient privileges e.g. Spotify's.
session_config.client_id = args[2].clone()
} else if args.len() != 2 {
eprintln!("Usage: {} ACCESS_TOKEN [CLIENT_ID]", args[0]);
return;
}
let access_token = &args[1];

println!("Connecting...");
let credentials = Credentials::with_password(&args[1], &args[2]);
let session = Session::new(session_config, None);

// Now create a new session with that token.
let session = Session::new(session_config.clone(), None);
let credentials = Credentials::with_access_token(access_token);
println!("Connecting with token..");
match session.connect(credentials, false).await {
Ok(()) => println!(
"Token: {:#?}",
session.token_provider().get_token(SCOPES).await.unwrap()
),
Err(e) => println!("Error connecting: {}", e),
}
Ok(()) => println!("Session username: {:#?}", session.username()),
Err(e) => {
println!("Error connecting: {e}");
return;
}
};

let token = session.token_provider().get_token(SCOPES).await.unwrap();
println!("Got me a token: {token:#?}");
}
8 changes: 4 additions & 4 deletions examples/play.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ async fn main() {
let audio_format = AudioFormat::default();

let args: Vec<_> = env::args().collect();
if args.len() != 4 {
eprintln!("Usage: {} USERNAME PASSWORD TRACK", args[0]);
if args.len() != 3 {
eprintln!("Usage: {} ACCESS_TOKEN TRACK", args[0]);
return;
}
let credentials = Credentials::with_password(&args[1], &args[2]);
let credentials = Credentials::with_access_token(&args[1]);

let mut track = SpotifyId::from_base62(&args[3]).unwrap();
let mut track = SpotifyId::from_base62(&args[2]).unwrap();
track.item_type = SpotifyItemType::Track;

let backend = audio_backend::find(None).unwrap();
Expand Down
8 changes: 4 additions & 4 deletions examples/play_connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,16 @@ async fn main() {
let connect_config = ConnectConfig::default();

let mut args: Vec<_> = env::args().collect();
let context_uri = if args.len() == 4 {
let context_uri = if args.len() == 3 {
args.pop().unwrap()
} else if args.len() == 3 {
} else if args.len() == 2 {
String::from("spotify:album:79dL7FLiJFOO0EoehUHQBv")
} else {
eprintln!("Usage: {} USERNAME PASSWORD (ALBUM URI)", args[0]);
eprintln!("Usage: {} ACCESS_TOKEN (ALBUM URI)", args[0]);
return;
};

let credentials = Credentials::with_password(&args[1], &args[2]);
let credentials = Credentials::with_access_token(&args[1]);
let backend = audio_backend::find(None).unwrap();

println!("Connecting...");
Expand Down
8 changes: 4 additions & 4 deletions examples/playlist_tracks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ async fn main() {
let session_config = SessionConfig::default();

let args: Vec<_> = env::args().collect();
if args.len() != 4 {
eprintln!("Usage: {} USERNAME PASSWORD PLAYLIST", args[0]);
if args.len() != 3 {
eprintln!("Usage: {} ACCESS_TOKEN PLAYLIST", args[0]);
return;
}
let credentials = Credentials::with_password(&args[1], &args[2]);
let credentials = Credentials::with_access_token(&args[1]);

let plist_uri = SpotifyId::from_uri(&args[3]).unwrap_or_else(|_| {
let plist_uri = SpotifyId::from_uri(&args[2]).unwrap_or_else(|_| {
eprintln!(
"PLAYLIST should be a playlist URI such as: \
\"spotify:playlist:37i9dQZF1DXec50AjHrNTq\""
Expand Down
Loading