Mercurial > beefweb_mpris
view src/player.rs @ 5:8f71820abe71 default tip
player: use winepath for URLs
this is optional, and it will be automagically disabled if winepath
got something invalid or whatever
| author | Paper <paper@tflc.us> |
|---|---|
| date | Sun, 05 Apr 2026 11:40:21 -0400 |
| parents | 26f695129c86 |
| children |
line wrap: on
line source
/* * Beefweb <-> mpris "compatibility" layer. * * Copyright (C) 2026 Paper * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see * <https://www.gnu.org/licenses/>. */ /* Fuck off, I don't care. */ #![allow(nonstandard_style)] #![allow(unused_variables)] use crate::beefweb; use zbus::fdo; use zbus::Result; use std::collections::HashMap; //use std::io::Write; use std::cell::RefCell; /* teehee */ pub struct BeefwebPlayer { bw: beefweb::Beefweb, wc: Option<winepath::WineConfig>, artcache: String, /* artmap * key: %path% column from beefweb * value: local path of the artwork * also LOL RUST FUCK */ artmap: RefCell<HashMap<String, String>>, } impl BeefwebPlayer { pub fn new(base: &str, artcache: &str) -> BeefwebPlayer { return BeefwebPlayer { bw: beefweb::Beefweb::new(base), /* teehee */ wc: winepath::WineConfig::from_env().ok(), artcache: artcache.to_string(), artmap: RefCell::new(HashMap::new()), }; } /* async fn get_artwork(&self, playlist_id: &str, index: i64, path: &str) -> fdo::Result<String> { match self.artmap.borrow().get(path) { Some(x) => return Ok(x.to_string()), _ => (), } /* Ok, art path isn't in the "cache". Ask beefweb for it. */ let art = self.bw.artwork(playlist_id, index); let artpath = format!("{}/{}", self.artcache, uuid::Uuid::new_v4()); /* XXX might be a good idea to check the bytes for an extension...? */ let fr = std::fs::OpenOptions::new() .write(true) .open(&artpath); match fr { Err(_) => return Err(fdo::Error::Failed("Failed to open file!".to_string())), _ => (), }; let mut f = fr.unwrap(); let artaw = art.await; match artaw { Err(_) => return Err(fdo::Error::Failed("Uh oh".to_string())), _ => (), }; f.write(artaw.unwrap().as_ref()); self.artmap.borrow_mut().insert(path.to_string(), artpath.to_string()); return Ok(artpath); } */ fn win32_path_to_file_url(&self, p: &str) -> fdo::Result<String> { match &(self.wc) { Some(_) => (), _ => { return Err(fdo::Error::Failed("Wine environment not loaded?".into())) }, }; let nps = self.wc.as_ref().unwrap(); println!("{:?}", nps.prefix()); match nps.to_native_path(p) { Ok(v) => { match v.to_str() { /* hax: don't kill slashes */ Some(vv) => return Ok(format!("file://{}", urlencoding::encode(vv).replace("%2F", "/"))), _ => (), }; }, /* TODO detect if this is a unix path and use it as such */ Err(x) => { println!("{}", x); }, }; return Err(fdo::Error::Failed("Converting to unix path failed...".into())); } } fn secs_to_time(x: f64) -> mpris_server::Time { return mpris_server::Time::from_micros((x * 1000000.0).round() as i64); } fn time_to_secs(x: mpris_server::Time) -> f64 { return (x.as_micros() as f64) / 1000000.0; } impl mpris_server::LocalRootInterface for BeefwebPlayer { async fn raise(&self) -> fdo::Result<()> { /* don't care */ return Ok(()); } async fn quit(&self) -> fdo::Result<()> { /* don't care */ return Ok(()); } async fn can_quit(&self) -> fdo::Result<bool> { /* don't care */ return Ok(false); } async fn fullscreen(&self) -> fdo::Result<bool> { /* don't care */ return Ok(false); } async fn set_fullscreen(&self, _fullscreen: bool) -> Result<()> { /* don't care */ return Ok(()); } async fn can_set_fullscreen(&self) -> fdo::Result<bool> { return Ok(false); } async fn can_raise(&self) -> fdo::Result<bool> { return Ok(false); } async fn has_track_list(&self) -> fdo::Result<bool> { /* ??? */ return Ok(false); } async fn identity(&self) -> fdo::Result<String> { /* TODO: allow changing this */ return Ok("beefweb".into()); } async fn desktop_entry(&self) -> fdo::Result<String> { return Ok("foobar2000".into()); } async fn supported_uri_schemes(&self) -> fdo::Result<Vec<String>> { return Ok([].to_vec()); } async fn supported_mime_types(&self) -> fdo::Result<Vec<String>> { /* needs moar */ return Ok([].to_vec()); } } impl mpris_server::LocalPlayerInterface for BeefwebPlayer { async fn next(&self) -> fdo::Result<()> { self.bw.next().await; return Ok(()); } async fn previous(&self) -> fdo::Result<()> { self.bw.previous().await; return Ok(()); } async fn pause(&self) -> fdo::Result<()> { self.bw.pause().await; return Ok(()); } async fn play_pause(&self) -> fdo::Result<()> { self.bw.play_pause().await; return Ok(()); } async fn stop(&self) -> fdo::Result<()> { self.bw.stop().await; return Ok(()); } async fn play(&self) -> fdo::Result<()> { self.bw.play().await; return Ok(()); } async fn seek(&self, offset: mpris_server::Time) -> fdo::Result<()> { let pl = self.bw.seek(time_to_secs(offset)).await; match pl { Err(_) => return Err(fdo::Error::Failed("uhoh".to_string())), _ => (), }; return Ok(()); } async fn position(&self) -> fdo::Result<mpris_server::Time> { let pl = self.bw.player().await; match pl { Err(_) => return Err(fdo::Error::Failed("uhoh".to_string())), _ => (), }; return Ok(secs_to_time(pl.unwrap().active_item.position)); } async fn playback_status(&self) -> fdo::Result<mpris_server::PlaybackStatus> { let p = self.bw.player().await; match p { Err(_) => return Err(fdo::Error::Failed("wtf".to_string())), _ => (), }; return match p.unwrap().playback_state { beefweb::PlaybackState::PLAYING => Ok(mpris_server::PlaybackStatus::Playing), beefweb::PlaybackState::STOPPED => Ok(mpris_server::PlaybackStatus::Stopped), beefweb::PlaybackState::PAUSED => Ok(mpris_server::PlaybackStatus::Paused), }; } async fn loop_status(&self) -> fdo::Result<mpris_server::LoopStatus> { /* TODO -- we can do this w/ self.bw.playback_order() */ return Ok(mpris_server::LoopStatus::None); } async fn set_loop_status(&self, loop_status: mpris_server::LoopStatus) -> Result<()> { /* TODO -- implement self.bw.set_playback_order() */ return Ok(()); } async fn shuffle(&self) -> fdo::Result<bool> { /* TODO -- we can do this w/ self.bw.playback_order() */ println!("Shuffle"); return Ok(false); } async fn set_shuffle(&self, shuffle: bool) -> Result<()> { /* TODO -- implement self.bw.set_playback_order() */ println!("SetShuffle({shuffle})"); return Ok(()); } async fn metadata(&self) -> fdo::Result<mpris_server::Metadata> { let pl = self.bw.player().await; match pl { Err(_) => return Err(fdo::Error::Failed("uhoh".to_string())), _ => (), }; let p = pl.unwrap(); 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; match playlist_items_result { Err(_) => return Err(fdo::Error::Failed("uhoh".to_string())), _ => (), }; let playlist_items = playlist_items_result.unwrap(); let track = playlist_items.items.get(0).unwrap(); /* let artwork = self.get_artwork(p.active_item.playlist_id.as_str(), p.active_item.index, track.columns.get(6).unwrap()); */ let mut x = mpris_server::Metadata::new(); x.set_length(Some(secs_to_time(p.active_item.duration))); if track.columns.len() >= 13 { let path = track.columns.get(6).unwrap(); x.set_title(Some(track.columns.get(0).unwrap())); /* XXX musicbrainz has %artists% we can use for a proper list */ x.set_artist(Some([track.columns.get(1).unwrap()])); x.set_album(Some(track.columns.get(2).unwrap())); match track.columns.get(3).unwrap().parse::<i32>() { Ok(v) => { x.set_disc_number(Some(v)); }, _ => (), }; match track.columns.get(4).unwrap().parse::<i32>() { Ok(v) => { x.set_track_number(Some(v)); }, _ => (), }; x.set_album_artist(Some([track.columns.get(5).unwrap()])); x.set_trackid(Some(mpris_server::TrackId::from(zbus::zvariant::ObjectPath::from_string_unchecked(format!("/org/foobar2000/foobar2000/trackids/{}", hex::encode(path)))))); /* Why is this an i32 ??? It would make more sense as f32 or f64 */ match track.columns.get(7).unwrap().parse::<i32>() { Ok(v) => { x.set_audio_bpm(Some(v)) }, _ => (), }; x.set_composer(Some([track.columns.get(8).unwrap()])); x.set_comment(Some([track.columns.get(9).unwrap()])); x.set_content_created(Some(track.columns.get(10).unwrap())); x.set_genre(Some(track.columns.get(11).unwrap().split(";"))); x.set_lyricist(Some([track.columns.get(12).unwrap()])); /* add file:// URI */ match self.win32_path_to_file_url(path) { Ok(a) => { x.set_url(Some(a)); }, /* If it failed, it's more than likely a URL already, * pointing to a stream */ Err(_) => { x.set_url(Some(path)); }, }; } /* return match artwork.await { Ok(x) => Ok(builder.art_url(urlencoding::encode(format!("file://{}", x).as_str())).build()), _ => Ok(builder.build()), }; */ return Ok(x); } async fn volume(&self) -> fdo::Result<mpris_server::Volume> { let vr = self.bw.volume().await; match vr { Err(_) => return Err(fdo::Error::Failed("uhoh".to_string())), _ => (), } let v = vr.unwrap(); /* dB -> linear */ return Ok(match v.r#type { beefweb::VolumeType::DB => 10.0_f64.powf(v.value / 20.0), beefweb::VolumeType::LINEAR => v.value, beefweb::VolumeType::UPDOWN => /* ??? */ v.value, }); } async fn set_volume(&self, volume: mpris_server::Volume) -> Result<()> { /* linear -> dB */ let v = 20.0 * volume.log10(); match self.bw.set_volume(v).await { Err(_) => return Err(zbus::Error::Failure("uhoh".to_string())), _ => (), } return Ok(()); } /* "can" functions -- all work */ async fn can_go_next(&self) -> fdo::Result<bool> { return Ok(true); } async fn can_go_previous(&self) -> fdo::Result<bool> { return Ok(true); } async fn can_play(&self) -> fdo::Result<bool> { return Ok(true); } async fn can_pause(&self) -> fdo::Result<bool> { return Ok(true); } async fn can_seek(&self) -> fdo::Result<bool> { return Ok(true); } async fn can_control(&self) -> fdo::Result<bool> { return Ok(true); } /* --- UNSUPPORTED */ async fn rate(&self) -> fdo::Result<mpris_server::PlaybackRate> { return Ok(mpris_server::PlaybackRate::default()); } async fn set_rate(&self, rate: mpris_server::PlaybackRate) -> Result<()> { return Ok(()); } async fn minimum_rate(&self) -> fdo::Result<mpris_server::PlaybackRate> { return Ok(mpris_server::PlaybackRate::default()); } async fn maximum_rate(&self) -> fdo::Result<mpris_server::PlaybackRate> { return Ok(mpris_server::PlaybackRate::default()); } /* FIXME to implement this we would have to search through each playlist * for the track, and if it doesn't exist, append it to the current playlist. * * THEN we can add it directly into the play queue. */ async fn set_position(&self, track_id: mpris_server::TrackId, position: mpris_server::Time) -> fdo::Result<()> { return Ok(()); } /* TODO: * * We can effectively implement this "proper" by detecting a file:// * path, URL decoding it, and prepending "Z:". */ async fn open_uri(&self, uri: String) -> fdo::Result<()> { return Ok(()); } } /* -- unfinished impl, don't mind this impl mpris_server::LocalPlaylistsInterface for BeefwebPlayer { async fn activate_playlist(&self, playlist_id: mpris_server::PlaylistId) -> fdo::Result<()> { return Err(fdo::Error::Failed("uhoh".to_string())); } async fn get_playlists(&self, index: u32, max_count: u32, order: mpris_server::PlaylistOrdering, reverse_order: bool) -> fdo::Result<Vec<mpris_server::Playlist>> { let mut v: Vec<mpris_server::Playlist> = Vec::new(); let playlists = self.bw.playlists().await; match playlists { Ok(ref v) => (), _ => return Err(fdo::Error::Failed("req failed".to_string())), } for playlist in playlists.unwrap() { } return Err(fdo::Error::Failed("uhoh".to_string())); } async fn playlist_count(&self) -> fdo::Result<u32> { return Err(fdo::Error::Failed("unimpl".to_string())); } async fn orderings(&self) -> fdo::Result<Vec<mpris_server::PlaylistOrdering>> { todo!() } async fn active_playlist(&self) -> fdo::Result<Option<mpris_server::Playlist>> { todo!() } } */
