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

Issue:
a way to open spotify links
**Is your feature request related to a problem? Please describe.**
there's currently no way to open spotify links.

**Describe the solution you'd like**
search page should recognize spotify urls and act accordingly

**Describe alternatives you've considered**
 i tried putting a link into search but it didn't give the result i wanted (and i also couldn't find the playlist by name)


**Additional context**
example of url - https://open.spotify.com/playlist/0Hm7mXPldSTuokTO7eqtVU?si=VU6LBFIVQkCMePwM26_rPA



Discussion / hints:
Let's me think about how this feature should be implemented.

> (and i also couldn't find the playlist by name)

I can search your example playlist just fine.

<img width="964" alt="Screenshot 2023-07-15 at 3 21 46 PM" src="https://github.com/aome510/spotify-player/assets/40011582/dec4bbf2-937d-4196-b641-ac98abf6bb88">

Its interesting because Spotify will detect urls in their search. Maybe you can do some regex to determine if its a url and then return the relevant results (whether the url contains "/tracks/" use the track API, etc).
> Its interesting because Spotify will detect urls in their search. Maybe you can do some regex to determine if its a url and then return the relevant results (whether the url contains "/tracks/" use the track API, etc).

yeah, that's what I have in mind to implement this feature. Unfortunately, I'm kinda busy nowadays, so I'm currently doing very basic maintaining work for this project.

**Edit**: not for searching, but a way to open `Context` page or play a track based on the link in clipboard.

Fix patch:
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index 2ce299d7..026ee6f1 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -22,19 +22,19 @@ jobs:
           - os: ubuntu-latest
             target: x86_64-unknown-linux-gnu
             container: rust
-            dependencies: "libssl-dev libasound2-dev libdbus-1-dev"
+            dependencies: "libssl-dev libasound2-dev libdbus-1-dev libxcb-shape0-dev libxcb-xfixes0-dev"
           - os: ubuntu-latest
             target: aarch64-unknown-linux-gnu
             container: rustembedded/cross:aarch64-unknown-linux-gnu
             cross_arch: "arm64"
             pkg_config_path: "/usr/lib/aarch64-linux-gnu/pkgconfig/"
-            dependencies: "libssl-dev:arm64 libasound2-dev:arm64 libdbus-1-dev:arm64"
+            dependencies: "libssl-dev:arm64 libasound2-dev:arm64 libdbus-1-dev:arm64 libxcb-shape0-dev:arm64 libxcb-xfixes0-dev:arm64"
           - os: ubuntu-latest
             target: armv7-unknown-linux-gnueabihf
             cross_arch: "armhf"
             pkg_config_path: "/usr/lib/arm-linux-gnueabihf/pkgconfig/"
             container: rustembedded/cross:armv7-unknown-linux-gnueabihf
-            dependencies: "libssl-dev:armhf libasound2-dev:armhf libdbus-1-dev:armhf"
+            dependencies: "libssl-dev:armhf libasound2-dev:armhf libdbus-1-dev:armhf libxcb-shape0-dev:armhf libxcb-xfixes0-dev:armhf"
           - os: windows-latest
             target: x86_64-pc-windows-msvc
 
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ccfa779f..60b87d14 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -23,7 +23,7 @@ jobs:
 
     steps:
       - name: Install dependencies
-        run: sudo apt-get update && sudo apt-get install libssl-dev libasound2-dev libdbus-1-dev
+        run: sudo apt-get update && sudo apt-get install libssl-dev libasound2-dev libdbus-1-dev libxcb-shape0-dev libxcb-xfixes0-dev
         if: ${{ runner.os == 'Linux' }}
 
       - name: Checkout source
diff --git a/Cargo.lock b/Cargo.lock
index cacbd317..d0c53ec0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -540,6 +540,28 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
 
+[[package]]
+name = "clipboard"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7"
+dependencies = [
+ "clipboard-win",
+ "objc",
+ "objc-foundation",
+ "objc_id",
+ "x11-clipboard",
+]
+
+[[package]]
+name = "clipboard-win"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "cocoa"
 version = "0.24.1"
@@ -3903,6 +3925,7 @@ dependencies = [
  "backtrace",
  "chrono",
  "clap",
+ "clipboard",
  "config_parser2",
  "crossterm 0.27.0",
  "daemonize",
@@ -5013,6 +5036,15 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "x11-clipboard"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89bd49c06c9eb5d98e6ba6536cf64ac9f7ee3a009b2f53996d405b3944f6bcea"
+dependencies = [
+ "xcb",
+]
+
 [[package]]
 name = "x11-dl"
 version = "2.21.0"
@@ -5024,6 +5056,16 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "xcb"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de"
+dependencies = [
+ "libc",
+ "log",
+]
+
 [[package]]
 name = "xcursor"
 version = "0.3.4"
diff --git a/README.md b/README.md
index cf82ad45..f696e2d4 100644
--- a/README.md
+++ b/README.md
@@ -93,14 +93,10 @@ A Spotify Premium account is **required**.
 ##### Linux
 
 - [Rust and cargo](https://www.rust-lang.org/tools/install) as the build dependencies
-- `openssl`, `alsa-lib` (`streaming` feature), `libdbus` (`media-control` feature) system libraries.
-  - On Debian based systems, run the below command to install application's dependencies:
+- install `openssl`, `alsa-lib` (`streaming` feature), `libdbus` (`media-control` feature), `libxcb` system libraries.
+  - For example, on Debian based systems, run the below command to install application's dependencies:
     ```shell
-    sudo apt install libssl-dev libasound2-dev libdbus-1-dev
-    ```
-  - On Fedora based systems, run the below command to install application's dependencies:
-    ```shell
-    sudo yum install openssl-devel alsa-lib-devel dbus-devel
+    sudo apt install libssl-dev libasound2-dev libdbus-1-dev libxcb-shape0-dev libxcb-xfixes0-dev
     ```
 
 ### Binaries
@@ -382,11 +378,12 @@ List of supported commands:
 | `SearchPage`                   | go to the search page                                                   | `g s`              |
 | `BrowsePage`                   | go to the browse page                                                   | `g b`              |
 | `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`              |
-| `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`              |
+| `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`              |
diff --git a/spotify_player/Cargo.toml b/spotify_player/Cargo.toml
index 79e75de7..ec863586 100644
--- a/spotify_player/Cargo.toml
+++ b/spotify_player/Cargo.toml
@@ -46,6 +46,7 @@ once_cell = "1.18.0"
 regex = "1.9.6"
 daemonize = { version = "0.5.0", optional = true }
 ttl_cache = "0.5.1"
+clipboard = "0.5.0"
 
 [target.'cfg(target_os = "windows")'.dependencies.windows]
 version = "0.44"
diff --git a/spotify_player/src/command.rs b/spotify_player/src/command.rs
index 9c31a521..7a2ef184 100644
--- a/spotify_player/src/command.rs
+++ b/spotify_player/src/command.rs
@@ -62,6 +62,7 @@ pub enum Command {
     SearchPage,
     BrowsePage,
     PreviousPage,
+    OpenSpotifyLinkFromClipboard,
 
     SortTrackByTitle,
     SortTrackByArtists,
@@ -198,6 +199,7 @@ impl Command {
             Self::SearchPage => "go to the search page",
             Self::BrowsePage => "go to the browse page",
             Self::PreviousPage => "go to the previous page",
+            Self::OpenSpotifyLinkFromClipboard => "open a Spotify link from clipboard",
             Self::SortTrackByTitle => "sort the track table (if any) by track's title",
             Self::SortTrackByArtists => "sort the track table (if any) by track's artists",
             Self::SortTrackByAlbum => "sort the track table (if any) by track's album",
diff --git a/spotify_player/src/config/keymap.rs b/spotify_player/src/config/keymap.rs
index 39924301..342d862f 100644
--- a/spotify_player/src/config/keymap.rs
+++ b/spotify_player/src/config/keymap.rs
@@ -182,6 +182,10 @@ impl Default for KeymapConfig {
                     key_sequence: "C-q".into(),
                     command: Command::PreviousPage,
                 },
+                Keymap {
+                    key_sequence: "O".into(),
+                    command: Command::OpenSpotifyLinkFromClipboard,
+                },
                 Keymap {
                     key_sequence: "?".into(),
                     command: Command::OpenCommandHelp,
diff --git a/spotify_player/src/event/mod.rs b/spotify_player/src/event/mod.rs
index 20bac8b8..d0c08410 100644
--- a/spotify_player/src/event/mod.rs
+++ b/spotify_player/src/event/mod.rs
@@ -7,7 +7,8 @@ use crate::{
 
 #[cfg(feature = "lyric-finder")]
 use crate::utils::map_join;
-use anyhow::Result;
+use anyhow::{Context as _, Result};
+use clipboard::{ClipboardContext, ClipboardProvider};
 
 mod page;
 mod popup;
@@ -359,6 +360,54 @@ fn handle_global_command(
                 ui.popup = None;
             }
         }
+        Command::OpenSpotifyLinkFromClipboard => {
+            let content = get_clipboard_content().context("get clipboard's content")?;
+            let re = regex::Regex::new(
+                r"https://open.spotify.com/(?P<type>.*?)/(?P<id>[[:alnum:]]*).*",
+            )?;
+            if let Some(cap) = re.captures(&content) {
+                let typ = cap.name("type").expect("valid capture").as_str();
+                let id = cap.name("id").expect("valid capture").as_str();
+                match typ {
+                    // for track link, play the song
+                    "track" => {
+                        let id = TrackId::from_id(id)?.into_static();
+                        client_pub.send(ClientRequest::Player(PlayerRequest::StartPlayback(
+                            Playback::URIs(vec![id], None),
+                            None,
+                        )))?;
+                    }
+                    // for playlist/artist/album link, go to the corresponding context page
+                    "playlist" => {
+                        let id = PlaylistId::from_id(id)?.into_static();
+                        ui.create_new_page(PageState::Context {
+                            id: None,
+                            context_page_type: ContextPageType::Browsing(ContextId::Playlist(id)),
+                            state: None,
+                        });
+                    }
+                    "artist" => {
+                        let id = ArtistId::from_id(id)?.into_static();
+                        ui.create_new_page(PageState::Context {
+                            id: None,
+                            context_page_type: ContextPageType::Browsing(ContextId::Artist(id)),
+                            state: None,
+                        });
+                    }
+                    "album" => {
+                        let id = AlbumId::from_id(id)?.into_static();
+                        ui.create_new_page(PageState::Context {
+                            id: None,
+                            context_page_type: ContextPageType::Browsing(ContextId::Album(id)),
+                            state: None,
+                        });
+                    }
+                    e => anyhow::bail!("unsupported Spotify type {e}!"),
+                }
+            } else {
+                tracing::warn!("clipboard's content ({content}) is not a valid Spotify link!");
+            }
+        }
         #[cfg(feature = "lyric-finder")]
         Command::LyricPage => {
             if let Some(track) = state.player.read().current_playing_track() {
@@ -412,3 +461,15 @@ fn handle_global_command(
     }
     Ok(true)
 }
+
+fn get_clipboard_content() -> Result<String> {
+    let mut clipboard_ctx: ClipboardContext = match ClipboardProvider::new() {
+        Ok(ctx) => ctx,
+        Err(err) => anyhow::bail!("{err:#}"),
+    };
+    let content = match clipboard_ctx.get_contents() {
+        Ok(content) => content,
+        Err(err) => anyhow::bail!("{err:#}"),
+    };
+    Ok(content)
+}


Test patch: