use std::path::{Path, PathBuf};
use std::time::Duration;
use crate::mpris::{self, Remote};
use crate::player::Audio;
use super::*;
impl App {
pub fn play_cursor(&mut self) {
if self.view.is_empty() {
self.error("nothing to play here");
return;
}
self.queue = self.view.clone();
self.qpos = self.cur;
self.history.clear();
self.play_queue_pos();
}
fn disk_path(&self, path: &Path) -> PathBuf {
self.moves
.iter()
.find(|(_, to)| to == path)
.map_or_else(|| path.to_path_buf(), |(from, _)| from.clone())
}
fn play_queue_pos(&mut self) {
let Some(&track_idx) = self.queue.get(self.qpos) else {
return;
};
let path = self.disk_path(&self.tracks[track_idx].path);
let Some(audio) = self.audio.as_mut() else {
self.error("no audio device: playback is disabled");
return;
};
match audio.play(&path) {
Ok(()) => {
self.playing = Some(track_idx);
self.msg = None;
let track = &self.tracks[track_idx];
self.played.push(if track.artist.is_empty() {
track.title.clone()
} else {
format!("{} - {}", track.artist, track.title)
});
}
Err(e) => {
self.playing = None;
self.error(format!("{e}"));
}
}
}
pub fn stop(&mut self) {
if let Some(audio) = self.audio.as_mut() {
audio.stop();
}
self.playing = None;
}
pub fn advance(&mut self, delta: isize, auto: bool) {
if self.queue.is_empty() {
return;
}
if auto && self.repeat == Repeat::One {
self.play_queue_pos();
return;
}
if self.shuffle {
if delta > 0 {
self.history.push(self.qpos);
self.qpos = self.next_random();
} else if let Some(previous) = self.history.pop() {
self.qpos = previous;
} else {
self.info("nothing played before this");
return;
}
self.play_queue_pos();
self.follow_playing();
return;
}
let next = self.qpos as isize + delta;
if next < 0 {
self.qpos = 0;
} else if next as usize >= self.queue.len() {
if self.repeat == Repeat::All {
self.qpos = 0;
} else {
self.stop();
return;
}
} else {
self.qpos = next as usize;
}
self.play_queue_pos();
self.follow_playing();
}
fn follow_playing(&mut self) {
if let Some(p) = self.playing
&& let Some(row) = self.view.iter().position(|&i| i == p)
{
self.goto(row);
}
}
pub fn tick(&mut self) {
if let Some(e) = self.audio.as_ref().and_then(Audio::take_error) {
self.error(e);
}
let finished = self
.audio
.as_ref()
.is_some_and(|a| a.has_track() && a.finished() && !a.is_paused());
if finished && self.playing.is_some() {
self.advance(1, true);
}
self.remote_control();
self.publish_status();
self.lyrics_tick();
}
fn lyrics_tick(&mut self) {
self.lyrics.poll();
if !self.show_lyrics {
return;
}
if let Some(track) = self.playing_track().cloned() {
self.lyrics.request(&track);
}
}
fn remote_control(&mut self) {
let Some(commands) = self
.mpris
.as_ref()
.map(|m| m.rx.try_iter().collect::<Vec<_>>())
else {
return;
};
for command in commands {
match command {
Remote::PlayPause => match self.audio.as_ref() {
Some(audio) if audio.has_track() => audio.toggle_pause(),
_ => self.play_cursor(),
},
Remote::Play => match self.audio.as_ref() {
Some(audio) if audio.has_track() => audio.resume(),
_ => self.play_cursor(),
},
Remote::Pause => {
if let Some(audio) = self.audio.as_ref() {
audio.pause();
}
}
Remote::Stop => self.stop(),
Remote::Next => self.advance(1, false),
Remote::Prev => self.advance(-1, false),
Remote::Seek(offset_us) => {
if let Some(audio) = self.audio.as_ref()
&& let Err(e) = audio.seek_by(offset_us / 1_000_000)
{
self.error(format!("{e}"));
}
}
Remote::Volume(v) => {
if let Some(audio) = self.audio.as_mut() {
audio.set_volume((v * 100.0).round() as u8);
}
}
Remote::Quit => self.quit = true,
}
}
}
fn publish_status(&self) {
let Some(mpris) = self.mpris.as_ref() else {
return;
};
let audio = self.audio.as_ref();
let track = self.playing_track();
mpris.publish(&mpris::Status {
has_track: audio.is_some_and(Audio::has_track),
paused: audio.is_some_and(Audio::is_paused),
title: track.map(|t| t.title.clone()).unwrap_or_default(),
artist: track.map(|t| t.artist.clone()).unwrap_or_default(),
album: track.map(|t| t.album.clone()).unwrap_or_default(),
path: track.map(|t| t.path.clone()).unwrap_or_default(),
length_us: track.map_or(0, |t| t.duration.as_micros() as u64),
pos_us: self.elapsed().as_micros() as u64,
volume: audio.map_or(0.0, |a| f64::from(a.volume()) / 100.0),
can_next: !self.queue.is_empty(),
can_prev: !self.queue.is_empty(),
});
}
pub fn elapsed(&self) -> Duration {
self.audio.as_ref().map_or(Duration::ZERO, Audio::pos)
}
fn next_random(&mut self) -> usize {
self.rng ^= self.rng << 13;
self.rng ^= self.rng >> 7;
self.rng ^= self.rng << 17;
(self.rng % self.queue.len() as u64) as usize
}
pub fn copy_to_clipboard(&mut self, text: &str) {
use std::io::Write;
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let bytes = text.as_bytes();
let mut encoded = String::new();
for chunk in bytes.chunks(3) {
let b = [
chunk[0],
chunk.get(1).copied().unwrap_or(0),
chunk.get(2).copied().unwrap_or(0),
];
let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]);
for i in 0..4 {
if i <= chunk.len() {
encoded.push(ALPHABET[(n >> (18 - i * 6)) as usize & 0x3F] as char);
} else {
encoded.push('=');
}
}
}
let mut out = std::io::stdout();
if write!(out, "\x1b]52;c;{encoded}\x07")
.and_then(|()| out.flush())
.is_ok()
{
self.info(format!("copied {text}"));
} else {
self.error("cannot reach the terminal clipboard");
}
}
}