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}