Mercurial > beefweb_mpris
annotate src/beefweb.rs @ 6:482bd968725f default tip
beefweb: remove unnecessary (de)serialize derives
| author | Paper <paper@tflc.us> |
|---|---|
| date | Mon, 06 Apr 2026 09:54:05 -0400 |
| parents | 26f695129c86 |
| children |
| rev | line source |
|---|---|
| 0 | 1 /* |
| 1 | 2 * Beefweb <-> mpris "compatibility" layer. |
| 0 | 3 * |
| 4 * Copyright (C) 2026 Paper | |
| 5 * | |
| 6 * This program is free software; you can redistribute it and/or | |
| 7 * modify it under the terms of the GNU General Public License | |
| 8 * as published by the Free Software Foundation; either version 2 | |
| 9 * of the License, or (at your option) any later version. | |
| 10 * | |
| 11 * This program is distributed in the hope that it will be useful, | |
| 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 14 * GNU General Public License for more details. | |
| 15 * | |
| 16 * You should have received a copy of the GNU General Public License | |
| 17 * along with this program; if not, see | |
| 18 * <https://www.gnu.org/licenses/>. | |
| 19 */ | |
| 20 | |
| 2 | 21 /* Fuck off, I don't care. */ |
| 22 #![allow(nonstandard_style)] | |
| 23 | |
|
6
482bd968725f
beefweb: remove unnecessary (de)serialize derives
Paper <paper@tflc.us>
parents:
4
diff
changeset
|
24 #[derive(serde::Deserialize, Debug)] |
| 0 | 25 pub struct PlayerInfo { |
| 26 pub name: String, | |
| 27 pub title: String, | |
| 28 pub version: String, | |
| 29 // fuck camel case | |
| 30 #[serde(rename = "pluginVersion")] | |
| 31 pub plugin_version: String, | |
| 32 } | |
| 33 | |
|
6
482bd968725f
beefweb: remove unnecessary (de)serialize derives
Paper <paper@tflc.us>
parents:
4
diff
changeset
|
34 #[derive(serde::Deserialize, Debug)] |
| 0 | 35 pub struct ActiveItemInfo { |
| 36 #[serde(rename = "playlistId")] | |
| 37 pub playlist_id: String, | |
| 38 #[serde(rename = "playlistIndex")] | |
| 39 pub playlist_index: i64, | |
| 40 pub index: i64, | |
| 41 pub position: f64, | |
| 42 pub duration: f64, | |
| 43 pub columns: Vec<String>, | |
| 44 } | |
| 45 | |
|
6
482bd968725f
beefweb: remove unnecessary (de)serialize derives
Paper <paper@tflc.us>
parents:
4
diff
changeset
|
46 #[derive(serde::Deserialize, Debug)] |
| 0 | 47 pub enum VolumeType { |
| 48 #[serde(rename = "db")] | |
| 49 DB, | |
| 50 #[serde(rename = "linear")] | |
| 51 LINEAR, | |
| 52 #[serde(rename = "upDown")] | |
| 53 UPDOWN, /* ??? */ | |
| 54 } | |
| 55 | |
|
6
482bd968725f
beefweb: remove unnecessary (de)serialize derives
Paper <paper@tflc.us>
parents:
4
diff
changeset
|
56 #[derive(serde::Deserialize, Debug)] |
| 0 | 57 pub struct VolumeInfo { |
| 58 pub r#type: VolumeType, | |
| 59 pub min: f64, | |
| 60 pub max: f64, | |
| 61 pub value: f64, | |
| 62 #[serde(rename = "isMuted")] | |
| 63 pub muted: bool, | |
| 64 } | |
| 65 | |
|
6
482bd968725f
beefweb: remove unnecessary (de)serialize derives
Paper <paper@tflc.us>
parents:
4
diff
changeset
|
66 #[derive(serde::Deserialize, Debug)] |
| 0 | 67 pub struct ApiPermissions { |
| 68 #[serde(rename = "changePlaylists")] | |
| 69 pub change_playlists: bool, | |
| 70 #[serde(rename = "changeOutput")] | |
| 71 pub change_output: bool, | |
| 72 #[serde(rename = "changeClientConfig")] | |
| 73 pub change_client_config: bool, | |
| 74 } | |
| 75 | |
|
6
482bd968725f
beefweb: remove unnecessary (de)serialize derives
Paper <paper@tflc.us>
parents:
4
diff
changeset
|
76 #[derive(serde::Deserialize, Debug)] |
| 2 | 77 pub enum PlaybackState { |
| 78 #[serde(rename = "stopped")] | |
| 79 STOPPED, | |
| 80 #[serde(rename = "playing")] | |
| 81 PLAYING, | |
| 82 #[serde(rename = "paused")] | |
| 83 PAUSED, | |
| 84 } | |
| 85 | |
| 86 /* fb2k playback order */ | |
| 87 #[derive(Debug)] | |
| 88 pub enum PlaybackOrder { | |
| 89 DEFAULT, | |
| 90 REPEAT_PLAYLIST, | |
| 91 REPEAT_TRACK, | |
| 92 RANDOM, | |
| 93 SHUFFLE_TRACKS, | |
| 94 SHUFFLE_ALBUMS, | |
| 95 SHUFFLE_FOLDERS, | |
| 96 } | |
| 97 | |
|
6
482bd968725f
beefweb: remove unnecessary (de)serialize derives
Paper <paper@tflc.us>
parents:
4
diff
changeset
|
98 #[derive(serde::Deserialize, Debug)] |
| 2 | 99 #[serde(tag = "type")] |
| 100 pub enum Options { | |
| 101 #[serde(rename = "enum")] | |
| 102 Enumeration { | |
| 103 #[serde(rename = "enumNames")] | |
| 104 enum_names: Vec<String>, | |
| 105 id: String, | |
| 106 name: String, | |
| 107 value: i64, | |
| 108 }, | |
| 109 #[serde(rename = "bool")] | |
| 110 Boolean { | |
| 111 id: String, | |
| 112 name: String, | |
| 113 value: bool, | |
| 114 }, | |
| 115 } | |
| 116 | |
|
6
482bd968725f
beefweb: remove unnecessary (de)serialize derives
Paper <paper@tflc.us>
parents:
4
diff
changeset
|
117 #[derive(serde::Deserialize, Debug)] |
| 0 | 118 pub struct Player { |
| 119 pub info: PlayerInfo, | |
| 120 #[serde(rename = "activeItem")] | |
| 121 pub active_item: ActiveItemInfo, | |
| 122 /* XXX actually an enum */ | |
| 123 #[serde(rename = "playbackState")] | |
| 2 | 124 pub playback_state: PlaybackState, |
| 0 | 125 pub volume: VolumeInfo, |
| 126 pub permissions: ApiPermissions, | |
| 2 | 127 |
| 128 options: Option<Vec<Options>>, | |
| 129 | |
| 130 /* these two fields are deprecated, | |
| 131 * replaced with "options" array */ | |
| 132 #[serde(skip_serializing_if = "Option::is_none")] | |
| 133 playbackMode: Option<i64>, | |
| 134 | |
| 135 #[serde(skip_serializing_if = "Option::is_none")] | |
| 136 playbackModes: Option<Vec<String>>, | |
| 0 | 137 } |
| 138 | |
| 139 /* {"player": {struct Player}} */ | |
|
6
482bd968725f
beefweb: remove unnecessary (de)serialize derives
Paper <paper@tflc.us>
parents:
4
diff
changeset
|
140 #[derive(serde::Deserialize, Debug)] |
| 0 | 141 struct PlayerHelper { |
| 142 player: Player, | |
| 143 } | |
| 144 | |
|
6
482bd968725f
beefweb: remove unnecessary (de)serialize derives
Paper <paper@tflc.us>
parents:
4
diff
changeset
|
145 #[derive(serde::Serialize, Debug)] |
| 0 | 146 #[serde(untagged)] |
| 147 enum SetPlayerOption { | |
| 148 Integer { id: String, value: i64 }, | |
| 149 Boolean { id: String, value: bool }, | |
| 150 } | |
| 151 | |
|
6
482bd968725f
beefweb: remove unnecessary (de)serialize derives
Paper <paper@tflc.us>
parents:
4
diff
changeset
|
152 #[derive(serde::Serialize, Debug)] |
| 0 | 153 #[serde_with::skip_serializing_none] |
| 154 struct SetPlayer { | |
| 155 /* dB */ | |
| 156 volume: Option<f64>, | |
| 157 /* ??? */ | |
| 158 #[serde(rename = "relativeVolume")] | |
| 159 relative_volume: Option<f64>, | |
| 160 #[serde(rename = "isMuted")] | |
| 161 muted: Option<bool>, | |
| 162 position: Option<f64>, | |
| 163 #[serde(rename = "relativePosition")] | |
| 164 relative_position: Option<f64>, | |
| 165 /* teehee */ | |
| 166 options: Option<Vec<SetPlayerOption>>, | |
| 167 } | |
| 168 | |
|
6
482bd968725f
beefweb: remove unnecessary (de)serialize derives
Paper <paper@tflc.us>
parents:
4
diff
changeset
|
169 #[derive(serde::Deserialize, Debug)] |
| 0 | 170 pub struct PlaylistItem { |
| 171 pub columns: Vec<String> | |
| 172 } | |
| 173 | |
|
6
482bd968725f
beefweb: remove unnecessary (de)serialize derives
Paper <paper@tflc.us>
parents:
4
diff
changeset
|
174 #[derive(serde::Deserialize, Debug)] |
| 0 | 175 pub struct PlaylistItems { |
| 176 pub offset: i64, | |
| 177 #[serde(rename = "totalCount")] | |
| 178 pub total_count: i64, | |
| 179 pub items: Vec<PlaylistItem>, | |
| 180 } | |
| 181 | |
|
6
482bd968725f
beefweb: remove unnecessary (de)serialize derives
Paper <paper@tflc.us>
parents:
4
diff
changeset
|
182 #[derive(serde::Deserialize, Debug)] |
| 0 | 183 struct PlaylistItemsHelper { |
| 184 #[serde(rename = "playlistItems")] | |
| 185 playlist_items: PlaylistItems, | |
| 186 } | |
| 187 | |
|
6
482bd968725f
beefweb: remove unnecessary (de)serialize derives
Paper <paper@tflc.us>
parents:
4
diff
changeset
|
188 #[derive(serde::Deserialize, Debug)] |
| 4 | 189 pub struct Playlist { |
| 190 pub id: String, | |
| 191 pub index: i64, | |
| 192 pub title: String, | |
| 193 #[serde(rename = "isCurrent")] | |
| 194 pub current: bool, | |
| 195 #[serde(rename = "itemCount")] | |
| 196 pub item_count: i64, | |
| 197 #[serde(rename = "totalTime")] | |
| 198 pub total_time: f64, | |
| 199 } | |
| 200 | |
|
6
482bd968725f
beefweb: remove unnecessary (de)serialize derives
Paper <paper@tflc.us>
parents:
4
diff
changeset
|
201 #[derive(serde::Deserialize, Debug)] |
| 4 | 202 pub struct Playlists { |
| 203 pub playlists: Vec<Playlist>, | |
| 204 } | |
| 205 | |
| 0 | 206 /* --- NOW, THE ACTUAL BEEFWEB THING */ |
| 207 | |
| 208 pub struct Beefweb { | |
| 209 client: reqwest::Client, | |
| 210 base_url: String, | |
| 211 } | |
| 212 | |
| 213 impl Beefweb { | |
| 214 pub fn new(base: &str) -> Beefweb | |
| 215 { | |
| 216 return Beefweb { | |
| 217 client: reqwest::Client::new(), | |
| 218 base_url: base.to_string(), | |
| 219 }; | |
| 220 } | |
| 221 | |
| 222 pub async fn player(&self) -> Result<Player, anyhow::Error> | |
| 223 { | |
| 224 return Ok(self.client | |
| 225 .get(format!("{}/player", self.base_url)) | |
| 226 .send() | |
| 227 .await? | |
| 228 .json::<PlayerHelper>() | |
| 229 .await? | |
| 230 .player); | |
| 231 } | |
| 232 | |
| 2 | 233 fn playback_order_lookup(playback_order: &str) -> Option<PlaybackOrder> |
| 234 { | |
| 235 return match playback_order { | |
| 236 "Default" => return Some(PlaybackOrder::DEFAULT), | |
| 237 "Repeat (playlist)" => return Some(PlaybackOrder::REPEAT_PLAYLIST), | |
| 238 "Repeat (track)" => return Some(PlaybackOrder::REPEAT_TRACK), | |
| 239 "Random" => return Some(PlaybackOrder::RANDOM), | |
| 240 "Shuffle (tracks)" => return Some(PlaybackOrder::SHUFFLE_TRACKS), | |
| 241 "Shuffle (albums)" => return Some(PlaybackOrder::SHUFFLE_ALBUMS), | |
| 242 "Shuffle (folders)" => return Some(PlaybackOrder::SHUFFLE_FOLDERS), | |
| 243 _ => None, | |
| 244 }; | |
| 245 } | |
| 246 | |
| 247 fn playback_order_cruft(v: &Vec<String>, s: i64) -> Option<PlaybackOrder> | |
| 248 { | |
| 249 let x = v.get(s as usize); | |
| 250 | |
| 251 match x { | |
| 252 Some(y) => return Self::playback_order_lookup(y), | |
| 253 _ => (), | |
| 254 }; | |
| 255 | |
| 256 return None; | |
| 257 } | |
| 258 | |
| 259 pub async fn playback_order(&self) -> Result<PlaybackOrder, anyhow::Error> | |
| 260 { | |
| 261 let p = self.player().await?; | |
| 262 | |
| 263 match p.options { | |
| 264 Some(x) => { | |
| 265 for i in x { | |
| 266 match i { | |
| 267 Options::Enumeration { enum_names: e, id: id, name: _, value: v } => { | |
| 268 match id.as_str() { | |
| 269 "playbackOrder" => { | |
| 270 match Self::playback_order_cruft(&e, v) { | |
| 271 Some(z) => return Ok(z), | |
| 272 _ => (), | |
| 273 }; | |
| 274 }, | |
| 275 _ => (), | |
| 276 }; | |
| 277 }, | |
| 278 _ => (), | |
| 279 }; | |
| 280 } | |
| 281 }, | |
| 282 _ => (), | |
| 283 } | |
| 284 | |
| 285 match p.playbackModes { | |
| 286 Some(x) => { | |
| 287 match p.playbackMode { | |
| 288 Some(y) => { | |
| 289 match Self::playback_order_cruft(&x, y) { | |
| 290 Some(v) => return Ok(v), | |
| 291 _ => (), | |
| 292 } | |
| 293 }, | |
| 294 _ => (), | |
| 295 }; | |
| 296 } | |
| 297 _ => (), | |
| 298 } | |
| 299 | |
| 300 return Err(anyhow::anyhow!("Unknown or invalid playback order?")); | |
| 301 } | |
| 302 | |
| 0 | 303 pub async fn volume(&self) -> Result<VolumeInfo, anyhow::Error> |
| 304 { | |
| 305 return Ok(self.player().await?.volume); | |
| 306 } | |
| 307 | |
| 308 pub async fn position(&self) -> Result<f64, anyhow::Error> | |
| 309 { | |
| 310 return Ok(self.player().await?.active_item.position); | |
| 311 } | |
| 312 | |
| 313 /* XXX might be able to use macros for this? idk --paper */ | |
| 314 async fn play_pause_stop(&self, x: &str) -> Result<(), anyhow::Error> | |
| 315 { | |
| 316 self.client | |
| 317 .post(format!("{}/player/{}", self.base_url, x)) | |
| 318 .send() | |
| 319 .await?; | |
| 320 | |
| 321 return Ok(()); | |
| 322 } | |
| 323 | |
| 324 pub async fn play(&self) -> Result<(), anyhow::Error> | |
| 325 { | |
| 326 return self.play_pause_stop("play").await; | |
| 327 } | |
| 328 | |
| 329 pub async fn pause(&self) -> Result<(), anyhow::Error> | |
| 330 { | |
| 331 return self.play_pause_stop("pause").await; | |
| 332 } | |
| 333 | |
| 334 pub async fn stop(&self) -> Result<(), anyhow::Error> | |
| 335 { | |
| 336 return self.play_pause_stop("stop").await; | |
| 337 } | |
| 338 | |
| 339 pub async fn play_pause(&self) -> Result<(), anyhow::Error> | |
| 340 { | |
| 341 return self.play_pause_stop("play-pause").await; | |
| 342 } | |
| 343 | |
| 344 pub async fn next(&self) -> Result<(), anyhow::Error> | |
| 345 { | |
| 346 return self.play_pause_stop("next").await; | |
| 347 } | |
| 348 | |
| 349 pub async fn previous(&self) -> Result<(), anyhow::Error> | |
| 350 { | |
| 351 return self.play_pause_stop("previous").await; | |
| 352 } | |
| 353 | |
| 354 /* XXX what should this return? */ | |
| 355 async fn set_player(&self, p: &SetPlayer) -> Result<(), anyhow::Error> | |
| 356 { | |
| 357 self.client | |
| 358 .post(format!("{}/player", self.base_url)) | |
| 359 .json::<SetPlayer>(p) | |
| 360 .send() | |
| 361 .await?; | |
| 362 | |
| 363 return Ok(()); | |
| 364 } | |
| 365 | |
| 366 pub async fn set_volume(&self, volume: f64) -> Result<(), anyhow::Error> | |
| 367 { | |
| 368 return self.set_player(&SetPlayer { | |
| 369 volume: Some(volume), | |
| 370 relative_volume: None, | |
| 371 muted: None, | |
| 372 position: None, | |
| 373 relative_position: None, | |
| 374 options: None, | |
| 375 }).await; | |
| 376 } | |
| 377 | |
| 378 pub async fn set_position(&self, position: f64) -> Result<(), anyhow::Error> | |
| 379 { | |
| 380 return self.set_player(&SetPlayer { | |
| 381 volume: None, | |
| 2 | 382 relative_volume: None, |
| 0 | 383 muted: None, |
| 2 | 384 position: Some(position), |
| 0 | 385 relative_position: None, |
| 386 options: None, | |
| 387 }).await; | |
| 388 } | |
| 389 | |
| 4 | 390 pub async fn seek(&self, position: f64) -> Result<(), anyhow::Error> |
| 391 { | |
| 392 return self.set_player(&SetPlayer { | |
| 393 volume: None, | |
| 394 relative_volume: None, | |
| 395 muted: None, | |
| 396 position: None, | |
| 397 relative_position: Some(position), | |
| 398 options: None, | |
| 399 }).await; | |
| 400 } | |
| 401 | |
| 2 | 402 /* TODO -- finish this; need to link between string and enum |
| 403 | |
| 404 pub async fn set_playback_order(&self, ) -> Result<(), anyhow::Error> | |
| 405 { | |
| 406 let p = self.set_player(&SetPlayer { | |
| 407 volume: None, | |
| 408 relative_volume: None, | |
| 409 muted: None, | |
| 410 position: None, | |
| 411 relative_position: None, | |
| 412 options: | |
| 413 }) | |
| 414 } | |
| 415 */ | |
| 416 | |
| 3 | 417 pub async fn playlist_items(&self, playlist_id: &str, offset: i64, count: i64, columns: &Vec<&str>) -> Result<PlaylistItems, anyhow::Error> |
| 0 | 418 { |
| 419 return Ok(self.client | |
| 420 .get(format!("{}/playlists/{}/items/{}:{}", self.base_url, playlist_id, offset, count)) | |
| 421 .query(&[("columns", columns.join(","))]) | |
| 422 .send() | |
| 423 .await? | |
| 424 .json::<PlaylistItemsHelper>() | |
| 425 .await? | |
| 426 .playlist_items); | |
| 427 } | |
| 428 | |
| 3 | 429 pub async fn playlist_item(&self, playlist_id: &str, index: i64, columns: &Vec<&str>) -> Result<PlaylistItems, anyhow::Error> |
| 430 { | |
| 431 return self.playlist_items(playlist_id, index, 1, columns).await; | |
| 432 } | |
| 433 | |
| 4 | 434 pub async fn playlists(&self) -> Result<Vec<Playlist>, anyhow::Error> |
| 435 { | |
| 436 return Ok(self.client | |
| 437 .get(format!("{}/playlists", self.base_url)) | |
| 438 .send() | |
| 439 .await? | |
| 440 .json::<Playlists>() | |
| 441 .await? | |
| 442 .playlists); | |
| 443 } | |
| 444 | |
| 0 | 445 pub async fn artwork(&self, playlist_id: &str, index: i64) -> Result<bytes::Bytes, anyhow::Error> |
| 446 { | |
| 447 return Ok(self.client | |
| 448 .get(format!("{}/artwork/{}/{}", self.base_url, playlist_id, index)) | |
| 449 .send() | |
| 450 .await? | |
| 451 .bytes() | |
| 452 .await?); | |
| 453 } | |
| 454 } |
