comparison src/beefweb.rs @ 0:d60ab8a4442f

*: check in
author Paper <paper@tflc.us>
date Sat, 04 Apr 2026 12:32:50 -0400
parents
children a5ee18c79a04
comparison
equal deleted inserted replaced
-1:000000000000 0:d60ab8a4442f
1 /*
2 * Tiny layer for interfacing with beefweb. Does basically the
3 * bare minimum, and does not expose the whole API.
4 *
5 * Copyright (C) 2026 Paper
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, see
19 * <https://www.gnu.org/licenses/>.
20 */
21
22 #[derive(serde::Serialize, serde::Deserialize, Debug)]
23 pub struct PlayerInfo {
24 pub name: String,
25 pub title: String,
26 pub version: String,
27 // fuck camel case
28 #[serde(rename = "pluginVersion")]
29 pub plugin_version: String,
30 }
31
32 #[derive(serde::Serialize, serde::Deserialize, Debug)]
33 pub struct ActiveItemInfo {
34 #[serde(rename = "playlistId")]
35 pub playlist_id: String,
36 #[serde(rename = "playlistIndex")]
37 pub playlist_index: i64,
38 pub index: i64,
39 pub position: f64,
40 pub duration: f64,
41 pub columns: Vec<String>,
42 }
43
44 #[derive(serde::Serialize, serde::Deserialize, Debug)]
45 pub enum VolumeType {
46 #[serde(rename = "db")]
47 DB,
48 #[serde(rename = "linear")]
49 LINEAR,
50 #[serde(rename = "upDown")]
51 UPDOWN, /* ??? */
52 }
53
54 #[derive(serde::Serialize, serde::Deserialize, Debug)]
55 pub struct VolumeInfo {
56 pub r#type: VolumeType,
57 pub min: f64,
58 pub max: f64,
59 pub value: f64,
60 #[serde(rename = "isMuted")]
61 pub muted: bool,
62 }
63
64 #[derive(serde::Serialize, serde::Deserialize, Debug)]
65 pub struct ApiPermissions {
66 #[serde(rename = "changePlaylists")]
67 pub change_playlists: bool,
68 #[serde(rename = "changeOutput")]
69 pub change_output: bool,
70 #[serde(rename = "changeClientConfig")]
71 pub change_client_config: bool,
72 }
73
74 #[derive(serde::Serialize, serde::Deserialize, Debug)]
75 pub struct Player {
76 pub info: PlayerInfo,
77 #[serde(rename = "activeItem")]
78 pub active_item: ActiveItemInfo,
79 /* XXX actually an enum */
80 #[serde(rename = "playbackState")]
81 pub playback_state: String,
82 pub volume: VolumeInfo,
83 pub permissions: ApiPermissions,
84 }
85
86 /* {"player": {struct Player}} */
87 #[derive(serde::Serialize, serde::Deserialize, Debug)]
88 struct PlayerHelper {
89 player: Player,
90 }
91
92 #[derive(serde::Serialize, serde::Deserialize, Debug)]
93 #[serde(untagged)]
94 enum SetPlayerOption {
95 Integer { id: String, value: i64 },
96 Boolean { id: String, value: bool },
97 }
98
99 #[derive(serde::Serialize, serde::Deserialize, Debug)]
100 #[serde_with::skip_serializing_none]
101 struct SetPlayer {
102 /* dB */
103 volume: Option<f64>,
104 /* ??? */
105 #[serde(rename = "relativeVolume")]
106 relative_volume: Option<f64>,
107 #[serde(rename = "isMuted")]
108 muted: Option<bool>,
109 position: Option<f64>,
110 #[serde(rename = "relativePosition")]
111 relative_position: Option<f64>,
112 /* teehee */
113 options: Option<Vec<SetPlayerOption>>,
114 }
115
116 #[derive(serde::Serialize, serde::Deserialize, Debug)]
117 pub struct PlaylistItem {
118 pub columns: Vec<String>
119 }
120
121 #[derive(serde::Serialize, serde::Deserialize, Debug)]
122 pub struct PlaylistItems {
123 pub offset: i64,
124 #[serde(rename = "totalCount")]
125 pub total_count: i64,
126 pub items: Vec<PlaylistItem>,
127 }
128
129 #[derive(serde::Serialize, serde::Deserialize, Debug)]
130 struct PlaylistItemsHelper {
131 #[serde(rename = "playlistItems")]
132 playlist_items: PlaylistItems,
133 }
134
135 /* --- NOW, THE ACTUAL BEEFWEB THING */
136
137 pub struct Beefweb {
138 client: reqwest::Client,
139 base_url: String,
140 }
141
142 impl Beefweb {
143 pub fn new(base: &str) -> Beefweb
144 {
145 return Beefweb {
146 client: reqwest::Client::new(),
147 base_url: base.to_string(),
148 };
149 }
150
151 pub async fn player(&self) -> Result<Player, anyhow::Error>
152 {
153 return Ok(self.client
154 .get(format!("{}/player", self.base_url))
155 .send()
156 .await?
157 .json::<PlayerHelper>()
158 .await?
159 .player);
160 }
161
162 pub async fn volume(&self) -> Result<VolumeInfo, anyhow::Error>
163 {
164 return Ok(self.player().await?.volume);
165 }
166
167 pub async fn position(&self) -> Result<f64, anyhow::Error>
168 {
169 return Ok(self.player().await?.active_item.position);
170 }
171
172 /* XXX might be able to use macros for this? idk --paper */
173 async fn play_pause_stop(&self, x: &str) -> Result<(), anyhow::Error>
174 {
175 self.client
176 .post(format!("{}/player/{}", self.base_url, x))
177 .send()
178 .await?;
179
180 return Ok(());
181 }
182
183 pub async fn play(&self) -> Result<(), anyhow::Error>
184 {
185 return self.play_pause_stop("play").await;
186 }
187
188 pub async fn pause(&self) -> Result<(), anyhow::Error>
189 {
190 return self.play_pause_stop("pause").await;
191 }
192
193 pub async fn stop(&self) -> Result<(), anyhow::Error>
194 {
195 return self.play_pause_stop("stop").await;
196 }
197
198 pub async fn play_pause(&self) -> Result<(), anyhow::Error>
199 {
200 return self.play_pause_stop("play-pause").await;
201 }
202
203 pub async fn next(&self) -> Result<(), anyhow::Error>
204 {
205 return self.play_pause_stop("next").await;
206 }
207
208 pub async fn previous(&self) -> Result<(), anyhow::Error>
209 {
210 return self.play_pause_stop("previous").await;
211 }
212
213 /* XXX what should this return? */
214 async fn set_player(&self, p: &SetPlayer) -> Result<(), anyhow::Error>
215 {
216 self.client
217 .post(format!("{}/player", self.base_url))
218 .json::<SetPlayer>(p)
219 .send()
220 .await?;
221
222 return Ok(());
223 }
224
225 pub async fn set_volume(&self, volume: f64) -> Result<(), anyhow::Error>
226 {
227 return self.set_player(&SetPlayer {
228 volume: Some(volume),
229 relative_volume: None,
230 muted: None,
231 position: None,
232 relative_position: None,
233 options: None,
234 }).await;
235 }
236
237 pub async fn set_position(&self, position: f64) -> Result<(), anyhow::Error>
238 {
239 return self.set_player(&SetPlayer {
240 volume: None,
241 relative_volume: Some(position),
242 muted: None,
243 position: None,
244 relative_position: None,
245 options: None,
246 }).await;
247 }
248
249 pub async fn playlist_items(&self, playlist_id: &str, offset: i64, count: i64, columns: Vec<&str>) -> Result<PlaylistItems, anyhow::Error>
250 {
251 return Ok(self.client
252 .get(format!("{}/playlists/{}/items/{}:{}", self.base_url, playlist_id, offset, count))
253 .query(&[("columns", columns.join(","))])
254 .send()
255 .await?
256 .json::<PlaylistItemsHelper>()
257 .await?
258 .playlist_items);
259 }
260
261 pub async fn artwork(&self, playlist_id: &str, index: i64) -> Result<bytes::Bytes, anyhow::Error>
262 {
263 return Ok(self.client
264 .get(format!("{}/artwork/{}/{}", self.base_url, playlist_id, index))
265 .send()
266 .await?
267 .bytes()
268 .await?);
269 }
270 }