diff --git a/.dir-locals.el b/.dir-locals.el index 7f682403..c38f96c8 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -2,5 +2,5 @@ ;;; For more information see (info "(emacs) Directory Variables") ((rustic-mode . ((eglot-workspace-configuration - . (:rust-analyzer (:cargo (:features ["lyric-finder" "image" "notify"]) + . (:rust-analyzer (:cargo (:features ["image" "notify"]) :check (:command "clippy"))))))) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fafcf068..48123fcd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 - RUST_FEATURES: "rodio-backend,lyric-finder,media-control,image,notify" + RUST_FEATURES: "rodio-backend,media-control,image,notify" jobs: rust-ci: diff --git a/Cargo.lock b/Cargo.lock index c8790fdb..0898fbaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5175,10 +5175,10 @@ dependencies = [ "image", "librespot-connect", "librespot-core", + "librespot-metadata", "librespot-oauth", "librespot-playback", "log", - "lyric_finder", "maybe-async", "notify-rust", "once_cell", diff --git a/Dockerfile b/Dockerfile index 4882825e..a761c2cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM rust as builder WORKDIR app COPY . . -RUN cargo build --release --bin spotify_player --no-default-features --features lyric-finder +RUN cargo build --release --bin spotify_player --no-default-features FROM gcr.io/distroless/cc # Create `./config` and `./cache` folders using WORKDIR commands. diff --git a/README.md b/README.md index 336c4fd6..7dc427b9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ - [Features](#features) - [Spotify Connect](#spotify-connect) - [Streaming](#streaming) - - [Lyric](#lyric) - [Media Control](#media-control) - [Image](#image) - [Notify](#notify) @@ -33,7 +32,7 @@ - Feature parity with the official Spotify application. - Support remote control with [Spotify Connect](#spotify-connect). - Support [streaming](#streaming) songs directly from the terminal. -- Support [lyric](#lyric) for most songs. +- Support synced lyrics. - Support [cross-platform media control](#media-control). - Support [image rendering](#image). - Support [desktop notification](#notify). @@ -187,18 +186,6 @@ The `streaming` feature can be also disabled upon installing by running cargo install spotify_player --no-default-features ``` -### Lyric - -To enable lyric support, `spotify_player` needs to be built/installed with `lyric-finder` feature (**disabled** by default). To install the application with `lyric-finder` feature included run: - -```shell -cargo install spotify_player --features lyric-finder -``` - -User can view lyric of the currently playing track by calling the `LyricPage` command to go the lyric page. To do this, `spotify_player` needs to be built with a `lyric-finder` feature. - -Under the hood, `spotify_player` retrieves the song's lyric using [Genius.com](https://genius.com). - ### Media Control To enable media control support, `spotify_player` needs to be built/installed with `media-control` feature (**enabled** by default) and set the `enable_media_control` config option to `true` in the [general configuration file](docs/config.md#media-control). @@ -312,63 +299,63 @@ To go to the shortcut help page, press `?` or `C-h` (default shortcuts for `Open 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` | -| `PlayRandom` | play a random track in the current context | `.` | -| `Repeat` | cycle the repeat mode | `C-r` | -| `ToggleFakeTrackRepeatMode` | toggle fake track repeat mode | `M-r` | -| `Shuffle` | toggle the shuffle mode | `C-s` | -| `VolumeChange` | change playback volume by an offset (default shortcuts use 5%) | `+`, `-` | -| `Mute` | toggle playback volume between 0% and previous level | `_` | -| `SeekForward` | seek forward by 5s | `>` | -| `SeekBackward` | seek backward by 5s | `<` | -| `Quit` | quit the application | `C-c`, `q` | -| `ClosePopup` | close a popup | `esc` | -| `SelectNextOrScrollDown` | select the next item in a list/table or scroll down | `j`, `C-n`, `down` | -| `SelectPreviousOrScrollUp` | select the previous item in a list/table or scroll up | `k`, `C-p`, `up` | -| `PageSelectNextOrScrollDown` | select the next page item in a list/table or scroll a page down | `page_down`, `C-f` | -| `PageSelectPreviousOrScrollUp` | select the previous page item in a list/table or scroll a page up | `page_up`, `C-b` | -| `SelectFirstOrScrollToTop` | select the first item in a list/table or scroll to the top | `g g`, `home` | -| `SelectLastOrScrollToBottom` | select the last item in a list/table or scroll to the bottom | `G`, `end` | -| `ChooseSelected` | choose the selected item | `enter` | -| `RefreshPlayback` | manually refresh the current playback | `r` | -| `RestartIntegratedClient` | restart the integrated client (`streaming` feature only) | `R` | -| `ShowActionsOnSelectedItem` | open a popup showing actions on a selected item | `g a`, `C-space` | -| `ShowActionsOnCurrentTrack` | open a popup showing actions on the current track | `a` | -| `AddSelectedItemToQueue` | add the selected item to queue | `Z`, `C-z` | -| `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` | -| `Search` | open a popup for searching in the current page | `/` | -| `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` | -| `CurrentlyPlayingContextPage` | go to the currently playing context page | `g space` | -| `TopTrackPage` | go to the user top track page | `g t` | -| `RecentlyPlayedTrackPage` | go to the user recently played track page | `g r` | -| `LikedTrackPage` | go to the user liked track page | `g y` | -| `LyricPage` | go to the lyric page of the current track (`lyric-finder` feature only) | `g L`, `l` | -| `LibraryPage` | go to the user library page | `g l` | -| `SearchPage` | go to the search page | `g s` | -| `BrowsePage` | go to the browse page | `g b` | -| `Queue` | go to the queue page | `z` | -| `OpenCommandHelp` | go to the command help page | `?`, `C-h` | -| `PreviousPage` | go to the previous page | `backspace`, `C-q` | -| `OpenSpotifyLinkFromClipboard` | open a Spotify link from clipboard | `O` | -| `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` | -| `SortTrackByAddedDate` | sort the track table (if any) by track's added date | `s D` | -| `SortTrackByDuration` | sort the track table (if any) by track's duration | `s d` | -| `ReverseOrder` | reverse the order of the track table (if any) | `s r` | -| `MovePlaylistItemUp` | move playlist item up one position | `C-k` | -| `MovePlaylistItemDown` | move playlist item down one position | `C-j` | -| `CreatePlaylist` | create a new playlist | `N` | -| `JumpToCurrentTrackInContext` | jump to the current track in the context | `g c` | +| Command | Description | Default shortcuts | +| ------------------------------ | ----------------------------------------------------------------- | ------------------ | +| `NextTrack` | next track | `n` | +| `PreviousTrack` | previous track | `p` | +| `ResumePause` | resume/pause based on the current playback | `space` | +| `PlayRandom` | play a random track in the current context | `.` | +| `Repeat` | cycle the repeat mode | `C-r` | +| `ToggleFakeTrackRepeatMode` | toggle fake track repeat mode | `M-r` | +| `Shuffle` | toggle the shuffle mode | `C-s` | +| `VolumeChange` | change playback volume by an offset (default shortcuts use 5%) | `+`, `-` | +| `Mute` | toggle playback volume between 0% and previous level | `_` | +| `SeekForward` | seek forward by 5s | `>` | +| `SeekBackward` | seek backward by 5s | `<` | +| `Quit` | quit the application | `C-c`, `q` | +| `ClosePopup` | close a popup | `esc` | +| `SelectNextOrScrollDown` | select the next item in a list/table or scroll down | `j`, `C-n`, `down` | +| `SelectPreviousOrScrollUp` | select the previous item in a list/table or scroll up | `k`, `C-p`, `up` | +| `PageSelectNextOrScrollDown` | select the next page item in a list/table or scroll a page down | `page_down`, `C-f` | +| `PageSelectPreviousOrScrollUp` | select the previous page item in a list/table or scroll a page up | `page_up`, `C-b` | +| `SelectFirstOrScrollToTop` | select the first item in a list/table or scroll to the top | `g g`, `home` | +| `SelectLastOrScrollToBottom` | select the last item in a list/table or scroll to the bottom | `G`, `end` | +| `ChooseSelected` | choose the selected item | `enter` | +| `RefreshPlayback` | manually refresh the current playback | `r` | +| `RestartIntegratedClient` | restart the integrated client (`streaming` feature only) | `R` | +| `ShowActionsOnSelectedItem` | open a popup showing actions on a selected item | `g a`, `C-space` | +| `ShowActionsOnCurrentTrack` | open a popup showing actions on the current track | `a` | +| `AddSelectedItemToQueue` | add the selected item to queue | `Z`, `C-z` | +| `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` | +| `Search` | open a popup for searching in the current page | `/` | +| `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` | +| `CurrentlyPlayingContextPage` | go to the currently playing context page | `g space` | +| `TopTrackPage` | go to the user top track page | `g t` | +| `RecentlyPlayedTrackPage` | go to the user recently played track page | `g r` | +| `LikedTrackPage` | go to the user liked track page | `g y` | +| `LyricsPage` | go to the lyrics page of the current track | `g L`, `l` | +| `LibraryPage` | go to the user library page | `g l` | +| `SearchPage` | go to the search page | `g s` | +| `BrowsePage` | go to the browse page | `g b` | +| `Queue` | go to the queue page | `z` | +| `OpenCommandHelp` | go to the command help page | `?`, `C-h` | +| `PreviousPage` | go to the previous page | `backspace`, `C-q` | +| `OpenSpotifyLinkFromClipboard` | open a Spotify link from clipboard | `O` | +| `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` | +| `SortTrackByAddedDate` | sort the track table (if any) by track's added date | `s D` | +| `SortTrackByDuration` | sort the track table (if any) by track's duration | `s d` | +| `ReverseOrder` | reverse the order of the track table (if any) | `s r` | +| `MovePlaylistItemUp` | move playlist item up one position | `C-k` | +| `MovePlaylistItemDown` | move playlist item down one position | `C-j` | +| `CreatePlaylist` | create a new playlist | `N` | +| `JumpToCurrentTrackInContext` | jump to the current track in the context | `g c` | To add new shortcuts or modify the default shortcuts, please refer to the [keymaps section](docs/config.md#keymaps) in the configuration documentation. diff --git a/docs/config.md b/docs/config.md index 6c764de3..29310f19 100644 --- a/docs/config.md +++ b/docs/config.md @@ -231,6 +231,7 @@ To define application's component styles, the user can specify any of the below - `selection` - `secondary_row` - `like` +- `lyrics_played` A field in `component_style` is a struct with three **optional** fields: `fg` (foreground), `bg` (background) and `modifiers` (terminal effects): @@ -261,7 +262,9 @@ current_playing = { fg = "Green", modifiers = ["Bold"] } page_desc = { fg = "Cyan", modifiers = ["Bold"] } playlist_desc = { fg = "BrightBlack", modifiers = ["Dim"] } table_header = { fg = "Blue" } -selection = { modifiers = ["Bold", "Reversed"] } +secondary_row = {} +like = {] +lyrics_played = { modifiers = ["Dim"] } ``` ## Keymaps diff --git a/examples/README.md b/examples/README.md index 07875a52..0e40baf5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -20,9 +20,9 @@ ![Search page example](https://user-images.githubusercontent.com/40011582/140253653-5b156a8f-538b-4e68-9d52-0a379477574f.png) -## Lyric page +## Lyrics page -![Lyric page example](https://user-images.githubusercontent.com/40011582/169437044-420cf0e2-5d75-4022-bd9f-34540f1fe230.png) +![Lyrics page example](https://user-images.githubusercontent.com/40011582/169437044-420cf0e2-5d75-4022-bd9f-34540f1fe230.png) ## Command help popup diff --git a/spotify_player/Cargo.toml b/spotify_player/Cargo.toml index b036ba75..22036a4d 100644 --- a/spotify_player/Cargo.toml +++ b/spotify_player/Cargo.toml @@ -19,6 +19,7 @@ librespot-connect = { version = "0.6.0", optional = true } librespot-core = "0.6.0" librespot-oauth = "0.6.0" librespot-playback = { version = "0.6.0", optional = true } +librespot-metadata = "0.6.0" log = "0.4.22" chrono = "0.4.38" reqwest = { version = "0.12.9", features = ["json"] } @@ -38,7 +39,6 @@ async-trait = "0.1.83" parking_lot = "0.12.3" tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } -lyric_finder = { version = "0.1.8", path = "../lyric_finder", optional = true } backtrace = "0.3.74" souvlaki = { version = "0.7.3", optional = true } viuer = { version = "0.9.1", optional = true } @@ -85,7 +85,6 @@ rodiojack-backend = ["streaming", "librespot-playback/rodiojack-backend"] sdl-backend = ["streaming", "librespot-playback/sdl-backend"] gstreamer-backend = ["streaming", "librespot-playback/gstreamer-backend"] streaming = ["librespot-playback", "librespot-connect"] -lyric-finder = ["lyric_finder"] media-control = ["souvlaki", "winit", "windows"] image = ["viuer", "dep:image"] sixel = ["image", "viuer/sixel"] diff --git a/spotify_player/src/cli/handlers.rs b/spotify_player/src/cli/handlers.rs index 4a517d24..d7914e71 100644 --- a/spotify_player/src/cli/handlers.rs +++ b/spotify_player/src/cli/handlers.rs @@ -46,8 +46,7 @@ fn get_id_or_name(args: &ArgMatches) -> IdOrName { } } -#[allow(clippy::unnecessary_wraps)] // we need this to match the other functions return type -fn handle_get_subcommand(args: &ArgMatches) -> Result { +fn handle_get_subcommand(args: &ArgMatches) -> Request { let (cmd, args) = args.subcommand().expect("playback subcommand is required"); let request = match cmd { @@ -69,7 +68,7 @@ fn handle_get_subcommand(args: &ArgMatches) -> Result { _ => unreachable!(), }; - Ok(request) + request } fn handle_playback_subcommand(args: &ArgMatches) -> Result { @@ -202,7 +201,7 @@ pub fn handle_cli_subcommand(cmd: &str, args: &ArgMatches) -> Result<()> { // construct a socket request based on the CLI command and its arguments let request = match cmd { - "get" => handle_get_subcommand(args)?, + "get" => handle_get_subcommand(args), "playback" => handle_playback_subcommand(args)?, "playlist" => handle_playlist_subcommand(args)?, "connect" => Request::Connect(get_id_or_name(args)), diff --git a/spotify_player/src/client/handlers.rs b/spotify_player/src/client/handlers.rs index 59b4b2f8..9dcdbe71 100644 --- a/spotify_player/src/client/handlers.rs +++ b/spotify_player/src/client/handlers.rs @@ -1,4 +1,5 @@ use anyhow::Context; +use rspotify::model::Id; use tracing::Instrument; use crate::{ @@ -6,7 +7,6 @@ use crate::{ state::{ContextId, ContextPageType, ContextPageUIState, PageState, PlayableId, SharedState}, }; -#[cfg(feature = "lyric-finder")] use crate::utils::map_join; use super::ClientRequest; @@ -167,25 +167,24 @@ fn handle_page_change_event( } } - #[cfg(feature = "lyric-finder")] - PageState::Lyric { + PageState::Lyrics { + track_uri, track, artists, - scroll_offset, } => { if let Some(rspotify::model::PlayableItem::Track(current_track)) = state.player.read().currently_playing() { if current_track.name != *track { - tracing::info!("Current playing track \"{}\" is different from the track \"{track}\" shown up in the lyric page. Updating the track and fetching its lyric...", current_track.name); - track.clone_from(¤t_track.name); - *artists = map_join(¤t_track.artists, |a| &a.name, ", "); - *scroll_offset = 0; - - client_pub.send(ClientRequest::GetLyric { - track: track.clone(), - artists: artists.clone(), - })?; + if let Some(id) = ¤t_track.id { + tracing::info!("Currently playing track \"{}\" is different from the track \"{track}\" shown up in the lyrics page. Fetching new track's lyrics...", current_track.name); + track.clone_from(¤t_track.name); + *artists = map_join(¤t_track.artists, |a| &a.name, ", "); + *track_uri = id.uri(); + client_pub.send(ClientRequest::GetLyrics { + track_id: id.clone_static(), + })?; + } } } } diff --git a/spotify_player/src/client/mod.rs b/spotify_player/src/client/mod.rs index 86f7150d..ea687d9c 100644 --- a/spotify_player/src/client/mod.rs +++ b/spotify_player/src/client/mod.rs @@ -1,6 +1,7 @@ use std::ops::Deref; use std::{borrow::Cow, collections::HashMap, sync::Arc}; +use crate::state::Lyrics; use crate::{auth, config}; use crate::{ auth::AuthConfig, @@ -329,22 +330,16 @@ impl Client { .category_playlists .insert(category.id, playlists); } - #[cfg(feature = "lyric-finder")] - ClientRequest::GetLyric { track, artists } => { - let client = lyric_finder::Client::from_http_client(&self.http); - let query = format!("{track} {artists}"); - - if !state.data.read().caches.lyrics.contains_key(&query) { - let result = client.get_lyric(&query).await.context(format!( - "failed to get lyric for track {track} - artists {artists}" - ))?; - + ClientRequest::GetLyrics { track_id } => { + let uri = track_id.uri(); + if !state.data.read().caches.lyrics.contains_key(&uri) { + let lyrics = self.lyrics(track_id).await?; state .data .write() .caches .lyrics - .insert(query, result, *TTL_CACHE_DURATION); + .insert(uri, lyrics, *TTL_CACHE_DURATION); } } #[cfg(feature = "streaming")] @@ -609,6 +604,22 @@ impl Client { Ok(()) } + /// Get lyrics of a given track, return None if no lyrics is available + pub async fn lyrics(&self, track_id: TrackId<'static>) -> Result> { + let session = self.session().await; + let id = librespot_core::spotify_id::SpotifyId::from_uri(&track_id.uri())?; + match librespot_metadata::Lyrics::get(&session, &id).await { + Ok(lyrics) => Ok(Some(lyrics.into())), + Err(err) => { + if err.to_string().to_lowercase().contains("not found") { + Ok(None) + } else { + Err(err.into()) + } + } + } + } + /// Get user available devices // This is a custom API to replace `rspotify::device` API to support Spotify Connect feature pub async fn available_devices(&self) -> Result> { @@ -1035,15 +1046,14 @@ impl Client { } /// Search for items of a specific type matching a given query - #[allow(clippy::used_underscore_binding)] // false positive as this is because of a crate pub async fn search_specific_type( &self, query: &str, - _type: rspotify::model::SearchType, + typ: rspotify::model::SearchType, ) -> Result { Ok(self .spotify - .search(query, _type, None, None, None, None) + .search(query, typ, None, None, None, None) .await?) } diff --git a/spotify_player/src/client/request.rs b/spotify_player/src/client/request.rs index 3aef69d5..60361533 100644 --- a/spotify_player/src/client/request.rs +++ b/spotify_player/src/client/request.rs @@ -55,10 +55,8 @@ pub enum ClientRequest { DeleteFromLibrary(ItemId), Player(PlayerRequest), GetCurrentUserQueue, - #[cfg(feature = "lyric-finder")] - GetLyric { - track: String, - artists: String, + GetLyrics { + track_id: TrackId<'static>, }, #[cfg(feature = "streaming")] RestartIntegratedClient, diff --git a/spotify_player/src/command.rs b/spotify_player/src/command.rs index 90bab2f6..19c01610 100644 --- a/spotify_player/src/command.rs +++ b/spotify_player/src/command.rs @@ -62,8 +62,7 @@ pub enum Command { TopTrackPage, RecentlyPlayedTrackPage, LikedTrackPage, - #[cfg(feature = "lyric-finder")] - LyricPage, + LyricsPage, LibraryPage, SearchPage, BrowsePage, @@ -340,8 +339,7 @@ impl Command { Self::TopTrackPage => "go to the user top track page", Self::RecentlyPlayedTrackPage => "go to the user recently played track page", Self::LikedTrackPage => "go to the user liked track page", - #[cfg(feature = "lyric-finder")] - Self::LyricPage => "go to the lyric page of the current track", + Self::LyricsPage => "go to the lyrics page of the current track", Self::LibraryPage => "go to the user library page", Self::SearchPage => "go to the search page", Self::BrowsePage => "go to the browse page", diff --git a/spotify_player/src/config/keymap.rs b/spotify_player/src/config/keymap.rs index ea0f346b..d5e44956 100644 --- a/spotify_player/src/config/keymap.rs +++ b/spotify_player/src/config/keymap.rs @@ -168,15 +168,13 @@ impl Default for KeymapConfig { key_sequence: "g y".into(), command: Command::LikedTrackPage, }, - #[cfg(feature = "lyric-finder")] Keymap { key_sequence: "g L".into(), - command: Command::LyricPage, + command: Command::LyricsPage, }, - #[cfg(feature = "lyric-finder")] Keymap { key_sequence: "l".into(), - command: Command::LyricPage, + command: Command::LyricsPage, }, Keymap { key_sequence: "g l".into(), diff --git a/spotify_player/src/config/theme.rs b/spotify_player/src/config/theme.rs index 6d50591e..d6116c32 100644 --- a/spotify_player/src/config/theme.rs +++ b/spotify_player/src/config/theme.rs @@ -78,6 +78,7 @@ pub struct ComponentStyle { pub selection: Option