gstspotify/
common.rs

1// Copyright (C) 2021 Guillaume Desmottes <guillaume@desmottes.be>
2//
3// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
4// If a copy of the MPL was not distributed with this file, You can obtain one at
5// <https://mozilla.org/MPL/2.0/>.
6//
7// SPDX-License-Identifier: MPL-2.0
8
9use anyhow::bail;
10
11use gst::glib;
12use gst::prelude::*;
13
14use futures::future::{AbortHandle, Aborted};
15use librespot_core::{
16    authentication::Credentials, cache::Cache, config::SessionConfig, session::Session, SpotifyUri,
17};
18
19#[derive(Default, Debug, Clone)]
20pub struct Settings {
21    access_token: String,
22    cache_credentials: String,
23    cache_files: String,
24    cache_max_size: u64,
25    pub track: String,
26}
27
28impl Settings {
29    pub fn properties() -> Vec<glib::ParamSpec> {
30        vec![
31            glib::ParamSpecString::builder("access-token")
32                .nick("Access token")
33                .blurb("Spotify access token, requires 'streaming' scope")
34                .default_value(Some(""))
35                .mutable_ready()
36                .build(),
37            glib::ParamSpecString::builder("cache-credentials")
38                .nick("Credentials cache")
39                .blurb("Directory where to cache Spotify credentials")
40                .default_value(Some(""))
41                .mutable_ready()
42                .build(),
43            glib::ParamSpecString::builder("cache-files")
44                .nick("Files cache")
45                .blurb("Directory where to cache downloaded files from Spotify")
46                .default_value(Some(""))
47                .mutable_ready()
48                .build(),
49            glib::ParamSpecUInt64::builder("cache-max-size")
50                .nick("Cache max size")
51                .blurb(
52                    "The max allowed size of the cache, in bytes, or 0 to disable the cache limit",
53                )
54                .default_value(0)
55                .mutable_ready()
56                .build(),
57            glib::ParamSpecString::builder("track")
58                .nick("Spotify URI")
59                .blurb("Spotify track URI, in the form 'spotify:track:$SPOTIFY_ID'")
60                .default_value(Some(""))
61                .mutable_ready()
62                .build(),
63        ]
64    }
65
66    pub fn set_property(&mut self, value: &glib::Value, pspec: &glib::ParamSpec) {
67        match pspec.name() {
68            "access-token" => {
69                self.access_token = value.get().expect("type checked upstream");
70            }
71            "cache-credentials" => {
72                self.cache_credentials = value.get().expect("type checked upstream");
73            }
74            "cache-files" => {
75                self.cache_files = value.get().expect("type checked upstream");
76            }
77            "cache-max-size" => {
78                self.cache_max_size = value.get().expect("type checked upstream");
79            }
80            "track" => {
81                self.track = value.get().expect("type checked upstream");
82            }
83            _ => unimplemented!(),
84        }
85    }
86
87    pub fn property(&self, pspec: &glib::ParamSpec) -> glib::Value {
88        match pspec.name() {
89            "access-token" => self.access_token.to_value(),
90            "cache-credentials" => self.cache_credentials.to_value(),
91            "cache-files" => self.cache_files.to_value(),
92            "cache-max-size" => self.cache_max_size.to_value(),
93            "track" => self.track.to_value(),
94            _ => unimplemented!(),
95        }
96    }
97
98    pub async fn connect_session<T>(
99        &self,
100        src: T,
101        cat: &gst::DebugCategory,
102    ) -> anyhow::Result<Session>
103    where
104        T: IsA<glib::Object>,
105    {
106        let credentials_cache = if self.cache_credentials.is_empty() {
107            None
108        } else {
109            Some(&self.cache_credentials)
110        };
111
112        let files_cache = if self.cache_files.is_empty() {
113            None
114        } else {
115            Some(&self.cache_files)
116        };
117
118        let max_size = if self.cache_max_size != 0 {
119            Some(self.cache_max_size)
120        } else {
121            None
122        };
123
124        let cache = Cache::new(credentials_cache, None, files_cache, max_size)?;
125
126        if let Some(cached_cred) = cache.credentials() {
127            let cached_username = cached_cred
128                .username
129                .as_ref()
130                .map_or("UNKNOWN", |s| s.as_str());
131            gst::debug!(
132                cat,
133                obj = &src,
134                "reuse cached credentials for user {}",
135                cached_username
136            );
137
138            let session = Session::new(SessionConfig::default(), Some(cache));
139            session.connect(cached_cred, true).await?;
140            return Ok(session);
141        }
142
143        gst::debug!(
144            cat,
145            obj = &src,
146            "credentials not in cache or cached credentials invalid",
147        );
148
149        if self.access_token.is_empty() {
150            bail!("access-token is not set and credentials are not in cache");
151        }
152
153        let cred = Credentials::with_access_token(&self.access_token);
154
155        let session = Session::new(SessionConfig::default(), Some(cache));
156        session.connect(cred, true).await?;
157
158        Ok(session)
159    }
160
161    pub fn track_uri(&self) -> anyhow::Result<SpotifyUri> {
162        if self.track.is_empty() {
163            bail!("track is not set");
164        }
165        let track = SpotifyUri::from_uri(&self.track)?;
166
167        Ok(track)
168    }
169}
170
171#[derive(Default)]
172pub enum SetupThread {
173    #[default]
174    None,
175    Pending {
176        thread_handle: Option<std::thread::JoinHandle<Result<anyhow::Result<()>, Aborted>>>,
177        abort_handle: AbortHandle,
178    },
179    Cancelled,
180    Done,
181}
182
183impl SetupThread {
184    pub fn abort(&mut self) {
185        // Cancel setup thread if it is pending and not done yet
186        if matches!(self, SetupThread::None | SetupThread::Done) {
187            return;
188        }
189
190        if let SetupThread::Pending {
191            ref abort_handle, ..
192        } = *self
193        {
194            abort_handle.abort();
195        }
196        *self = SetupThread::Cancelled;
197    }
198}