Skip to main content

bluos_api_rs/
device.rs

1mod command;
2mod responses;
3
4use crate::error::Error;
5use command::Command;
6use reqwest::Response;
7use responses::StateResponse;
8pub use responses::{Browse, IdResponse, Playlist, PlaylistEntry, State, Status};
9use serde::Deserialize;
10use std::net::{Ipv4Addr, SocketAddr};
11
12#[cfg(feature = "discover")]
13use crate::DiscoveredBluOSDevice;
14
15// Documented here
16// https://bluos.net/wp-content/uploads/2021/03/Custom-Integration-API-v1.0_March-2021.pdf
17#[derive(Debug)]
18pub struct BluOS {
19    hostname: String,
20    port: u16,
21
22    client: reqwest::Client,
23}
24
25impl BluOS {
26    /// Create a new BluOS device from an Ipv4Addr
27    ///
28    /// - If you for some reason managed to make your BluOS device listen on another port, define it using custom_port
29    pub fn new(addr: Ipv4Addr, custom_port: Option<u16>) -> Result<BluOS, Error> {
30        let port = match custom_port {
31            Some(v) => v,
32            None => 11000,
33        };
34
35        Ok(BluOS {
36            hostname: addr.to_string(),
37            port,
38            client: reqwest::Client::new(),
39        })
40    }
41
42    pub fn with_socket_addr(addr: SocketAddr) -> Result<BluOS, Error> {
43        Ok(BluOS {
44            hostname: addr.ip().to_string(),
45            port: addr.port(),
46            client: reqwest::Client::new(),
47        })
48    }
49
50    /// Create a new BluOS device from a discovered device
51    #[cfg(feature = "discover")]
52    pub fn new_from_discovered(d: DiscoveredBluOSDevice) -> Result<BluOS, Error> {
53        Ok(BluOS {
54            hostname: d.hostname,
55            port: d.port,
56            client: reqwest::Client::new(),
57        })
58    }
59
60    fn cmd(&self, action: &str) -> Command {
61        Command::new(&self.hostname, self.port, action)
62    }
63
64    /// Send your own command to the BluOS Device
65    async fn command(&self, cmd: Command) -> Result<Response, Error> {
66        Ok(self.client.get(cmd.build()).send().await?)
67    }
68
69    /// Send your own command to the BluOS device and expect a response
70    /// The function is generic and uses the type to determine what struct to deserialize to
71    async fn command_response<'a, T: Deserialize<'a>>(&self, cmd: Command) -> Result<T, Error> {
72        let t = self.client.get(cmd.build()).send().await?.text().await?;
73        Ok(serde_xml_rs::from_str(&t)?)
74    }
75
76    /// Get the current status of the BluOS device
77    pub async fn status(&self) -> Result<Status, Error> {
78        let status: Status = self.command_response(self.cmd("Status")).await?;
79
80        Ok(status)
81    }
82
83    pub async fn browse(&self, key: Option<&str>) -> Result<Browse, Error> {
84        let mut cmd = self.cmd("Browse");
85        cmd.add_optional("key", key);
86        let browse: Browse = self.command_response(cmd).await?;
87        Ok(browse)
88    }
89
90    /// Re-indexes the library
91    ///
92    /// This function is why I wrote this wrapper and once that worked I figured
93    /// why not just keep going and write the rest LOL
94    pub async fn update_library(&self) -> Result<(), Error> {
95        self.command(self.cmd("Reindex")).await?;
96
97        Ok(())
98    }
99
100    ///////////////////
101    // Playback functions
102    ///////////////////
103
104    /// Plays whatever source is currently active
105    pub async fn play(&self) -> Result<State, Error> {
106        Ok(self.play_with_options(None, None, None).await?)
107    }
108
109    /// Play with the ability to define options
110    /// - seek: time to seek in the track, max is total_length from status of the track
111    /// - input_type:  Selects an input before starting playback.
112    /// Possible values for inputType are: analog, spdif, hdmi or bluetooth.
113    /// - index: For players with more than one input, this indicates which input of the specified
114    /// type to play. Used only with inputType parameter. Default value is 1.
115    pub async fn play_with_options(
116        &self,
117        seek: Option<i64>,
118        input_type: Option<String>,
119        index: Option<i64>,
120    ) -> Result<State, Error> {
121        let mut cmd = self.cmd("Play");
122
123        cmd.add_optional("seek", seek);
124        cmd.add_optional("inputType", input_type);
125        cmd.add_optional("index", index);
126
127        let state: StateResponse = self.command_response(cmd).await?;
128        Ok(state.state)
129    }
130    /// Pause playback
131    /// - toggle: If set to 1, then the current pause state is toggled.
132    pub async fn pause(&self, toggle: bool) -> Result<State, Error> {
133        let mut cmd = self.cmd("Pause");
134        if toggle {
135            cmd.add_param("toggle", 1);
136        }
137        let state: StateResponse = self.command_response(cmd).await?;
138        Ok(state.state)
139    }
140    /// Stop playback
141    pub async fn stop(&self) -> Result<State, Error> {
142        let state: StateResponse = self.command_response(self.cmd("Stop")).await?;
143        Ok(state.state)
144    }
145    /// Skip: Skip to the next audio track in the play queue
146    pub async fn skip(&self) -> Result<IdResponse, Error> {
147        let id: IdResponse = self.command_response(self.cmd("Skip")).await?;
148        Ok(id)
149    }
150    /// Back: If a track is playing and has been playing for more than four seconds, then back, will return to the start of the track.
151    /// Otherwise, the back command will go to the previous song in the current playlist. If on the first song in the playlist
152    /// calling back will go to the last song. It will go to the previous or first track in the queue regardless of the state of
153    /// the repeat setting.
154    pub async fn back(&self) -> Result<IdResponse, Error> {
155        let id: IdResponse = self.command_response(self.cmd("Back")).await?;
156        Ok(id)
157    }
158
159    /// Shuffle
160    /// The shuffle command creates a new queue by shuffling the current queue.
161    /// The original (not shuffled) queue is retained for restore when shuffle is disabled.
162    pub async fn shuffle(&self, enable: bool) -> Result<(), Error> {
163        let mut cmd = self.cmd("Shuffle");
164        if enable {
165            cmd.add_param("state", 1 as u8);
166        } else {
167            cmd.add_param("state", 0 as u8);
168        }
169        self.command(cmd).await?;
170        Ok(())
171    }
172
173    /// Repeat... repeats
174    ///
175    /// Takes RepeatSetting enum which defines what kind of repeat you need
176    pub async fn repeat(&self, setting: RepeatSetting) -> Result<(), Error> {
177        let mut cmd = self.cmd("Repeat");
178
179        cmd.add_param("state", setting as u8);
180
181        self.command(cmd).await?;
182        Ok(())
183    }
184
185    ///////////////////
186    // Play Queue Management
187    ///////////////////
188
189    /// Get the current play queue from the BluOS device
190    pub async fn queue(&self, pagination: Option<Pagination>) -> Result<Playlist, Error> {
191        let mut cmd = self.cmd("Playlist");
192        match pagination {
193            Some(p) => {
194                cmd.add_param("start", p.start);
195                cmd.add_param("end", p.end);
196            }
197            None => {}
198        };
199        let pl: Playlist = self.command_response(cmd).await?;
200
201        Ok(pl)
202    }
203
204    /// Delete a song at POSITION
205    pub async fn queue_delete_song(&self, position: u64) -> Result<(), Error> {
206        let mut cmd = self.cmd("Delete");
207
208        cmd.add_param("id", position);
209
210        self.command(cmd).await?;
211        Ok(())
212    }
213
214    /// Clear the play queue
215    pub async fn queue_clear(&self) -> Result<(), Error> {
216        let cmd = self.cmd("Clear");
217
218        self.command(cmd).await?;
219        Ok(())
220    }
221}
222
223pub struct Pagination {
224    start: u64,
225    end: u64,
226}
227
228pub enum RepeatSetting {
229    EntireQueue = 0,
230    CurrentTrack = 1,
231    Disable = 2,
232}