use crate::app::notifications::TrackInfo;
use tracing::{debug, error, info, warn};
use super::*;
impl App {
pub(super) async fn update_playback_info(&mut self) {
let state = self.state.read().await;
let is_playing = state.now_playing.state == PlaybackState::Playing;
let is_active = is_playing || state.now_playing.state == PlaybackState::Paused;
drop(state);
if !is_active {
return;
}
if !self.mpv.is_running() {
warn!("MPV not running during active playback — attempting restart");
let mut state = self.state.write().await;
state.notify_error("Playback stopped: MPV crashed. Attempting to restart…".to_string());
state.now_playing.state = PlaybackState::Stopped;
drop(state);
if let Err(e) = self.mpv.start() {
error!("Failed to restart MPV: {}", e);
}
return;
}
if is_playing {
{
let state = self.state.read().await;
let time_remaining = state.now_playing.duration - state.now_playing.position;
let has_next = state
.queue_position
.map(|p| p + 1 < state.queue.len())
.unwrap_or(false);
drop(state);
if has_next && time_remaining > 0.0 && time_remaining < 2.0 {
if let Ok(count) = self.mpv.get_playlist_count() {
if count < 2 {
info!("Near end of track with no preloaded next — advancing early");
let _ = self.next_track().await;
return;
}
}
}
}
if let Ok(count) = self.mpv.get_playlist_count() {
if count == 1 {
let state = self.state.read().await;
if let Some(pos) = state.queue_position {
if pos + 1 < state.queue.len() {
drop(state);
debug!("Playlist count is 1, re-preloading next track");
self.preload_next_track(pos).await;
}
}
}
}
if let Ok(Some(mpv_pos)) = self.mpv.get_playlist_pos() {
if mpv_pos == 1 {
let state = self.state.read().await;
if let Some(current_pos) = state.queue_position {
let next_pos = current_pos + 1;
if next_pos < state.queue.len() {
drop(state);
info!("Gapless advancement to track {}", next_pos);
let mut state = self.state.write().await;
state.queue_position = Some(next_pos);
if let Some(song) = state.queue.get(next_pos).cloned() {
state.now_playing.song = Some(song.clone());
state.now_playing.position = 0.0;
state.now_playing.duration = song.duration.unwrap_or(0) as f64;
state.now_playing.scrobbled = false;
}
drop(state);
self.notify_track_change(next_pos).await;
let _ = self.mpv.playlist_remove(0);
self.preload_next_track(next_pos).await;
return;
}
}
drop(state);
}
}
if let Ok(idle) = self.mpv.is_idle() {
if idle {
info!("Track ended, advancing to next");
let _ = self.next_track().await;
return;
}
}
}
if let Ok(position) = self.mpv.get_time_pos() {
let mut state = self.state.write().await;
state.now_playing.position = position;
}
{
let (should_check, position, duration, song_id) = {
let state = self.state.read().await;
let should_check = state.settings_state.scrobble_enabled
&& !state.now_playing.scrobbled
&& state.now_playing.state == PlaybackState::Playing
&& state.now_playing.duration > 0.0;
let position = state.now_playing.position;
let duration = state.now_playing.duration;
let song_id = state.now_playing.song.as_ref().map(|s| s.id.clone());
(should_check, position, duration, song_id)
};
if should_check {
let threshold = (duration * 0.5_f64).min(240.0);
if position >= threshold {
if let Some(song_id) = song_id {
if let Some(ref client) = self.subsonic {
match client.scrobble(&song_id, true).await {
Ok(()) => {
info!("Scrobbled track: {}", song_id);
}
Err(e) => {
warn!("Scrobble failed (non-fatal): {}", e);
}
}
}
let mut state = self.state.write().await;
state.now_playing.scrobbled = true;
}
}
}
}
{
let state = self.state.read().await;
if state.now_playing.duration <= 0.0 {
drop(state);
if let Ok(duration) = self.mpv.get_duration() {
if duration > 0.0 {
let mut state = self.state.write().await;
state.now_playing.duration = duration;
}
}
}
}
{
let state = self.state.read().await;
let need_sample_rate = state.now_playing.sample_rate.is_none();
drop(state);
if need_sample_rate {
let sample_rate = self.mpv.get_sample_rate().ok().flatten();
let bit_depth = self.mpv.get_bit_depth().ok().flatten();
let format = self.mpv.get_audio_format().ok().flatten();
let channels = self.mpv.get_channels().ok().flatten();
if let Some(rate) = sample_rate {
let current_pw_rate = self.pipewire.get_current_rate();
if current_pw_rate != Some(rate) {
info!("Sample rate change: {:?} -> {} Hz", current_pw_rate, rate);
if let Err(e) = self.pipewire.set_rate(rate) {
warn!("Failed to set PipeWire sample rate: {}", e);
}
} else {
debug!(
"Sample rate unchanged at {} Hz, skipping PipeWire switch",
rate
);
}
let mut state = self.state.write().await;
state.now_playing.sample_rate = Some(rate);
state.now_playing.bit_depth = bit_depth;
state.now_playing.format = format;
state.now_playing.channels = channels;
}
}
}
if let Some(ref server) = self.mpris_server {
if let Err(e) = update_mpris_properties(server, &self.state).await {
debug!("Failed to update MPRIS properties: {}", e);
}
}
}
pub(super) async fn toggle_pause(&mut self) -> Result<(), Error> {
let state = self.state.read().await;
let is_playing = state.now_playing.state == PlaybackState::Playing;
let is_paused = state.now_playing.state == PlaybackState::Paused;
drop(state);
if !is_playing && !is_paused {
return Ok(());
}
match self.mpv.toggle_pause() {
Ok(now_paused) => {
let mut state = self.state.write().await;
if now_paused {
state.now_playing.state = PlaybackState::Paused;
debug!("Paused playback");
} else {
state.now_playing.state = PlaybackState::Playing;
debug!("Resumed playback");
}
}
Err(e) => {
error!("Failed to toggle pause: {}", e);
}
}
Ok(())
}
pub(super) async fn pause_playback(&mut self) -> Result<(), Error> {
let state = self.state.read().await;
if state.now_playing.state != PlaybackState::Playing {
return Ok(());
}
drop(state);
match self.mpv.pause() {
Ok(()) => {
let mut state = self.state.write().await;
state.now_playing.state = PlaybackState::Paused;
debug!("Paused playback");
}
Err(e) => {
error!("Failed to pause: {}", e);
}
}
Ok(())
}
pub(super) async fn resume_playback(&mut self) -> Result<(), Error> {
let state = self.state.read().await;
if state.now_playing.state != PlaybackState::Paused {
return Ok(());
}
drop(state);
match self.mpv.resume() {
Ok(()) => {
let mut state = self.state.write().await;
state.now_playing.state = PlaybackState::Playing;
debug!("Resumed playback");
}
Err(e) => {
error!("Failed to resume: {}", e);
}
}
Ok(())
}
pub(super) async fn next_track(&mut self) -> Result<(), Error> {
let state = self.state.read().await;
let queue_len = state.queue.len();
let current_pos = state.queue_position;
drop(state);
if queue_len == 0 {
return Ok(());
}
let next_pos = match current_pos {
Some(pos) if pos + 1 < queue_len => pos + 1,
_ => {
info!("Reached end of queue");
let _ = self.mpv.stop();
let mut state = self.state.write().await;
state.now_playing.state = PlaybackState::Stopped;
state.now_playing.position = 0.0;
return Ok(());
}
};
self.play_queue_position(next_pos).await
}
pub(super) async fn prev_track(&mut self) -> Result<(), Error> {
let state = self.state.read().await;
let queue_len = state.queue.len();
let current_pos = state.queue_position;
let position = state.now_playing.position;
drop(state);
if queue_len == 0 {
return Ok(());
}
if position < 3.0 {
if let Some(pos) = current_pos {
if pos > 0 {
return self.play_queue_position(pos - 1).await;
}
}
if let Err(e) = self.mpv.seek(0.0) {
error!("Failed to restart track: {}", e);
} else {
let mut state = self.state.write().await;
state.now_playing.position = 0.0;
}
return Ok(());
}
debug!("Restarting current track (position: {:.1}s)", position);
if let Err(e) = self.mpv.seek(0.0) {
error!("Failed to restart track: {}", e);
} else {
let mut state = self.state.write().await;
state.now_playing.position = 0.0;
}
Ok(())
}
pub(super) async fn play_queue_position(&mut self, pos: usize) -> Result<(), Error> {
let state = self.state.read().await;
let song = match state.queue.get(pos) {
Some(s) => s.clone(),
None => return Ok(()),
};
drop(state);
let stream_url = if let Some(ref client) = self.subsonic {
match client.get_stream_url(&song.id) {
Ok(url) => url,
Err(e) => {
error!("Failed to get stream URL: {}", e);
let mut state = self.state.write().await;
state.notify_error(format!("Failed to get stream URL: {}", e));
return Ok(());
}
}
} else {
return Ok(());
};
{
let mut state = self.state.write().await;
state.queue_position = Some(pos);
state.now_playing.song = Some(song.clone());
state.now_playing.state = PlaybackState::Playing;
state.now_playing.position = 0.0;
state.now_playing.duration = song.duration.unwrap_or(0) as f64;
state.now_playing.sample_rate = None;
state.now_playing.bit_depth = None;
state.now_playing.format = None;
state.now_playing.channels = None;
state.now_playing.scrobbled = false;
}
info!("Playing: {} (queue pos {})", song.title, pos);
if self.mpv.is_paused().unwrap_or(false) {
let _ = self.mpv.resume();
}
if let Err(e) = self.mpv.loadfile(&stream_url) {
error!("Failed to play: {}", e);
let mut state = self.state.write().await;
state.notify_error(format!("MPV error: {}", e));
return Ok(());
}
self.preload_next_track(pos).await;
Ok(())
}
pub(super) async fn preload_next_track(&mut self, current_pos: usize) {
let state = self.state.read().await;
let next_pos = current_pos + 1;
if next_pos >= state.queue.len() {
return;
}
let next_song = match state.queue.get(next_pos) {
Some(s) => s.clone(),
None => return,
};
drop(state);
if let Some(ref client) = self.subsonic {
if let Ok(url) = client.get_stream_url(&next_song.id) {
debug!("Pre-loading next track for gapless: {}", next_song.title);
if let Err(e) = self.mpv.loadfile_append(&url) {
debug!("Failed to pre-load next track: {}", e);
} else if let Ok(count) = self.mpv.get_playlist_count() {
if count < 2 {
warn!(
"Preload may have failed: playlist count is {} (expected 2)",
count
);
} else {
debug!("Preload confirmed: playlist count is {}", count);
}
}
}
}
}
pub(super) async fn stop_playback(&mut self) -> Result<(), Error> {
if let Err(e) = self.mpv.stop() {
error!("Failed to stop: {}", e);
}
let mut state = self.state.write().await;
state.now_playing.state = PlaybackState::Stopped;
state.now_playing.song = None;
state.now_playing.position = 0.0;
state.now_playing.duration = 0.0;
state.now_playing.sample_rate = None;
state.now_playing.bit_depth = None;
state.now_playing.format = None;
state.now_playing.channels = None;
state.queue.clear();
state.queue_position = None;
Ok(())
}
async fn notify_track_change(&mut self, pos: usize) {
let (notifications_enabled, song) = {
let state = self.state.read().await;
let song = match state.queue.get(pos) {
Some(s) => s.clone(),
None => return,
};
(state.settings_state.notifications_enabled, song)
};
if notifications_enabled {
notifications::notify_track_change(&TrackInfo {
title: song.title.clone(),
artist: song.artist.unwrap_or_default(),
album: song.album.unwrap_or_default(),
});
}
}
}