use std::path::PathBuf;
use tracing::debug;
use super::{MusicPlayer, Track, TrackListKind, TrackPos};
use crate::{
app::{pane::PaneId, Dialog, ViewKind},
data::{cache::StreamCache, JsonStore},
providers::ProviderId,
};
impl MusicPlayer {
pub fn handle_play_track(&mut self, pos: TrackPos) {
let TrackPos { index, list, pane } = pos;
match list {
TrackListKind::Recent => {
if let Some(track) = self.get_track_at(pos) {
self.set_queue(vec![track]);
}
}
TrackListKind::Queue => {
let queue = self.queue.tracks.get(index..).unwrap_or(&[]);
self.set_queue(queue.to_vec());
}
TrackListKind::Active => {
let queue = self.view_tracks_in(pane).get(index..).unwrap_or(&[]);
self.set_queue(queue.to_vec());
self.record_now_playing_origin();
}
}
}
pub(super) fn record_now_playing_origin(&mut self) {
self.now_playing_from = Some(self.view_data().clone());
}
pub(super) fn set_track_at(&mut self, pos: TrackPos, track: Track) {
let TrackPos { index, list, pane } = pos;
match list {
TrackListKind::Queue => {
if let Some(t) = self.queue.tracks.get_mut(index) {
*t = track;
}
}
TrackListKind::Active => {
let target = match &self.view_data_in(pane).kind {
ViewKind::Playlist(entry) => Some(entry.index),
_ => None,
};
match target {
Some(sp) => {
if let Some(pl) = self.playlists.playlists.get_mut(sp) {
if let Some(t) = pl.tracks.get_mut(index) {
*t = track;
}
}
self.playlists.save();
}
None => {
if let Some(t) = self
.view_data_in_mut(pane)
.tracks_mut()
.and_then(|ts| ts.get_mut(index))
{
*t = track;
}
}
}
}
TrackListKind::Recent => {}
}
}
pub fn set_queue(&mut self, tracks: Vec<Track>) {
if !tracks.is_empty() {
let first = &tracks[0];
let preferred = if first.best_stream_provider(first.source).is_some() {
first.source
} else {
self.config.default_provider
};
self.play_track_internal(first, preferred);
}
self.queue
.set_queue(tracks, self.config.max_recently_played);
self.save_session();
self.media_controls_dirty = true;
}
pub fn play_track_via_provider(&mut self, provider: ProviderId, pos: TrackPos) {
let Some(track) = self.get_track_at(pos) else {
return;
};
if track.has_provider(provider) {
let mut t = track;
t.source = provider;
self.set_track_at(pos, t);
self.handle_play_track(pos);
} else {
self.resolve_provider(provider, track, pos, true);
}
}
pub fn download_track_via_provider(&mut self, provider: ProviderId) {
let dialog = self.dialog.take();
let Some(Dialog::ContextMenu(menu)) = dialog else {
self.dialog = dialog;
self.bounds.context_menu = None;
return;
};
self.bounds.context_menu = None;
let list = menu.pos.list;
let pane = menu.pos.pane;
let indices = menu.target_indices;
let mut to_download: Vec<Track> = Vec::new();
for &idx in &indices {
if let Some(track) = self.get_track_at(TrackPos::new(idx, list, pane)) {
if track.can_download_from(provider) {
to_download.push(track);
} else {
let pos = TrackPos::new(idx, list, pane);
self.resolve_provider(provider, track, pos, false);
}
}
}
if !to_download.is_empty() {
let msg = (self.strings.downloading_n)(to_download.len());
self.notify(msg);
for track in to_download {
self.spawn_download_thread_for(provider, track);
}
}
}
fn resolve_provider(&mut self, provider: ProviderId, track: Track, pos: TrackPos, play: bool) {
let title = track.title.clone();
let msg = (self.strings.resolving_on)(&title, provider.label());
self.notify(msg);
let rid = self.slot_request_id(pos.pane);
let tx = self.result_tx.clone();
std::thread::spawn(move || {
let resolved = crate::providers::resolve_id(provider, &track);
let _ = tx.send(match resolved {
Ok(resolved) => crate::app::BackendResult::ProviderResolved {
original: track,
provider,
resolved,
rid,
pos,
play,
},
Err(e) => crate::app::BackendResult::ProviderResolveError {
title,
provider,
message: e.to_string(),
},
});
});
}
pub fn play_track_internal(&mut self, track: &Track, preferred: ProviderId) {
self.track_loading = true;
self.clear_lyrics_for_track_change();
let gain = self.normalization_gain_for(track);
let mut analysis_path: Option<PathBuf> = None;
let mut streaming = false;
let dl_key = track.cache_key();
let local_path = track
.local_path()
.or_else(|| self.download_registry.path_for(&dl_key));
if let Some(dl_path) = local_path {
let path = PathBuf::from(&dl_path);
if path.exists() {
debug!("Playing local file: {}", path.display());
self.audio
.play_cached(path.clone(), track.duration() as f32, gain);
self.pending_cache_id = None;
analysis_path = Some(path);
}
}
if analysis_path.is_none() {
match track.best_stream_provider(preferred) {
None => {
let cached = track.providers.keys().find_map(|p| {
let id = track.provider_id(*p)?;
(self.stream_cache.contains(*p, id)).then(|| StreamCache::path_for(*p, id))
});
if let Some(path) = cached.filter(|p| p.exists()) {
debug!("Playing cached copy (no yt-dlp needed): {}", path.display());
self.audio
.play_cached(path.clone(), track.duration() as f32, gain);
self.pending_cache_id = None;
analysis_path = Some(path);
}
if analysis_path.is_none() {
if track.providers.keys().any(|p| p.uses_ytdlp()) {
self.notify(self.strings.deps_play_requires_yt_dlp);
} else {
self.notify(self.strings.deps_source_not_playable);
}
return;
}
}
Some(provider) => {
let id = track.provider_id(provider).unwrap_or_default().to_string();
if self.stream_cache.contains(provider, &id) {
let path = StreamCache::path_for(provider, &id);
debug!("Playing from cache: {}", path.display());
self.audio
.play_cached(path.clone(), track.duration() as f32, gain);
self.pending_cache_id = None;
analysis_path = Some(path);
} else {
let url = track.provider_url(provider).unwrap_or_default().to_string();
self.pending_cache_id = Some(crate::app::PendingCache {
provider_id: provider,
id: id.clone(),
});
let cache_path = StreamCache::path_for(provider, &id);
debug!("Streaming ({}): {}", provider.label(), cache_path.display());
if provider.uses_ytdlp() {
self.audio.play_stream_cache(
&url,
track.duration() as f32,
cache_path.clone(),
gain,
);
} else {
self.audio.play_stream_http(
&url,
track.duration() as f32,
cache_path.clone(),
gain,
);
}
streaming = true;
analysis_path = Some(cache_path);
}
}
}
}
if self.config.volume_normalization
&& !self.normalization_cache.contains_key(&track.cache_key())
{
if streaming {
self.pending_normalization_id = Some(track.cache_key());
} else if let Some(path) = analysis_path {
self.request_normalization_analysis(&track.cache_key(), path);
}
}
}
fn normalization_gain_for(&self, track: &Track) -> f32 {
if self.config.volume_normalization {
self.normalization_cache
.get(&track.cache_key())
.copied()
.unwrap_or(1.0)
} else {
1.0
}
}
pub(super) fn request_normalization_analysis(&self, track_id: &str, path: PathBuf) {
let tx = self.result_tx.clone();
let id = track_id.to_string();
std::thread::spawn(move || {
if let Some(gain) = crate::audio::compute_normalization_gain(&path) {
let _ = tx.send(crate::app::BackendResult::NormalizationComputed(id, gain));
}
});
}
pub fn handle_reorder_queue(
&mut self,
drop_idx: usize,
indices: &[usize],
selection: &[usize],
) -> Vec<usize> {
let new_positions =
crate::util::reorder_tracks(&mut self.queue.tracks, drop_idx, indices, selection);
self.save_session();
new_positions
}
pub fn handle_add_to_queue(&mut self, pane: PaneId, list: TrackListKind, indices: &[usize]) {
let tracks: Vec<Track> = indices
.iter()
.filter_map(|&i| self.get_track_at(TrackPos::new(i, list, pane)))
.collect();
if tracks.is_empty() {
return;
}
let inserted = tracks.len();
for (j, track) in tracks.into_iter().enumerate() {
self.queue.tracks.insert(j + 1, track);
}
self.save_session();
let tr = self.strings;
self.notify((tr.added_to)(inserted, tr.queue));
self.clear_selection_if_touched_in(pane, indices, list);
}
pub fn handle_remove_from_queue_batch(&mut self, indices: &[usize]) {
let removed = crate::util::remove_at(&mut self.queue.tracks, indices);
self.media_controls_dirty = true;
self.save_session();
let tr = self.strings;
let msg = (tr.removed_from)(removed, tr.queue);
self.notify(msg);
self.clear_selection_if_touched(indices, TrackListKind::Queue);
}
pub fn handle_remove_from_recent_batch(&mut self, indices: &[usize]) {
let removed = self.queue.remove_recent_at(indices);
self.save_session();
let tr = self.strings;
let msg = (tr.removed_from)(removed, tr.recently_played);
self.notify(msg);
self.clear_selection_if_touched(indices, TrackListKind::Recent);
}
pub fn toggle_play_pause(&mut self) {
if let Some(track) = self.queue.current().cloned() {
if self.is_playing {
self.audio.pause();
} else if self.audio.has_output() {
self.audio.resume();
} else {
self.play_track_internal(&track, track.source);
}
}
self.media_controls_dirty = true;
}
pub fn next_track(&mut self) {
if let Some(old) = self.queue.current().cloned() {
self.queue
.record_played(&old, self.config.max_recently_played);
self.queue.advance();
}
if let Some(t) = self.queue.current() {
self.track_loading = true;
let t = t.clone();
self.play_track_internal(&t, t.source);
self.save_session();
self.media_controls_dirty = true;
}
}
pub fn previous_track(&mut self) {
if self.queue.restore_previous() {
self.track_loading = true;
if let Some(t) = self.queue.current() {
let t = t.clone();
self.play_track_internal(&t, t.source);
}
self.save_session();
self.media_controls_dirty = true;
}
}
pub fn set_volume(&mut self, vol: f32) {
self.volume = vol.clamp(0.0, 1.0);
self.audio.set_volume(self.volume);
self.save_session();
self.media_controls_dirty = true;
}
pub fn adjust_volume(&mut self, delta: f32) {
if delta > 0.0 {
self.muted_volume = None;
}
self.set_volume(self.volume + delta);
}
pub fn toggle_mute(&mut self) {
if self.volume > 0.0 {
self.muted_volume = Some(self.volume);
self.set_volume(0.0);
} else {
let restore = self.muted_volume.take().unwrap_or(0.8);
self.set_volume(restore);
}
}
pub fn seek_by_seconds(&mut self, delta: f32) {
if self.duration <= 0.0 {
return;
}
let pos = self.progress * self.duration + delta;
self.seek(pos / self.duration);
}
pub fn seek(&mut self, frac: f32) {
let frac = frac.clamp(0.0, 1.0);
self.progress = frac;
self.audio
.seek(std::time::Duration::from_secs_f32(frac * self.duration));
self.media_controls_dirty = true;
}
pub fn seek_to_seconds(&mut self, secs: f32) {
if self.duration <= 0.0 {
return;
}
let frac = (secs / self.duration).clamp(0.0, 1.0);
self.seek(frac);
}
}