mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2024-11-22 11:30:59 +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]
|
||||
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"
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -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,52 +27,46 @@ 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/")
|
||||
.default_value(Some(""))
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecString::builder("cache-credentials")
|
||||
.nick("Credentials cache")
|
||||
.blurb("Directory where to cache Spotify credentials")
|
||||
.default_value(Some(""))
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecString::builder("cache-files")
|
||||
.nick("Files cache")
|
||||
.blurb("Directory where to cache downloaded files from Spotify")
|
||||
.default_value(Some(""))
|
||||
.mutable_ready()
|
||||
.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")
|
||||
.default_value(0)
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecString::builder("track")
|
||||
.nick("Spotify URI")
|
||||
.blurb("Spotify track URI, in the form 'spotify:track:$SPOTIFY_ID'")
|
||||
.default_value(Some(""))
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
]
|
||||
vec![
|
||||
glib::ParamSpecString::builder("access-token")
|
||||
.nick("Access token")
|
||||
.blurb("Spotify access token, requires 'streaming' scope")
|
||||
.default_value(Some(""))
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecString::builder("cache-credentials")
|
||||
.nick("Credentials cache")
|
||||
.blurb("Directory where to cache Spotify credentials")
|
||||
.default_value(Some(""))
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecString::builder("cache-files")
|
||||
.nick("Files cache")
|
||||
.blurb("Directory where to cache downloaded files from Spotify")
|
||||
.default_value(Some(""))
|
||||
.mutable_ready()
|
||||
.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",
|
||||
)
|
||||
.default_value(0)
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecString::builder("track")
|
||||
.nick("Spotify URI")
|
||||
.blurb("Spotify track URI, in the form 'spotify:track:$SPOTIFY_ID'")
|
||||
.default_value(Some(""))
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
]
|
||||
}
|
||||
|
||||
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,32 +124,20 @@ 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 {
|
||||
gst::debug!(
|
||||
cat,
|
||||
obj = &src,
|
||||
"reuse cached credentials for user {}",
|
||||
cached_cred.username
|
||||
);
|
||||
if let Ok((session, _credentials)) = Session::connect(
|
||||
SessionConfig::default(),
|
||||
cached_cred,
|
||||
Some(cache.clone()),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Ok(session);
|
||||
}
|
||||
}
|
||||
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_username
|
||||
);
|
||||
|
||||
let session = Session::new(SessionConfig::default(), Some(cache));
|
||||
session.connect(cached_cred, true).await?;
|
||||
return Ok(session);
|
||||
}
|
||||
|
||||
gst::debug!(
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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), || {
|
||||
Box::new(BufferSink { sender })
|
||||
});
|
||||
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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue