genius_rust/
lib.rs

1//! # genius-rs
2//!
3//!  Rust library that allows interact with Genius API.
4//!
5//!  Create an API Client at <https://genius.com/developers> and get the token to get Genius API access.
6//! ## Searching for a Song
7//!
8//! ```rust
9//! use genius_rs::Genius;
10//!
11//! #[tokio::main]
12//! async fn main() {
13//!     let genius = Genius::new(dotenv::var("TOKEN").unwrap());
14//!     let response = genius.search("Ariana Grande").await.unwrap();
15//!     println!("{}", response[0].result.full_title);
16//! }
17//! ```
18//!
19//! ## Getting lyrics
20//!
21//! ```rust
22//! use genius_rs::Genius;
23//!
24//! #[tokio::main]
25//! async fn main() {
26//!     let genius = Genius::new(dotenv::var("TOKEN").unwrap());
27//!     let response = genius.search("Sia").await.unwrap();
28//!     let lyrics = genius.get_lyrics(response[0].result.id).await.unwrap();
29//!     for verse in lyrics {
30//!         println!("{}", verse);
31//!     }
32//! }
33//! ```
34//!
35//! ## Getting deeper information for a song by id
36//!
37//! ```rust
38//! use genius_rs::Genius;
39//!
40//! #[tokio::main]
41//! async fn main() {
42//!     let genius = Genius::new(dotenv::var("TOKEN").unwrap());
43//!     let response = genius.search("Weeknd").await.unwrap();
44//!     let song = genius.get_song(response[0].result.id, "plain").await.unwrap();
45//!     println!("{}", song.media.unwrap()[0].url)
46//! }
47//! ```
48
49#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::str_to_string)]
50#![allow(clippy::module_name_repetitions, clippy::struct_excessive_bools)]
51
52/// Album response
53pub mod album;
54/// Annotation response
55pub mod annotation;
56/// Authentication methods
57pub mod auth;
58/// Error response
59pub mod error;
60/// Search response
61pub mod search;
62/// Song response
63pub mod song;
64/// User response
65pub mod user;
66
67use album::Album;
68use error::GeniusError;
69use reqwest::Client;
70use search::Hit;
71use serde::{Serialize, Deserialize};
72use song::Song;
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[tokio::test]
79    async fn search_test() {
80        let genius = Genius::new(dotenv::var("TOKEN").unwrap());
81        let result = genius.search("Ariana Grande").await;
82        assert!(result.is_ok());
83    }
84
85    #[tokio::test]
86    async fn get_lyrics_test() {
87        let genius = Genius::new(dotenv::var("TOKEN").unwrap());
88        let lyrics = genius.get_lyrics(1).await.unwrap();
89        for verse in lyrics {
90            println!("{}", verse);
91        }
92    }
93
94    #[tokio::test]
95    async fn get_song_test() {
96        let genius = Genius::new(dotenv::var("TOKEN").unwrap());
97        genius.get_song(378_195, "plain").await.unwrap();
98    }
99
100    #[tokio::test]
101    async fn get_album_test() {
102        let genius = Genius::new(dotenv::var("TOKEN").unwrap());
103        genius.get_album(27501, "plain").await.unwrap();
104    }
105}
106
107const URL: &str = "https://api.genius.com";
108
109/// The main hub for interacting with the Genius API
110pub struct Genius {
111    reqwest: Client,
112    token: String,
113}
114
115impl Genius {
116    /// Create an API Client at <https://genius.com/developers> and get the token to get basic Genius API access. The token will be level client.
117    #[must_use]
118    pub fn new(token: String) -> Self {
119        Self {
120            reqwest: Client::new(),
121            token,
122        }
123    }
124
125    /// Search for a song in Genius the result will be [`search::Hit`]
126    ///
127    /// # Errors
128    ///
129    /// Will return [`GeniusError::RequestError`] if the request fails.
130    /// Will return [`GeniusError::Unauthorized`] if the token is invalid.
131    /// Will return [`GeniusError::ParseError`] if the response is not valid JSON if this occurs you should contact the developer.
132    /// Will return [`GeniusError::NotFound`] if the field `hits` is empty in the response if this occurs you should contact the developer.
133    pub async fn search(&self, q: &str) -> Result<Vec<Hit>, GeniusError> {
134        let request = self
135            .reqwest
136            .get(format!("{}/search?q={}", URL, q))
137            .bearer_auth(&self.token)
138            .send()
139            .await;
140        let request = match request {
141            Ok(request) => request.json::<Response>().await,
142            Err(e) => return Err(GeniusError::RequestError(e.to_string())),
143        };
144        let res = match request {
145            Ok(res) => res.response.hits,
146            Err(e) => {
147                if let Some(status) = e.status() {
148                    if status.is_client_error() {
149                        return Err(GeniusError::Unauthorized(e.to_string()));
150                    }
151                }
152                return Err(GeniusError::ParseError(e.to_string()));
153            }
154        };
155        match res {
156            Some(res) => Ok(res),
157            None => Err(GeniusError::NotFound("Hits not found in data".to_owned())),
158        }
159    }
160
161    /// Get lyrics with an url of genius song like: <https://genius.com/Sia-chandelier-lyrics>
162    ///
163    /// # Errors
164    ///
165    /// Will return [`GeniusError::RequestError`] if the request fails.
166    /// Will return [`GeniusError::ParseError`] if the response is not valid JSON if this occurs you should contact the developer.
167    /// Will return [`GeniusError::NotFound`] if the field `hits` is empty in the response if this occurs you should contact the developer.
168    pub async fn get_lyrics(&self, id: u32) -> Result<Vec<String>, GeniusError> {
169        let request = self
170            .reqwest
171            .get(format!("https://lyrics.altart.tk/api/lyrics/{}", id))
172            .send()
173            .await;
174        let request = match request {
175            Ok(request) => request.json::<Body>().await,
176            Err(e) => return Err(GeniusError::RequestError(e.to_string())),
177        };
178        let plain = match request {
179            Ok(res) => res.plain,
180            Err(e) => {
181                if let Some(status) = e.status() {
182                    if status.is_client_error() {
183                        return Err(GeniusError::Unauthorized(e.to_string()));
184                    }
185                }
186                return Err(GeniusError::ParseError(e.to_string()));
187            }
188        };
189        match plain {
190            Some(text) => Ok(text.split('\n').map(String::from).collect::<Vec<String>>()),
191            None => Err(GeniusError::NotFound("Lyrics not found in data".to_owned())),
192        }
193    }
194
195    /// Get deeper information from a song by it's id, `text_format` is the field for the format of text bodies related to the document. Available text formats are `plain` and `html`
196    ///
197    /// # Errors
198    ///
199    /// Will return [`GeniusError::RequestError`] if the request fails.
200    /// Will return [`GeniusError::Unauthorized`] if the token is invalid.
201    /// Will return [`GeniusError::ParseError`] if the response is not valid JSON if this occurs you should contact the developer.
202    /// Will return [`GeniusError::NotFound`] if the field `hits` is empty in the response if this occurs you should contact the developer.
203    pub async fn get_song(&self, id: u32, text_format: &str) -> Result<Song, GeniusError> {
204        let request = self
205            .reqwest
206            .get(format!("{}/songs/{}?text_format={}", URL, id, text_format))
207            .bearer_auth(&self.token)
208            .send()
209            .await;
210        let request = match request {
211            Ok(request) => request.json::<Response>().await,
212            Err(e) => return Err(GeniusError::RequestError(e.to_string())),
213        };
214        let res = match request {
215            Ok(res) => res.response.song,
216            Err(e) => {
217                if let Some(status) = e.status() {
218                    if status.is_client_error() {
219                        return Err(GeniusError::Unauthorized(e.to_string()));
220                    }
221                }
222                return Err(GeniusError::ParseError(e.to_string()));
223            }
224        };
225        match res {
226            Some(res) => Ok(res),
227            None => Err(GeniusError::NotFound("Song not found in data".to_owned())),
228        }
229    }
230    /// Get deeper information from a album by it's id, `text_format` is the field for the format of text bodies related to the document. Available text formats are `plain` and `html`
231    ///
232    /// # Errors
233    ///
234    /// Will return [`GeniusError::RequestError`] if the request fails.
235    /// Will return [`GeniusError::Unauthorized`] if the token is invalid.
236    /// Will return [`GeniusError::ParseError`] if the response is not valid JSON if this occurs you should contact the developer.
237    /// Will return [`GeniusError::NotFound`] if the field `hits` is empty in the response if this occurs you should contact the developer.
238    pub async fn get_album(&self, id: u32, text_format: &str) -> Result<Album, GeniusError> {
239        let request = self
240            .reqwest
241            .get(format!("{}/albums/{}?text_format={}", URL, id, text_format))
242            .bearer_auth(&self.token)
243            .send()
244            .await;
245        let request = match request {
246            Ok(request) => request.json::<Response>().await,
247            Err(e) => return Err(GeniusError::RequestError(e.to_string())),
248        };
249        let res = match request {
250            Ok(res) => res.response.album,
251            Err(e) => return Err(GeniusError::ParseError(e.to_string())),
252        };
253        match res {
254            Some(res) => Ok(res),
255            None => Err(GeniusError::NotFound("Album not found in data".to_owned())),
256        }
257    }
258}
259
260#[derive(Serialize, Deserialize, Debug)]
261pub struct Body {
262    pub plain: Option<String>,
263    pub html: Option<String>,
264}
265
266#[derive(Serialize, Deserialize, Debug)]
267pub struct Date {
268    pub year: Option<u32>,
269    pub month: Option<u32>,
270    pub day: Option<u32>,
271}
272
273#[derive(Serialize, Deserialize, Debug)]
274struct Response {
275    response: BlobResponse,
276}
277
278#[derive(Serialize, Deserialize, Debug)]
279struct BlobResponse {
280    song: Option<Song>,
281    hits: Option<Vec<Hit>>,
282    album: Option<Album>,
283}