aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRuben Beltran del Rio <git@r.bdr.sh>2024-03-13 22:43:47 +0100
committerRuben Beltran del Rio <git@r.bdr.sh>2024-03-13 22:43:47 +0100
commit738ec06d26a2a19bdda8a992d2250e731d954631 (patch)
treec963a76fc51d5d4712026efaf9e5ac6accba9526 /src
parent4adf8d5182b7ad3720e3160e0f4530547625dbce (diff)
Add rust implementation test
Diffstat (limited to 'src')
-rw-r--r--src/lyrics_engine/genius.rs73
-rw-r--r--src/lyrics_engine/mod.rs26
-rw-r--r--src/main.rs78
-rw-r--r--src/sources/mod.rs82
4 files changed, 259 insertions, 0 deletions
diff --git a/src/lyrics_engine/genius.rs b/src/lyrics_engine/genius.rs
new file mode 100644
index 0000000..e14589d
--- /dev/null
+++ b/src/lyrics_engine/genius.rs
@@ -0,0 +1,73 @@
+use std::io::{Result, Error, ErrorKind::Other};
+use serde::Deserialize;
+use reqwest::get;
+use scraper::{ElementRef, Html, Selector, node::Node::{Text, Element}};
+
+#[derive(Deserialize, Debug)]
+struct GeniusApiResponse {
+ response: GeniusResponseBlock,
+}
+
+#[derive(Deserialize, Debug)]
+struct GeniusResponseBlock {
+ hits: Vec<GeniusHit>,
+}
+
+#[derive(Deserialize, Debug)]
+struct GeniusHit {
+ #[serde(rename = "type")]
+ hit_type: String,
+ result: GeniusResult,
+}
+
+#[derive(Deserialize, Debug)]
+struct GeniusResult {
+ url: String,
+}
+
+pub async fn search(url: &str) -> Result<String> {
+ let response = get(url).await
+ .map_err(|_| Error::new(Other, "Could not perform lyrics search in engine."))?
+ .json::<GeniusApiResponse>().await
+ .map_err(|_| Error::new(Other, "Lyrics engine returned invalid response from search."))?;
+ let url = response.response.hits.into_iter()
+ .find(|hit| hit.hit_type == "song")
+ .map(|hit| hit.result.url)
+ .ok_or_else(|| Error::new(Other, "Could not find a matching track in lyrics engine."))?;
+
+ Ok(url)
+}
+
+pub async fn get_lyrics(url: &str) -> Result<String> {
+ let song_html = get(url).await
+ .map_err(|_| Error::new(Other, "Could not fetch lyrics from engine."))?
+ .text().await
+ .map_err(|_| Error::new(Other, "Lyrics engine returned invalid response."))?;
+
+ let document = Html::parse_document(&song_html);
+ let selector = Selector::parse(r#"div[data-lyrics-container="true"]"#).unwrap();
+ let lyrics_div = document.select(&selector).next()
+ .ok_or_else(|| Error::new(Other, "Could not find lyrics in response."))?;
+
+ let mut lyrics = String::new();
+ for node in lyrics_div.children() {
+ match node.value() {
+ Element(element) => {
+ if element.name() == "br" {
+ lyrics.push('\n');
+ } else {
+ if let Some(element_ref) = ElementRef::wrap(node) {
+ let text = element_ref.text().collect::<Vec<_>>().join("");
+ lyrics.push_str(&text);
+ }
+ }
+ },
+ Text(text) => {
+ lyrics.push_str(text);
+ },
+ _ => {}
+ }
+ }
+
+ Ok(lyrics)
+}
diff --git a/src/lyrics_engine/mod.rs b/src/lyrics_engine/mod.rs
new file mode 100644
index 0000000..f83d219
--- /dev/null
+++ b/src/lyrics_engine/mod.rs
@@ -0,0 +1,26 @@
+use std::io::Result;
+use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
+
+mod genius;
+
+use crate::Track;
+use genius::{get_lyrics, search};
+
+const GENIUS_CLIENT_TOKEN: &str = env!("LYRICLI_GENIUS_TOKEN");
+const GENIUS_API_URL: &str = "https://api.genius.com/search";
+
+pub async fn print_lyrics(track: Track, show_title: bool) -> Result<()> {
+ if show_title {
+ println!("{} - {}", track.artist, track.name);
+ }
+
+ let artist = utf8_percent_encode(&track.artist, NON_ALPHANUMERIC).to_string();
+ let name = utf8_percent_encode(&track.name, NON_ALPHANUMERIC).to_string();
+ let url = format!("{}?access_token={}&q={}%20{}", GENIUS_API_URL, GENIUS_CLIENT_TOKEN, artist, name);
+
+ let song_url = search(&url).await?;
+ let lyrics = get_lyrics(&song_url).await?;
+
+ println!("{}", lyrics);
+ Ok(())
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..13b7e37
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,78 @@
+// mod configuration;
+mod lyrics_engine;
+mod sources;
+
+use std::io::{Result, Error, ErrorKind::Other};
+use clap::{Parser};
+
+use sources::{enable, disable, get_track, reset, list};
+use lyrics_engine::print_lyrics;
+
+#[derive(Parser, Debug)]
+#[command(version, about, long_about = None)]
+struct Arguments {
+
+ // Positional Arguments
+ /// Specify the artist.
+ artist: Option<String>,
+
+ /// Specify the artist.
+ track_name: Option<String>,
+
+ /// Show title of track if present
+ #[arg(short = 't', long)]
+ show_title: bool,
+
+ /// Lists all sources
+ #[arg(short, long)]
+ list_sources: bool,
+
+ /// Enables a source
+ #[arg(short, long, value_name = "SOURCE")]
+ enable_source: Option<String>,
+
+ #[arg(short, long, value_name = "SOURCE")]
+ disable_source: Option<String>,
+
+ #[arg(short, long, value_name = "SOURCE")]
+ reset_source: Option<String>,
+}
+
+pub struct Track {
+ pub name: String,
+ pub artist: String
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ let arguments = Arguments::parse();
+
+ if arguments.list_sources {
+ return list();
+ }
+
+ if let Some(source_name) = arguments.enable_source {
+ return enable(source_name);
+ }
+
+ if let Some(source_name) = arguments.disable_source {
+ return disable(source_name);
+ }
+
+ if let Some(source_name) = arguments.reset_source {
+ return reset(source_name);
+ }
+
+ let current_track: Track;
+ if let Some(artist) = arguments.artist {
+ current_track = Track {
+ name: arguments.track_name.unwrap_or("".to_string()),
+ artist: artist
+ };
+ } else {
+ current_track = get_track()
+ .ok_or_else(|| Error::new(Other, "No Artist/Song could be found :("))?
+ }
+
+ print_lyrics(current_track, arguments.show_title).await
+}
diff --git a/src/sources/mod.rs b/src/sources/mod.rs
new file mode 100644
index 0000000..fb7af8f
--- /dev/null
+++ b/src/sources/mod.rs
@@ -0,0 +1,82 @@
+use std::io::Result;
+
+// #[cfg(target_os = "macos")]
+// mod applie_music;
+// #[cfg(target_os = "macos")]
+// mod spotify;
+// #[cfg(not(target_os = "macos"))]
+// mod rhythmbox;
+// #[cfg(not(target_os = "macos"))]
+// mod quod_libe;
+// #[cfg(not(target_os = "macos"))]
+// mod strawberry;
+// #[cfg(not(target_os = "macos"))]
+// mod tauon;
+
+// #[cfg(target_os = "macos")]
+// use apple_music::AppleMusic;
+// #[cfg(target_os = "macos")]
+// use spotify::Spotify;
+
+// #[cfg(not(target_os = "macos"))]
+// use rhythmbox::Rhythmbox;
+// #[cfg(not(target_os = "macos"))]
+// use quod_libet::QuodLibet;
+// #[cfg(not(target_os = "macos"))]
+// use strawberry::Strawberry;
+// #[cfg(not(target_os = "macos"))]
+// use tauon::Tauon;
+
+use crate::Track;
+
+pub trait LyricsSource {
+ fn name(&self) -> String;
+
+ fn current_track(&self) -> Option<Track>;
+
+ fn disable(&self) -> Result<()>;
+ fn enable(&self) -> Result<()>;
+ fn reset(&self) -> Result<()>;
+}
+
+pub fn list() -> Result<()> {
+ Ok(())
+}
+
+pub fn enable(source_name: String) -> Result<()> {
+ println!("Enabling {}", source_name);
+ Ok(())
+}
+
+pub fn disable(source_name: String) -> Result<()> {
+ println!("Disabling {}", source_name);
+ Ok(())
+}
+
+pub fn reset(source_name: String) -> Result<()> {
+ println!("Reset {}", source_name);
+ Ok(())
+}
+
+pub fn get_track() -> Option<Track> {
+ return None
+}
+
+pub fn available_sources() -> Vec<Box<dyn LyricsSource>> {
+ let mut sources: Vec<Box<dyn LyricsSource>> = Vec::new();
+ #[cfg(target_os = "macos")]
+ {
+ // sources.push(Box::new(AppleMusic::new()));
+ // sources.push(Box::new(Spotify::new()));
+ }
+
+ #[cfg(not(target_os = "macos"))]
+ {
+ // sources.push(Box::new(Rhythmbox::new()));
+ // sources.push(Box::new(QuodLibet::new()));
+ // sources.push(Box::new(Strawberry::new()));
+ // sources.push(Box::new(Tauon::new()));
+ }
+
+ sources
+}