Skip to content

Commit

Permalink
Add show actions on item (#13)
Browse files Browse the repository at this point in the history
## Brief description of changes
- add `ShowActionsOnSelectedItem` to show a popup containing of possible actions to act on a Spotify item (Track, Album, Artist, Album, Playlist)
- add `command::Action` to represent an action to act on a Spotify item.
- implement Spotify client handler functions for
   - get current user
   -  adding track to a user's playlist
   - save a Spotify item to user's library.
- update the authorization token's scopes to add user's library modify permissions
- replace the use of `playlist::SimplifiedPlaylist` with `state::Playlist`
- simplify the init codes to get Spotify data to run the application on startup.
- remove `BrowseSelectedTrack(Album|Artist)` commands.
- decrease the refresh duration for the player event handling thread.
- small refactors, remove code duplications, etc
  • Loading branch information
aome510 authored Oct 11, 2021
1 parent 0f3f810 commit 20c22c7
Show file tree
Hide file tree
Showing 12 changed files with 615 additions and 354 deletions.
77 changes: 38 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,44 +153,43 @@ To open a command shortcut help popup when running the application, press `?` or

List of supported commands:

| Command | Description | Default shortcuts |
| ---------------------------- | --------------------------------------------------------- | ------------------ |
| `NextTrack` | next track | `n` |
| `PreviousTrack` | previous track | `p` |
| `ResumePause` | resume/pause based on the current playback | `space` |
| `PlayContext` | play a random track in the current context | `.` |
| `Repeat` | cycle the repeat mode | `C-r` |
| `Shuffle` | toggle the shuffle mode | `C-s` |
| `VolumeUp` | increase playback volume | `+` |
| `VolumeDown` | decrease playback volume | `-` |
| `Quit` | quit the application | `C-c`, `q` |
| `OpenCommandHelp` | open a command help popup | `?`, `C-h` |
| `ClosePopup` | close a popup | `esc` |
| `SelectNextOrScrollDown` | select the next item in a list/table or scroll down | `j`, `C-j`, `down` |
| `SelectPreviousOrScrollUp` | select the previous item in a list/table or scroll up | `k`, `C-k`, `up` |
| `ChooseSelected` | choose the selected item and act on it | `enter` |
| `RefreshPlayback` | manually refresh the current playback | `r` |
| `FocusNextWindow` | focus the next focusable window (if any) | `tab` |
| `FocusPreviousWindow` | focus the previous focusable window (if any) | `backtab` |
| `SwitchTheme` | open a popup for switching theme | `T` |
| `SwitchDevice` | open a popup for switching device | `D` |
| `SearchContext` | open a popup for searching the current context | `/` |
| `BrowseUserPlaylists` | open a popup for browsing user's playlists | `u p` |
| `BrowseUserFollowedArtists` | open a popup for browsing user's followed artists | `u a` |
| `BrowseUserSavedAlbums` | open a popup for browsing user's saved albums | `u A` |
| `BrowsePlayingTrackArtists` | open a popup for browsing current playing track's artists | `a` |
| `BrowsePlayingTrackAlbum` | browse the current playing track's album | `A` |
| `BrowsePlayingContext` | browse the current playing context | `g space` |
| `BrowseSelectedTrackArtists` | open a popup for browsing the selected track's artists | `g a`, `C-g a` |
| `BrowseSelectedTrackAlbum` | browse to the selected track's album | `g A`, `C-g A` |
| `SearchPage` | go to the search page | `g s` |
| `PreviousPage` | go to the previous page | `backspace`, `C-p` |
| `SortTrackByTitle` | sort the track table (if any) by track's title | `s t` |
| `SortTrackByArtists` | sort the track table (if any) by track's artists | `s a` |
| `SortTrackByAlbum` | sort the track table (if any) by track's album | `s A` |
| `SortTrackByDuration` | sort the track table (if any) by track's duration | `s d` |
| `SortTrackByAddedDate` | sort the track table (if any) by track's added date | `s D` |
| `ReverseOrder` | reverse the order of the track table (if any) | `s r` |
| Command | Description | Default shortcuts |
| --------------------------- | --------------------------------------------------------- | ------------------ |
| `NextTrack` | next track | `n` |
| `PreviousTrack` | previous track | `p` |
| `ResumePause` | resume/pause based on the current playback | `space` |
| `PlayContext` | play a random track in the current context | `.` |
| `Repeat` | cycle the repeat mode | `C-r` |
| `Shuffle` | toggle the shuffle mode | `C-s` |
| `VolumeUp` | increase playback volume | `+` |
| `VolumeDown` | decrease playback volume | `-` |
| `Quit` | quit the application | `C-c`, `q` |
| `OpenCommandHelp` | open a command help popup | `?`, `C-h` |
| `ClosePopup` | close a popup | `esc` |
| `SelectNextOrScrollDown` | select the next item in a list/table or scroll down | `j`, `C-j`, `down` |
| `SelectPreviousOrScrollUp` | select the previous item in a list/table or scroll up | `k`, `C-k`, `up` |
| `ChooseSelected` | choose the selected item and act on it | `enter` |
| `RefreshPlayback` | manually refresh the current playback | `r` |
| `ShowActionsOnSelectedItem` | show actions on a selected item | `g a`, `C-space` |
| `FocusNextWindow` | focus the next focusable window (if any) | `tab` |
| `FocusPreviousWindow` | focus the previous focusable window (if any) | `backtab` |
| `SwitchTheme` | open a popup for switching theme | `T` |
| `SwitchDevice` | open a popup for switching device | `D` |
| `SearchContext` | open a popup for searching the current context | `/` |
| `BrowseUserPlaylists` | open a popup for browsing user's playlists | `u p` |
| `BrowseUserFollowedArtists` | open a popup for browsing user's followed artists | `u a` |
| `BrowseUserSavedAlbums` | open a popup for browsing user's saved albums | `u A` |
| `BrowsePlayingTrackArtists` | open a popup for browsing current playing track's artists | `a` |
| `BrowsePlayingTrackAlbum` | browse the current playing track's album | `A` |
| `BrowsePlayingContext` | browse the current playing context | `g space` |
| `SearchPage` | go to the search page | `g s` |
| `PreviousPage` | go to the previous page | `backspace`, `C-p` |
| `SortTrackByTitle` | sort the track table (if any) by track's title | `s t` |
| `SortTrackByArtists` | sort the track table (if any) by track's artists | `s a` |
| `SortTrackByAlbum` | sort the track table (if any) by track's album | `s A` |
| `SortTrackByDuration` | sort the track table (if any) by track's duration | `s d` |
| `SortTrackByAddedDate` | sort the track table (if any) by track's added date | `s D` |
| `ReverseOrder` | reverse the order of the track table (if any) | `s r` |

To add new shortcuts or modify the default shortcuts, please refer to the [keymaps section](https://github.com/aome510/spotify-player/blob/master/doc/config.md#keymaps) in the configuration documentation.

Expand All @@ -214,7 +213,7 @@ Please refer to [the configuration documentation](https://github.com/aome510/spo

- [x] integrate Spotify's [search APIs](https://developer.spotify.com/documentation/web-api/reference/#category-search)
- [ ] integrate Spotify's [recommendation API](https://developer.spotify.com/console/get-recommendations/)
- [ ] add supports for add track to playlist, save album, follow artist, and related commands.
- [x] add supports for add track to playlist, save album, follow artist, and related commands.
- [ ] integrate Spotify's [recently played API](https://developer.spotify.com/console/get-recently-played/)
- [ ] handle networking error when running
- [x] add a (optional?) integrated spotify client (possibly use [librespot](https://github.com/librespot-org/librespot))
Expand Down
144 changes: 109 additions & 35 deletions spotify_player/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ impl Client {
log::info!("handle the client request {:?}", request);

match request {
ClientRequest::GetCurrentUser => {
let user = self.get_current_user()?;
state.player.write().unwrap().user_id = user.id;
}
ClientRequest::Player(event) => {
self.handle_player_request(state, event)?;

Expand Down Expand Up @@ -137,21 +141,37 @@ impl Client {
ClientRequest::Search(query) => {
self.search(state, query)?;
}
ClientRequest::AddTrackToPlaylist(playlist_id, track_id) => {
self.add_track_to_playlist(
state.player.read().unwrap().user_id.clone(),
playlist_id,
track_id,
)?;
}
ClientRequest::SaveToLibrary(item) => {
self.save_to_library(state, item)?;
}
};

Ok(())
}

/// gets the current user
pub fn get_current_user(&self) -> Result<user::PrivateUser> {
Self::handle_rspotify_result(self.spotify.current_user())
}

/// gets all available devices
pub fn get_devices(&self) -> Result<Vec<device::Device>> {
Ok(Self::handle_rspotify_result(self.spotify.device())?.devices)
}

/// gets all playlists of the current user
pub async fn get_current_user_playlists(&self) -> Result<Vec<playlist::SimplifiedPlaylist>> {
pub async fn get_current_user_playlists(&self) -> Result<Vec<Playlist>> {
let first_page =
Self::handle_rspotify_result(self.spotify.current_user_playlists(50, None))?;
self.get_all_paging_items(first_page).await
let playlists = self.get_all_paging_items(first_page).await?;
Ok(playlists.into_iter().map(|p| p.into()).collect())
}

/// gets all followed artists of the current user
Expand Down Expand Up @@ -460,7 +480,7 @@ impl Client {
tracks: Self::into_page(tracks),
artists: Self::into_page(artists),
albums: Self::into_page(albums),
playlists,
playlists: Self::into_page(playlists),
};

// update the search cache stored inside the player state
Expand All @@ -475,6 +495,89 @@ impl Client {
Ok(())
}

/// adds track to a user's playlist
pub fn add_track_to_playlist(
&self,
user_id: String,
playlist_id: String,
track_id: String,
) -> Result<()> {
// remove all the occurrences of the track to ensure no duplication in the playlist
Self::handle_rspotify_result(self.spotify.user_playlist_remove_all_occurrences_of_tracks(
&user_id,
&playlist_id,
&[track_id.clone()],
None,
))?;

Self::handle_rspotify_result(self.spotify.user_playlist_add_tracks(
&user_id,
&playlist_id,
&[track_id],
None,
))?;
Ok(())
}

/// saves a Spotify item to current user's library
pub fn save_to_library(&self, state: &SharedState, item: Item) -> Result<()> {
match item {
Item::Track(track) => {
if let Some(id) = track.id {
let contains = Self::handle_rspotify_result(
self.spotify
.current_user_saved_tracks_contains(&[id.clone()]),
)?;
if !contains[0] {
Self::handle_rspotify_result(
self.spotify.current_user_saved_tracks_add(&[id]),
)?;
}
}
}
Item::Album(album) => {
if let Some(id) = album.id {
let contains = Self::handle_rspotify_result(
self.spotify
.current_user_saved_albums_contains(&[id.clone()]),
)?;
if !contains[0] {
Self::handle_rspotify_result(
self.spotify.current_user_saved_albums_add(&[id]),
)?;
}
}
}
Item::Artist(artist) => {
if let Some(id) = artist.id {
let follows = Self::handle_rspotify_result(
self.spotify.user_artist_check_follow(&[id.clone()]),
)?;
if !follows[0] {
Self::handle_rspotify_result(self.spotify.user_follow_artists(&[id]))?;
}
}
}
Item::Playlist(playlist) => {
let user_id = state.player.read().unwrap().user_id.clone();
let follows =
Self::handle_rspotify_result(self.spotify.user_playlist_check_follow(
&playlist.owner.0,
&playlist.id,
&[user_id],
))?;
if !follows[0] {
Self::handle_rspotify_result(self.spotify.user_playlist_follow_playlist(
&playlist.owner.0,
&playlist.id,
None,
))?;
}
}
}
Ok(())
}

/// converts a page of items with type `Y` into a page of items with type `X`
/// given that type `Y` can be converted to type `X` through the `Into<X>` trait
fn into_page<X, Y: Into<X>>(page_y: page::Page<Y>) -> page::Page<X> {
Expand Down Expand Up @@ -756,32 +859,6 @@ pub async fn start_player_event_watchers(
send: std::sync::mpsc::Sender<ClientRequest>,
session: Session,
) {
std::thread::spawn({
let send = send.clone();
move || {
// need to get the playback data on startup to render the player
//
// - Why needs to query the APIs multiple times?
// On startup, the integrated librespot device (enabled by the "streaming" feature)
// may not be initialized at the time the outer function is called.
// Querying the APIs multiple times is kinda a workaround to ensure
// that the initial playback data is in sync.
for _ in 0..5 {
// on startup, the UI needs to know the current playback to render the current playing context
send.send(ClientRequest::GetCurrentPlayback)
.unwrap_or_else(|err| {
log::warn!("failed to get the current playback: {}", err);
});
// needs to know all available devices on startup to connect to the first available device if none is running
send.send(ClientRequest::GetDevices).unwrap_or_else(|err| {
log::warn!("failed to get devices: {}", err);
});

std::thread::sleep(std::time::Duration::from_millis(1000));
}
}
});

// start a playback pooling (every `playback_refresh_duration_in_ms` ms) thread
// A zero-value `playback_refresh_in_ms` indicates no playback pooling thread
if state.app_config.playback_refresh_duration_in_ms > 0 {
Expand All @@ -791,18 +868,15 @@ pub async fn start_player_event_watchers(
std::time::Duration::from_millis(state.app_config.playback_refresh_duration_in_ms);
move || -> Result<()> {
loop {
send.send(ClientRequest::GetCurrentPlayback)
.unwrap_or_else(|err| {
log::warn!("failed to get the current playback: {}", err);
});
send.send(ClientRequest::GetCurrentPlayback).unwrap();
std::thread::sleep(playback_refresh_duration);
}
}
});
}

// start the main event watcher watching for new events every second
let refresh_duration = std::time::Duration::from_millis(1000);
// start the main event watcher watching for new events every `refresh_duration` ms
let refresh_duration = std::time::Duration::from_millis(500);
loop {
watch_player_events(&state, &send, &session)
.await
Expand Down
19 changes: 12 additions & 7 deletions spotify_player/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,17 @@ pub enum Command {
SwitchDevice,
SearchContext,

ShowActionsOnSelectedItem,

BrowseUserPlaylists,
BrowseUserFollowedArtists,
BrowseUserSavedAlbums,

BrowsePlayingTrackArtists,
BrowsePlayingTrackAlbum,
BrowsePlayingContext,
BrowseSelectedTrackArtists,
BrowseSelectedTrackAlbum,

SearchPage,

PreviousPage,

SortTrackByTitle,
Expand All @@ -51,6 +50,15 @@ pub enum Command {
ReverseTrackOrder,
}

/// An action on a Spotify item (track,album,artist,playlist)
#[derive(Copy, Clone, Debug)]
pub enum Action {
AddTrackToPlaylist,
SaveToLibrary,
BrowseArtist,
BrowseAlbum,
}

impl Command {
pub fn desc(&self) -> &'static str {
match self {
Expand All @@ -71,6 +79,7 @@ impl Command {
}
Self::ChooseSelected => "choose the selected item and act on it",
Self::RefreshPlayback => "manually refresh the current playback",
Self::ShowActionsOnSelectedItem => "show actions on a selected item",
Self::FocusNextWindow => "focus the next focusable window (if any)",
Self::FocusPreviousWindow => "focus the previous focusable window (if any)",
Self::SwitchTheme => "open a popup for switching theme",
Expand All @@ -84,10 +93,6 @@ impl Command {
}
Self::BrowsePlayingTrackAlbum => "browse the current playing track's album",
Self::BrowsePlayingContext => "browse the current playing context",
Self::BrowseSelectedTrackArtists => {
"open a popup for browsing the selected track's artists"
}
Self::BrowseSelectedTrackAlbum => "browse to the selected track's album",
Self::SearchPage => "go to the search page",
Self::PreviousPage => "go to the previous page",
Self::SortTrackByTitle => "sort the track table (if any) by track's title",
Expand Down
Loading

0 comments on commit 20c22c7

Please sign in to comment.