spotify: replace username/password auth with access token.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1801>
This commit is contained in:
Guillaume Desmottes 2021-12-15 17:15:20 +01:00 committed by GStreamer Marge Bot
parent 13dd1b03c9
commit 9b4942c6dd
6 changed files with 901 additions and 311 deletions

1009
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -11,9 +11,9 @@ rust-version.workspace = true
[dependencies]
gst.workspace = true
gst-base.workspace = true
librespot-core = "0.4"
librespot-playback = "0.4"
tokio = { version = "1", features = ["rt-multi-thread"] }
librespot-core = "0.5"
librespot-playback = { version = "0.5", features = ['passthrough-decoder'] }
tokio = { version = "1.0", features = ["rt-multi-thread"] }
futures = "0.3"
anyhow = "1.0"
url = "2.3"

View file

@ -9,23 +9,36 @@ to respect their legal/licensing restrictions.
## Spotify Credentials
This plugin requires a [Spotify Premium](https://www.spotify.com/premium/) account.
If your account is linked with Facebook, you'll need to setup
a [device username and password](https://www.spotify.com/us/account/set-device-password/).
Those username and password are then set using the `username` and `password` properties.
Provide a Spotify access token with 'streaming' scope using the `access-token` property. Such a token can be obtained by completing
[Spotify's OAuth flow](https://developer.spotify.com/documentation/web-api/concepts/authorization) or using the facility on their
[Web SDK getting started guide](https://developer.spotify.com/documentation/web-playback-sdk/tutorials/getting-started).
A token can also be obtained using [librespot-oauth](https://github.com/librespot-org/librespot/blob/dev/oauth/examples/oauth.rs):
You may also want to cache credentials and downloaded files, see the `cache-` properties on the element.
```console
cargo install librespot-oauth --example oauth && oauth
```
Note, Spotify access tokens are only valid for 1 hour and must be [refreshed](https://developer.spotify.com/documentation/web-api/tutorials/refreshing-tokens)
for usage beyond that.
It is therefore advisable to also use the `cache-credentials` property. On first usage, your access token is exchanged for a reusable credentials blob and
stored at the location specified by this property. Once obtained, that credentials blob is used for login and any provided `access-token` is ignored.
Unlike Spotify access tokens, the user's credentials blob does not expire. Avoiding handling token refresh greatly simplifies plugin usage.
If you do not set `cache-credentials`, you must manage refreshing your Spotify access token so it's valid for login when the element starts.
You may also want to cache downloaded files, see the `cache-files` property.
## spotifyaudiosrc
The `spotifyaudiosrc` element can be used to play a song from Spotify using its [Spotify URI](https://community.spotify.com/t5/FAQs/What-s-a-Spotify-URI/ta-p/919201).
```
gst-launch-1.0 spotifyaudiosrc username=$USERNAME password=$PASSWORD track=spotify:track:3i3P1mGpV9eRlfKccjDjwi ! oggdemux ! vorbisdec ! audioconvert ! autoaudiosink
gst-launch-1.0 spotifyaudiosrc access-token=$ACCESS_TOKEN track=spotify:track:3i3P1mGpV9eRlfKccjDjwi ! oggdemux ! vorbisdec ! audioconvert ! autoaudiosink
```
The element also implements an URI handler which accepts credentials and cache settings as URI parameters:
```console
gst-launch-1.0 playbin3 uri=spotify:track:3i3P1mGpV9eRlfKccjDjwi?username=$USERNAME\&password=$PASSWORD\&cache-credentials=cache\&cache-files=cache
gst-launch-1.0 playbin3 uri=spotify:track:3i3P1mGpV9eRlfKccjDjwi?access-token=$ACCESS_TOKEN\&cache-credentials=cache\&cache-files=cache
```

View file

@ -18,8 +18,7 @@ use librespot_core::{
#[derive(Default, Debug, Clone)]
pub struct Settings {
username: String,
password: String,
access_token: String,
cache_credentials: String,
cache_files: String,
cache_max_size: u64,
@ -28,15 +27,10 @@ pub struct Settings {
impl Settings {
pub fn properties() -> Vec<glib::ParamSpec> {
vec![glib::ParamSpecString::builder("username")
.nick("Username")
.blurb("Spotify username, Facebook accounts need a device username from https://www.spotify.com/us/account/set-device-password/")
.default_value(Some(""))
.mutable_ready()
.build(),
glib::ParamSpecString::builder("password")
.nick("Password")
.blurb("Spotify password, Facebook accounts need a device password from https://www.spotify.com/us/account/set-device-password/")
vec![
glib::ParamSpecString::builder("access-token")
.nick("Access token")
.blurb("Spotify access token, requires 'streaming' scope")
.default_value(Some(""))
.mutable_ready()
.build(),
@ -54,7 +48,9 @@ impl Settings {
.build(),
glib::ParamSpecUInt64::builder("cache-max-size")
.nick("Cache max size")
.blurb("The max allowed size of the cache, in bytes, or 0 to disable the cache limit")
.blurb(
"The max allowed size of the cache, in bytes, or 0 to disable the cache limit",
)
.default_value(0)
.mutable_ready()
.build(),
@ -69,11 +65,8 @@ impl Settings {
pub fn set_property(&mut self, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"username" => {
self.username = value.get().expect("type checked upstream");
}
"password" => {
self.password = value.get().expect("type checked upstream");
"access-token" => {
self.access_token = value.get().expect("type checked upstream");
}
"cache-credentials" => {
self.cache_credentials = value.get().expect("type checked upstream");
@ -93,8 +86,7 @@ impl Settings {
pub fn property(&self, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"username" => self.username.to_value(),
"password" => self.password.to_value(),
"access-token" => self.access_token.to_value(),
"cache-credentials" => self.cache_credentials.to_value(),
"cache-files" => self.cache_files.to_value(),
"cache-max-size" => self.cache_max_size.to_value(),
@ -132,33 +124,21 @@ impl Settings {
let cache = Cache::new(credentials_cache, None, files_cache, max_size)?;
if let Some(cached_cred) = cache.credentials() {
if !self.username.is_empty() && self.username != cached_cred.username {
gst::debug!(
cat,
obj = &src,
"ignore cached credentials for user {} which mismatch user {}",
cached_cred.username,
self.username
);
} else {
let cached_username = cached_cred
.username
.as_ref()
.map_or("UNKNOWN", |s| s.as_str());
gst::debug!(
cat,
obj = &src,
"reuse cached credentials for user {}",
cached_cred.username
cached_username
);
if let Ok((session, _credentials)) = Session::connect(
SessionConfig::default(),
cached_cred,
Some(cache.clone()),
true,
)
.await
{
let session = Session::new(SessionConfig::default(), Some(cache));
session.connect(cached_cred, true).await?;
return Ok(session);
}
}
}
gst::debug!(
cat,
@ -166,17 +146,14 @@ impl Settings {
"credentials not in cache or cached credentials invalid",
);
if self.username.is_empty() {
bail!("username is not set and credentials are not in cache");
}
if self.password.is_empty() {
bail!("password is not set and credentials are not in cache");
if self.access_token.is_empty() {
bail!("access-token is not set and credentials are not in cache");
}
let cred = Credentials::with_password(&self.username, &self.password);
let cred = Credentials::with_access_token(&self.access_token);
let (session, _credentials) =
Session::connect(SessionConfig::default(), cred, Some(cache), true).await?;
let session = Session::new(SessionConfig::default(), Some(cache));
session.connect(cred, true).await?;
Ok(session)
}
@ -185,9 +162,7 @@ impl Settings {
if self.track.is_empty() {
bail!("track is not set");
}
let track = SpotifyId::from_uri(&self.track).map_err(|_| {
anyhow::anyhow!("failed to create Spotify URI from track {}", self.track)
})?;
let track = SpotifyId::from_uri(&self.track)?;
Ok(track)
}

View file

@ -52,7 +52,7 @@ enum Message {
}
struct State {
player: Player,
player: Arc<Player>,
/// receiver sending buffer to streaming thread
receiver: mpsc::Receiver<Message>,
@ -321,11 +321,10 @@ struct BufferSink {
impl Sink for BufferSink {
fn write(&mut self, packet: AudioPacket, _converter: &mut Converter) -> SinkResult<()> {
let oggdata = match packet {
AudioPacket::OggData(data) => data,
AudioPacket::Samples(_) => unimplemented!(),
let buffer = match packet {
AudioPacket::Samples(_) => unreachable!(),
AudioPacket::Raw(ogg) => gst::Buffer::from_slice(ogg),
};
let buffer = gst::Buffer::from_slice(oggdata);
// ignore if sending fails as that means the source element is being shutdown
let _ = self.sender.send(Message::Buffer(buffer));
@ -360,7 +359,7 @@ impl URIHandlerImpl for SpotifyAudioSrc {
// allow to configure auth and cache settings from the URI
for (key, value) in url.query_pairs() {
match key.as_ref() {
"username" | "password" | "cache-credentials" | "cache-files" => {
"access-token" | "cache-credentials" | "cache-files" => {
self.obj().set_property(&key, value.as_ref());
}
_ => {
@ -435,10 +434,10 @@ impl SpotifyAudioSrc {
let (sender, receiver) = mpsc::sync_channel(2);
let sender_clone = sender.clone();
let (mut player, mut player_event_channel) =
Player::new(player_config, session, Box::new(NoOpVolume), || {
let player = Player::new(player_config, session, Box::new(NoOpVolume), || {
Box::new(BufferSink { sender })
});
let mut player_event_channel = player.get_player_event_channel();
player.load(track, true, 0);

View file

@ -12109,6 +12109,18 @@
}
},
"properties": {
"access-token": {
"blurb": "Spotify access token, requires 'streaming' scope",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "",
"mutable": "ready",
"readable": true,
"type": "gchararray",
"writable": true
},
"bitrate": {
"blurb": "Spotify audio bitrate in kbit/s",
"conditionally-available": false,