1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#![warn(missing_docs)]
//! The Spotify crate.
//!
//! This crate contains methods to retrieve information from
//! and manipulate the local Spotify client instance.

// Extern crates
extern crate winapi;
extern crate kernel32;
extern crate reqwest;
extern crate time;
extern crate json;

// Modules
#[cfg(windows)]
mod windows_process;
mod connector;
pub mod status;

// Imports
use std::thread::{self, JoinHandle};
use std::time::Duration;
#[cfg(windows)]
use windows_process::WindowsProcess;
use connector::{SpotifyConnector, InternalSpotifyError};
use status::{SpotifyStatus, SpotifyStatusChange};

/// The `Result` type used in this crate.
type Result<T> = std::result::Result<T, SpotifyError>;

/// The `SpotifyError` enum.
#[derive(Debug)]
pub enum SpotifyError {
    /// An internal error.
    InternalError(InternalSpotifyError),
    /// Indicates that the Spotify Client is not running.
    ClientNotRunning,
    /// Indicates that the SpotifyWebHelper process it not running.
    WebHelperNotRunning,
}

/// The Spotify API.
pub struct Spotify {
    /// The Spotify connector.
    connector: SpotifyConnector,
}

/// Fetches the current status from Spotify.
fn get_status(connector: &SpotifyConnector) -> Result<SpotifyStatus> {
    match connector.fetch_status_json() {
        Ok(result) => Ok(SpotifyStatus::from(result)),
        Err(error) => Err(SpotifyError::InternalError(error)),
    }
}

/// Implements `Spotify`.
impl Spotify {
    /// Connects to the local Spotify client.
    #[cfg(windows)]
    pub fn connect() -> Result<Spotify> {
        // TODO:
        // At some point, the connector should automatically
        // open Spotify in the case  that Spotify is closed.
        // That would also be a much better cross-platform solution,
        // because it would work on Linux and macOS and make
        // the dependency on winapi and kernel32-sys unnecessary.
        if !Spotify::spotify_webhelper_alive() {
            return Err(SpotifyError::WebHelperNotRunning);
        }
        Spotify::new_unchecked()
    }
    /// Connects to the local Spotify client.
    #[cfg(not(windows))]
    pub fn connect() -> Result<Spotify> {
        Spotify::new_unchecked()
    }
    /// Constructs a new `self::Result<Spotify>`.
    fn new_unchecked() -> Result<Spotify> {
        match SpotifyConnector::connect_new() {
            Ok(result) => Ok(Spotify { connector: result }),
            Err(error) => Err(SpotifyError::InternalError(error)),
        }
    }
    /// Moves `self` to a new thread and begins polling the
    /// client status. Sends the updated status to the specified
    /// closure, together with information of which fields had changed
    /// since the last update. Returns the `JoinHandle` of the new thread.
    pub fn poll<'a, F: 'static>(self, f: F) -> JoinHandle<()>
        where F: Fn(&Spotify, SpotifyStatus, SpotifyStatusChange) -> bool,
              F: std::marker::Send
    {
        thread::spawn(move || {
            let sleep_time = Duration::from_millis(250);
            let mut last: Option<SpotifyStatus> = None;
            let mut curr: Option<SpotifyStatus>;
            let mut first = true;
            loop {
                curr = get_status(&self.connector).ok();
                {
                    let last = last.clone();
                    if first && curr.is_some() {
                        let curr = curr.clone().unwrap();
                        if !f(&self, curr.clone(), SpotifyStatusChange::new_true()) {
                            break;
                        }
                        first = false;
                    } else if !first && curr.is_some() && last.is_some() {
                        let curr = curr.clone().unwrap();
                        let last = last.unwrap();
                        if !f(&self, curr.clone(), SpotifyStatusChange::from((curr, last))) {
                            break;
                        }
                    }
                }
                if curr.is_some() {
                    last = curr.clone();
                }
                thread::sleep(sleep_time);
            }
        })
    }
    /// Fetches the current status from the client.
    pub fn status(&self) -> Result<SpotifyStatus> {
        get_status(&self.connector)
    }
    /// Optionally plays a track or adds it to the queue.
    pub fn play(&self, track: String, queue: bool) -> bool {
        self.connector.request_play(track, queue)
    }
    /// Pauses the currently playing track.
    /// Has no effect if the track is already paused.
    pub fn pause(&self) -> bool {
        self.connector.request_pause(true)
    }
    /// Resumes the currently paused track.
    /// Has no effect if the track is already playing.
    pub fn resume(&self) -> bool {
        self.connector.request_pause(false)
    }
    /// Tests whether the SpotifyWebHelper process is running.
    #[cfg(windows)]
    fn spotify_webhelper_alive() -> bool {
        let process = "SpotifyWebHelper.exe";
        WindowsProcess::find_by_name(process).is_some()
    }
}