mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2024-11-25 13:01:07 +00:00
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:
parent
13dd1b03c9
commit
9b4942c6dd
6 changed files with 901 additions and 311 deletions
1009
Cargo.lock
generated
1009
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -11,9 +11,9 @@ rust-version.workspace = true
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gst.workspace = true
|
gst.workspace = true
|
||||||
gst-base.workspace = true
|
gst-base.workspace = true
|
||||||
librespot-core = "0.4"
|
librespot-core = "0.5"
|
||||||
librespot-playback = "0.4"
|
librespot-playback = { version = "0.5", features = ['passthrough-decoder'] }
|
||||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
tokio = { version = "1.0", features = ["rt-multi-thread"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
url = "2.3"
|
url = "2.3"
|
||||||
|
|
|
@ -9,23 +9,36 @@ to respect their legal/licensing restrictions.
|
||||||
## Spotify Credentials
|
## Spotify Credentials
|
||||||
|
|
||||||
This plugin requires a [Spotify Premium](https://www.spotify.com/premium/) account.
|
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
|
## 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).
|
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:
|
The element also implements an URI handler which accepts credentials and cache settings as URI parameters:
|
||||||
|
|
||||||
```console
|
```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
|
||||||
```
|
```
|
|
@ -18,8 +18,7 @@ use librespot_core::{
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
username: String,
|
access_token: String,
|
||||||
password: String,
|
|
||||||
cache_credentials: String,
|
cache_credentials: String,
|
||||||
cache_files: String,
|
cache_files: String,
|
||||||
cache_max_size: u64,
|
cache_max_size: u64,
|
||||||
|
@ -28,15 +27,10 @@ pub struct Settings {
|
||||||
|
|
||||||
impl Settings {
|
impl Settings {
|
||||||
pub fn properties() -> Vec<glib::ParamSpec> {
|
pub fn properties() -> Vec<glib::ParamSpec> {
|
||||||
vec![glib::ParamSpecString::builder("username")
|
vec![
|
||||||
.nick("Username")
|
glib::ParamSpecString::builder("access-token")
|
||||||
.blurb("Spotify username, Facebook accounts need a device username from https://www.spotify.com/us/account/set-device-password/")
|
.nick("Access token")
|
||||||
.default_value(Some(""))
|
.blurb("Spotify access token, requires 'streaming' scope")
|
||||||
.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/")
|
|
||||||
.default_value(Some(""))
|
.default_value(Some(""))
|
||||||
.mutable_ready()
|
.mutable_ready()
|
||||||
.build(),
|
.build(),
|
||||||
|
@ -54,7 +48,9 @@ impl Settings {
|
||||||
.build(),
|
.build(),
|
||||||
glib::ParamSpecUInt64::builder("cache-max-size")
|
glib::ParamSpecUInt64::builder("cache-max-size")
|
||||||
.nick("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)
|
.default_value(0)
|
||||||
.mutable_ready()
|
.mutable_ready()
|
||||||
.build(),
|
.build(),
|
||||||
|
@ -69,11 +65,8 @@ impl Settings {
|
||||||
|
|
||||||
pub fn set_property(&mut self, value: &glib::Value, pspec: &glib::ParamSpec) {
|
pub fn set_property(&mut self, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||||
match pspec.name() {
|
match pspec.name() {
|
||||||
"username" => {
|
"access-token" => {
|
||||||
self.username = value.get().expect("type checked upstream");
|
self.access_token = value.get().expect("type checked upstream");
|
||||||
}
|
|
||||||
"password" => {
|
|
||||||
self.password = value.get().expect("type checked upstream");
|
|
||||||
}
|
}
|
||||||
"cache-credentials" => {
|
"cache-credentials" => {
|
||||||
self.cache_credentials = value.get().expect("type checked upstream");
|
self.cache_credentials = value.get().expect("type checked upstream");
|
||||||
|
@ -93,8 +86,7 @@ impl Settings {
|
||||||
|
|
||||||
pub fn property(&self, pspec: &glib::ParamSpec) -> glib::Value {
|
pub fn property(&self, pspec: &glib::ParamSpec) -> glib::Value {
|
||||||
match pspec.name() {
|
match pspec.name() {
|
||||||
"username" => self.username.to_value(),
|
"access-token" => self.access_token.to_value(),
|
||||||
"password" => self.password.to_value(),
|
|
||||||
"cache-credentials" => self.cache_credentials.to_value(),
|
"cache-credentials" => self.cache_credentials.to_value(),
|
||||||
"cache-files" => self.cache_files.to_value(),
|
"cache-files" => self.cache_files.to_value(),
|
||||||
"cache-max-size" => self.cache_max_size.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)?;
|
let cache = Cache::new(credentials_cache, None, files_cache, max_size)?;
|
||||||
|
|
||||||
if let Some(cached_cred) = cache.credentials() {
|
if let Some(cached_cred) = cache.credentials() {
|
||||||
if !self.username.is_empty() && self.username != cached_cred.username {
|
let cached_username = cached_cred
|
||||||
gst::debug!(
|
.username
|
||||||
cat,
|
.as_ref()
|
||||||
obj = &src,
|
.map_or("UNKNOWN", |s| s.as_str());
|
||||||
"ignore cached credentials for user {} which mismatch user {}",
|
|
||||||
cached_cred.username,
|
|
||||||
self.username
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
gst::debug!(
|
gst::debug!(
|
||||||
cat,
|
cat,
|
||||||
obj = &src,
|
obj = &src,
|
||||||
"reuse cached credentials for user {}",
|
"reuse cached credentials for user {}",
|
||||||
cached_cred.username
|
cached_username
|
||||||
);
|
);
|
||||||
if let Ok((session, _credentials)) = Session::connect(
|
|
||||||
SessionConfig::default(),
|
let session = Session::new(SessionConfig::default(), Some(cache));
|
||||||
cached_cred,
|
session.connect(cached_cred, true).await?;
|
||||||
Some(cache.clone()),
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
return Ok(session);
|
return Ok(session);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gst::debug!(
|
gst::debug!(
|
||||||
cat,
|
cat,
|
||||||
|
@ -166,17 +146,14 @@ impl Settings {
|
||||||
"credentials not in cache or cached credentials invalid",
|
"credentials not in cache or cached credentials invalid",
|
||||||
);
|
);
|
||||||
|
|
||||||
if self.username.is_empty() {
|
if self.access_token.is_empty() {
|
||||||
bail!("username is not set and credentials are not in cache");
|
bail!("access-token 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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let cred = Credentials::with_password(&self.username, &self.password);
|
let cred = Credentials::with_access_token(&self.access_token);
|
||||||
|
|
||||||
let (session, _credentials) =
|
let session = Session::new(SessionConfig::default(), Some(cache));
|
||||||
Session::connect(SessionConfig::default(), cred, Some(cache), true).await?;
|
session.connect(cred, true).await?;
|
||||||
|
|
||||||
Ok(session)
|
Ok(session)
|
||||||
}
|
}
|
||||||
|
@ -185,9 +162,7 @@ impl Settings {
|
||||||
if self.track.is_empty() {
|
if self.track.is_empty() {
|
||||||
bail!("track is not set");
|
bail!("track is not set");
|
||||||
}
|
}
|
||||||
let track = SpotifyId::from_uri(&self.track).map_err(|_| {
|
let track = SpotifyId::from_uri(&self.track)?;
|
||||||
anyhow::anyhow!("failed to create Spotify URI from track {}", self.track)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(track)
|
Ok(track)
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ enum Message {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
player: Player,
|
player: Arc<Player>,
|
||||||
|
|
||||||
/// receiver sending buffer to streaming thread
|
/// receiver sending buffer to streaming thread
|
||||||
receiver: mpsc::Receiver<Message>,
|
receiver: mpsc::Receiver<Message>,
|
||||||
|
@ -321,11 +321,10 @@ struct BufferSink {
|
||||||
|
|
||||||
impl Sink for BufferSink {
|
impl Sink for BufferSink {
|
||||||
fn write(&mut self, packet: AudioPacket, _converter: &mut Converter) -> SinkResult<()> {
|
fn write(&mut self, packet: AudioPacket, _converter: &mut Converter) -> SinkResult<()> {
|
||||||
let oggdata = match packet {
|
let buffer = match packet {
|
||||||
AudioPacket::OggData(data) => data,
|
AudioPacket::Samples(_) => unreachable!(),
|
||||||
AudioPacket::Samples(_) => unimplemented!(),
|
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
|
// ignore if sending fails as that means the source element is being shutdown
|
||||||
let _ = self.sender.send(Message::Buffer(buffer));
|
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
|
// allow to configure auth and cache settings from the URI
|
||||||
for (key, value) in url.query_pairs() {
|
for (key, value) in url.query_pairs() {
|
||||||
match key.as_ref() {
|
match key.as_ref() {
|
||||||
"username" | "password" | "cache-credentials" | "cache-files" => {
|
"access-token" | "cache-credentials" | "cache-files" => {
|
||||||
self.obj().set_property(&key, value.as_ref());
|
self.obj().set_property(&key, value.as_ref());
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -435,10 +434,10 @@ impl SpotifyAudioSrc {
|
||||||
let (sender, receiver) = mpsc::sync_channel(2);
|
let (sender, receiver) = mpsc::sync_channel(2);
|
||||||
let sender_clone = sender.clone();
|
let sender_clone = sender.clone();
|
||||||
|
|
||||||
let (mut player, mut player_event_channel) =
|
let player = Player::new(player_config, session, Box::new(NoOpVolume), || {
|
||||||
Player::new(player_config, session, Box::new(NoOpVolume), || {
|
|
||||||
Box::new(BufferSink { sender })
|
Box::new(BufferSink { sender })
|
||||||
});
|
});
|
||||||
|
let mut player_event_channel = player.get_player_event_channel();
|
||||||
|
|
||||||
player.load(track, true, 0);
|
player.load(track, true, 0);
|
||||||
|
|
||||||
|
|
|
@ -12109,6 +12109,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"properties": {
|
"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": {
|
"bitrate": {
|
||||||
"blurb": "Spotify audio bitrate in kbit/s",
|
"blurb": "Spotify audio bitrate in kbit/s",
|
||||||
"conditionally-available": false,
|
"conditionally-available": false,
|
||||||
|
|
Loading…
Reference in a new issue