|
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
|
|
|
21 #[derive(serde::Serialize, serde::Deserialize, Debug)]
|
|
|
22 pub struct PlayerInfo {
|
|
|
23 pub name: String,
|
|
|
24 pub title: String,
|
|
|
25 pub version: String,
|
|
|
26 // fuck camel case
|
|
|
27 #[serde(rename = "pluginVersion")]
|
|
|
28 pub plugin_version: String,
|
|
|
29 }
|
|
|
30
|
|
|
31 #[derive(serde::Serialize, serde::Deserialize, Debug)]
|
|
|
32 pub struct ActiveItemInfo {
|
|
|
33 #[serde(rename = "playlistId")]
|
|
|
34 pub playlist_id: String,
|
|
|
35 #[serde(rename = "playlistIndex")]
|
|
|
36 pub playlist_index: i64,
|
|
|
37 pub index: i64,
|
|
|
38 pub position: f64,
|
|
|
39 pub duration: f64,
|
|
|
40 pub columns: Vec<String>,
|
|
|
41 }
|
|
|
42
|
|
|
43 #[derive(serde::Serialize, serde::Deserialize, Debug)]
|
|
|
44 pub enum VolumeType {
|
|
|
45 #[serde(rename = "db")]
|
|
|
46 DB,
|
|
|
47 #[serde(rename = "linear")]
|
|
|
48 LINEAR,
|
|
|
49 #[serde(rename = "upDown")]
|
|
|
50 UPDOWN, /* ??? */
|
|
|
51 }
|
|
|
52
|
|
|
53 #[derive(serde::Serialize, serde::Deserialize, Debug)]
|
|
|
54 pub struct VolumeInfo {
|
|
|
55 pub r#type: VolumeType,
|
|
|
56 pub min: f64,
|
|
|
57 pub max: f64,
|
|
|
58 pub value: f64,
|
|
|
59 #[serde(rename = "isMuted")]
|
|
|
60 pub muted: bool,
|
|
|
61 }
|
|
|
62
|
|
|
63 #[derive(serde::Serialize, serde::Deserialize, Debug)]
|
|
|
64 pub struct ApiPermissions {
|
|
|
65 #[serde(rename = "changePlaylists")]
|
|
|
66 pub change_playlists: bool,
|
|
|
67 #[serde(rename = "changeOutput")]
|
|
|
68 pub change_output: bool,
|
|
|
69 #[serde(rename = "changeClientConfig")]
|
|
|
70 pub change_client_config: bool,
|
|
|
71 }
|
|
|
72
|
|
|
73 #[derive(serde::Serialize, serde::Deserialize, Debug)]
|
|
|
74 pub struct Player {
|
|
|
75 pub info: PlayerInfo,
|
|
|
76 #[serde(rename = "activeItem")]
|
|
|
77 pub active_item: ActiveItemInfo,
|
|
|
78 /* XXX actually an enum */
|
|
|
79 #[serde(rename = "playbackState")]
|
|
|
80 pub playback_state: String,
|
|
|
81 pub volume: VolumeInfo,
|
|
|
82 pub permissions: ApiPermissions,
|
|
|
83 }
|
|
|
84
|
|
|
85 /* {"player": {struct Player}} */
|
|
|
86 #[derive(serde::Serialize, serde::Deserialize, Debug)]
|
|
|
87 struct PlayerHelper {
|
|
|
88 player: Player,
|
|
|
89 }
|
|
|
90
|
|
|
91 #[derive(serde::Serialize, serde::Deserialize, Debug)]
|
|
|
92 #[serde(untagged)]
|
|
|
93 enum SetPlayerOption {
|
|
|
94 Integer { id: String, value: i64 },
|
|
|
95 Boolean { id: String, value: bool },
|
|
|
96 }
|
|
|
97
|
|
|
98 #[derive(serde::Serialize, serde::Deserialize, Debug)]
|
|
|
99 #[serde_with::skip_serializing_none]
|
|
|
100 struct SetPlayer {
|
|
|
101 /* dB */
|
|
|
102 volume: Option<f64>,
|
|
|
103 /* ??? */
|
|
|
104 #[serde(rename = "relativeVolume")]
|
|
|
105 relative_volume: Option<f64>,
|
|
|
106 #[serde(rename = "isMuted")]
|
|
|
107 muted: Option<bool>,
|
|
|
108 position: Option<f64>,
|
|
|
109 #[serde(rename = "relativePosition")]
|
|
|
110 relative_position: Option<f64>,
|
|
|
111 /* teehee */
|
|
|
112 options: Option<Vec<SetPlayerOption>>,
|
|
|
113 }
|
|
|
114
|
|
|
115 #[derive(serde::Serialize, serde::Deserialize, Debug)]
|
|
|
116 pub struct PlaylistItem {
|
|
|
117 pub columns: Vec<String>
|
|
|
118 }
|
|
|
119
|
|
|
120 #[derive(serde::Serialize, serde::Deserialize, Debug)]
|
|
|
121 pub struct PlaylistItems {
|
|
|
122 pub offset: i64,
|
|
|
123 #[serde(rename = "totalCount")]
|
|
|
124 pub total_count: i64,
|
|
|
125 pub items: Vec<PlaylistItem>,
|
|
|
126 }
|
|
|
127
|
|
|
128 #[derive(serde::Serialize, serde::Deserialize, Debug)]
|
|
|
129 struct PlaylistItemsHelper {
|
|
|
130 #[serde(rename = "playlistItems")]
|
|
|
131 playlist_items: PlaylistItems,
|
|
|
132 }
|
|
|
133
|
|
|
134 /* --- NOW, THE ACTUAL BEEFWEB THING */
|
|
|
135
|
|
|
136 pub struct Beefweb {
|
|
|
137 client: reqwest::Client,
|
|
|
138 base_url: String,
|
|
|
139 }
|
|
|
140
|
|
|
141 impl Beefweb {
|
|
|
142 pub fn new(base: &str) -> Beefweb
|
|
|
143 {
|
|
|
144 return Beefweb {
|
|
|
145 client: reqwest::Client::new(),
|
|
|
146 base_url: base.to_string(),
|
|
|
147 };
|
|
|
148 }
|
|
|
149
|
|
|
150 pub async fn player(&self) -> Result<Player, anyhow::Error>
|
|
|
151 {
|
|
|
152 return Ok(self.client
|
|
|
153 .get(format!("{}/player", self.base_url))
|
|
|
154 .send()
|
|
|
155 .await?
|
|
|
156 .json::<PlayerHelper>()
|
|
|
157 .await?
|
|
|
158 .player);
|
|
|
159 }
|
|
|
160
|
|
|
161 pub async fn volume(&self) -> Result<VolumeInfo, anyhow::Error>
|
|
|
162 {
|
|
|
163 return Ok(self.player().await?.volume);
|
|
|
164 }
|
|
|
165
|
|
|
166 pub async fn position(&self) -> Result<f64, anyhow::Error>
|
|
|
167 {
|
|
|
168 return Ok(self.player().await?.active_item.position);
|
|
|
169 }
|
|
|
170
|
|
|
171 /* XXX might be able to use macros for this? idk --paper */
|
|
|
172 async fn play_pause_stop(&self, x: &str) -> Result<(), anyhow::Error>
|
|
|
173 {
|
|
|
174 self.client
|
|
|
175 .post(format!("{}/player/{}", self.base_url, x))
|
|
|
176 .send()
|
|
|
177 .await?;
|
|
|
178
|
|
|
179 return Ok(());
|
|
|
180 }
|
|
|
181
|
|
|
182 pub async fn play(&self) -> Result<(), anyhow::Error>
|
|
|
183 {
|
|
|
184 return self.play_pause_stop("play").await;
|
|
|
185 }
|
|
|
186
|
|
|
187 pub async fn pause(&self) -> Result<(), anyhow::Error>
|
|
|
188 {
|
|
|
189 return self.play_pause_stop("pause").await;
|
|
|
190 }
|
|
|
191
|
|
|
192 pub async fn stop(&self) -> Result<(), anyhow::Error>
|
|
|
193 {
|
|
|
194 return self.play_pause_stop("stop").await;
|
|
|
195 }
|
|
|
196
|
|
|
197 pub async fn play_pause(&self) -> Result<(), anyhow::Error>
|
|
|
198 {
|
|
|
199 return self.play_pause_stop("play-pause").await;
|
|
|
200 }
|
|
|
201
|
|
|
202 pub async fn next(&self) -> Result<(), anyhow::Error>
|
|
|
203 {
|
|
|
204 return self.play_pause_stop("next").await;
|
|
|
205 }
|
|
|
206
|
|
|
207 pub async fn previous(&self) -> Result<(), anyhow::Error>
|
|
|
208 {
|
|
|
209 return self.play_pause_stop("previous").await;
|
|
|
210 }
|
|
|
211
|
|
|
212 /* XXX what should this return? */
|
|
|
213 async fn set_player(&self, p: &SetPlayer) -> Result<(), anyhow::Error>
|
|
|
214 {
|
|
|
215 self.client
|
|
|
216 .post(format!("{}/player", self.base_url))
|
|
|
217 .json::<SetPlayer>(p)
|
|
|
218 .send()
|
|
|
219 .await?;
|
|
|
220
|
|
|
221 return Ok(());
|
|
|
222 }
|
|
|
223
|
|
|
224 pub async fn set_volume(&self, volume: f64) -> Result<(), anyhow::Error>
|
|
|
225 {
|
|
|
226 return self.set_player(&SetPlayer {
|
|
|
227 volume: Some(volume),
|
|
|
228 relative_volume: None,
|
|
|
229 muted: None,
|
|
|
230 position: None,
|
|
|
231 relative_position: None,
|
|
|
232 options: None,
|
|
|
233 }).await;
|
|
|
234 }
|
|
|
235
|
|
|
236 pub async fn set_position(&self, position: f64) -> Result<(), anyhow::Error>
|
|
|
237 {
|
|
|
238 return self.set_player(&SetPlayer {
|
|
|
239 volume: None,
|
|
|
240 relative_volume: Some(position),
|
|
|
241 muted: None,
|
|
|
242 position: None,
|
|
|
243 relative_position: None,
|
|
|
244 options: None,
|
|
|
245 }).await;
|
|
|
246 }
|
|
|
247
|
|
|
248 pub async fn playlist_items(&self, playlist_id: &str, offset: i64, count: i64, columns: Vec<&str>) -> Result<PlaylistItems, anyhow::Error>
|
|
|
249 {
|
|
|
250 return Ok(self.client
|
|
|
251 .get(format!("{}/playlists/{}/items/{}:{}", self.base_url, playlist_id, offset, count))
|
|
|
252 .query(&[("columns", columns.join(","))])
|
|
|
253 .send()
|
|
|
254 .await?
|
|
|
255 .json::<PlaylistItemsHelper>()
|
|
|
256 .await?
|
|
|
257 .playlist_items);
|
|
|
258 }
|
|
|
259
|
|
|
260 pub async fn artwork(&self, playlist_id: &str, index: i64) -> Result<bytes::Bytes, anyhow::Error>
|
|
|
261 {
|
|
|
262 return Ok(self.client
|
|
|
263 .get(format!("{}/artwork/{}/{}", self.base_url, playlist_id, index))
|
|
|
264 .send()
|
|
|
265 .await?
|
|
|
266 .bytes()
|
|
|
267 .await?);
|
|
|
268 }
|
|
|
269 }
|