Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add show actions on item #13

Merged
merged 19 commits into from
Oct 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0c06435
remove browse selected track's artist/album commands
aome510 Oct 10, 2021
b8d66da
add `ActionList` popup state
aome510 Oct 10, 2021
16bffb2
add `ShowActionsOnSelectedItem` and `event/item.rs` for handling comm…
aome510 Oct 10, 2021
6e13e4d
add `todo!()` for `PopupState::ActionList` and reduce the refresh
aome510 Oct 10, 2021
075f901
add `command::Action` represents an action on an item
aome510 Oct 11, 2021
5d42100
refactor the list popup rendering codes and add rendering logic for `…
aome510 Oct 11, 2021
e168291
handle `ShowActionsOnSelectedItem` command for album,playlist,artist
aome510 Oct 11, 2021
6bda474
fix clippy large enum variant error
aome510 Oct 11, 2021
3a4addc
move the action list event handler to a separate helper function
aome510 Oct 11, 2021
fabf255
add `ShowActionsOnSelectedItem` to `README`
aome510 Oct 11, 2021
08518b3
add user's library/playlists modify authorization scopes
aome510 Oct 11, 2021
8b25c7b
add `PlaylistPopupAction`, use custom `Playlist` instead of rspotify'…
aome510 Oct 11, 2021
2e7868c
add init thread
aome510 Oct 11, 2021
6a2e930
add `GetCurrentUser` and `AddTrackToPlaylist` client requests
aome510 Oct 11, 2021
47db40f
implement `SaveToLibrary` action handler for track item
aome510 Oct 11, 2021
81fd192
add `AddTrackToLibrary` action
aome510 Oct 11, 2021
6f5ec5c
handle `SaveToLibrary` client request and `SaveToLibrary` action on item
aome510 Oct 11, 2021
f830c51
update roadmap
aome510 Oct 11, 2021
e1f2821
clean up
aome510 Oct 11, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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