use crate::app::AppCore;
use crate::events::AppEvent;
use crate::library::Track;
use gtk::glib;
use mpris_server::zbus::zvariant::ObjectPath;
use mpris_server::{LoopStatus, Metadata, PlaybackStatus, Player, Time};
use std::path::PathBuf;
use std::rc::Rc;
pub fn start(core: &Rc<AppCore>) {
let core = Rc::clone(core);
glib::spawn_future_local(async move {
let bus_suffix = if crate::config::demo_mode() {
"cc.midgarcorp.Flaccy.Demo"
} else {
"cc.midgarcorp.Flaccy"
};
let player = match Player::builder(bus_suffix)
.identity("Flaccy")
.desktop_entry("cc.midgarcorp.Flaccy")
.can_play(true)
.can_pause(true)
.can_go_next(true)
.can_go_previous(true)
.can_seek(true)
.can_control(true)
.build()
.await
{
Ok(player) => Rc::new(player),
Err(err) => {
crate::logger::warn("mpris", &format!("mpris unavailable: {err}"));
return;
}
};
{
let core = Rc::clone(&core);
player.connect_play_pause(move |_| core.toggle_play_pause());
}
{
let core = Rc::clone(&core);
player.connect_play(move |_| {
if !core.player.is_playing() {
core.toggle_play_pause();
}
});
}
{
let core = Rc::clone(&core);
player.connect_pause(move |_| {
if core.player.is_playing() {
core.toggle_play_pause();
}
});
}
{
let core = Rc::clone(&core);
player.connect_next(move |_| core.next());
}
{
let core = Rc::clone(&core);
player.connect_previous(move |_| core.previous());
}
{
let core = Rc::clone(&core);
player.connect_seek(move |_, offset| {
let position = core.player.position().unwrap_or(0.0);
core.player.seek(position + offset.as_secs() as f64);
});
}
{
let core = Rc::clone(&core);
player.connect_set_position(move |_, _, position| {
core.player.seek(position.as_millis() as f64 / 1000.0);
});
}
{
let core = Rc::clone(&core);
player.connect_set_volume(move |_, volume| {
core.set_volume(volume.clamp(0.0, 1.0));
});
}
{
let core = Rc::clone(&core);
player.connect_set_shuffle(move |_, wanted| {
if core.player.shuffle_enabled() != wanted {
core.player.toggle_shuffle();
}
});
}
{
let core = Rc::clone(&core);
player.connect_set_loop_status(move |_, wanted| {
let target = match wanted {
LoopStatus::None => crate::player::RepeatMode::Off,
LoopStatus::Playlist => crate::player::RepeatMode::All,
LoopStatus::Track => crate::player::RepeatMode::One,
};
for _ in 0..3 {
if core.player.repeat_mode() == target {
break;
}
core.player.cycle_repeat();
}
});
}
glib::spawn_future_local(player.run());
*core.mpris.borrow_mut() = Some(Rc::clone(&player));
let hub = Rc::clone(&core.hub);
let weak_core = Rc::downgrade(&core);
hub.subscribe(move |event| {
let Some(core) = weak_core.upgrade() else { return false };
let Some(player) = core.mpris.borrow().clone() else { return true };
match event {
AppEvent::TrackChanged(track) => {
let metadata = metadata_for(&core, track.as_ref());
glib::spawn_future_local(async move {
let _ = player.set_metadata(metadata).await;
});
}
AppEvent::PlayingChanged(playing) => {
let status = if *playing {
PlaybackStatus::Playing
} else {
PlaybackStatus::Paused
};
glib::spawn_future_local(async move {
let _ = player.set_playback_status(status).await;
});
}
AppEvent::Tick { position, .. } => {
player.set_position(Time::from_millis((*position * 1000.0) as i64));
}
AppEvent::Seeked(position) => {
let time = Time::from_millis((*position * 1000.0) as i64);
player.set_position(time);
glib::spawn_future_local(async move {
let _ = player.seeked(time).await;
});
}
AppEvent::ShuffleChanged(enabled) => {
let enabled = *enabled;
glib::spawn_future_local(async move {
let _ = player.set_shuffle(enabled).await;
});
}
AppEvent::RepeatChanged(mode) => {
let status = match mode {
crate::player::RepeatMode::Off => LoopStatus::None,
crate::player::RepeatMode::All => LoopStatus::Playlist,
crate::player::RepeatMode::One => LoopStatus::Track,
};
glib::spawn_future_local(async move {
let _ = player.set_loop_status(status).await;
});
}
AppEvent::VolumeChanged(volume) => {
let volume = *volume;
glib::spawn_future_local(async move {
let _ = player.set_volume(volume).await;
});
}
_ => {}
}
true
});
crate::logger::info("mpris", "MPRIS2 interface registered");
});
}
fn metadata_for(core: &Rc<AppCore>, track: Option<&Track>) -> Metadata {
let Some(track) = track else {
return Metadata::new();
};
let mut builder = Metadata::builder()
.title(track.title.clone())
.artist([track.artist.clone()])
.album(track.album.clone())
.length(Time::from_millis((track.duration * 1000.0) as i64));
if let Ok(path) = ObjectPath::try_from(format!("/cc/midgarcorp/Flaccy/Track/{}", track.id)) {
builder = builder.trackid(path);
}
if let Some(art) = export_artwork(core, track) {
if let Ok(uri) = glib::filename_to_uri(&art, None::<&str>) {
builder = builder.art_url(uri.to_string());
}
}
builder.build()
}
fn export_artwork(core: &Rc<AppCore>, track: &Track) -> Option<PathBuf> {
let cache_dir = dirs::cache_dir()?.join("flaccy").join("mpris");
std::fs::create_dir_all(&cache_dir).ok()?;
let key = crate::palette::fnv1a_64(&format!("{}|{}", track.album, track.artist));
let path = cache_dir.join(format!("{key:016x}.img"));
if path.exists() {
return Some(path);
}
let data = core.db.fetch_album_artwork(&track.album, &track.artist)?;
std::fs::write(&path, &data).ok()?;
Some(path)
}