use super::action::AppAction;
use crate::app::component::actionhandler::{
Action, ActionHandler, ComponentEffect, KeyRouter, Scrollable, TextHandler, YoutuiEffect,
};
use crate::app::server::song_downloader::DownloadProgressUpdateType;
use crate::app::server::song_thumbnail_downloader::SongThumbnailID;
use crate::app::server::{
AutoplayDecodedSong, DecodeSong, DownloadSong, GetSongThumbnail, IncreaseVolume, Pause,
PausePlay, PlayDecodedSong, QueueDecodedSong, Resume, Seek, SeekTo, Stop, StopAll,
TaskMetadata,
};
use crate::app::structures::{
AlbumArtState, BrowserSongsList, DownloadStatus, ListSong, ListSongDisplayableField,
ListSongID, Percentage, PlayState, SongListComponent,
};
use crate::app::ui::playlist::effect_handlers::{
HandleAllStopped, HandleAutoplayUpdateOk, HandleGetSongThumbnailError,
HandleGetSongThumbnailOk, HandlePausePlayResponse, HandlePausedResponse, HandlePlayUpdateError,
HandlePlayUpdateOk, HandleQueueUpdateOk, HandleResumeResponse, HandleSetSongPlayProgress,
HandleSongDownloadProgressUpdate, HandleStopped, HandleVolumeUpdate,
};
use crate::app::ui::{AppCallback, WindowContext};
use crate::app::view::draw::{draw_loadable, draw_panel_mut, draw_table};
use crate::app::view::{BasicConstraint, DrawableMut, HasTitle, Loadable, TableView};
use crate::async_rodio_sink::{
AllStopped, AutoplayUpdate, PlayUpdate, QueueUpdate, SeekDirection, Stopped, VolumeUpdate,
};
use crate::config::Config;
use crate::config::keymap::Keymap;
use crate::widgets::ScrollingTableState;
use async_callback_manager::{AsyncTask, Constraint, TryBackendTaskExt};
use ratatui::Frame;
use ratatui::layout::Rect;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::Debug;
use std::iter;
use std::option::Option;
use std::sync::Arc;
use std::time::Duration;
use tracing::{error, info, warn};
use ytmapi_rs::common::Thumbnail;
mod effect_handlers;
#[cfg(test)]
mod tests;
const SONGS_AHEAD_TO_BUFFER: usize = 3;
const SONGS_BEHIND_TO_SAVE: usize = 1;
const GAPLESS_PLAYBACK_THRESHOLD: Duration = Duration::from_secs(1);
pub const DEFAULT_UI_VOLUME: Percentage = Percentage(50);
#[derive(Debug, Clone, PartialEq)]
pub struct Playlist {
pub list: BrowserSongsList,
pub cur_played_dur: Option<Duration>,
pub play_status: PlayState,
pub queue_status: QueueState,
pub volume: Percentage,
cur_selected: usize,
pub widget_state: ScrollingTableState,
}
impl_youtui_component!(Playlist);
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PlaylistAction {
ViewBrowser,
PlaySelected,
DeleteSelected,
DeleteAll,
}
impl Action for PlaylistAction {
fn context(&self) -> std::borrow::Cow<'_, str> {
"Playlist".into()
}
fn describe(&self) -> std::borrow::Cow<'_, str> {
match self {
PlaylistAction::ViewBrowser => "View Browser",
PlaylistAction::PlaySelected => "Play Selected",
PlaylistAction::DeleteSelected => "Delete Selected",
PlaylistAction::DeleteAll => "Delete All",
}
.into()
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum QueueState {
NotQueued,
Queued(ListSongID),
}
impl ActionHandler<PlaylistAction> for Playlist {
fn apply_action(&mut self, action: PlaylistAction) -> impl Into<YoutuiEffect<Playlist>> {
match action {
PlaylistAction::ViewBrowser => (AsyncTask::new_no_op(), Some(self.view_browser())),
PlaylistAction::PlaySelected => (self.play_selected(), None),
PlaylistAction::DeleteSelected => (self.delete_selected(), None),
PlaylistAction::DeleteAll => (self.delete_all(), None),
}
}
}
impl KeyRouter<AppAction> for Playlist {
fn get_all_keybinds<'a>(
&self,
config: &'a Config,
) -> impl Iterator<Item = &'a Keymap<AppAction>> + 'a {
self.get_active_keybinds(config)
}
fn get_active_keybinds<'a>(
&self,
config: &'a Config,
) -> impl Iterator<Item = &'a Keymap<AppAction>> + 'a {
std::iter::once(&config.keybinds.playlist)
}
}
impl TextHandler for Playlist {
fn is_text_handling(&self) -> bool {
false
}
fn get_text(&self) -> std::option::Option<&str> {
None
}
fn replace_text(&mut self, _text: impl Into<String>) {}
fn clear_text(&mut self) -> bool {
false
}
fn handle_text_event_impl(
&mut self,
_event: &crossterm::event::Event,
) -> Option<ComponentEffect<Self>> {
None
}
}
impl DrawableMut for Playlist {
fn draw_mut_chunk(&mut self, f: &mut Frame, chunk: Rect, selected: bool, cur_tick: u64) {
draw_panel_mut(f, self, chunk, selected, |t, f, chunk| {
draw_loadable(f, t, chunk, |t, f, chunk| {
Some(draw_table(f, t, chunk, cur_tick))
})
});
}
}
impl Loadable for Playlist {
fn is_loading(&self) -> bool {
false
}
}
impl Scrollable for Playlist {
fn increment_list(&mut self, amount: isize) {
self.cur_selected = self
.cur_selected
.saturating_add_signed(amount)
.min(self.list.get_list_iter().len().saturating_sub(1))
}
fn is_scrollable(&self) -> bool {
true
}
}
impl TableView for Playlist {
fn get_selected_item(&self) -> usize {
self.cur_selected
}
fn get_state(&self) -> &ScrollingTableState {
&self.widget_state
}
fn get_layout(&self) -> &[BasicConstraint] {
&[
BasicConstraint::Length(3),
BasicConstraint::Length(6),
BasicConstraint::Length(3),
BasicConstraint::Percentage(Percentage(33)),
BasicConstraint::Percentage(Percentage(33)),
BasicConstraint::Percentage(Percentage(33)),
BasicConstraint::Length(9),
BasicConstraint::Length(4),
]
}
fn get_items(&self) -> impl ExactSizeIterator<Item = impl Iterator<Item = Cow<'_, str>> + '_> {
self.list.get_list_iter().enumerate().map(|(i, ls)| {
let first_field = if Some(i) == self.get_cur_playing_index() {
match self.play_status {
PlayState::NotPlaying => ">>>".to_string(),
PlayState::Playing(_) => "".to_string(),
PlayState::Paused(_) => "".to_string(),
PlayState::Stopped => ">>>".to_string(),
PlayState::Error(_) => ">>>".to_string(),
PlayState::Buffering(_) => "".to_string(),
}
} else {
(i + 1).to_string()
};
iter::once(first_field.to_string().into()).chain(ls.get_fields([
ListSongDisplayableField::DownloadStatus,
ListSongDisplayableField::TrackNo,
ListSongDisplayableField::Artists,
ListSongDisplayableField::Album,
ListSongDisplayableField::Song,
ListSongDisplayableField::Duration,
ListSongDisplayableField::Year,
]))
})
}
fn get_headings(&self) -> impl Iterator<Item = &'static str> {
[
"p#", "", "t#", "Artist", "Album", "Song", "Duration", "Year",
]
.into_iter()
}
fn get_highlighted_row(&self) -> Option<usize> {
self.get_cur_playing_index()
}
fn get_mut_state(&mut self) -> &mut ScrollingTableState {
&mut self.widget_state
}
}
impl HasTitle for Playlist {
fn get_title(&self) -> Cow<'_, str> {
format!("Local playlist - {} songs", self.list.get_list_iter().len()).into()
}
}
impl SongListComponent for Playlist {
fn get_song_from_idx(&self, idx: usize) -> Option<&ListSong> {
self.list.get_list_iter().nth(idx)
}
}
impl Playlist {
pub fn new() -> (Self, ComponentEffect<Self>) {
let task = AsyncTask::new_future_option(
IncreaseVolume(0),
HandleVolumeUpdate,
Some(Constraint::new_block_same_type()),
);
let playlist = Playlist {
volume: DEFAULT_UI_VOLUME,
play_status: PlayState::NotPlaying,
list: Default::default(),
cur_played_dur: None,
cur_selected: 0,
queue_status: QueueState::NotQueued,
widget_state: Default::default(),
};
(playlist, task)
}
pub fn stop_song_id(&self, song_id: ListSongID) -> ComponentEffect<Self> {
AsyncTask::new_future_option(
Stop(song_id),
HandleStopped,
Some(Constraint::new_block_matching_metadata(
TaskMetadata::PlayPause,
)),
)
}
pub fn play_song_id(&mut self, id: ListSongID) -> ComponentEffect<Self> {
self.drop_unscoped_from_id(id);
let mut effect = self.download_upcoming_from_id(id);
self.cur_played_dur = None;
if let Some(song_index) = self.get_index_from_id(id) {
if let DownloadStatus::Downloaded(pointer) = &self
.get_song_from_idx(song_index)
.expect("Checked previously")
.download_status
{
let task = DecodeSong(pointer.clone()).map_stream(PlayDecodedSong(id));
let constraint = Some(Constraint::new_block_matching_metadata(
TaskMetadata::PlayingSong,
));
let effect = effect.push(AsyncTask::new_stream_try(
task,
HandlePlayUpdateOk,
HandlePlayUpdateError(id),
constraint,
));
self.play_status = PlayState::Playing(id);
self.queue_status = QueueState::NotQueued;
return effect;
} else {
let maybe_effect = self
.get_cur_playing_id()
.map(|cur_id| self.stop_song_id(cur_id));
self.play_status = PlayState::Buffering(id);
self.queue_status = QueueState::NotQueued;
if let Some(stop_effect) = maybe_effect {
effect = effect.push(stop_effect);
}
}
}
effect
}
pub fn autoplay_song_id(&mut self, id: ListSongID) -> ComponentEffect<Self> {
self.drop_unscoped_from_id(id);
let mut effect = self.download_upcoming_from_id(id);
self.cur_played_dur = None;
if let Some(song_index) = self.get_index_from_id(id) {
if let DownloadStatus::Downloaded(pointer) = &self
.get_song_from_idx(song_index)
.expect("Checked previously")
.download_status
{
let task = DecodeSong(pointer.clone()).map_stream(AutoplayDecodedSong(id));
let effect = effect.push(AsyncTask::new_stream_try(
task,
HandleAutoplayUpdateOk,
HandlePlayUpdateError(id),
None,
));
self.play_status = PlayState::Playing(id);
self.queue_status = QueueState::NotQueued;
return effect;
} else {
let maybe_effect = self
.get_cur_playing_id()
.map(|cur_id| self.stop_song_id(cur_id));
self.play_status = PlayState::Buffering(id);
self.queue_status = QueueState::NotQueued;
if let Some(stop_effect) = maybe_effect {
effect = effect.push(stop_effect);
}
}
};
effect
}
pub fn reset(&mut self) -> ComponentEffect<Self> {
let mut effect = AsyncTask::new_no_op();
if let Some(cur_id) = self.get_cur_playing_id() {
effect = self.stop_song_id(cur_id);
}
self.clear();
effect
}
pub fn clear(&mut self) {
self.cur_played_dur = None;
self.play_status = PlayState::NotPlaying;
self.list.clear();
}
pub fn play_prev(&mut self) -> ComponentEffect<Self> {
let cur = &self.play_status;
match cur {
PlayState::NotPlaying | PlayState::Stopped => {
warn!("Asked to play prev, but not currently playing");
}
PlayState::Paused(id)
| PlayState::Playing(id)
| PlayState::Buffering(id)
| PlayState::Error(id) => {
let prev_song_id = self
.get_index_from_id(*id)
.and_then(|i| i.checked_sub(1))
.and_then(|i| self.get_song_from_idx(i))
.map(|i| i.id);
info!("Next song id {:?}", prev_song_id);
match prev_song_id {
Some(id) => {
return self.play_song_id(id);
}
None => {
warn!("No previous song. Doing nothing")
}
}
}
}
AsyncTask::new_no_op()
}
pub fn handle_song_downloaded(&mut self, id: ListSongID) -> ComponentEffect<Self> {
if let PlayState::Buffering(target_id) = self.play_status
&& target_id == id
{
info!("Received downloaded song {id:?}, now trying to play it.");
return self.play_song_id(id);
}
AsyncTask::new_no_op()
}
pub fn download_song_if_exists(&mut self, id: ListSongID) -> ComponentEffect<Self> {
let Some(song_index) = self.get_index_from_id(id) else {
return AsyncTask::new_no_op();
};
let song = self
.list
.get_list_iter_mut()
.nth(song_index)
.expect("We got the index from the id, so song must exist");
match song.download_status {
DownloadStatus::Downloading(_)
| DownloadStatus::Downloaded(_)
| DownloadStatus::Queued => return AsyncTask::new_no_op(),
_ => (),
};
let effect = AsyncTask::new_stream(
DownloadSong(song.video_id.clone(), id),
HandleSongDownloadProgressUpdate,
None,
);
song.download_status = DownloadStatus::Queued;
effect
}
pub fn increase_volume(&mut self, inc: i8) {
self.volume.0 = self.volume.0.saturating_add_signed(inc).clamp(0, 100);
}
pub fn set_volume(&mut self, new_vol: u8) {
self.volume.0 = new_vol.clamp(0, 100);
}
pub fn push_song_list(
&mut self,
mut song_list: Vec<ListSong>,
) -> (ListSongID, ComponentEffect<Self>) {
let get_largest_thumbnails_url = |thumbs: &Vec<Thumbnail>| {
thumbs
.iter()
.max_by_key(|thumbs| thumbs.height * thumbs.width)
.map(|thumb| thumb.url.clone())
};
let albums = song_list
.iter_mut()
.filter_map(|song| {
let Some(thumb_url) = get_largest_thumbnails_url(song.thumbnails.as_ref()) else {
song.album_art = AlbumArtState::None;
return None;
};
let thumbnail_id = SongThumbnailID::from(song as &ListSong).into_owned();
Some((thumbnail_id, thumb_url))
})
.collect::<HashMap<SongThumbnailID, String>>();
let effect = albums
.into_iter()
.map(|(thumbnail_id, thumbnail_url)| {
AsyncTask::new_future_try(
GetSongThumbnail {
thumbnail_url,
thumbnail_id: thumbnail_id.clone(),
},
HandleGetSongThumbnailOk,
HandleGetSongThumbnailError(thumbnail_id),
None,
)
})
.collect::<ComponentEffect<Self>>();
(self.list.push_song_list(song_list), effect)
}
pub fn play_next_or_stop(&mut self, prev_id: ListSongID) -> ComponentEffect<Self> {
let cur = &self.play_status;
match cur {
PlayState::NotPlaying | PlayState::Stopped => {
warn!("Asked to play next, but not currently playing");
AsyncTask::new_no_op()
}
PlayState::Paused(id)
| PlayState::Playing(id)
| PlayState::Buffering(id)
| PlayState::Error(id) => {
if id > &prev_id {
return AsyncTask::new_no_op();
}
let next_song_id = self
.get_index_from_id(*id)
.map(|i| i + 1)
.and_then(|i| self.get_id_from_index(i));
match next_song_id {
Some(id) => self.play_song_id(id),
None => {
info!("No next song - finishing playback");
self.queue_status = QueueState::NotQueued;
self.stop_song_id(*id)
}
}
}
}
}
pub fn autoplay_next_or_stop(&mut self, prev_id: ListSongID) -> ComponentEffect<Self> {
let cur = &self.play_status;
match cur {
PlayState::NotPlaying | PlayState::Stopped => {
warn!("Asked to play next, but not currently playing");
AsyncTask::new_no_op()
}
PlayState::Paused(id)
| PlayState::Playing(id)
| PlayState::Buffering(id)
| PlayState::Error(id) => {
if id > &prev_id {
return AsyncTask::new_no_op();
}
let next_song_id = self
.get_index_from_id(*id)
.map(|i| i + 1)
.and_then(|i| self.get_id_from_index(i));
match next_song_id {
Some(id) => self.autoplay_song_id(id),
None => {
info!("No next song - resetting play status");
self.queue_status = QueueState::NotQueued;
self.stop_song_id(*id)
}
}
}
}
}
pub fn download_upcoming_from_id(&mut self, id: ListSongID) -> ComponentEffect<Self> {
let Some(song_index) = self.get_index_from_id(id) else {
return AsyncTask::new_no_op();
};
let mut song_ids_list = Vec::new();
song_ids_list.push(id);
for i in 1..SONGS_AHEAD_TO_BUFFER {
let next_id = self.get_song_from_idx(song_index + i).map(|song| song.id);
if let Some(id) = next_id {
song_ids_list.push(id);
}
}
song_ids_list
.into_iter()
.map(|song_id| self.download_song_if_exists(song_id))
.collect()
}
pub fn drop_unscoped_from_id(&mut self, id: ListSongID) {
let Some(song_index) = self.get_index_from_id(id) else {
return;
};
let forward_limit = song_index + SONGS_AHEAD_TO_BUFFER;
let backwards_limit = song_index.saturating_sub(SONGS_BEHIND_TO_SAVE);
for song in self.list.get_list_iter_mut().take(backwards_limit) {
song.download_status = DownloadStatus::None
}
for song in self.list.get_list_iter_mut().skip(forward_limit) {
song.download_status = DownloadStatus::None
}
}
pub fn get_cur_playing_id(&self) -> Option<ListSongID> {
match self.play_status {
PlayState::Error(id)
| PlayState::Playing(id)
| PlayState::Paused(id)
| PlayState::Buffering(id) => Some(id),
PlayState::NotPlaying | PlayState::Stopped => None,
}
}
pub fn get_cur_playing_song(&self) -> Option<&ListSong> {
self.get_cur_playing_id()
.and_then(|id| self.get_song_from_id(id))
}
pub fn get_next_song(&self) -> Option<&ListSong> {
self.get_cur_playing_id()
.and_then(|id| self.get_index_from_id(id))
.and_then(|idx| self.list.get_list_iter().nth(idx + 1))
}
pub fn get_index_from_id(&self, id: ListSongID) -> Option<usize> {
self.list.get_list_iter().position(|s| s.id == id)
}
pub fn get_id_from_index(&self, index: usize) -> Option<ListSongID> {
self.get_song_from_idx(index).map(|s| s.id)
}
pub fn get_mut_song_from_id(&mut self, id: ListSongID) -> Option<&mut ListSong> {
self.list.get_list_iter_mut().find(|s| s.id == id)
}
pub fn get_song_from_id(&self, id: ListSongID) -> Option<&ListSong> {
self.list.get_list_iter().find(|s| s.id == id)
}
pub fn check_id_is_cur(&self, check_id: ListSongID) -> bool {
self.get_cur_playing_id().is_some_and(|id| id == check_id)
}
pub fn get_cur_playing_index(&self) -> Option<usize> {
self.get_cur_playing_id()
.and_then(|id| self.get_index_from_id(id))
}
}
impl Playlist {
pub async fn handle_tick(&mut self) {
}
pub fn handle_seek(
&mut self,
duration: Duration,
direction: SeekDirection,
) -> ComponentEffect<Self> {
AsyncTask::new_future_option(
Seek {
duration,
direction,
},
HandleSetSongPlayProgress,
None,
)
}
pub fn handle_seek_to(&mut self, position: Duration) -> ComponentEffect<Self> {
let id = match self.play_status {
PlayState::Playing(id) => {
self.play_status = PlayState::Paused(id);
id
}
PlayState::Paused(id) => {
self.play_status = PlayState::Playing(id);
id
}
_ => return AsyncTask::new_no_op(),
};
AsyncTask::new_future_option(SeekTo { position, id }, HandleSetSongPlayProgress, None)
}
pub fn handle_next(&mut self) -> ComponentEffect<Self> {
match self.play_status {
PlayState::NotPlaying | PlayState::Stopped => {
warn!("Asked to play next, but not currently playing");
AsyncTask::new_no_op()
}
PlayState::Paused(id)
| PlayState::Playing(id)
| PlayState::Buffering(id)
| PlayState::Error(id) => self.play_next_or_stop(id),
}
}
pub fn handle_previous(&mut self) -> ComponentEffect<Self> {
self.play_prev()
}
pub fn play_selected(&mut self) -> ComponentEffect<Self> {
let Some(id) = self.get_id_from_index(self.cur_selected) else {
return AsyncTask::new_no_op();
};
self.play_song_id(id)
}
pub fn delete_selected(&mut self) -> ComponentEffect<Self> {
let mut return_task = AsyncTask::new_no_op();
let cur_selected_idx = self.cur_selected;
if let Some(cur_playing_id) = self.get_cur_playing_id()
&& Some(cur_selected_idx) == self.get_cur_playing_index()
{
self.play_status = PlayState::NotPlaying;
return_task = self.stop_song_id(cur_playing_id);
}
self.list.remove_song_index(cur_selected_idx);
if self.cur_selected >= cur_selected_idx && cur_selected_idx != 0 {
self.cur_selected -= 1;
};
return_task
}
pub fn delete_all(&mut self) -> ComponentEffect<Self> {
self.reset()
}
pub fn view_browser(&mut self) -> AppCallback {
AppCallback::ChangeContext(WindowContext::Browser)
}
pub fn pauseplay(&mut self) -> ComponentEffect<Self> {
let id = match self.play_status {
PlayState::Playing(id) => {
self.play_status = PlayState::Paused(id);
id
}
PlayState::Paused(id) => {
self.play_status = PlayState::Playing(id);
id
}
_ => return AsyncTask::new_no_op(),
};
AsyncTask::new_future_option(
PausePlay(id),
HandlePausePlayResponse,
Some(Constraint::new_block_matching_metadata(
TaskMetadata::PlayPause,
)),
)
}
pub fn resume(&mut self) -> ComponentEffect<Self> {
let id = match self.play_status {
PlayState::Paused(id) => {
self.play_status = PlayState::Playing(id);
id
}
_ => return AsyncTask::new_no_op(),
};
AsyncTask::new_future_option(
Resume(id),
HandleResumeResponse,
Some(Constraint::new_block_matching_metadata(
TaskMetadata::PlayPause,
)),
)
}
pub fn pause(&mut self) -> ComponentEffect<Self> {
let id = match self.play_status {
PlayState::Playing(id) => {
self.play_status = PlayState::Paused(id);
id
}
_ => return AsyncTask::new_no_op(),
};
AsyncTask::new_future_option(
Pause(id),
HandlePausedResponse,
Some(Constraint::new_block_matching_metadata(
TaskMetadata::PlayPause,
)),
)
}
pub fn stop(&mut self) -> ComponentEffect<Self> {
self.play_status = PlayState::Stopped;
AsyncTask::new_future_option(
StopAll,
HandleAllStopped,
Some(Constraint::new_block_matching_metadata(
TaskMetadata::PlayPause,
)),
)
}
}
impl Playlist {
pub fn handle_song_download_progress_update(
&mut self,
update: DownloadProgressUpdateType,
id: ListSongID,
) -> ComponentEffect<Self> {
if let Some(song) = self.get_song_from_id(id) {
match song.download_status {
DownloadStatus::None | DownloadStatus::Downloaded(_) | DownloadStatus::Failed => {
return AsyncTask::new_no_op();
}
_ => (),
}
} else {
return AsyncTask::new_no_op();
}
tracing::info!("Task valid - updating song download status");
match update {
DownloadProgressUpdateType::Started => {
if let Some(song) = self.list.get_list_iter_mut().find(|x| x.id == id) {
song.download_status = DownloadStatus::Queued;
}
}
DownloadProgressUpdateType::Completed(song_buf) => {
if let Some(new_id) = self.get_mut_song_from_id(id).map(|s| {
s.download_status = DownloadStatus::Downloaded(Arc::new(song_buf));
s.id
}) {
return self.handle_song_downloaded(new_id);
};
}
DownloadProgressUpdateType::Error => {
if let Some(song) = self.list.get_list_iter_mut().find(|x| x.id == id) {
song.download_status = DownloadStatus::Failed;
}
}
DownloadProgressUpdateType::Retrying { times_retried } => {
if let Some(song) = self.list.get_list_iter_mut().find(|x| x.id == id) {
song.download_status = DownloadStatus::Retrying { times_retried };
}
}
DownloadProgressUpdateType::Downloading(p) => {
if let Some(song) = self.list.get_list_iter_mut().find(|x| x.id == id) {
song.download_status = DownloadStatus::Downloading(p);
}
}
}
AsyncTask::new_no_op()
}
pub fn handle_volume_update(&mut self, response: VolumeUpdate) {
self.volume = Percentage(response.0.into())
}
pub fn handle_play_update(&mut self, update: PlayUpdate<ListSongID>) -> ComponentEffect<Self> {
match update {
PlayUpdate::PlayProgress(duration, id) => {
return self.handle_set_song_play_progress(duration, id);
}
PlayUpdate::Playing(duration, id) => self.handle_playing(duration, id),
PlayUpdate::DonePlaying(id) => return self.handle_done_playing(id),
PlayUpdate::Error(e) => error!("{e}"),
}
AsyncTask::new_no_op()
}
pub fn handle_queue_update(
&mut self,
update: QueueUpdate<ListSongID>,
) -> ComponentEffect<Self> {
match update {
QueueUpdate::PlayProgress(duration, id) => {
return self.handle_set_song_play_progress(duration, id);
}
QueueUpdate::Queued(duration, id) => self.handle_queued(duration, id),
QueueUpdate::DonePlaying(id) => return self.handle_done_playing(id),
QueueUpdate::Error(e) => error!("{e}"),
}
AsyncTask::new_no_op()
}
pub fn handle_autoplay_update(
&mut self,
update: AutoplayUpdate<ListSongID>,
) -> ComponentEffect<Self> {
match update {
AutoplayUpdate::PlayProgress(duration, id) => {
return self.handle_set_song_play_progress(duration, id);
}
AutoplayUpdate::Playing(duration, id) => self.handle_playing(duration, id),
AutoplayUpdate::DonePlaying(id) => return self.handle_done_playing(id),
AutoplayUpdate::AutoplayQueued(id) => self.handle_autoplay_queued(id),
AutoplayUpdate::Error(e) => error!("{e}"),
}
AsyncTask::new_no_op()
}
pub fn handle_set_song_play_progress(
&mut self,
d: Duration,
id: ListSongID,
) -> ComponentEffect<Self> {
if !self.check_id_is_cur(id) {
return AsyncTask::new_no_op();
}
self.cur_played_dur = Some(d);
if let Some(duration_dif) = {
let cur_dur = self
.get_cur_playing_song()
.and_then(|song| song.actual_duration);
self.cur_played_dur
.as_ref()
.zip(cur_dur)
.map(|(d1, d2)| d2.saturating_sub(*d1))
} && duration_dif
.saturating_sub(GAPLESS_PLAYBACK_THRESHOLD)
.is_zero()
&& !matches!(self.queue_status, QueueState::Queued(_))
&& let Some(next_song) = self.get_next_song()
&& let DownloadStatus::Downloaded(song) = &next_song.download_status
{
let task = DecodeSong(song.clone()).map_stream(QueueDecodedSong(id));
info!("Queuing up song!");
let effect = AsyncTask::new_stream_try(
task,
HandleQueueUpdateOk,
HandlePlayUpdateError(id),
None,
);
self.queue_status = QueueState::Queued(next_song.id);
return effect;
}
AsyncTask::new_no_op()
}
pub fn handle_done_playing(&mut self, id: ListSongID) -> ComponentEffect<Self> {
if QueueState::Queued(id) == self.queue_status {
self.queue_status = QueueState::NotQueued;
return AsyncTask::new_no_op();
}
if !self.check_id_is_cur(id) {
return AsyncTask::new_no_op();
}
self.autoplay_next_or_stop(id)
}
pub fn handle_queued(&mut self, duration: Option<Duration>, id: ListSongID) {
if let Some(song) = self.get_mut_song_from_id(id) {
song.actual_duration = duration;
}
}
pub fn handle_autoplay_queued(&mut self, id: ListSongID) {
match self.queue_status {
QueueState::NotQueued => (),
QueueState::Queued(q_id) => {
if id == q_id {
self.queue_status = QueueState::NotQueued
}
}
}
}
pub fn handle_playing(&mut self, duration: Option<Duration>, id: ListSongID) {
if let Some(song) = self.get_mut_song_from_id(id) {
song.actual_duration = duration;
}
if let PlayState::Paused(p_id) = self.play_status
&& p_id == id
{
self.play_status = PlayState::Playing(id)
}
}
pub fn handle_set_to_error(&mut self, id: ListSongID) {
info!("Received message that song had a playback error {:?}", id);
if self.check_id_is_cur(id) {
info!("Setting song state to Error {:?}", id);
self.play_status = PlayState::Error(id)
}
}
pub fn handle_paused(&mut self, s_id: ListSongID) {
if let PlayState::Playing(p_id) = self.play_status
&& p_id == s_id
{
self.play_status = PlayState::Paused(s_id)
}
}
pub fn handle_resumed(&mut self, id: ListSongID) {
if let PlayState::Paused(p_id) = self.play_status
&& p_id == id
{
self.play_status = PlayState::Playing(id)
}
}
pub fn handle_stopped(&mut self, id: Stopped<ListSongID>) {
let Stopped(id) = id;
info!("Received message that playback {:?} has been stopped", id);
if self.check_id_is_cur(id) {
info!("Stopping {:?}", id);
self.play_status = PlayState::Stopped
}
}
pub fn handle_all_stopped(&mut self, _: AllStopped) {
self.play_status = PlayState::Stopped
}
}