spotify_info/
lib.rs

1//! Gets metadata from spotify using a
2//! [spicetify](https://github.com/khanhas/spicetify-cli)
3//! extension using websockets
4//!
5//! More information can be found on https://github.com/Ricky12Awesome/spotify_info
6
7use std::fmt::{Display, Formatter};
8use std::io::ErrorKind;
9use std::net::SocketAddr;
10use std::time::Duration;
11
12use futures_util::{SinkExt, StreamExt};
13use tokio::net::{TcpListener, TcpStream};
14use tokio_tungstenite::{accept_async, WebSocketStream};
15use tokio_tungstenite::tungstenite::{Error, Message};
16
17/// The state of the track weather it's **Playing**, **Paused** or **Stopped**
18///
19/// Default: Stopped
20#[repr(u32)]
21#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
22pub enum TrackState {
23  Playing = 2,
24  Paused = 1,
25  Stopped = 0,
26}
27
28impl Display for TrackState {
29  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
30    match self {
31      TrackState::Playing => write!(f, "Playing"),
32      TrackState::Paused => write!(f, "Paused"),
33      TrackState::Stopped => write!(f, "Stopped"),
34    }
35  }
36}
37
38impl TrackState {
39  /// 2 will be [Self::Playing]
40  ///
41  /// 1 will be [Self::Paused]
42  ///
43  /// anything else will be [Self::Stopped]
44  pub fn from_u32(n: u32) -> Self {
45    match n {
46      2 => Self::Playing,
47      1 => Self::Paused,
48      _ => Self::Stopped
49    }
50  }
51}
52
53impl Default for TrackState {
54  fn default() -> Self {
55    Self::Stopped
56  }
57}
58
59/// Stores information about the track
60#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd)]
61pub struct TrackInfo {
62  /// UID of track
63  pub uid: String,
64  // URI of track
65  pub uri: String,
66  /// State of the track
67  pub state: TrackState,
68  /// Duration of the track
69  pub duration: Duration,
70  /// Title of the track
71  pub title: String,
72  /// Album of the track
73  pub album: String,
74  /// Vec since there can be multiple artists
75  pub artist: Vec<String>,
76  /// Cover art of the track, option because it may not exist
77  pub cover_url: Option<String>,
78  /// Background art of the track, option because it may nto exist
79  /// (when you hit the "full screen" thing in the bottom-right corner of spotify)
80  pub background_url: Option<String>,
81}
82
83impl TrackInfo {
84  pub fn eq_ignore_state(&self, other: &Self) -> bool {
85    self.uid == other.uid
86  }
87}
88
89#[derive(Debug)]
90pub enum SpotifyEvent {
91  /// Gets called when user changes track
92  TrackChanged(TrackInfo),
93  /// Gets called when user changes state (if song is playing, paused or stopped)
94  ///
95  /// **NOTE**: Doesn't get called when user changes track
96  StateChanged(TrackState),
97  /// Gets called on a set interval, wont get called if player is paused or stopped,
98  /// Value is a percentage of the position between 0 and 1
99  ///
100  /// **NOTE**: Doesn't get called when user changes track
101  ProgressChanged(f64),
102}
103
104pub struct SpotifyListener {
105  pub listener: TcpListener,
106}
107
108#[derive(Debug)]
109pub struct SpotifyConnection {
110  pub ws: WebSocketStream<TcpStream>,
111}
112
113impl SpotifyConnection {
114  fn parse_track_info(data: &[&str]) -> TrackInfo {
115    TrackInfo {
116      uid: data[0].to_string(),
117      uri: data[1].to_string(),
118      state: TrackState::from_u32(data[2].parse().unwrap_or(0)),
119      duration: Duration::from_millis(data[3].parse().unwrap_or(0)),
120      title: data[4].to_string().replace("${#{#{SEMI_COLON}#}#}$", ";"),
121      album: data[5].to_string().replace("${#{#{SEMI_COLON}#}#}$", ";"),
122      artist: vec![data[6].to_string().replace("${#{#{SEMI_COLON}#}#}$", ";")],
123      cover_url: Some(data[7].to_string()).filter(|it| !it.contains("NONE")),
124      background_url: Some(data[8].to_string()).filter(|it| !it.contains("NONE")),
125    }
126  }
127
128  fn handle_message(message: String) -> Option<Result<SpotifyEvent, Error>> {
129    let mut data = message.split(';').collect::<Vec<_>>();
130    let invalid_data_err = Some(Err(Error::Io(std::io::Error::new(ErrorKind::InvalidData, "Invalid data"))));
131
132    if data.is_empty() {
133      return invalid_data_err;
134    }
135
136    match data.remove(0) {
137      "TRACK_CHANGED" if data.len() >= 9 => {
138        let info = Self::parse_track_info(&data);
139
140        Some(Ok(SpotifyEvent::TrackChanged(info)))
141      }
142      "STATE_CHANGED" if !data.is_empty() => {
143        let state = TrackState::from_u32(data[0].parse().unwrap_or(0));
144
145        Some(Ok(SpotifyEvent::StateChanged(state)))
146      }
147      "PROGRESS_CHANGED" if !data.is_empty() => {
148        let progress = data[0].parse().unwrap_or(0f64);
149
150        Some(Ok(SpotifyEvent::ProgressChanged(progress)))
151      }
152      _ => invalid_data_err
153    }
154  }
155
156  /// Sets how often it should update the progress,
157  ///
158  /// by default it's set to 1 second
159  pub async fn set_progress_interval(&mut self, interval: Duration) -> Result<(), Error> {
160    let ms = interval.as_millis();
161    let text = format!("SET_PROGRESS_INTERVAL;{}", ms);
162
163    self.ws.send(Message::Text(text)).await
164  }
165
166  /// Waits for the next message to be received
167  pub async fn next(&mut self) -> Option<Result<SpotifyEvent, Error>> {
168    let message = self.ws.next().await?;
169
170    match message {
171      Ok(Message::Text(message)) => Self::handle_message(message),
172      Ok(_) => Some(Err(Error::Io(std::io::Error::new(ErrorKind::Unsupported, "Unsupported message type, only supports Text")))),
173      Err(err) => Some(Err(err))
174    }
175  }
176}
177
178impl SpotifyListener {
179  /// Binds to 127.0.0.1:19532
180  pub async fn bind_default() -> std::io::Result<Self> {
181    Self::bind_local(19532).await
182  }
183
184  /// Binds to 127.0.0.1 with a custom port
185  pub async fn bind_local(port: u16) -> std::io::Result<Self> {
186    Self::bind(format!("127.0.0.1:{}", port).parse().unwrap()).await
187  }
188
189  /// Binds to the given address, same as calling [TcpListener::bind(addr)]
190  pub async fn bind(addr: SocketAddr) -> std::io::Result<Self> {
191    let listener = TcpListener::bind(addr).await?;
192
193    Ok(Self { listener })
194  }
195
196  /// Establishes a websocket connection to the spotify extension
197  pub async fn get_connection(&self) -> Result<SpotifyConnection, Error> {
198    let (stream, _) = self.listener.accept().await.map_err(|_| Error::ConnectionClosed)?;
199    let ws = accept_async(stream).await?;
200
201    Ok(SpotifyConnection { ws })
202  }
203}