use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::mpsc::{self, Receiver};
use std::sync::Arc;
use std::time::{Duration, Instant};
use playr_core::audio::{Cmd, Player, State, Status};
use playr_core::db::query::{Mark, Playlist};
use playr_core::db::Track;
use playr_core::event::{Event, EventSink, JobId};
use playr_core::notice::{Notice, Outcome, Refusal, Task};
use playr_core::samples::Plan;
use playr_core::session::Session;
use rusqlite::Connection;
use crate::action::{Action, Keymap, Zoom};
use crate::command::{self, CommandLine, History};
use crate::config::Config;
use crate::dispatch::{self, Confirm, Frontend, Presentation, Prompt};
use crate::media::Media;
use crate::message::{self, Message};
use crate::sampler::{DetailRead, Sampler, Wave, DETAIL_MARGIN};
use crate::View;
pub const MESSAGE_FOR: Duration = Duration::from_secs(4);
pub const PEAK_HOLD: Duration = Duration::from_millis(1500);
#[derive(Debug, Clone, PartialEq, Default)]
pub enum Input {
#[default]
None,
Search(String),
SavePlaylist(String),
RenamePlaylist {
from: Playlist,
name: String,
},
Confirm(Confirm),
Help,
CommandHelp,
Roots(Vec<PathBuf>),
Command(CommandLine),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Cursors {
pub library: Option<usize>,
pub selection: Option<usize>,
pub playlists: Option<usize>,
}
#[derive(Debug, Clone, Default)]
pub struct Snapshot {
pub status: Status,
pub position: Duration,
pub volume: f32,
pub loudness: Option<f32>,
pub peak: Option<f32>,
pub marks: Vec<Duration>,
}
pub struct Model {
session: Session,
view: View,
results: Option<Vec<Track>>,
cursors: Cursors,
playing: Vec<Track>,
playing_source: Arc<[PathBuf]>,
input: Input,
history: History,
keys: Keymap,
onset_sensitivity: f32,
auto_prune: bool,
events: Receiver<Event>,
wake: Arc<dyn Fn() + Send + Sync>,
media: Media,
sampler: Sampler,
theme: crate::Theme,
message: Option<(Message, String, Instant)>,
quit: bool,
peak_hold: Option<(f32, Instant)>,
snapshot: Snapshot,
}
impl Model {
pub fn new(conn: Connection, player: Player, tracks: Vec<Track>, config: Config) -> Model {
Model::waking(conn, player, tracks, config, || {})
}
pub fn waking(
conn: Connection,
player: Player,
tracks: Vec<Track>,
config: Config,
wake: impl Fn() + Send + Sync + 'static,
) -> Model {
let (send, events) = mpsc::channel();
let wake: Arc<dyn Fn() + Send + Sync> = Arc::new(wake);
let sending = wake.clone();
let sink: EventSink = Arc::new(move |event| {
let _ = send.send(event);
sending();
});
let mut session = Session::new(conn, player, sink);
session.set_samples_dir(config.settings.samples);
let mut model = Model {
session,
view: View::Library,
results: None,
cursors: Cursors::default(),
playing: Vec::new(),
playing_source: Arc::default(),
input: Input::None,
history: History::default(),
keys: config.keys,
onset_sensitivity: config.settings.onset_sensitivity,
auto_prune: config.settings.auto_prune,
events,
wake,
media: Media::none(),
sampler: Sampler::default(),
theme: config.theme,
message: None,
quit: false,
peak_hold: None,
snapshot: Snapshot::default(),
};
model.session.send(Cmd::SetVolume(config.settings.volume));
model.session.send(Cmd::SetMode(config.settings.mode));
model.session.send(Cmd::SetSpeed(config.settings.speed));
if !tracks.is_empty() {
model.open_tracks(tracks);
} else if let Some((path, at)) = model.session.resumable() {
model.input = Input::Confirm(Confirm::Resume { path, at });
}
model.reload();
model
}
pub fn refresh(&mut self) {
self.follow_player();
let player = self.session.player();
self.peak_hold = hold_peak(self.peak_hold, player.take_peak(), Instant::now());
self.snapshot = Snapshot {
status: player.status(),
position: player.position(),
volume: player.volume(),
loudness: player.loudness(),
peak: self.peak_hold.map(|(p, _)| 20.0 * p.log10()),
marks: Vec::new(),
};
let current = self.snapshot.status.current().cloned();
self.snapshot.marks = self
.session
.marks_for(current.as_ref())
.iter()
.map(Mark::time)
.collect();
self.drain_events(current.as_ref());
for action in self
.media
.drain(self.snapshot.status.state == State::Playing)
{
self.perform(action);
}
self.publish_media();
self.follow_wave(current.as_ref());
}
pub fn perform(&mut self, action: Action) {
self.follow_player();
dispatch::dispatch(action, self);
self.follow_player();
}
pub fn answer(&mut self, yes: bool) {
let Input::Confirm(question) = std::mem::take(&mut self.input) else {
return;
};
if yes {
dispatch::confirmed(question, self);
} else {
self.notify(Message::Cancelled);
}
}
pub fn run_command(&mut self, line: &str) {
self.input = Input::None;
if line.trim().is_empty() {
return;
}
self.history.push(line);
match command::parse(line, self.view) {
Ok(action) => self.perform(action),
Err(e) => self.notify(Message::Command(e)),
}
}
pub fn search_as_typed(&mut self, query: String) {
dispatch::search(self, &query);
self.input = Input::Search(query);
}
pub fn end_search(&mut self, keep: bool) {
self.input = Input::None;
if !keep {
self.results = None;
} else if self.listed().is_empty() {
self.notify(Message::NoMatches);
}
}
pub fn save_as(&mut self, name: &str) {
self.input = Input::None;
dispatch::save_as(self, name);
}
pub fn rename_to(&mut self, from: &Playlist, name: &str) {
self.input = Input::None;
dispatch::rename(self, from, name);
}
pub fn expire_message(&mut self) {
if self
.message
.as_ref()
.is_some_and(|(_, _, at)| at.elapsed() > MESSAGE_FOR)
{
self.message = None;
}
}
pub fn quit(&mut self) {
self.remember_position();
self.quit = true;
}
fn remember_position(&mut self) {
match self.snapshot.status.current() {
Some(path) => self.session.remember(path, self.snapshot.position),
None => self.session.forget_resume(),
}
}
pub fn attach_media(&mut self) {
let wake = self.wake.clone();
self.media = Media::new(move || wake());
}
fn publish_media(&mut self) {
let status = &self.snapshot.status;
let row = self.playing.get(status.index);
let named = || {
status
.current()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned())
};
let track = match row {
Some(t) => Some((
t.display_title(),
t.display_artist().to_string(),
t.display_album().to_string(),
)),
None => named().map(|n| (n, String::new(), String::new())),
};
let playing = match status.state {
State::Playing => Some(true),
State::Paused => Some(false),
State::Stopped => None,
};
let track = track
.as_ref()
.map(|(t, a, b)| (t.as_str(), a.as_str(), b.as_str(), status.duration));
let position = self.snapshot.position;
self.media.publish(track, playing, position);
}
pub fn quitting(&self) -> bool {
self.quit
}
pub fn snapshot(&self) -> &Snapshot {
&self.snapshot
}
pub fn playing(&self) -> &[Track] {
&self.playing
}
pub fn results(&self) -> Option<&[Track]> {
self.results.as_deref()
}
pub fn cursors(&self) -> Cursors {
self.cursors
}
pub fn input(&self) -> &Input {
&self.input
}
pub fn set_input(&mut self, input: Input) {
self.input = input;
}
pub fn history(&self) -> &History {
&self.history
}
pub fn keymap(&self) -> &Keymap {
&self.keys
}
pub fn sampler(&self) -> &Sampler {
&self.sampler
}
pub fn theme(&self) -> crate::Theme {
self.theme
}
pub fn set_display(&mut self, display: crate::Display) {
self.sampler.display = display;
}
pub fn set_zoom(&mut self, zoom: u32) {
self.sampler.zoom = zoom;
}
pub fn set_scale(&mut self, scale: crate::sampler::Scale) {
self.sampler.scale = Some(scale);
}
pub fn message(&self) -> Option<&Message> {
self.message.as_ref().map(|(m, _, _)| m)
}
pub fn message_text(&self) -> Option<&str> {
self.message.as_ref().map(|(_, text, _)| text.as_str())
}
pub fn follow_player(&mut self) {
let current = self.session.player().queue();
if Arc::ptr_eq(¤t, &self.playing_source) {
return;
}
let known: HashMap<&str, &Track> = self
.playing
.iter()
.chain(self.session.selection())
.chain(self.session.tracks())
.map(|t| (t.path.as_str(), t))
.collect();
self.playing = current
.iter()
.map(|p| {
let path = p.to_string_lossy();
known
.get(path.as_ref())
.map(|t| (*t).clone())
.unwrap_or(Track {
path: path.into_owned(),
..Default::default()
})
})
.collect();
self.playing_source = current;
}
fn reload(&mut self) {
self.session.reload();
self.choose_first_rows();
}
fn choose_first_rows(&mut self) {
if !self.session.tracks().is_empty() && self.cursors.library.is_none() {
self.cursors.library = Some(0);
}
if !self.session.playlists().is_empty() && self.cursors.playlists.is_none() {
self.cursors.playlists = Some(0);
}
}
fn drain_events(&mut self, current: Option<&PathBuf>) {
let mut error: Option<(String, u64)> = None;
while let Ok(event) = self.events.try_recv() {
match event {
Event::TrackChanged { .. } => self.remember_position(),
Event::StateChanged(_) => {}
Event::PlaybackError(e) => {
let missed = error.map_or(0, |(_, n)| n + 1);
error = Some((e, missed));
}
Event::Peaks { job, track, result } => {
if !matches!(self.sampler.wave, Wave::Reading { job: reading, .. } if reading == job)
{
continue;
}
self.sampler.wave = match result {
Ok(peaks) => Wave::Ready { path: track, peaks },
Err(error) => Wave::Failed { path: track, error },
};
}
Event::Detail { job, track, result } => {
let DetailRead::Reading {
job: reading,
start,
end,
..
} = self.sampler.detail
else {
continue;
};
if reading != job {
continue;
}
self.sampler.detail = match result {
Ok(detail) => DetailRead::Ready {
path: track,
detail,
},
Err(_) => DetailRead::Failed {
path: track,
start,
end,
},
};
}
Event::Planned { job, track, result } => {
if self.sampler.planning != Some(job) {
continue;
}
self.sampler.planning = None;
if Some(&track) != current {
continue;
}
match result {
Ok(plan) => {
let slices = plan.spans.len();
self.sampler.pending = Some(plan);
self.notify(Outcome::Planned { slices });
}
Err(error) => self.notify(Notice::Failed {
task: Task::Slice,
error,
}),
}
}
Event::Snapped {
track,
from,
result,
..
} => {
if current != Some(&track) {
continue;
}
match result {
Ok(Some(to)) => {
let notice = self.session.move_mark(from, to);
if matches!(notice, Notice::Done(Outcome::MarkMoved { .. })) {
self.sampler.cursor = Some(to);
}
self.notify(notice);
}
Ok(None) => self.notify(Notice::Refused(Refusal::NoOnsetNear)),
Err(error) => self.notify(Notice::Failed {
task: Task::MoveMark,
error,
}),
}
}
Event::ScanProgress { seen, added, .. } => {
self.notify(Outcome::Scanning { seen, added })
}
Event::Scanned { dir, result, .. } => {
self.session.scanned();
self.choose_first_rows();
match result {
Ok(report) => {
let (missing, unavailable) = (report.missing, report.unavailable);
self.notify(Outcome::Scanned {
dir: dir.clone(),
report,
});
if missing > 0 {
after_missing(self, dir, unavailable == 0);
}
}
Err(error) => self.notify(Notice::Failed {
task: Task::Scan,
error,
}),
}
}
Event::Pruned { dir, result, .. } => {
self.session.pruned();
match result {
Ok(removed) => self.notify(Outcome::Pruned { dir, removed }),
Err(error) => self.notify(Notice::Failed {
task: Task::Prune,
error,
}),
}
}
Event::Opened { playable, .. } => {
let skipped = playable.problems.len();
let tracks = playable.tracks.len();
if tracks == 0 {
self.notify(Notice::Failed {
task: Task::Open,
error: "nothing playable in those paths".into(),
});
continue;
}
self.open_tracks(playable.tracks);
self.notify(Outcome::Opened { tracks, skipped });
}
Event::Exported { result, .. } => match result {
Ok(out) => self.notify(Outcome::Exported {
slices: out.slices.len(),
dir: out.dir,
}),
Err(error) => self.notify(Notice::Failed {
task: Task::Export,
error,
}),
},
}
}
if let Some((error, missed)) = error {
self.notify(Notice::PlaybackError { error, missed });
}
}
fn notify(&mut self, message: impl Into<Message>) {
let message = message.into();
let text = message::text(&message);
self.message = Some((message, text, Instant::now()));
}
fn open_tracks(&mut self, tracks: Vec<Track>) {
let mut selection = self.session.selection().to_vec();
self.cursors.selection = Some(selection.len());
selection.extend(tracks.iter().cloned());
self.session.set_selection(selection);
self.play(tracks, 0);
self.view = View::Selection;
}
fn play(&mut self, tracks: Vec<Track>, index: usize) {
self.session.play(&tracks, index);
self.playing = tracks;
self.playing_source = self.session.player().queue();
}
fn follow_wave(&mut self, current: Option<&PathBuf>) {
if self
.sampler
.pending
.as_ref()
.is_some_and(|p| Some(&p.job.path) != current)
{
self.sampler.pending = None;
}
if self
.sampler
.range
.as_ref()
.is_some_and(|r| Some(&r.path) != current)
{
self.sampler.range = None;
}
if self
.sampler
.detail
.path()
.is_some_and(|p| Some(p) != current)
{
self.sampler.detail = DetailRead::None;
}
if self.view == View::Sampler && self.sampler.wave.path() == current {
self.follow_detail(current);
}
if self.view != View::Sampler || self.sampler.wave.path() == current {
return;
}
self.sampler.wave = match current.cloned() {
Some(path) => Wave::Reading {
job: self.session.read_peaks(path.clone()),
path,
},
None => {
self.session.cancel_peaks();
Wave::None
}
};
}
}
impl Model {
fn follow_detail(&mut self, current: Option<&PathBuf>) {
let (Some(path), Some(scale)) = (current, self.sampler.scale) else {
return;
};
let Wave::Ready { peaks, .. } = &self.sampler.wave else {
return;
};
if !scale.needs_detail() {
return;
}
let (rate, frames) = (peaks.rate, peaks.frames);
let (a, b) = scale.shown();
let b = b.min(frames);
if a >= b || self.sampler.detail.answers(path, a, b) {
return;
}
let margin = crate::sampler::frame_of(DETAIL_MARGIN, rate);
let (start, end) = (a.saturating_sub(margin), (b + margin).min(frames));
let job = self.session.read_detail(path.clone(), rate, start, end);
self.sampler.detail = DetailRead::Reading {
path: path.clone(),
job,
start,
end,
};
}
}
impl Frontend for Model {
fn session(&self) -> &Session {
&self.session
}
fn session_mut(&mut self) -> &mut Session {
&mut self.session
}
fn keys(&mut self) -> &mut Keymap {
&mut self.keys
}
fn view(&self) -> View {
self.view
}
fn set_view(&mut self, view: View) {
self.view = view;
}
fn cursor(&self, view: View) -> Option<usize> {
match view {
View::Library => self.cursors.library,
View::Selection => self.cursors.selection,
View::Playlists => self.cursors.playlists,
View::Sampler => None,
}
}
fn set_cursor(&mut self, view: View, row: Option<usize>) {
match view {
View::Library => self.cursors.library = row,
View::Selection => self.cursors.selection = row,
View::Playlists => self.cursors.playlists = row,
View::Sampler => {}
}
}
fn listed(&self) -> &[Track] {
self.results.as_deref().unwrap_or(self.session.tracks())
}
fn set_results(&mut self, results: Option<Vec<Track>>) -> Option<Vec<Track>> {
std::mem::replace(&mut self.results, results)
}
fn onset_sensitivity(&self) -> f32 {
self.onset_sensitivity
}
fn notify(&mut self, message: Message) {
Model::notify(self, message);
}
fn confirm(&mut self, question: Confirm) {
self.input = Input::Confirm(question);
}
fn prompt(&mut self, prompt: Prompt) {
self.input = match prompt {
Prompt::Search => Input::Search(String::new()),
Prompt::Command => Input::Command(CommandLine::default()),
Prompt::Save => Input::SavePlaylist(String::new()),
Prompt::Rename(from) => {
let name = from.name.clone();
Input::RenamePlaylist { from, name }
}
};
}
fn present(&mut self, presentation: Presentation) {
match presentation {
Presentation::Quit => self.quit = true,
Presentation::KeyList => self.input = Input::Help,
Presentation::CommandList => self.input = Input::CommandHelp,
Presentation::RootList => self.input = Input::Roots(self.session.roots()),
Presentation::Zoom(zoom) => {
self.sampler.zoom = match zoom {
Zoom::In => self.sampler.zoom + 1,
Zoom::Out => self.sampler.zoom.saturating_sub(1),
Zoom::All => 0,
}
}
Presentation::Display(display) => {
self.sampler.display = display.unwrap_or(self.sampler.display.next());
Model::notify(self, Message::Display(self.sampler.display));
}
Presentation::Theme(theme) => {
self.theme = theme;
Model::notify(self, Message::Theme(theme));
}
}
}
fn sampler(&self) -> &Sampler {
&self.sampler
}
fn sampler_mut(&mut self) -> &mut Sampler {
&mut self.sampler
}
fn planning(&mut self, job: JobId) {
self.sampler.planning = Some(job);
}
fn take_plan(&mut self) -> Option<Plan> {
self.sampler.pending.take()
}
}
fn after_missing(model: &mut Model, dir: Option<PathBuf>, read: bool) {
if model.session.check_prune(dir.as_deref()).is_err() {
return;
}
if model.auto_prune && read {
match model.session.prune(dir.clone()) {
Ok(_) => model.notify(Outcome::PruneStarted { dir }),
Err(refusal) => model.notify(refusal),
}
} else if model.input == Input::None {
model.confirm(Confirm::Prune(dir));
}
}
pub fn hold_peak(
held: Option<(f32, Instant)>,
reading: f32,
now: Instant,
) -> Option<(f32, Instant)> {
match held {
Some((level, at)) if level >= reading && now.duration_since(at) < PEAK_HOLD => held,
_ => (reading > 0.0).then_some((reading, now)),
}
}
pub fn now_playing(playing: &[Track], status: &Status) -> Option<String> {
playing
.get(status.index)
.map(|t| format!("{} - {}", t.display_title(), t.display_artist()))
.or_else(|| {
status.current().map(|p| {
p.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned()
})
})
}