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}