Repository: aome510/spotify-player (language: rust)

Issue:
Alternative lyrics source
Genius is being aggressive on some regions, result in unable to get lyrics with current `lyric_finder` crate.

Some logs like
```
 30:2023-03-04T12:08:13.305693Z ERROR client_request{request=GetLyric { track: "Kill V. Maim", artists: "Grimes" }}: spotify_player::client::handlers: Failed to handle client request: failed to get lyric for track Kill V. Maim - artists Grimes: error decoding response body: expected value at line 1 column 1: expected value at line 1 column 1
```

It would be nice to have some alternative lyrics source like musixmatch or netease.

Some sources - 
[spicetify](https://github.com/spicetify/spicetify-cli/tree/master/CustomApps/lyrics-plus)

[spotify-qt](https://github.com/kraxarn/spotify-qt/blob/402d1a0a3838be4c6b62950f3634af469c59070f/lib/src/lyrics/api.cpp)




Discussion / hints:

Fix patch:
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<Request> {
+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<Request> {
         _ => unreachable!(),
     };
 
-    Ok(request)
+    request
 }
 
 fn handle_playback_subcommand(args: &ArgMatches) -> Result<Request> {
@@ -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(&current_track.name);
-                    *artists = map_join(&current_track.artists, |a| &a.name, ", ");
-                    *scroll_offset = 0;
-
-                    client_pub.send(ClientRequest::GetLyric {
-                        track: track.clone(),
-                        artists: artists.clone(),
-                    })?;
+                    if let Some(id) = &current_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(&current_track.name);
+                        *artists = map_join(&current_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<Option<Lyrics>> {
+        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<Vec<rspotify::model::Device>> {
@@ -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<rspotify::model::SearchResult> {
         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<Style>,
     pub secondary_row: Option<Style>,
     pub like: Option<Style>,
+    pub lyrics_played: Option<Style>,
 }
 
 #[derive(Default, Clone, Debug, Deserialize)]
@@ -165,6 +166,7 @@ impl ThemeConfig {
     }
 }
 
+// TODO: cleanup implementation for style getter methods
 impl Theme {
     pub fn app(&self) -> style::Style {
         let mut style = style::Style::default();
@@ -319,6 +321,14 @@ impl Theme {
             Some(s) => s.style(&self.palette),
         }
     }
+    pub fn lyrics_played(&self) -> tui::style::Style {
+        match &self.component_style.lyrics_played {
+            None => Style::default()
+                .modifiers(vec![StyleModifier::Dim])
+                .style(&self.palette),
+            Some(s) => s.style(&self.palette),
+        }
+    }
 }
 
 impl Style {
diff --git a/spotify_player/src/event/mod.rs b/spotify_player/src/event/mod.rs
index b1e9afa8..3f824e3c 100644
--- a/spotify_player/src/event/mod.rs
+++ b/spotify_player/src/event/mod.rs
@@ -19,7 +19,6 @@ use crate::{
     utils::parse_uri,
 };
 
-#[cfg(feature = "lyric-finder")]
 use crate::utils::map_join;
 use anyhow::{Context as _, Result};
 
@@ -734,22 +733,22 @@ fn handle_global_command(
                 tracing::warn!("clipboard's content ({content}) is not a valid Spotify link!");
             }
         }
-        #[cfg(feature = "lyric-finder")]
-        Command::LyricPage => {
+        Command::LyricsPage => {
             if let Some(rspotify::model::PlayableItem::Track(track)) =
                 state.player.read().currently_playing()
             {
-                let artists = map_join(&track.artists, |a| &a.name, ", ");
-                ui.new_page(PageState::Lyric {
-                    track: track.name.clone(),
-                    artists: artists.clone(),
-                    scroll_offset: 0,
-                });
+                if let Some(id) = &track.id {
+                    let artists = map_join(&track.artists, |a| &a.name, ", ");
+                    ui.new_page(PageState::Lyrics {
+                        track_uri: id.uri(),
+                        track: track.name.clone(),
+                        artists,
+                    });
 
-                client_pub.send(ClientRequest::GetLyric {
-                    track: track.name.clone(),
-                    artists,
-                })?;
+                    client_pub.send(ClientRequest::GetLyrics {
+                        track_id: id.clone_static(),
+                    })?;
+                }
             }
         }
         Command::SwitchDevice => {
diff --git a/spotify_player/src/event/page.rs b/spotify_player/src/event/page.rs
index 331214b0..a089b11a 100644
--- a/spotify_player/src/event/page.rs
+++ b/spotify_player/src/event/page.rs
@@ -25,10 +25,10 @@ pub fn handle_key_sequence_for_page(
             PageType::Library => handle_command_for_library_page(command, client_pub, ui, state),
             PageType::Context => handle_command_for_context_page(command, client_pub, ui, state),
             PageType::Browse => handle_command_for_browse_page(command, client_pub, ui, state),
-            #[cfg(feature = "lyric-finder")]
-            PageType::Lyric => handle_command_for_lyric_page(command, ui),
-            PageType::Queue => handle_command_for_queue_page(command, ui),
-            PageType::CommandHelp => handle_command_for_command_help_page(command, ui),
+            // lyrics page doesn't support any commands
+            PageType::Lyrics => Ok(false),
+            PageType::Queue => Ok(handle_command_for_queue_page(command, ui)),
+            PageType::CommandHelp => Ok(handle_command_for_command_help_page(command, ui)),
         },
         Some(CommandOrAction::Action(action, ActionTarget::SelectedItem)) => match page_type {
             PageType::Search => anyhow::bail!("page search type should already be handled!"),
@@ -416,54 +416,24 @@ fn handle_command_for_browse_page(
     Ok(true)
 }
 
-#[cfg(feature = "lyric-finder")]
-#[allow(clippy::unnecessary_wraps)] // match return type
-fn handle_command_for_lyric_page(command: Command, ui: &mut UIStateGuard) -> Result<bool> {
-    let scroll_offset = match ui.current_page() {
-        PageState::Lyric { scroll_offset, .. } => *scroll_offset,
-        _ => return Ok(false),
-    };
-    Ok(handle_navigation_command(
-        command,
-        ui.current_page_mut(),
-        scroll_offset,
-        10000,
-    ))
-}
-
-#[allow(clippy::unnecessary_wraps)] // for consistency
-fn handle_command_for_queue_page(
-    command: Command,
-    ui: &mut UIStateGuard,
-) -> Result<bool, anyhow::Error> {
+fn handle_command_for_queue_page(command: Command, ui: &mut UIStateGuard) -> bool {
     let scroll_offset = match ui.current_page() {
         PageState::Queue { scroll_offset } => *scroll_offset,
-        _ => return Ok(false),
+        _ => return false,
     };
-    Ok(handle_navigation_command(
-        command,
-        ui.current_page_mut(),
-        scroll_offset,
-        10000,
-    ))
+    handle_navigation_command(command, ui.current_page_mut(), scroll_offset, 10000)
 }
 
-#[allow(clippy::unnecessary_wraps)] // for consistency
-fn handle_command_for_command_help_page(command: Command, ui: &mut UIStateGuard) -> Result<bool> {
+fn handle_command_for_command_help_page(command: Command, ui: &mut UIStateGuard) -> bool {
     let scroll_offset = match ui.current_page() {
         PageState::CommandHelp { scroll_offset } => *scroll_offset,
-        _ => return Ok(false),
+        _ => return false,
     };
     if command == Command::Search {
         ui.new_search_popup();
-        return Ok(true);
+        return true;
     }
-    Ok(handle_navigation_command(
-        command,
-        ui.current_page_mut(),
-        scroll_offset,
-        10000,
-    ))
+    handle_navigation_command(command, ui.current_page_mut(), scroll_offset, 10000)
 }
 
 pub fn handle_navigation_command(
diff --git a/spotify_player/src/event/popup.rs b/spotify_player/src/event/popup.rs
index d9852c1f..2aae01a9 100644
--- a/spotify_player/src/event/popup.rs
+++ b/spotify_player/src/event/popup.rs
@@ -285,19 +285,18 @@ pub fn handle_key_sequence_for_popup(
     }
 }
 
-#[allow(clippy::manual_let_else)] // we need this to get mutable reference to the popup state
 fn handle_key_sequence_for_create_playlist_popup(
     key_sequence: &KeySequence,
     client_pub: &flume::Sender<ClientRequest>,
     ui: &mut UIStateGuard,
 ) -> Result<bool> {
-    let (name, desc, current_field) = match ui.popup {
-        Some(PopupState::PlaylistCreate {
-            ref mut name,
-            ref mut desc,
-            ref mut current_field,
-        }) => (name, desc, current_field),
-        _ => return Ok(false),
+    let Some(PopupState::PlaylistCreate {
+        name,
+        desc,
+        current_field,
+    }) = &mut ui.popup
+    else {
+        return Ok(false);
     };
     if key_sequence.keys.len() == 1 {
         match &key_sequence.keys[0] {
@@ -332,7 +331,6 @@ fn handle_key_sequence_for_create_playlist_popup(
     Ok(false)
 }
 
-#[allow(clippy::manual_let_else)] // we need this to get mutable reference to the popup state
 fn handle_key_sequence_for_search_popup(
     key_sequence: &KeySequence,
     client_pub: &flume::Sender<ClientRequest>,
@@ -340,9 +338,8 @@ fn handle_key_sequence_for_search_popup(
     ui: &mut UIStateGuard,
 ) -> Result<bool> {
     // handle user's input that updates the search query
-    let query = match ui.popup {
-        Some(PopupState::Search { ref mut query }) => query,
-        _ => return Ok(false),
+    let Some(PopupState::Search { ref mut query }) = &mut ui.popup else {
+        return Ok(false);
     };
     if key_sequence.keys.len() == 1 {
         if let Key::None(c) = key_sequence.keys[0] {
diff --git a/spotify_player/src/state/data.rs b/spotify_player/src/state/data.rs
index ef002173..6d90d96c 100644
--- a/spotify_player/src/state/data.rs
+++ b/spotify_player/src/state/data.rs
@@ -8,10 +8,11 @@ use super::model::{
     Album, Artist, Category, Context, ContextId, Id, Playlist, PlaylistFolderItem,
     PlaylistFolderNode, SearchResults, Show, Track,
 };
+use super::Lyrics;
 
 pub type DataReadGuard<'a> = parking_lot::RwLockReadGuard<'a, AppData>;
 
-#[derive(Debug)]
+#[derive(Debug, Copy, Clone)]
 pub enum FileCacheKey {
     Playlists,
     PlaylistFolders,
@@ -48,8 +49,7 @@ pub struct UserData {
 pub struct MemoryCaches {
     pub context: ttl_cache::TtlCache<String, Context>,
     pub search: ttl_cache::TtlCache<String, SearchResults>,
-    #[cfg(feature = "lyric-finder")]
-    pub lyrics: ttl_cache::TtlCache<String, lyric_finder::LyricResult>,
+    pub lyrics: ttl_cache::TtlCache<String, Option<Lyrics>>,
     #[cfg(feature = "image")]
     pub images: ttl_cache::TtlCache<String, image::DynamicImage>,
 }
@@ -66,7 +66,6 @@ impl MemoryCaches {
         Self {
             context: ttl_cache::TtlCache::new(64),
             search: ttl_cache::TtlCache::new(64),
-            #[cfg(feature = "lyric-finder")]
             lyrics: ttl_cache::TtlCache::new(64),
             #[cfg(feature = "image")]
             images: ttl_cache::TtlCache::new(64),
@@ -188,7 +187,6 @@ impl UserData {
     }
 }
 
-#[allow(clippy::needless_pass_by_value)] // that's ok here
 pub fn store_data_into_file_cache<T: Serialize>(
     key: FileCacheKey,
     cache_folder: &Path,
@@ -200,7 +198,6 @@ pub fn store_data_into_file_cache<T: Serialize>(
     Ok(())
 }
 
-#[allow(clippy::needless_pass_by_value)] // that's ok here
 pub fn load_data_from_file_cache<T>(key: FileCacheKey, cache_folder: &Path) -> Option<T>
 where
     T: DeserializeOwned,
diff --git a/spotify_player/src/state/model.rs b/spotify_player/src/state/model.rs
index c0f2cb22..edd34e7e 100644
--- a/spotify_player/src/state/model.rs
+++ b/spotify_player/src/state/model.rs
@@ -677,3 +677,27 @@ impl PlaybackMetadata {
         }
     }
 }
+
+#[derive(Debug)]
+pub struct Lyrics {
+    /// Timestamped lines
+    pub lines: Vec<(chrono::Duration, String)>,
+}
+
+impl From<librespot_metadata::lyrics::Lyrics> for Lyrics {
+    fn from(value: librespot_metadata::lyrics::Lyrics) -> Self {
+        let mut lines = value
+            .lyrics
+            .lines
+            .into_iter()
+            .map(|l| {
+                let t = chrono::Duration::milliseconds(
+                    l.start_time_ms.parse::<i64>().expect("invalid number"),
+                );
+                (t, l.words)
+            })
+            .collect::<Vec<_>>();
+        lines.sort_by_key(|l| l.0);
+        Self { lines }
+    }
+}
diff --git a/spotify_player/src/state/ui/page.rs b/spotify_player/src/state/ui/page.rs
index 0ea16f17..604ef4fe 100644
--- a/spotify_player/src/state/ui/page.rs
+++ b/spotify_player/src/state/ui/page.rs
@@ -19,11 +19,10 @@ pub enum PageState {
         current_query: String,
         state: SearchPageUIState,
     },
-    #[cfg(feature = "lyric-finder")]
-    Lyric {
+    Lyrics {
+        track_uri: String,
         track: String,
         artists: String,
-        scroll_offset: usize,
     },
     Browse {
         state: BrowsePageUIState,
@@ -42,8 +41,7 @@ pub enum PageType {
     Context,
     Search,
     Browse,
-    #[cfg(feature = "lyric-finder")]
-    Lyric,
+    Lyrics,
     Queue,
     CommandHelp,
 }
@@ -146,8 +144,7 @@ impl PageState {
             PageState::Context { .. } => PageType::Context,
             PageState::Search { .. } => PageType::Search,
             PageState::Browse { .. } => PageType::Browse,
-            #[cfg(feature = "lyric-finder")]
-            PageState::Lyric { .. } => PageType::Lyric,
+            PageState::Lyrics { .. } => PageType::Lyrics,
             PageState::Queue { .. } => PageType::Queue,
             PageState::CommandHelp { .. } => PageType::CommandHelp,
         }
@@ -234,8 +231,7 @@ impl PageState {
                     Some(MutableWindowState::List(state))
                 }
             },
-            #[cfg(feature = "lyric-finder")]
-            Self::Lyric { scroll_offset, .. } => Some(MutableWindowState::Scroll(scroll_offset)),
+            Self::Lyrics { .. } => None,
             Self::CommandHelp { scroll_offset } | Self::Queue { scroll_offset } => {
                 Some(MutableWindowState::Scroll(scroll_offset))
             }
diff --git a/spotify_player/src/ui/mod.rs b/spotify_player/src/ui/mod.rs
index b7abbaf0..8a7f058f 100644
--- a/spotify_player/src/ui/mod.rs
+++ b/spotify_player/src/ui/mod.rs
@@ -130,8 +130,7 @@ fn render_main_layout(
         PageType::Search => page::render_search_page(is_active, frame, state, ui, rect),
         PageType::Context => page::render_context_page(is_active, frame, state, ui, rect),
         PageType::Browse => page::render_browse_page(is_active, frame, state, ui, rect),
-        #[cfg(feature = "lyric-finder")]
-        PageType::Lyric => page::render_lyric_page(is_active, frame, state, ui, rect),
+        PageType::Lyrics => page::render_lyrics_page(is_active, frame, state, ui, rect),
         PageType::Queue => page::render_queue_page(frame, state, ui, rect),
         PageType::CommandHelp => page::render_commands_help_page(frame, ui, rect),
     }
diff --git a/spotify_player/src/ui/page.rs b/spotify_player/src/ui/page.rs
index d8e04d15..876b1c0f 100644
--- a/spotify_player/src/ui/page.rs
+++ b/spotify_player/src/ui/page.rs
@@ -3,6 +3,8 @@ use std::{
     fmt::Display,
 };
 
+use tui::text::Line;
+
 use crate::{state::Episode, utils::format_duration};
 
 use super::{
@@ -542,8 +544,7 @@ pub fn render_browse_page(
     utils::render_list_window(frame, list, rect, len, list_state);
 }
 
-#[cfg(feature = "lyric-finder")]
-pub fn render_lyric_page(
+pub fn render_lyrics_page(
     _is_active: bool,
     frame: &mut Frame,
     state: &SharedState,
@@ -551,54 +552,77 @@ pub fn render_lyric_page(
     rect: Rect,
 ) {
     // 1. Get data
+    let progress = state
+        .player
+        .read()
+        .playback_progress()
+        .expect("non-empty playback");
     let data = state.data.read();
 
     // 2. Construct the page's layout
-    let rect = construct_and_render_block("Lyric", &ui.theme, Borders::ALL, frame, rect);
-    let chunks = Layout::vertical([Constraint::Length(1), Constraint::Fill(0)]).split(rect);
+    let rect = construct_and_render_block("Lyrics", &ui.theme, Borders::ALL, frame, rect);
+    let chunks = Layout::vertical([Constraint::Length(2), Constraint::Fill(0)]).split(rect);
 
     // 3. Construct the page's widgets
-    let PageState::Lyric {
+    let PageState::Lyrics {
+        track_uri,
         track,
         artists,
-        scroll_offset,
     } = ui.current_page_mut()
     else {
         return;
     };
 
-    let (desc, lyric) = match data.caches.lyrics.get(&format!("{track} {artists}")) {
+    let lyrics = match data.caches.lyrics.get(track_uri) {
         None => {
             frame.render_widget(Paragraph::new("Loading..."), rect);
             return;
         }
-        Some(lyric_finder::LyricResult::None) => {
-            frame.render_widget(Paragraph::new("Lyric not found"), rect);
+        Some(None) => {
+            frame.render_widget(Paragraph::new("Lyrics not found"), rect);
             return;
         }
-        Some(lyric_finder::LyricResult::Some {
-            track,
-            artists,
-            lyric,
-        }) => (format!("{track} by {artists}"), format!("\n{lyric}")),
+        Some(Some(lyrics)) => lyrics,
     };
 
-    // update the scroll offset so that it doesn't exceed the lyric's length
-    let n_rows = lyric.matches('\n').count() + 1;
-    if *scroll_offset >= n_rows {
-        *scroll_offset = n_rows - 1;
-    }
-    let scroll_offset = *scroll_offset;
-
     // 4. Render the page's widgets
     // render lyric page description text
-    frame.render_widget(Paragraph::new(desc).style(ui.theme.page_desc()), chunks[0]);
-
-    // render lyric text
     frame.render_widget(
-        Paragraph::new(lyric).scroll((scroll_offset as u16, 0)),
-        chunks[1],
+        Paragraph::new(format!("{track} by {artists}")).style(ui.theme.page_desc()),
+        chunks[0],
     );
+
+    // render lyric text
+
+    // the last played line id (1-based)
+    // zero value indicates no line has been played yet
+    let mut last_played_line_id = 0;
+    for (id, (t, _)) in lyrics.lines.iter().enumerate() {
+        if *t <= progress {
+            last_played_line_id = id + 1;
+        }
+    }
+    let lines = lyrics
+        .lines
+        .iter()
+        .enumerate()
+        .map(|(id, (_, line))| {
+            if id < last_played_line_id {
+                Line::styled(line, ui.theme.lyrics_played())
+            } else {
+                Line::raw(line)
+            }
+        })
+        .collect::<Vec<_>>();
+
+    let mut paragraph = Paragraph::new(lines);
+    // keep the currently playing line in the center if
+    // the line goes pass the lower half of lyrics section
+    let half_height = (chunks[1].height / 2) as usize;
+    if let Some(offset) = last_played_line_id.checked_sub(half_height) {
+        paragraph = paragraph.scroll((offset as u16, 0));
+    }
+    frame.render_widget(paragraph, chunks[1]);
 }
 
 pub fn render_commands_help_page(frame: &mut Frame, ui: &mut UIStateGuard, rect: Rect) {


Test patch: