rust_spotify_web_playback_sdk/
lib.rs

1//! # A wrapper around the Spotify web playback SDK for targeting wasm with rust
2//! ## All the methods now are functions
3//! Because you only can have only 1 player per page, so there is no need for an explicit class, rust calls all the methods of the class  through JS
4//! **Use the `init` function first** this function adds the script to the document, and creates an instance of the `Spotify.Player` class, if you don't call this function all the other functions will be useless
5//! ## [Docs](https://there.is.none.right.now)
6//! ## [Repo](https://github.com/KOEGlike/rust_spotify_web_playback_sdk)
7//!
8//! # Example in leptos:
9//! ```rust
10//! use leptos::*;
11//! #[component]
12//! fn Player() -> impl IntoView {
13//!     use leptos::logging::log;
14//!     use rust_spotify_web_playback_sdk::prelude as sp;
15//!
16//!     let (current_song_name, set_current_song_name) = create_signal(String::new());
17//!
18//!     let token = "BQAdHQqBLczVFdCIM58tVbF0eaztF-83cXczNdz2Aua-U7JyOdIlpiG5M7oEww-dK7jo3qjcpMJ4isuyU2RYy3EoD_SWEOX1uW39bpR-KDbjSYeBPb0Jn4QtwXQw2yjQ33oRzVdyRufKF8o7kwXYW-ij6rtio6oDq0PNYIGIyMsDxKhgM5ijt4LXWz-iWQykftBMXdeSWZuU-Z51VyFOPuznUBQj";
19//!
20//!     let connect = create_action(|_| async {
21//!         match sp::connect().await {
22//!             Ok(_) => log!("connected"),
23//!             Err(e) => log!("error {:?}", e),
24//!         };
25//!     });
26//!
27//!     create_effect(move |_| {
28//!         sp::init(
29//!             || {
30//!                 log!("oauth was called");
31//!                 token.to_string()
32//!             },
33//!             move || {
34//!                 log!("ready");
35//!                 connect.dispatch(());
36//!
37//!                 sp::add_listener!("player_state_changed", move |state: sp::StateChange| {
38//!                     log!("state changed, {}", state.track_window.current_track.name);
39//!                     set_current_song_name(state.track_window.current_track.name);
40//!                 });
41//!             },
42//!             "example player",
43//!             1.0,
44//!             false,
45//!         );
46//!     });
47//!
48//!     let get_state = create_action(|_| async {
49//!         let state = sp::get_current_state().await.unwrap();
50//!         log!("{:#?}", state);
51//!     });
52//!
53//!     let activate_player = create_action(|_| async {
54//!        sp::activate_element().await
55//!     });
56//!
57//!    
58//!     view! {
59//!         <h1>"Welcome to Leptos"</h1>
60//!         <button on:click=move |_| activate_player.dispatch(())>
61//!             "activate player"
62//!         </button>
63//!         {
64//!             move || match activate_player.value().get() {
65//!             Some(Ok(_)) => {
66//!                 view! {
67//!                     <button  on:click=move |_| get_state.dispatch(())>
68//!                         "log state in console"
69//!                     </button>
70//!                     <p>"Current song: " {current_song_name}</p>
71//!                 }.into_view()
72//!             }
73//!             Some(Err(e)) => {
74//!                 view! {
75//!                     <p>"Error activating player: " {e}</p>
76//!                 }.into_view()
77//!             }
78//!             None => {
79//!                 view! {
80//!                     <p>"Activating player..."</p>
81//!                 }.into_view()
82//!             }
83//!         }
84//!     }
85//!     
86//!     }
87//! }
88//! ```
89
90use wasm_bindgen::prelude::*;
91use wasm_bindgen_futures::JsFuture;
92
93pub mod js_wrapper;
94pub mod structs;
95pub mod prelude {
96    pub use crate::{
97        js_wrapper::player_ready,
98        structs::{
99            state_change::StateChange,
100            web_playback::{Error, Player, State},
101            Track,
102        },
103        *,
104    };
105    pub mod wasm_bindgen {
106        pub use wasm_bindgen::*;
107    }
108    pub use rust_spotify_web_playback_sdk_macro::*;
109}
110
111///this function adds the script to the document, and creates an instance of the Spotify.Player class, if you don't call this function all the other functions will be useless
112/// # Arguments
113/// * `oauth` - A closure that returns a String containing the Spotify OAuth token.
114/// * `on_ready` - A closure that is called when the Web Playback SDK is ready.
115/// * `name` - A String containing the name of the player.
116/// * `volume` - A Float containing the initial volume of the player.
117/// * `enableMediaSession` - A Boolean indicating whether to enable media session support.
118///
119pub fn init<T, F>(oauth: T, on_ready: F, name: &str, volume: f32, enable_media_session: bool)
120where
121    T: FnMut() -> String + 'static,
122    F: FnMut() + 'static,
123{
124    let oauth = Closure::wrap(Box::new(oauth) as Box<dyn FnMut() -> String>);
125    let on_ready = Closure::wrap(Box::new(on_ready) as Box<dyn FnMut()>);
126    //leak these closures so they don't get cleaned up
127    let oauth = Box::leak(Box::new(oauth)) as &'static Closure<dyn FnMut() -> String>;
128    let on_ready = Box::leak(Box::new(on_ready)) as &'static Closure<dyn FnMut()>;
129    js_wrapper::init(oauth, on_ready, name.into(), volume, enable_media_session);
130}
131
132/// Connect our Web Playback SDK instance to Spotify with the credentials provided during initialization.
133///
134/// # Response
135/// a Promise containing a Boolean (either true or false) with the success of the connection.
136pub async fn connect() -> Result<(), String> {
137    if !js_wrapper::player_ready() {
138        return Err("player not ready".into());
139    }
140    let promise = js_wrapper::connect();
141    let result = match JsFuture::from(promise).await {
142        Ok(e) => e,
143        Err(e) => return Err(format!("{:#?}", e)),
144    };
145    match result.as_bool() {
146        Some(b) => {
147            if b {
148                Ok(())
149            } else {
150                Err("could not connect".into())
151            }
152        }
153        None => Err(format!("not bool, error: {:#?}", result)),
154    }
155}
156
157/// Closes the current session our Web Playback SDK has with Spotify.
158pub fn disconnect() -> Result<(), String> {
159    if !js_wrapper::player_ready() {
160        return Err("player not ready".into());
161    }
162    js_wrapper::disconnect();
163    Ok(())
164}
165
166/// Remove a specific event listener in the Web Playback SDK.
167///
168/// # Response
169/// Returns a Boolean. Returns true if the event name is valid with registered callbacks from #addListener.
170///
171/// # Arguments
172/// * `event` - A valid event name. See Web Playback SDK Events.
173/// * `callback` - The callback function you would like to remove from the listener.
174pub fn remove_specific_listener(
175    event: &str,
176    callback: &Closure<dyn FnMut(JsValue)>,
177) -> Result<bool, JsValue> {
178    if !js_wrapper::player_ready() {
179        return Err("player not ready".into());
180    }
181    Ok(if event_check(event) {
182        js_wrapper::removeSpecificListener(event.to_string(), callback)
183    } else {
184        false
185    })
186}
187
188fn event_check(event: &str) -> bool {
189    matches!(
190        event,
191        "ready"
192            | "not_ready"
193            | "player_state_changed"
194            | "autoplay_failed"
195            | "initialization_error"
196            | "authentication_error"
197            | "account_error"
198            | "playback_error"
199    )
200}
201
202/// Remove an event listener in the Web Playback SDK.
203///
204/// # Response
205/// Returns a Boolean. Returns true if the event name is valid with
206/// registered callbacks from #addListener.
207///
208/// # Arguments
209/// * `event` - A valid event name. See Web Playback SDK Events.
210pub fn remove_listener(event: &str) -> Result<(), String> {
211    if !js_wrapper::player_ready() {
212        return Err("player not ready".into());
213    }
214    if event_check(event) {
215        if js_wrapper::removeListener(event.to_string()) {
216            Ok(())
217        } else {
218            Err("the event name is not valid with registered callbacks from add_listener".into())
219        }
220    } else {
221        Err("event does not exist".into())
222    }
223}
224
225use crate::structs::web_playback::State;
226/// Collect metadata on local playback.
227///
228/// # Response
229/// Returns a Promise. It will return either a WebPlaybackState object or null depending on if the user is successfully connected. Wrapped in result if the future throws an exception
230pub async fn get_current_state() -> Result<Option<State>, String> {
231    if !js_wrapper::player_ready() {
232        return Err("player not ready".into());
233    }
234    let promise = js_wrapper::getCurrentState();
235    let result = match JsFuture::from(promise).await {
236        Ok(e) => e,
237        Err(e) => return Err(format!("{:#?}", e)),
238    };
239    // web_sys::console::log_1(&result);
240    if result.is_null() {
241        return Ok(None);
242    }
243    Ok(Some(structs::from_js(result)))
244}
245
246/// Rename the Spotify Player device. This is visible across all Spotify Connect devices.
247///
248/// # Response
249/// Returns a Promise.
250///
251/// # Arguments
252/// * `name` - The new desired player name.
253pub async fn set_name(name: String) -> Result<(), String> {
254    if !js_wrapper::player_ready() {
255        return Err("player not ready".into());
256    }
257    let promise = js_wrapper::setName(name);
258    match JsFuture::from(promise).await {
259        Ok(_) => Ok(()),
260        Err(e) => Err(format!("{:#?}", e)),
261    }
262}
263
264/// Get the local volume currently set in the Web Playback SDK.
265///
266/// # Response
267/// Returns a Promise containing the local volume (as a Float between 0 and 1).
268pub async fn get_volume() -> Result<f32, String> {
269    if !js_wrapper::player_ready() {
270        return Err("player not ready".into());
271    }
272    let promise = js_wrapper::getVolume();
273    let result = match JsFuture::from(promise).await {
274        Ok(e) => e,
275        Err(e) => return Err(format!("{:#?}", e)),
276    };
277    match serde_wasm_bindgen::from_value(result) {
278        Ok(e) => Ok(e),
279        Err(e) => Err(format!("{:#?}", e)),
280    }
281}
282
283/// Set the local volume for the Web Playback SDK.
284///
285/// # Response
286/// Returns an empty Promise
287///
288/// # Arguments
289/// * `volume` - The new desired volume for local playback. Between 0 and 1. Note: On iOS devices, the audio level is always under the user’s physical control. The volume property is not settable in JavaScript. Reading the volume property always returns 1. More details can be found in the iOS-specific Considerations documentation page by Apple.
290pub async fn set_volume(volume: f32) -> Result<(), String> {
291    if !js_wrapper::player_ready() {
292        return Err("player not ready".into());
293    }
294    let promise = js_wrapper::setVolume(volume);
295    match JsFuture::from(promise).await {
296        Ok(_) => Ok(()),
297        Err(e) => Err(format!("{:#?}", e)),
298    }
299}
300
301/// Pause the local playback.
302///
303/// # Response
304/// Returns an empty Promise
305pub async fn pause() -> Result<(), String> {
306    if !js_wrapper::player_ready() {
307        return Err("player not ready".into());
308    }
309    let promise = js_wrapper::pause();
310    match JsFuture::from(promise).await {
311        Ok(_) => Ok(()),
312        Err(e) => Err(format!("{:#?}", e)),
313    }
314}
315
316/// Resume the local playback.
317///
318/// # Response
319/// Returns an empty Promise
320pub async fn resume() -> Result<(), String> {
321    if !js_wrapper::player_ready() {
322        return Err("player not ready".into());
323    }
324    let promise = js_wrapper::resume();
325    match JsFuture::from(promise).await {
326        Ok(_) => Ok(()),
327        Err(e) => Err(format!("{:#?}", e)),
328    }
329}
330
331/// Resume/pause the local playback.
332///
333/// # Response
334/// Returns an empty Promise
335pub async fn toggle_play() -> Result<(), String> {
336    if !js_wrapper::player_ready() {
337        return Err("player not ready".into());
338    }
339    let promise = js_wrapper::togglePlay();
340    match JsFuture::from(promise).await {
341        Ok(_) => Ok(()),
342        Err(e) => Err(format!("{:#?}", e)),
343    }
344}
345
346/// Seek to a position in the current track in local playback.
347///
348/// # Response
349/// Returns an empty Promise
350///
351/// # Arguments
352/// * `position_ms` - The position in milliseconds to seek to.
353pub async fn seek(position_ms: u32) -> Result<(), String> {
354    if !js_wrapper::player_ready() {
355        return Err("player not ready".into());
356    }
357    let promise = js_wrapper::seek(position_ms);
358    match JsFuture::from(promise).await {
359        Ok(_) => Ok(()),
360        Err(e) => Err(format!("{:#?}", e)),
361    }
362}
363
364/// Switch to the previous track in local playback.
365///
366/// # Response
367/// Returns an empty Promise
368pub async fn previous_track() -> Result<(), String> {
369    if !js_wrapper::player_ready() {
370        return Err("player not ready".into());
371    }
372    let promise = js_wrapper::previousTrack();
373    match JsFuture::from(promise).await {
374        Ok(_) => Ok(()),
375        Err(e) => Err(format!("{:#?}", e)),
376    }
377}
378
379/// Skip to the next track in local playback.
380///
381/// # Response
382/// Returns an empty Promise
383pub async fn next_track() -> Result<(), String> {
384    if !js_wrapper::player_ready() {
385        return Err("player not ready".into());
386    }
387    let promise = js_wrapper::nextTrack();
388    match JsFuture::from(promise).await {
389        Ok(_) => Ok(()),
390        Err(e) => Err(format!("{:#?}", e)),
391    }
392}
393
394/// Some browsers prevent autoplay of media by ensuring that all playback is triggered
395/// by synchronous event-paths originating from user interaction such as a click. In the autoplay
396/// disabled browser, to be able to keep the playing state during transfer from other applications to yours,
397/// this function needs to be called in advance. Otherwise it will be in pause state once it’s transferred.
398///
399/// # Response
400/// Returns an empty Promise
401pub async fn activate_element() -> Result<(), String> {
402    if !js_wrapper::player_ready() {
403        return Err("player not ready".into());
404    }
405    let promise = js_wrapper::activateElement();
406    match JsFuture::from(promise).await {
407        Ok(_) => Ok(()),
408        Err(e) => Err(format!("{:#?}", e)),
409    }
410}