From 38ec4e9c04ba2308646627cab332037c22af3501 Mon Sep 17 00:00:00 2001 From: Andrei Ionescu <129840688+ionescuaandrei@users.noreply.github.com> Date: Sun, 23 Mar 2025 16:40:08 +0200 Subject: [PATCH 1/2] Play playlist 'CTRL+o' added #488 --- spotify_player/src/client/mod.rs | 102 +++++++++++++-------------- spotify_player/src/client/request.rs | 1 + spotify_player/src/command.rs | 21 +++++- spotify_player/src/config/keymap.rs | 5 ++ spotify_player/src/event/mod.rs | 68 ++++++++++++++++++ spotify_player/src/event/page.rs | 19 +++++ spotify_player/src/ui/page.rs | 16 ++++- 7 files changed, 175 insertions(+), 57 deletions(-) diff --git a/spotify_player/src/client/mod.rs b/spotify_player/src/client/mod.rs index 4de8dd93..bb94b20d 100644 --- a/spotify_player/src/client/mod.rs +++ b/spotify_player/src/client/mod.rs @@ -236,71 +236,71 @@ impl Client { PlayerRequest::NextTrack => self.next_track(device_id).await?, PlayerRequest::PreviousTrack => self.previous_track(device_id).await?, PlayerRequest::Resume => { - if !playback.is_playing { - self.resume_playback(device_id, None).await?; - playback.is_playing = true; - } - } - + if !playback.is_playing { + self.resume_playback(device_id, None).await?; + playback.is_playing = true; + } + } PlayerRequest::Pause => { - if playback.is_playing { - self.pause_playback(device_id).await?; - playback.is_playing = false; - } - } + if playback.is_playing { + self.pause_playback(device_id).await?; + playback.is_playing = false; + } + } PlayerRequest::ResumePause => { - if playback.is_playing { - self.pause_playback(device_id).await?; - } else { - self.resume_playback(device_id, None).await?; - } - playback.is_playing = !playback.is_playing; - } + if playback.is_playing { + self.pause_playback(device_id).await?; + } else { + self.resume_playback(device_id, None).await?; + } + playback.is_playing = !playback.is_playing; + } PlayerRequest::SeekTrack(position_ms) => { - self.seek_track(position_ms, device_id).await?; - } + self.seek_track(position_ms, device_id).await?; + } PlayerRequest::Repeat => { - let next_repeat_state = match playback.repeat_state { - rspotify::model::RepeatState::Off => rspotify::model::RepeatState::Track, - rspotify::model::RepeatState::Track => rspotify::model::RepeatState::Context, - rspotify::model::RepeatState::Context => rspotify::model::RepeatState::Off, - }; + let next_repeat_state = match playback.repeat_state { + rspotify::model::RepeatState::Off => rspotify::model::RepeatState::Track, + rspotify::model::RepeatState::Track => rspotify::model::RepeatState::Context, + rspotify::model::RepeatState::Context => rspotify::model::RepeatState::Off, + }; - self.repeat(next_repeat_state, device_id).await?; + self.repeat(next_repeat_state, device_id).await?; - playback.repeat_state = next_repeat_state; - } + playback.repeat_state = next_repeat_state; + } PlayerRequest::Shuffle => { - self.shuffle(!playback.shuffle_state, device_id).await?; + self.shuffle(!playback.shuffle_state, device_id).await?; - playback.shuffle_state = !playback.shuffle_state; - } + playback.shuffle_state = !playback.shuffle_state; + } PlayerRequest::Volume(volume) => { - self.volume(volume, device_id).await?; + self.volume(volume, device_id).await?; - playback.volume = Some(u32::from(volume)); - playback.mute_state = None; - } - PlayerRequest::ToggleMute => { - let new_mute_state = match playback.mute_state { - None => { - self.volume(0, device_id).await?; - Some(playback.volume.unwrap_or_default()) + playback.volume = Some(u32::from(volume)); + playback.mute_state = None; } - Some(volume) => { - self.volume(volume as u8, device_id).await?; - None - } - }; + PlayerRequest::ToggleMute => { + let new_mute_state = match playback.mute_state { + None => { + self.volume(0, device_id).await?; + Some(playback.volume.unwrap_or_default()) + } + Some(volume) => { + self.volume(volume as u8, device_id).await?; + None + } + }; - playback.mute_state = new_mute_state; - } + playback.mute_state = new_mute_state; + } PlayerRequest::StartPlayback(..) => { - anyhow::bail!("`StartPlayback` should be handled earlier") - } + anyhow::bail!("`StartPlayback` should be handled earlier") + } PlayerRequest::TransferPlayback(..) => { - anyhow::bail!("`TransferPlayback` should be handled earlier") - } + anyhow::bail!("`TransferPlayback` should be handled earlier") + } +PlayerRequest::PlayCurrentPlaylist => todo!(), }; Ok(Some(playback)) diff --git a/spotify_player/src/client/request.rs b/spotify_player/src/client/request.rs index 60361533..920d9c0d 100644 --- a/spotify_player/src/client/request.rs +++ b/spotify_player/src/client/request.rs @@ -10,6 +10,7 @@ pub enum PlayerRequest { Resume, Pause, ResumePause, + PlayCurrentPlaylist, SeekTrack(chrono::Duration), Repeat, Shuffle, diff --git a/spotify_player/src/command.rs b/spotify_player/src/command.rs index 19c01610..90dd8e9e 100644 --- a/spotify_player/src/command.rs +++ b/spotify_player/src/command.rs @@ -80,6 +80,7 @@ pub enum Command { MovePlaylistItemDown, CreatePlaylist, + PlaySelectedPlaylist, } #[derive(Clone, Copy, Debug, Deserialize)] @@ -102,6 +103,7 @@ pub enum Action { CopyLink, Follow, Unfollow, + PlayContext, } #[derive(Debug)] @@ -214,6 +216,7 @@ pub fn construct_track_actions(track: &Track, data: &DataReadGuard) -> Vec Vec { let mut actions = vec![ + Action::PlayContext, Action::GoToArtist, Action::GoToRadio, Action::ShowActionsOnArtist, @@ -230,7 +233,11 @@ pub fn construct_album_actions(album: &Album, data: &DataReadGuard) -> Vec Vec { - let mut actions = vec![Action::GoToRadio, Action::CopyLink]; + let mut actions = vec![ + Action::PlayContext, + Action::GoToRadio, + Action::CopyLink, + ]; if data .user_data @@ -247,7 +254,11 @@ pub fn construct_artist_actions(artist: &Artist, data: &DataReadGuard) -> Vec Vec { - let mut actions = vec![Action::GoToRadio, Action::CopyLink]; + let mut actions = vec![ + Action::PlayContext, + Action::GoToRadio, + Action::CopyLink, + ]; if data .user_data @@ -264,7 +275,10 @@ pub fn construct_playlist_actions(playlist: &Playlist, data: &DataReadGuard) -> /// constructs a list of actions on a show pub fn construct_show_actions(show: &Show, data: &DataReadGuard) -> Vec { - let mut actions = vec![Action::CopyLink]; + let mut actions = vec![ + Action::PlayContext, + Action::CopyLink, + ]; if data.user_data.saved_shows.iter().any(|s| s.id == show.id) { actions.push(Action::DeleteFromLibrary); } else { @@ -356,6 +370,7 @@ impl Command { Self::MovePlaylistItemUp => "move playlist item up one position", Self::MovePlaylistItemDown => "move playlist item down one position", Self::CreatePlaylist => "create a new playlist", + Self::PlaySelectedPlaylist => "play the selected playlist", Self::VolumeChange { offset: _ } => unreachable!(), } .to_string() diff --git a/spotify_player/src/config/keymap.rs b/spotify_player/src/config/keymap.rs index d5e44956..ba569d8e 100644 --- a/spotify_player/src/config/keymap.rs +++ b/spotify_player/src/config/keymap.rs @@ -316,6 +316,11 @@ impl Default for KeymapConfig { key_sequence: "g c".into(), command: Command::JumpToCurrentTrackInContext, }, + Keymap { + key_sequence: "C-o".into(), + command: Command::PlaySelectedPlaylist, + }, + ], } } diff --git a/spotify_player/src/event/mod.rs b/spotify_player/src/event/mod.rs index 3f824e3c..22d144be 100644 --- a/spotify_player/src/event/mod.rs +++ b/spotify_player/src/event/mod.rs @@ -118,6 +118,14 @@ fn handle_key_event( } }; + // Example key binding: Alt+P to play the current playlist + if &key_sequence.keys[..] == [Key::Alt(crossterm::event::KeyCode::Char('p'))] { + client_pub.send(ClientRequest::Player(PlayerRequest::PlayCurrentPlaylist))?; + // Mark this event as handled and reset the key sequence + ui.input_key_sequence.keys.clear(); + return Ok(()); + } + // if the key sequence is not handled, let the global handler handle it let handled = if handled { true @@ -253,6 +261,14 @@ pub fn handle_action_in_context( ui.popup = None; Ok(true) } + Action::PlayContext => { + client_pub.send(ClientRequest::Player(PlayerRequest::StartPlayback( + Playback::URIs(vec![track.id.clone().into()], None), + None, + )))?; + ui.popup = None; + Ok(true) + } _ => Ok(false), }, ActionContext::Album(album) => match action { @@ -295,6 +311,15 @@ pub fn handle_action_in_context( ui.popup = None; Ok(true) } + Action::PlayContext => { + let context_id = ContextId::Album(album.id.clone()); + client_pub.send(ClientRequest::Player(PlayerRequest::StartPlayback( + Playback::Context(context_id, None), + None, + )))?; + ui.popup = None; + Ok(true) + } _ => Ok(false), }, ActionContext::Artist(artist) => match action { @@ -324,6 +349,15 @@ pub fn handle_action_in_context( })?; Ok(true) } + Action::PlayContext => { + let context_id = ContextId::Artist(artist.id.clone()); + client_pub.send(ClientRequest::Player(PlayerRequest::StartPlayback( + Playback::Context(context_id, None), + None, + )))?; + ui.popup = None; + Ok(true) + } _ => Ok(false), }, ActionContext::Playlist(playlist) => match action { @@ -356,6 +390,15 @@ pub fn handle_action_in_context( ui.popup = None; Ok(true) } + Action::PlayContext => { + let context_id = ContextId::Playlist(playlist.id.clone()); + client_pub.send(ClientRequest::Player(PlayerRequest::StartPlayback( + Playback::Context(context_id, None), + None, + )))?; + ui.popup = None; + Ok(true) + } _ => Ok(false), }, ActionContext::Show(show) => match action { @@ -375,6 +418,15 @@ pub fn handle_action_in_context( ui.popup = None; Ok(true) } + Action::PlayContext => { + let context_id = ContextId::Show(show.id.clone()); + client_pub.send(ClientRequest::Player(PlayerRequest::StartPlayback( + Playback::Context(context_id, None), + None, + )))?; + ui.popup = None; + Ok(true) + } _ => Ok(false), }, ActionContext::Episode(episode) => match action { @@ -428,6 +480,22 @@ pub fn handle_action_in_context( } Ok(false) } + Action::PlayContext => { + if let Some(show) = &episode.show { + let context_id = ContextId::Show(show.id.clone()); + client_pub.send(ClientRequest::Player(PlayerRequest::StartPlayback( + Playback::Context(context_id, Some(rspotify::model::Offset::Uri(episode.id.uri()))), + None, + )))?; + } else { + client_pub.send(ClientRequest::Player(PlayerRequest::StartPlayback( + Playback::URIs(vec![episode.id.clone().into()], None), + None, + )))?; + } + ui.popup = None; + Ok(true) + } _ => Ok(false), }, // TODO: support actions for playlist folders diff --git a/spotify_player/src/event/page.rs b/spotify_player/src/event/page.rs index a089b11a..b4a40cdb 100644 --- a/spotify_player/src/event/page.rs +++ b/spotify_player/src/event/page.rs @@ -298,6 +298,25 @@ fn handle_command_for_context_page( ui.new_search_popup(); Ok(true) } + Command::PlaySelectedPlaylist => { + if let PageState::Context { + context_page_type: ContextPageType::Browsing(context_id), + .. + } = ui.current_page() + { + if let ContextId::Playlist(_) = context_id { + client_pub.send(ClientRequest::Player(PlayerRequest::StartPlayback( + Playback::Context(context_id.clone(), None), + None, + )))?; + Ok(true) + } else { + Ok(false) + } + } else { + Ok(false) + } + } _ => window::handle_command_for_focused_context_window(command, client_pub, ui, state), } } diff --git a/spotify_player/src/ui/page.rs b/spotify_player/src/ui/page.rs index 6a96748b..9177cb1c 100644 --- a/spotify_player/src/ui/page.rs +++ b/spotify_player/src/ui/page.rs @@ -287,14 +287,24 @@ pub fn render_context_page( let data = state.data.read(); match data.caches.context.get(&id.uri()) { Some(context) => { - // render context description - let chunks = Layout::vertical([Constraint::Length(1), Constraint::Fill(0)]).split(rect); + let chunks = Layout::vertical([Constraint::Length(2), Constraint::Fill(0)]).split(rect); + // Render context description frame.render_widget( Paragraph::new(context.description()).style(ui.theme.page_desc()), chunks[0], ); - let rect = chunks[1]; + // Insert a "Play Context" button + let button_rect = Layout::horizontal([Constraint::Length(14), Constraint::Fill(0)]) + .split(chunks[0])[0]; + frame.render_widget( + Paragraph::new("Playing") + .alignment(tui::layout::Alignment::Center) + .style(ui.theme.app()), + button_rect, + ); + + let rect = chunks[1]; match context { Context::Artist { top_tracks, From e8314dbcad80bf7e67e91b30fe55cfac9a9b3cb0 Mon Sep 17 00:00:00 2001 From: Andrei Ionescu <129840688+ionescuaandrei@users.noreply.github.com> Date: Wed, 26 Mar 2025 09:25:53 +0200 Subject: [PATCH 2/2] Play playlist 'CTRL+o' added (fixing the CI)#488 --- spotify_player/src/client/mod.rs | 102 ++++++++++++++-------------- spotify_player/src/command.rs | 17 +---- spotify_player/src/config/keymap.rs | 1 - spotify_player/src/event/mod.rs | 5 +- 4 files changed, 58 insertions(+), 67 deletions(-) diff --git a/spotify_player/src/client/mod.rs b/spotify_player/src/client/mod.rs index bb94b20d..e6c82439 100644 --- a/spotify_player/src/client/mod.rs +++ b/spotify_player/src/client/mod.rs @@ -236,71 +236,71 @@ impl Client { PlayerRequest::NextTrack => self.next_track(device_id).await?, PlayerRequest::PreviousTrack => self.previous_track(device_id).await?, PlayerRequest::Resume => { - if !playback.is_playing { - self.resume_playback(device_id, None).await?; - playback.is_playing = true; - } - } + if !playback.is_playing { + self.resume_playback(device_id, None).await?; + playback.is_playing = true; + } + } PlayerRequest::Pause => { - if playback.is_playing { - self.pause_playback(device_id).await?; - playback.is_playing = false; - } - } + if playback.is_playing { + self.pause_playback(device_id).await?; + playback.is_playing = false; + } + } PlayerRequest::ResumePause => { - if playback.is_playing { - self.pause_playback(device_id).await?; - } else { - self.resume_playback(device_id, None).await?; - } - playback.is_playing = !playback.is_playing; - } + if playback.is_playing { + self.pause_playback(device_id).await?; + } else { + self.resume_playback(device_id, None).await?; + } + playback.is_playing = !playback.is_playing; + } PlayerRequest::SeekTrack(position_ms) => { - self.seek_track(position_ms, device_id).await?; - } + self.seek_track(position_ms, device_id).await?; + } PlayerRequest::Repeat => { - let next_repeat_state = match playback.repeat_state { - rspotify::model::RepeatState::Off => rspotify::model::RepeatState::Track, - rspotify::model::RepeatState::Track => rspotify::model::RepeatState::Context, - rspotify::model::RepeatState::Context => rspotify::model::RepeatState::Off, - }; + let next_repeat_state = match playback.repeat_state { + rspotify::model::RepeatState::Off => rspotify::model::RepeatState::Track, + rspotify::model::RepeatState::Track => rspotify::model::RepeatState::Context, + rspotify::model::RepeatState::Context => rspotify::model::RepeatState::Off, + }; - self.repeat(next_repeat_state, device_id).await?; + self.repeat(next_repeat_state, device_id).await?; - playback.repeat_state = next_repeat_state; - } + playback.repeat_state = next_repeat_state; + } PlayerRequest::Shuffle => { - self.shuffle(!playback.shuffle_state, device_id).await?; + self.shuffle(!playback.shuffle_state, device_id).await?; - playback.shuffle_state = !playback.shuffle_state; - } + playback.shuffle_state = !playback.shuffle_state; + } PlayerRequest::Volume(volume) => { - self.volume(volume, device_id).await?; + self.volume(volume, device_id).await?; - playback.volume = Some(u32::from(volume)); - playback.mute_state = None; - } + playback.volume = Some(u32::from(volume)); + playback.mute_state = None; + } PlayerRequest::ToggleMute => { - let new_mute_state = match playback.mute_state { - None => { - self.volume(0, device_id).await?; - Some(playback.volume.unwrap_or_default()) - } - Some(volume) => { - self.volume(volume as u8, device_id).await?; - None - } - }; - - playback.mute_state = new_mute_state; + let new_mute_state = match playback.mute_state { + None => { + self.volume(0, device_id).await?; + Some(playback.volume.unwrap_or_default()) } - PlayerRequest::StartPlayback(..) => { - anyhow::bail!("`StartPlayback` should be handled earlier") + Some(volume) => { + self.volume(volume as u8, device_id).await?; + None } + }; + + playback.mute_state = new_mute_state; + } + PlayerRequest::StartPlayback(..) => { + anyhow::bail!("`StartPlayback` should be handled earlier") + } PlayerRequest::TransferPlayback(..) => { - anyhow::bail!("`TransferPlayback` should be handled earlier") - } -PlayerRequest::PlayCurrentPlaylist => todo!(), + anyhow::bail!("`TransferPlayback` should be handled earlier") + } + PlayerRequest::PlayCurrentPlaylist => todo!(), }; Ok(Some(playback)) diff --git a/spotify_player/src/command.rs b/spotify_player/src/command.rs index 90dd8e9e..46feabd1 100644 --- a/spotify_player/src/command.rs +++ b/spotify_player/src/command.rs @@ -233,11 +233,7 @@ pub fn construct_album_actions(album: &Album, data: &DataReadGuard) -> Vec Vec { - let mut actions = vec![ - Action::PlayContext, - Action::GoToRadio, - Action::CopyLink, - ]; + let mut actions = vec![Action::PlayContext, Action::GoToRadio, Action::CopyLink]; if data .user_data @@ -254,11 +250,7 @@ pub fn construct_artist_actions(artist: &Artist, data: &DataReadGuard) -> Vec Vec { - let mut actions = vec![ - Action::PlayContext, - Action::GoToRadio, - Action::CopyLink, - ]; + let mut actions = vec![Action::PlayContext, Action::GoToRadio, Action::CopyLink]; if data .user_data @@ -275,10 +267,7 @@ pub fn construct_playlist_actions(playlist: &Playlist, data: &DataReadGuard) -> /// constructs a list of actions on a show pub fn construct_show_actions(show: &Show, data: &DataReadGuard) -> Vec { - let mut actions = vec![ - Action::PlayContext, - Action::CopyLink, - ]; + let mut actions = vec![Action::PlayContext, Action::CopyLink]; if data.user_data.saved_shows.iter().any(|s| s.id == show.id) { actions.push(Action::DeleteFromLibrary); } else { diff --git a/spotify_player/src/config/keymap.rs b/spotify_player/src/config/keymap.rs index ba569d8e..d63900d0 100644 --- a/spotify_player/src/config/keymap.rs +++ b/spotify_player/src/config/keymap.rs @@ -320,7 +320,6 @@ impl Default for KeymapConfig { key_sequence: "C-o".into(), command: Command::PlaySelectedPlaylist, }, - ], } } diff --git a/spotify_player/src/event/mod.rs b/spotify_player/src/event/mod.rs index 22d144be..e91c3736 100644 --- a/spotify_player/src/event/mod.rs +++ b/spotify_player/src/event/mod.rs @@ -484,7 +484,10 @@ pub fn handle_action_in_context( if let Some(show) = &episode.show { let context_id = ContextId::Show(show.id.clone()); client_pub.send(ClientRequest::Player(PlayerRequest::StartPlayback( - Playback::Context(context_id, Some(rspotify::model::Offset::Uri(episode.id.uri()))), + Playback::Context( + context_id, + Some(rspotify::model::Offset::Uri(episode.id.uri())), + ), None, )))?; } else {