spotify/
connector.rs

1use json::{self, JsonValue};
2use reqwest::header::{ORIGIN, REFERER, USER_AGENT};
3use reqwest::{self, Client};
4use std::io::Read;
5use std::net::TcpListener;
6use std::sync::Mutex;
7
8// Headers
9const HEADER_UA: &str = "Mozilla/5.0 (Windows; rv:50.0) Gecko/20100101 Firefox/50.0";
10const HEADER_ORIGIN_SCHEME: &str = "https";
11const HEADER_ORIGIN_HOST: &str = "embed.spotify.com";
12
13// Spotify base URLs
14const URL_EMBED: &str = "https://embed.spotify.com";
15const URL_TOKEN: &str = "https://open.spotify.com/token";
16const URL_LOCAL: &str = "http://spotifyrs.spotilocal.com";
17
18// Spotify local ports
19const PORT_START: u16 = 4370;
20const PORT_END: u16 = 4399;
21
22// Spotify request end-points
23const REQUEST_CSRF: &str = "simplecsrf/token.json";
24const REQUEST_STATUS: &str = "remote/status.json";
25const REQUEST_PLAY: &str = "remote/play.json";
26const REQUEST_OPEN: &str = "remote/open.json";
27const REQUEST_PAUSE: &str = "remote/pause.json";
28
29// The referal track
30const REFERAL_TRACK: &str = "track/4uLU6hMCjMI75M1A2tKUQC";
31
32/// The `Result` type used in this module.
33type Result<T> = ::std::result::Result<T, InternalSpotifyError>;
34
35/// The `InternalSpotifyError` enum.
36#[derive(Debug)]
37pub enum InternalSpotifyError {
38    // Reqwest
39    ReqwestError(reqwest::Error),
40    // JSON
41    JSONParseError(json::Error),
42    // OAUth
43    InvalidOAuthToken,
44    // CSRF
45    InvalidCSRFToken,
46    // Other
47    IOError(::std::io::Error),
48}
49
50/// The `SpotifyConnector` struct.
51pub struct SpotifyConnector {
52    /// The Reqwest client.
53    client: Mutex<Client>,
54    /// The Spotify OAuth token.
55    oauth_token: String,
56    /// The Spotify CSRF token.
57    csrf_token: String,
58    /// The port used to connect to Spotify.
59    port: i32,
60}
61
62/// Implements `SpotifyConnector`.
63impl SpotifyConnector {
64    /// Constructs a new `SpotifyConnector`.
65    /// Retrieves the OAuth and CSRF tokens in the process.
66    pub fn connect_new() -> Result<SpotifyConnector> {
67        // Create the reqwest client.
68        let client = Client::new();
69        // Create the connector.
70        let mut connector = SpotifyConnector {
71            client: Mutex::new(client),
72            oauth_token: String::default(),
73            csrf_token: String::default(),
74            port: 0, // will be populated later
75        };
76        connector.update_port();
77        // Connect to SpotifyWebHelper and start Spotify.
78        connector.start_spotify()?;
79        // Fetch the OAuth token.
80        connector.oauth_token = match connector.fetch_oauth_token() {
81            Ok(result) => result,
82            Err(error) => return Err(error),
83        };
84        // Fetch the CSRF token.
85        connector.csrf_token = match connector.fetch_csrf_token() {
86            Ok(result) => result,
87            Err(error) => return Err(error),
88        };
89        // Return the connector.
90        Ok(connector)
91    }
92    /// Updates the local Spotify port.
93    fn update_port(&mut self) {
94        for port in PORT_START..PORT_END {
95            if TcpListener::bind(("127.0.0.1", port)).is_err() {
96                self.port = port as i32;
97                return;
98            }
99        }
100    }
101    /// Constructs the local Spotify url.
102    fn get_local_url(&self) -> String {
103        format!("{}:{}", URL_LOCAL, self.port)
104    }
105    /// Attempts to start the Spotify client.
106    fn start_spotify(&self) -> Result<bool> {
107        match self.query(&self.get_local_url(), REQUEST_OPEN, false, false, None) {
108            Ok(result) => Ok(result["running"] == true),
109            Err(error) => Err(error),
110        }
111    }
112    /// Fetches the OAuth token from Spotify.
113    fn fetch_oauth_token(&self) -> Result<String> {
114        let json = match self.query(URL_TOKEN, "", false, false, None) {
115            Ok(result) => result,
116            Err(error) => return Err(error),
117        };
118        match json["t"].as_str() {
119            Some(token) => Ok(token.to_owned()),
120            None => Err(InternalSpotifyError::InvalidOAuthToken),
121        }
122    }
123    /// Fetches the CSRF token from Spotify.
124    fn fetch_csrf_token(&self) -> Result<String> {
125        let json = match self.query(&self.get_local_url(), REQUEST_CSRF, false, false, None) {
126            Ok(result) => result,
127            Err(error) => return Err(error),
128        };
129        match json["token"].as_str() {
130            Some(token) => Ok(token.to_owned()),
131            None => Err(InternalSpotifyError::InvalidCSRFToken),
132        }
133    }
134    /// Fetches the current status from Spotify.
135    pub fn fetch_status_json(&self) -> Result<JsonValue> {
136        self.query(&self.get_local_url(), REQUEST_STATUS, true, true, None)
137    }
138    /// Requests a track to be played.
139    pub fn request_play(&self, track: String) -> bool {
140        let params = vec![format!("uri={0}", track)];
141        self.query(
142            &self.get_local_url(),
143            REQUEST_PLAY,
144            true,
145            true,
146            Some(params),
147        )
148        .is_ok()
149    }
150    /// Requests the currently playing track to be paused or resumed.
151    pub fn request_pause(&self, pause: bool) -> bool {
152        let params = vec![format!("pause={}", pause)];
153        self.query(
154            &self.get_local_url(),
155            REQUEST_PAUSE,
156            true,
157            true,
158            Some(params),
159        )
160        .is_ok()
161    }
162    /// Queries the specified base url with the specified query.
163    /// Optionally includes the OAuth and/or CSRF token in the query.
164    fn query(
165        &self,
166        base: &str,
167        query: &str,
168        with_oauth: bool,
169        with_csrf: bool,
170        params: Option<Vec<String>>,
171    ) -> Result<JsonValue> {
172        let timestamp = time::now_utc().to_timespec().sec;
173        let arguments = {
174            let mut arguments = String::new();
175            if !query.contains('?') {
176                arguments.push('?');
177            }
178            arguments.push_str("&ref=&cors=");
179            arguments.push_str(format!("&_={}", timestamp).as_ref());
180            if with_oauth {
181                arguments.push_str(format!("&oauth={}", self.oauth_token).as_ref());
182            }
183            if with_csrf {
184                arguments.push_str(format!("&csrf={}", self.csrf_token).as_ref());
185            }
186            if let Some(params) = params {
187                for elem in params {
188                    arguments.push_str(format!("&{}", elem).as_ref());
189                }
190            }
191            arguments
192        };
193        let url = format!("{}/{}{}", base, query, arguments);
194        let response = {
195            let mut content = String::new();
196            let mut resp = match self
197                .client
198                .lock()
199                .unwrap()
200                .get::<&str>(url.as_ref())
201                .header(USER_AGENT, HEADER_UA)
202                .header(
203                    ORIGIN,
204                    format!("{}://{}", HEADER_ORIGIN_SCHEME, HEADER_ORIGIN_HOST),
205                )
206                .header(REFERER, format!("{}/{}", URL_EMBED, REFERAL_TRACK))
207                .send()
208            {
209                Ok(result) => result,
210                Err(error) => return Err(InternalSpotifyError::ReqwestError(error)),
211            };
212            match resp.read_to_string(&mut content) {
213                Ok(_) => content,
214                Err(error) => return Err(InternalSpotifyError::IOError(error)),
215            }
216        };
217        match json::parse(response.as_ref()) {
218            Ok(result) => Ok(result),
219            Err(error) => Err(InternalSpotifyError::JSONParseError(error)),
220        }
221    }
222}