From e840aa9b263e17a4ad3c2a22645bfe2892508067 Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Sat, 6 Dec 2025 12:49:46 +0100 Subject: [PATCH] Implement MPV integration for audio/video playback - Initialize libmpv with audio-only configuration - Implement play, pause, resume, stop, seek controls - Add position and duration tracking from MPV - Auto-advance to next track when current ends - Update keybindings to use actual player - Add shell.nix for development environment with libmpv - Real playback now working with Enter/Space/n/p keys --- shell.nix | 15 +++++++++ src/main.rs | 30 +++++++++++++++--- src/player/mod.rs | 80 ++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 shell.nix diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..0a441a5 --- /dev/null +++ b/shell.nix @@ -0,0 +1,15 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + buildInputs = with pkgs; [ + rustc + cargo + mpv + pkg-config + ]; + + shellHook = '' + echo "cm-player development environment" + echo "libmpv available for linking" + ''; +} diff --git a/src/main.rs b/src/main.rs index e913965..e3b9724 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,7 +46,7 @@ async fn main() -> Result<()> { } // Initialize player - let _player = player::Player::new()?; + let mut player = player::Player::new()?; // Initialize app state let mut state = AppState::new(cache, config); @@ -59,7 +59,7 @@ async fn main() -> Result<()> { let mut terminal = Terminal::new(backend)?; // Run app - let result = run_app(&mut terminal, &mut state).await; + let result = run_app(&mut terminal, &mut state, &mut player).await; // Restore terminal disable_raw_mode()?; @@ -76,14 +76,31 @@ async fn main() -> Result<()> { async fn run_app( terminal: &mut Terminal, state: &mut AppState, + player: &mut player::Player, ) -> Result<()> { loop { + // Update position and duration from player + state.current_position = player.get_position().unwrap_or(0.0); + state.current_duration = player.get_duration().unwrap_or(0.0); + + // Check if track ended and play next + if player.is_idle() && state.player_state == PlayerState::Playing { + if state.playlist_index + 1 < state.playlist.len() { + state.play_next(); + if let Some(ref path) = state.current_file { + player.play(path)?; + } + } else { + state.player_state = PlayerState::Stopped; + } + } + terminal.draw(|f| ui::render(f, state))?; if event::poll(std::time::Duration::from_millis(100))? { if let Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Press { - handle_key_event(state, key.code).await?; + handle_key_event(state, player, key.code).await?; } } } @@ -96,7 +113,7 @@ async fn run_app( Ok(()) } -async fn handle_key_event(state: &mut AppState, key_code: KeyCode) -> Result<()> { +async fn handle_key_event(state: &mut AppState, player: &mut player::Player, key_code: KeyCode) -> Result<()> { match key_code { KeyCode::Char('q') => { state.should_quit = true; @@ -122,28 +139,33 @@ async fn handle_key_event(state: &mut AppState, key_code: KeyCode) -> Result<()> KeyCode::Char('n') => { state.play_next(); if let Some(ref path) = state.current_file { + player.play(path)?; tracing::info!("Next track: {:?}", path); } } KeyCode::Char('p') => { state.play_previous(); if let Some(ref path) = state.current_file { + player.play(path)?; tracing::info!("Previous track: {:?}", path); } } KeyCode::Enter => { state.play_selection(); if let Some(ref path) = state.current_file { + player.play(path)?; tracing::info!("Playing: {:?} (playlist: {} tracks)", path, state.playlist.len()); } } KeyCode::Char(' ') => { match state.player_state { PlayerState::Playing => { + player.pause()?; state.player_state = PlayerState::Paused; tracing::info!("Paused"); } PlayerState::Paused => { + player.resume()?; state.player_state = PlayerState::Playing; tracing::info!("Resumed"); } diff --git a/src/player/mod.rs b/src/player/mod.rs index bafac66..24d047e 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -1,38 +1,88 @@ -// Player module - MPV integration placeholder -// Full implementation in Phase 3 - -use anyhow::Result; +use anyhow::{anyhow, Result}; +use libmpv::Mpv; use std::path::Path; pub struct Player { - // MPV instance will be added in Phase 3 + mpv: Mpv, } impl Player { pub fn new() -> Result { - Ok(Self {}) + let mpv = Mpv::new().map_err(|e| anyhow!("Failed to create MPV instance: {:?}", e))?; + + // Configure MPV for audio/video playback + mpv.set_property("vo", "null").ok(); // No video output for TUI mode + mpv.set_property("video", "no").ok(); // Disable video decoding + mpv.set_property("volume", 100).ok(); + mpv.set_property("pause", false).ok(); + + Ok(Self { mpv }) } - pub fn play(&mut self, _path: &Path) -> Result<()> { - // TODO: Implement MPV playback in Phase 3 - tracing::info!("Play called (not yet implemented)"); + pub fn play(&mut self, path: &Path) -> Result<()> { + let path_str = path.to_string_lossy(); + self.mpv + .command("loadfile", &[&path_str, "replace"]) + .map_err(|e| anyhow!("Failed to load file: {:?}", e))?; + self.mpv.set_property("pause", false).ok(); + tracing::info!("Playing: {}", path_str); Ok(()) } pub fn pause(&mut self) -> Result<()> { - // TODO: Implement pause in Phase 3 - tracing::info!("Pause called (not yet implemented)"); + self.mpv + .set_property("pause", true) + .map_err(|e| anyhow!("Failed to pause: {:?}", e))?; + Ok(()) + } + + pub fn resume(&mut self) -> Result<()> { + self.mpv + .set_property("pause", false) + .map_err(|e| anyhow!("Failed to resume: {:?}", e))?; Ok(()) } pub fn stop(&mut self) -> Result<()> { - // TODO: Implement stop in Phase 3 - tracing::info!("Stop called (not yet implemented)"); + self.mpv + .command("stop", &[]) + .map_err(|e| anyhow!("Failed to stop: {:?}", e))?; Ok(()) } - pub fn set_volume(&mut self, _volume: i64) -> Result<()> { - // TODO: Implement volume control in Phase 3 + pub fn set_volume(&mut self, volume: i64) -> Result<()> { + self.mpv + .set_property("volume", volume) + .map_err(|e| anyhow!("Failed to set volume: {:?}", e))?; + Ok(()) + } + + pub fn get_position(&self) -> Option { + self.mpv.get_property("time-pos").ok() + } + + pub fn get_duration(&self) -> Option { + self.mpv.get_property("duration").ok() + } + + pub fn is_playing(&self) -> bool { + self.mpv + .get_property::("pause") + .map(|paused| !paused) + .unwrap_or(false) + } + + pub fn is_idle(&self) -> bool { + self.mpv + .get_property::("idle-active") + .map(|s| s == "yes") + .unwrap_or(true) + } + + pub fn seek(&mut self, seconds: f64) -> Result<()> { + self.mpv + .command("seek", &[&seconds.to_string(), "relative"]) + .map_err(|e| anyhow!("Failed to seek: {:?}", e))?; Ok(()) } }