|
1
|
1 /*
|
|
|
2 * Beefweb <-> mpris "compatibility" layer.
|
|
|
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
|
|
5
|
21 /* Fuck off, I don't care. */
|
|
|
22 #![allow(nonstandard_style)]
|
|
|
23 #![allow(unused_variables)]
|
|
|
24
|
|
0
|
25 use crate::beefweb;
|
|
|
26
|
|
|
27 use zbus::fdo;
|
|
|
28 use zbus::Result;
|
|
|
29 use std::collections::HashMap;
|
|
2
|
30 //use std::io::Write;
|
|
0
|
31 use std::cell::RefCell;
|
|
|
32
|
|
|
33 /* teehee */
|
|
|
34 pub struct BeefwebPlayer {
|
|
|
35 bw: beefweb::Beefweb,
|
|
5
|
36 wc: Option<winepath::WineConfig>,
|
|
0
|
37 artcache: String,
|
|
|
38 /* artmap
|
|
|
39 * key: %path% column from beefweb
|
|
|
40 * value: local path of the artwork
|
|
|
41 * also LOL RUST FUCK */
|
|
|
42 artmap: RefCell<HashMap<String, String>>,
|
|
|
43 }
|
|
|
44
|
|
|
45 impl BeefwebPlayer {
|
|
|
46 pub fn new(base: &str, artcache: &str) -> BeefwebPlayer
|
|
|
47 {
|
|
|
48 return BeefwebPlayer {
|
|
|
49 bw: beefweb::Beefweb::new(base),
|
|
5
|
50 /* teehee */
|
|
|
51 wc: winepath::WineConfig::from_env().ok(),
|
|
0
|
52 artcache: artcache.to_string(),
|
|
|
53 artmap: RefCell::new(HashMap::new()),
|
|
|
54 };
|
|
|
55 }
|
|
|
56
|
|
|
57 /*
|
|
|
58 async fn get_artwork(&self, playlist_id: &str, index: i64, path: &str) -> fdo::Result<String>
|
|
|
59 {
|
|
|
60 match self.artmap.borrow().get(path) {
|
|
|
61 Some(x) => return Ok(x.to_string()),
|
|
|
62 _ => (),
|
|
|
63 }
|
|
|
64
|
|
|
65 /* Ok, art path isn't in the "cache". Ask beefweb for it. */
|
|
|
66 let art = self.bw.artwork(playlist_id, index);
|
|
|
67 let artpath = format!("{}/{}", self.artcache, uuid::Uuid::new_v4());
|
|
|
68
|
|
|
69 /* XXX might be a good idea to check the bytes for an extension...? */
|
|
|
70 let fr = std::fs::OpenOptions::new()
|
|
|
71 .write(true)
|
|
|
72 .open(&artpath);
|
|
|
73
|
|
|
74 match fr {
|
|
|
75 Err(_) => return Err(fdo::Error::Failed("Failed to open file!".to_string())),
|
|
|
76 _ => (),
|
|
|
77 };
|
|
|
78
|
|
|
79 let mut f = fr.unwrap();
|
|
|
80
|
|
|
81 let artaw = art.await;
|
|
|
82
|
|
|
83 match artaw {
|
|
|
84 Err(_) => return Err(fdo::Error::Failed("Uh oh".to_string())),
|
|
|
85 _ => (),
|
|
|
86 };
|
|
|
87
|
|
|
88 f.write(artaw.unwrap().as_ref());
|
|
|
89
|
|
|
90 self.artmap.borrow_mut().insert(path.to_string(), artpath.to_string());
|
|
|
91
|
|
|
92 return Ok(artpath);
|
|
|
93 }
|
|
|
94 */
|
|
5
|
95
|
|
|
96 fn win32_path_to_file_url(&self, p: &str) -> fdo::Result<String>
|
|
|
97 {
|
|
|
98 match &(self.wc) {
|
|
|
99 Some(_) => (),
|
|
|
100 _ => { return Err(fdo::Error::Failed("Wine environment not loaded?".into())) },
|
|
|
101 };
|
|
|
102
|
|
|
103 let nps = self.wc.as_ref().unwrap();
|
|
|
104
|
|
|
105 println!("{:?}", nps.prefix());
|
|
|
106
|
|
|
107 match nps.to_native_path(p) {
|
|
|
108 Ok(v) => {
|
|
|
109 match v.to_str() {
|
|
|
110 /* hax: don't kill slashes */
|
|
|
111 Some(vv) => return Ok(format!("file://{}", urlencoding::encode(vv).replace("%2F", "/"))),
|
|
|
112 _ => (),
|
|
|
113 };
|
|
|
114 },
|
|
|
115 /* TODO detect if this is a unix path and use it as such */
|
|
|
116 Err(x) => { println!("{}", x); },
|
|
|
117 };
|
|
|
118
|
|
|
119 return Err(fdo::Error::Failed("Converting to unix path failed...".into()));
|
|
|
120 }
|
|
0
|
121 }
|
|
|
122
|
|
|
123 fn secs_to_time(x: f64) -> mpris_server::Time
|
|
|
124 {
|
|
|
125 return mpris_server::Time::from_micros((x * 1000000.0).round() as i64);
|
|
|
126 }
|
|
|
127
|
|
|
128 fn time_to_secs(x: mpris_server::Time) -> f64
|
|
|
129 {
|
|
|
130 return (x.as_micros() as f64) / 1000000.0;
|
|
|
131 }
|
|
|
132
|
|
|
133 impl mpris_server::LocalRootInterface for BeefwebPlayer {
|
|
|
134 async fn raise(&self) -> fdo::Result<()>
|
|
|
135 {
|
|
|
136 /* don't care */
|
|
|
137 return Ok(());
|
|
|
138 }
|
|
|
139
|
|
|
140 async fn quit(&self) -> fdo::Result<()>
|
|
|
141 {
|
|
|
142 /* don't care */
|
|
|
143 return Ok(());
|
|
|
144 }
|
|
|
145
|
|
|
146 async fn can_quit(&self) -> fdo::Result<bool>
|
|
|
147 {
|
|
|
148 /* don't care */
|
|
|
149 return Ok(false);
|
|
|
150 }
|
|
|
151
|
|
|
152 async fn fullscreen(&self) -> fdo::Result<bool>
|
|
|
153 {
|
|
|
154 /* don't care */
|
|
|
155 return Ok(false);
|
|
|
156 }
|
|
|
157
|
|
|
158 async fn set_fullscreen(&self, _fullscreen: bool) -> Result<()>
|
|
|
159 {
|
|
|
160 /* don't care */
|
|
|
161 return Ok(());
|
|
|
162 }
|
|
|
163
|
|
|
164 async fn can_set_fullscreen(&self) -> fdo::Result<bool>
|
|
|
165 {
|
|
|
166 return Ok(false);
|
|
|
167 }
|
|
|
168
|
|
|
169 async fn can_raise(&self) -> fdo::Result<bool>
|
|
|
170 {
|
|
|
171 return Ok(false);
|
|
|
172 }
|
|
|
173
|
|
|
174 async fn has_track_list(&self) -> fdo::Result<bool>
|
|
|
175 {
|
|
|
176 /* ??? */
|
|
|
177 return Ok(false);
|
|
|
178 }
|
|
|
179
|
|
|
180 async fn identity(&self) -> fdo::Result<String>
|
|
|
181 {
|
|
|
182 /* TODO: allow changing this */
|
|
|
183 return Ok("beefweb".into());
|
|
|
184 }
|
|
|
185
|
|
|
186 async fn desktop_entry(&self) -> fdo::Result<String>
|
|
|
187 {
|
|
|
188 return Ok("foobar2000".into());
|
|
|
189 }
|
|
|
190
|
|
|
191 async fn supported_uri_schemes(&self) -> fdo::Result<Vec<String>>
|
|
|
192 {
|
|
|
193 return Ok([].to_vec());
|
|
|
194 }
|
|
|
195
|
|
|
196 async fn supported_mime_types(&self) -> fdo::Result<Vec<String>>
|
|
|
197 {
|
|
|
198 /* needs moar */
|
|
|
199 return Ok([].to_vec());
|
|
|
200 }
|
|
|
201 }
|
|
|
202
|
|
|
203 impl mpris_server::LocalPlayerInterface for BeefwebPlayer {
|
|
|
204 async fn next(&self) -> fdo::Result<()>
|
|
|
205 {
|
|
|
206 self.bw.next().await;
|
|
|
207 return Ok(());
|
|
|
208 }
|
|
|
209
|
|
|
210 async fn previous(&self) -> fdo::Result<()>
|
|
|
211 {
|
|
|
212 self.bw.previous().await;
|
|
|
213 return Ok(());
|
|
|
214 }
|
|
|
215
|
|
|
216 async fn pause(&self) -> fdo::Result<()>
|
|
|
217 {
|
|
|
218 self.bw.pause().await;
|
|
|
219 return Ok(());
|
|
|
220 }
|
|
|
221
|
|
|
222 async fn play_pause(&self) -> fdo::Result<()>
|
|
|
223 {
|
|
|
224 self.bw.play_pause().await;
|
|
|
225 return Ok(());
|
|
|
226 }
|
|
|
227
|
|
|
228 async fn stop(&self) -> fdo::Result<()>
|
|
|
229 {
|
|
|
230 self.bw.stop().await;
|
|
|
231 return Ok(());
|
|
|
232 }
|
|
|
233
|
|
|
234 async fn play(&self) -> fdo::Result<()>
|
|
|
235 {
|
|
|
236 self.bw.play().await;
|
|
|
237 return Ok(());
|
|
|
238 }
|
|
|
239
|
|
|
240 async fn seek(&self, offset: mpris_server::Time) -> fdo::Result<()>
|
|
|
241 {
|
|
4
|
242 let pl = self.bw.seek(time_to_secs(offset)).await;
|
|
0
|
243
|
|
|
244 match pl {
|
|
|
245 Err(_) => return Err(fdo::Error::Failed("uhoh".to_string())),
|
|
|
246 _ => (),
|
|
|
247 };
|
|
|
248
|
|
|
249 return Ok(());
|
|
|
250 }
|
|
|
251
|
|
|
252 async fn position(&self) -> fdo::Result<mpris_server::Time>
|
|
|
253 {
|
|
|
254 let pl = self.bw.player().await;
|
|
|
255
|
|
|
256 match pl {
|
|
|
257 Err(_) => return Err(fdo::Error::Failed("uhoh".to_string())),
|
|
|
258 _ => (),
|
|
|
259 };
|
|
|
260
|
|
|
261 return Ok(secs_to_time(pl.unwrap().active_item.position));
|
|
|
262 }
|
|
|
263
|
|
|
264 async fn playback_status(&self) -> fdo::Result<mpris_server::PlaybackStatus>
|
|
|
265 {
|
|
|
266 let p = self.bw.player().await;
|
|
|
267
|
|
|
268 match p {
|
|
|
269 Err(_) => return Err(fdo::Error::Failed("wtf".to_string())),
|
|
|
270 _ => (),
|
|
|
271 };
|
|
|
272
|
|
2
|
273 return match p.unwrap().playback_state {
|
|
|
274 beefweb::PlaybackState::PLAYING => Ok(mpris_server::PlaybackStatus::Playing),
|
|
|
275 beefweb::PlaybackState::STOPPED => Ok(mpris_server::PlaybackStatus::Stopped),
|
|
|
276 beefweb::PlaybackState::PAUSED => Ok(mpris_server::PlaybackStatus::Paused),
|
|
0
|
277 };
|
|
|
278 }
|
|
|
279
|
|
|
280 async fn loop_status(&self) -> fdo::Result<mpris_server::LoopStatus>
|
|
|
281 {
|
|
2
|
282 /* TODO -- we can do this w/ self.bw.playback_order() */
|
|
0
|
283 return Ok(mpris_server::LoopStatus::None);
|
|
|
284 }
|
|
|
285
|
|
|
286 async fn set_loop_status(&self, loop_status: mpris_server::LoopStatus) -> Result<()>
|
|
|
287 {
|
|
2
|
288 /* TODO -- implement self.bw.set_playback_order() */
|
|
0
|
289 return Ok(());
|
|
|
290 }
|
|
|
291
|
|
|
292 async fn shuffle(&self) -> fdo::Result<bool>
|
|
|
293 {
|
|
2
|
294 /* TODO -- we can do this w/ self.bw.playback_order() */
|
|
0
|
295 println!("Shuffle");
|
|
|
296 return Ok(false);
|
|
|
297 }
|
|
|
298
|
|
|
299 async fn set_shuffle(&self, shuffle: bool) -> Result<()>
|
|
|
300 {
|
|
2
|
301 /* TODO -- implement self.bw.set_playback_order() */
|
|
0
|
302 println!("SetShuffle({shuffle})");
|
|
|
303 return Ok(());
|
|
|
304 }
|
|
|
305
|
|
|
306 async fn metadata(&self) -> fdo::Result<mpris_server::Metadata>
|
|
|
307 {
|
|
|
308 let pl = self.bw.player().await;
|
|
|
309
|
|
|
310 match pl {
|
|
|
311 Err(_) => return Err(fdo::Error::Failed("uhoh".to_string())),
|
|
|
312 _ => (),
|
|
|
313 };
|
|
|
314
|
|
|
315 let p = pl.unwrap();
|
|
|
316
|
|
3
|
317 let playlist_items_result = self.bw.playlist_item(p.active_item.playlist_id.as_str(), p.active_item.index, &["%title%", "%artist%", "%album%", "%discnumber%", "%tracknumber%", "%album artist%", "%path%", "%bpm%", "%composer%", "%comment%", "%date%", "%genre%", "%lyricist%"].to_vec()).await;
|
|
0
|
318
|
|
|
319 match playlist_items_result {
|
|
|
320 Err(_) => return Err(fdo::Error::Failed("uhoh".to_string())),
|
|
|
321 _ => (),
|
|
|
322 };
|
|
|
323
|
|
|
324 let playlist_items = playlist_items_result.unwrap();
|
|
|
325
|
|
|
326 let track = playlist_items.items.get(0).unwrap();
|
|
|
327
|
|
|
328 /*
|
|
|
329 let artwork = self.get_artwork(p.active_item.playlist_id.as_str(), p.active_item.index, track.columns.get(6).unwrap());
|
|
|
330 */
|
|
|
331
|
|
3
|
332 let mut x = mpris_server::Metadata::new();
|
|
|
333
|
|
|
334 x.set_length(Some(secs_to_time(p.active_item.duration)));
|
|
|
335 if track.columns.len() >= 13 {
|
|
5
|
336 let path = track.columns.get(6).unwrap();
|
|
|
337
|
|
3
|
338 x.set_title(Some(track.columns.get(0).unwrap()));
|
|
|
339 /* XXX musicbrainz has %artists% we can use for a proper list */
|
|
|
340 x.set_artist(Some([track.columns.get(1).unwrap()]));
|
|
|
341 x.set_album(Some(track.columns.get(2).unwrap()));
|
|
|
342 match track.columns.get(3).unwrap().parse::<i32>() {
|
|
|
343 Ok(v) => { x.set_disc_number(Some(v)); },
|
|
|
344 _ => (),
|
|
|
345 };
|
|
|
346 match track.columns.get(4).unwrap().parse::<i32>() {
|
|
|
347 Ok(v) => { x.set_track_number(Some(v)); },
|
|
|
348 _ => (),
|
|
|
349 };
|
|
|
350 x.set_album_artist(Some([track.columns.get(5).unwrap()]));
|
|
5
|
351 x.set_trackid(Some(mpris_server::TrackId::from(zbus::zvariant::ObjectPath::from_string_unchecked(format!("/org/foobar2000/foobar2000/trackids/{}", hex::encode(path))))));
|
|
3
|
352 /* Why is this an i32 ??? It would make more sense as f32 or f64 */
|
|
|
353 match track.columns.get(7).unwrap().parse::<i32>() {
|
|
|
354 Ok(v) => { x.set_audio_bpm(Some(v)) },
|
|
|
355 _ => (),
|
|
|
356 };
|
|
|
357 x.set_composer(Some([track.columns.get(8).unwrap()]));
|
|
|
358 x.set_comment(Some([track.columns.get(9).unwrap()]));
|
|
|
359 x.set_content_created(Some(track.columns.get(10).unwrap()));
|
|
|
360 x.set_genre(Some(track.columns.get(11).unwrap().split(";")));
|
|
|
361 x.set_lyricist(Some([track.columns.get(12).unwrap()]));
|
|
5
|
362
|
|
|
363 /* add file:// URI */
|
|
|
364 match self.win32_path_to_file_url(path) {
|
|
|
365 Ok(a) => { x.set_url(Some(a)); },
|
|
|
366 /* If it failed, it's more than likely a URL already,
|
|
|
367 * pointing to a stream */
|
|
|
368 Err(_) => { x.set_url(Some(path)); },
|
|
|
369 };
|
|
3
|
370 }
|
|
0
|
371
|
|
|
372 /*
|
|
|
373 return match artwork.await {
|
|
|
374 Ok(x) => Ok(builder.art_url(urlencoding::encode(format!("file://{}", x).as_str())).build()),
|
|
|
375 _ => Ok(builder.build()),
|
|
|
376 };
|
|
|
377 */
|
|
3
|
378 return Ok(x);
|
|
0
|
379 }
|
|
|
380
|
|
|
381 async fn volume(&self) -> fdo::Result<mpris_server::Volume>
|
|
|
382 {
|
|
2
|
383 let vr = self.bw.volume().await;
|
|
|
384
|
|
|
385 match vr {
|
|
|
386 Err(_) => return Err(fdo::Error::Failed("uhoh".to_string())),
|
|
|
387 _ => (),
|
|
|
388 }
|
|
|
389
|
|
|
390 let v = vr.unwrap();
|
|
|
391
|
|
|
392 /* dB -> linear */
|
|
|
393 return Ok(match v.r#type {
|
|
|
394 beefweb::VolumeType::DB => 10.0_f64.powf(v.value / 20.0),
|
|
|
395 beefweb::VolumeType::LINEAR => v.value,
|
|
|
396 beefweb::VolumeType::UPDOWN => /* ??? */ v.value,
|
|
|
397 });
|
|
0
|
398 }
|
|
|
399
|
|
|
400 async fn set_volume(&self, volume: mpris_server::Volume) -> Result<()>
|
|
|
401 {
|
|
2
|
402 /* linear -> dB */
|
|
|
403 let v = 20.0 * volume.log10();
|
|
|
404
|
|
|
405 match self.bw.set_volume(v).await {
|
|
|
406 Err(_) => return Err(zbus::Error::Failure("uhoh".to_string())),
|
|
|
407 _ => (),
|
|
|
408 }
|
|
|
409
|
|
0
|
410 return Ok(());
|
|
|
411 }
|
|
|
412
|
|
|
413 /* "can" functions -- all work */
|
|
|
414
|
|
|
415 async fn can_go_next(&self) -> fdo::Result<bool>
|
|
|
416 {
|
|
|
417 return Ok(true);
|
|
|
418 }
|
|
|
419
|
|
|
420 async fn can_go_previous(&self) -> fdo::Result<bool>
|
|
|
421 {
|
|
|
422 return Ok(true);
|
|
|
423 }
|
|
|
424
|
|
|
425 async fn can_play(&self) -> fdo::Result<bool>
|
|
|
426 {
|
|
|
427 return Ok(true);
|
|
|
428 }
|
|
|
429
|
|
|
430 async fn can_pause(&self) -> fdo::Result<bool>
|
|
|
431 {
|
|
|
432 return Ok(true);
|
|
|
433 }
|
|
|
434
|
|
|
435 async fn can_seek(&self) -> fdo::Result<bool>
|
|
|
436 {
|
|
|
437 return Ok(true);
|
|
|
438 }
|
|
|
439
|
|
|
440 async fn can_control(&self) -> fdo::Result<bool>
|
|
|
441 {
|
|
|
442 return Ok(true);
|
|
|
443 }
|
|
|
444
|
|
|
445 /* --- UNSUPPORTED */
|
|
|
446
|
|
|
447 async fn rate(&self) -> fdo::Result<mpris_server::PlaybackRate>
|
|
|
448 {
|
|
|
449 return Ok(mpris_server::PlaybackRate::default());
|
|
|
450 }
|
|
|
451
|
|
|
452 async fn set_rate(&self, rate: mpris_server::PlaybackRate) -> Result<()>
|
|
|
453 {
|
|
|
454 return Ok(());
|
|
|
455 }
|
|
|
456
|
|
|
457 async fn minimum_rate(&self) -> fdo::Result<mpris_server::PlaybackRate>
|
|
|
458 {
|
|
|
459 return Ok(mpris_server::PlaybackRate::default());
|
|
|
460 }
|
|
|
461
|
|
|
462 async fn maximum_rate(&self) -> fdo::Result<mpris_server::PlaybackRate>
|
|
|
463 {
|
|
|
464 return Ok(mpris_server::PlaybackRate::default());
|
|
|
465 }
|
|
|
466
|
|
4
|
467 /* FIXME to implement this we would have to search through each playlist
|
|
|
468 * for the track, and if it doesn't exist, append it to the current playlist.
|
|
|
469 *
|
|
|
470 * THEN we can add it directly into the play queue. */
|
|
0
|
471 async fn set_position(&self, track_id: mpris_server::TrackId, position: mpris_server::Time) -> fdo::Result<()>
|
|
|
472 {
|
|
|
473 return Ok(());
|
|
|
474 }
|
|
|
475
|
|
4
|
476 /* TODO:
|
|
|
477 *
|
|
|
478 * We can effectively implement this "proper" by detecting a file://
|
|
|
479 * path, URL decoding it, and prepending "Z:". */
|
|
0
|
480 async fn open_uri(&self, uri: String) -> fdo::Result<()>
|
|
|
481 {
|
|
|
482 return Ok(());
|
|
|
483 }
|
|
|
484 }
|
|
4
|
485
|
|
|
486 /* -- unfinished impl, don't mind this
|
|
|
487
|
|
|
488
|
|
|
489 impl mpris_server::LocalPlaylistsInterface for BeefwebPlayer {
|
|
|
490 async fn activate_playlist(&self, playlist_id: mpris_server::PlaylistId) -> fdo::Result<()>
|
|
|
491 {
|
|
|
492 return Err(fdo::Error::Failed("uhoh".to_string()));
|
|
|
493 }
|
|
|
494
|
|
|
495 async fn get_playlists(&self, index: u32, max_count: u32, order: mpris_server::PlaylistOrdering, reverse_order: bool) -> fdo::Result<Vec<mpris_server::Playlist>>
|
|
|
496 {
|
|
|
497 let mut v: Vec<mpris_server::Playlist> = Vec::new();
|
|
|
498
|
|
|
499 let playlists = self.bw.playlists().await;
|
|
|
500
|
|
|
501 match playlists {
|
|
|
502 Ok(ref v) => (),
|
|
|
503 _ => return Err(fdo::Error::Failed("req failed".to_string())),
|
|
|
504 }
|
|
|
505
|
|
|
506 for playlist in playlists.unwrap() {
|
|
|
507
|
|
|
508 }
|
|
|
509
|
|
|
510 return Err(fdo::Error::Failed("uhoh".to_string()));
|
|
|
511 }
|
|
|
512
|
|
|
513 async fn playlist_count(&self) -> fdo::Result<u32>
|
|
|
514 {
|
|
|
515 return Err(fdo::Error::Failed("unimpl".to_string()));
|
|
|
516 }
|
|
|
517
|
|
|
518 async fn orderings(&self) -> fdo::Result<Vec<mpris_server::PlaylistOrdering>>
|
|
|
519 {
|
|
|
520 todo!()
|
|
|
521 }
|
|
|
522
|
|
|
523 async fn active_playlist(&self) -> fdo::Result<Option<mpris_server::Playlist>>
|
|
|
524 {
|
|
|
525 todo!()
|
|
|
526 }
|
|
|
527 }
|
|
|
528 */
|