diff --git a/Cargo.toml b/Cargo.toml index d47d162..55232bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,12 +13,17 @@ license = "Apache-2.0/MIT" edition = "2021" [dependencies] -failure = "0.1.3" -log = "0.4.6" -md5 = "0.6.0" -rand = "0.6.1" +thiserror = "1.0" +log = "0.4" +md5 = "0.7" +rand = "0.8" readonly = "0.2" -serde = "1.0.80" -serde_derive = "1.0.80" -serde_json = "1.0.33" -reqwest = "0.9.5" +serde = "1" +serde_derive = "1" +serde_json = "1" +reqwest = { version = "0.11", features = ["json"] } +async-trait = "0.1.67" +url = "2.3.1" + +[dev-dependencies] +tokio-test = "0.4.2" diff --git a/src/annotate.rs b/src/annotate.rs index b1afb58..273a93d 100644 --- a/src/annotate.rs +++ b/src/annotate.rs @@ -4,15 +4,16 @@ use crate::query::Query; use crate::{Album, Artist, Client, Error, Result, Song}; /// Allows starring, rating, and scrobbling media. +#[async_trait::async_trait] pub trait Annotatable { /// Attaches a star to the content. - fn star(&self, client: &Client) -> Result<()>; + async fn star(&self, client: &Client) -> Result<()>; /// Removes a star from the content. - fn unstar(&self, client: &Client) -> Result<()>; + async fn unstar(&self, client: &Client) -> Result<()>; /// Sets the rating for the content. - fn set_rating(&self, client: &Client, rating: u8) -> Result<()>; + async fn set_rating(&self, client: &Client, rating: u8) -> Result<()>; /// Registers the local playback of the content. Typically used when playing /// media that is cached on the client. This operation includes the @@ -29,113 +30,125 @@ pub trait Annotatable { /// /// `time` should be a valid ISO8601 timestamp. In the future, this will be /// validated. - fn scrobble<'a, B, T>(&self, client: &Client, time: T, now_playing: B) -> Result<()> + async fn scrobble<'a, B: Send, T: Send>( + &self, + client: &Client, + time: T, + now_playing: B, + ) -> Result<()> where B: Into>, T: Into>; } +#[async_trait::async_trait] impl Annotatable for Artist { - fn star(&self, client: &Client) -> Result<()> { - client.get("star", Query::with("artistId", self.id))?; + async fn star(&self, client: &Client) -> Result<()> { + client.get("star", Query::with("artistId", self.id)).await?; Ok(()) } - fn unstar(&self, client: &Client) -> Result<()> { - client.get("unstar", Query::with("artistId", self.id))?; + async fn unstar(&self, client: &Client) -> Result<()> { + client + .get("unstar", Query::with("artistId", self.id)) + .await?; Ok(()) } - fn set_rating(&self, client: &Client, rating: u8) -> Result<()> { + async fn set_rating(&self, client: &Client, rating: u8) -> Result<()> { if rating > 5 { return Err(Error::Other("rating must be between 0 and 5 inclusive")); } let args = Query::with("id", self.id).arg("rating", rating).build(); - client.get("setRating", args)?; + client.get("setRating", args).await?; Ok(()) } - fn scrobble<'a, B, T>(&self, client: &Client, time: T, now_playing: B) -> Result<()> + async fn scrobble<'a, B, T>(&self, client: &Client, time: T, now_playing: B) -> Result<()> where - B: Into>, - T: Into>, + B: Into> + Send, + T: Into> + Send, { let args = Query::with("id", self.id) .arg("time", time.into()) .arg("submission", now_playing.into().map(|b| !b)) .build(); - client.get("scrobble", args)?; + client.get("scrobble", args).await?; Ok(()) } } +#[async_trait::async_trait] impl Annotatable for Album { - fn star(&self, client: &Client) -> Result<()> { - client.get("star", Query::with("albumId", self.id))?; + async fn star(&self, client: &Client) -> Result<()> { + client.get("star", Query::with("albumId", self.id)).await?; Ok(()) } - fn unstar(&self, client: &Client) -> Result<()> { - client.get("unstar", Query::with("albumId", self.id))?; + async fn unstar(&self, client: &Client) -> Result<()> { + client + .get("unstar", Query::with("albumId", self.id)) + .await?; Ok(()) } - fn set_rating(&self, client: &Client, rating: u8) -> Result<()> { + async fn set_rating(&self, client: &Client, rating: u8) -> Result<()> { if rating > 5 { return Err(Error::Other("rating must be between 0 and 5 inclusive")); } let args = Query::with("id", self.id).arg("rating", rating).build(); - client.get("setRating", args)?; + client.get("setRating", args).await?; Ok(()) } - fn scrobble<'a, B, T>(&self, client: &Client, time: T, now_playing: B) -> Result<()> + async fn scrobble<'a, B, T>(&self, client: &Client, time: T, now_playing: B) -> Result<()> where - B: Into>, - T: Into>, + B: Into> + Send, + T: Into> + Send, { let args = Query::with("id", self.id) .arg("time", time.into()) .arg("submission", now_playing.into().map(|b| !b)) .build(); - client.get("scrobble", args)?; + client.get("scrobble", args).await?; Ok(()) } } +#[async_trait::async_trait] impl Annotatable for Song { - fn star(&self, client: &Client) -> Result<()> { - client.get("star", Query::with("id", self.id))?; + async fn star(&self, client: &Client) -> Result<()> { + client.get("star", Query::with("id", self.id)).await?; Ok(()) } - fn unstar(&self, client: &Client) -> Result<()> { - client.get("unstar", Query::with("id", self.id))?; + async fn unstar(&self, client: &Client) -> Result<()> { + client.get("unstar", Query::with("id", self.id)).await?; Ok(()) } - fn set_rating(&self, client: &Client, rating: u8) -> Result<()> { + async fn set_rating(&self, client: &Client, rating: u8) -> Result<()> { if rating > 5 { return Err(Error::Other("rating must be between 0 and 5 inclusive")); } let args = Query::with("id", self.id).arg("rating", rating).build(); - client.get("setRating", args)?; + client.get("setRating", args).await?; Ok(()) } - fn scrobble<'a, B, T>(&self, client: &Client, time: T, now_playing: B) -> Result<()> + async fn scrobble<'a, B, T>(&self, client: &Client, time: T, now_playing: B) -> Result<()> where - B: Into>, - T: Into>, + B: Into> + Send, + T: Into> + Send, { let args = Query::with("id", self.id) .arg("time", time.into()) .arg("submission", now_playing.into().map(|b| !b)) .build(); - client.get("scrobble", args)?; + client.get("scrobble", args).await?; Ok(()) } } diff --git a/src/client.rs b/src/client.rs index c84cb9a..162cc34 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,11 +1,8 @@ -use std::io::Read; use std::iter; -use md5; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use reqwest::Client as ReqwestClient; use reqwest::Url; -use serde_json; use crate::media::NowPlaying; use crate::query::Query; @@ -81,8 +78,8 @@ impl SubsonicAuth { let auth = if ver >= "1.13.0".into() { let mut rng = thread_rng(); let salt: String = iter::repeat(()) - .map(|()| rng.sample(Alphanumeric)) .take(SALT_SIZE) + .map(|_| char::from(rng.sample(Alphanumeric))) .collect(); let pre_t = self.password.to_string() + &salt; let token = format!("{:x}", md5::compute(pre_t.as_bytes())); @@ -95,13 +92,7 @@ impl SubsonicAuth { let format = "json"; let crate_name = env!("CARGO_PKG_NAME"); - format!( - "{auth}&v={v}&c={c}&f={f}", - auth = auth, - v = ver, - c = crate_name, - f = format - ) + format!("{auth}&v={ver}&c={crate_name}&f={format}") } } @@ -109,7 +100,9 @@ impl Client { /// Constructs a client to interact with a Subsonic instance. pub fn new(url: &str, user: &str, password: &str) -> Result { let auth = SubsonicAuth::new(user, password); - let url = url.parse::()?; + let url = url + .parse::() + .map_err(>::into)?; let ver = Version::from("1.14.0"); let target_ver = ver; @@ -173,14 +166,14 @@ impl Client { /// - server is built with an incomplete URL /// - connecting to the server fails /// - the server returns an API error - pub(crate) fn get(&self, query: &str, args: Query) -> Result { + pub(crate) async fn get(&self, query: &str, args: Query) -> Result { let uri: Url = self.build_url(query, args)?.parse().unwrap(); info!("Connecting to {}", uri); - let mut res = self.reqclient.get(uri).send()?; + let res = self.reqclient.get(uri).send().await?; if res.status().is_success() { - let response = res.json::()?; + let response = res.json::().await?; if response.is_ok() { Ok(match response.into_value() { Some(v) => v, @@ -199,29 +192,32 @@ impl Client { /// Fetches an unprocessed response from the server rather than a JSON- or /// XML-parsed one. - pub(crate) fn get_raw(&self, query: &str, args: Query) -> Result { + pub(crate) async fn get_raw(&self, query: &str, args: Query) -> Result { let uri: Url = self.build_url(query, args)?.parse().unwrap(); - let mut res = self.reqclient.get(uri).send()?; - Ok(res.text()?) + let res = self.reqclient.get(uri).send().await?; + Ok(res.text().await?) } /// Returns a response as a vector of bytes rather than serialising it. - pub(crate) fn get_bytes(&self, query: &str, args: Query) -> Result> { + pub(crate) async fn get_bytes(&self, query: &str, args: Query) -> Result> { let uri: Url = self.build_url(query, args)?.parse().unwrap(); - let res = self.reqclient.get(uri).send()?; - Ok(res.bytes().map(|b| b.unwrap()).collect()) + let res = self.reqclient.get(uri).send().await?; + Ok(res.bytes().await?.to_vec()) } /// Returns the raw bytes of a HLS slice. - pub fn hls_bytes(&self, hls: &Hls) -> Result> { - let url: Url = self.url.join(&hls.url)?; - let res = self.reqclient.get(url).send()?; - Ok(res.bytes().map(|b| b.unwrap()).collect()) + pub async fn hls_bytes(&self, hls: &Hls) -> Result> { + let url: Url = self + .url + .join(&hls.url) + .map_err(>::into)?; + let res = self.reqclient.get(url).send().await?; + Ok(res.bytes().await?.to_vec()) } /// Tests a connection with the server. - pub fn ping(&self) -> Result<()> { - self.get("ping", Query::none())?; + pub async fn ping(&self) -> Result<()> { + self.get("ping", Query::none()).await?; Ok(()) } @@ -232,8 +228,8 @@ impl Client { /// Forks of Subsonic (Libresonic, Airsonic, etc.) do not require licenses; /// this method will always return a valid license and trial when attempting /// to connect to these services. - pub fn check_license(&self) -> Result { - let res = self.get("getLicense", Query::none())?; + pub async fn check_license(&self) -> Result { + let res = self.get("getLicense", Query::none()).await?; Ok(serde_json::from_value::(res)?) } @@ -243,8 +239,8 @@ impl Client { /// /// This method was introduced in version 1.15.0. It will not be supported /// on servers with earlier versions of the Subsonic API. - pub fn scan_library(&self) -> Result<()> { - self.get("startScan", Query::none())?; + pub async fn scan_library(&self) -> Result<()> { + self.get("startScan", Query::none()).await?; Ok(()) } @@ -255,8 +251,8 @@ impl Client { /// /// This method was introduced in version 1.15.0. It will not be supported /// on servers with earlier versions of the Subsonic API. - pub fn scan_status(&self) -> Result<(bool, u64)> { - let res = self.get("getScanStatus", Query::none())?; + pub async fn scan_status(&self) -> Result<(bool, u64)> { + let res = self.get("getScanStatus", Query::none()).await?; #[derive(Deserialize)] struct ScanStatus { @@ -269,36 +265,36 @@ impl Client { } /// Returns all configured top-level music folders. - pub fn music_folders(&self) -> Result> { + pub async fn music_folders(&self) -> Result> { #[allow(non_snake_case)] - let musicFolder = self.get("getMusicFolders", Query::none())?; + let music_folder = self.get("getMusicFolders", Query::none()).await?; - Ok(get_list_as!(musicFolder, MusicFolder)) + Ok(get_list_as!(music_folder, MusicFolder)) } /// Returns all genres. - pub fn genres(&self) -> Result> { - let genre = self.get("getGenres", Query::none())?; + pub async fn genres(&self) -> Result> { + let genre = self.get("getGenres", Query::none()).await?; Ok(get_list_as!(genre, Genre)) } /// Returns all currently playing media on the server. - pub fn now_playing(&self) -> Result> { - let entry = self.get("getNowPlaying", Query::none())?; + pub async fn now_playing(&self) -> Result> { + let entry = self.get("getNowPlaying", Query::none()).await?; Ok(get_list_as!(entry, NowPlaying)) } /// Searches for lyrics matching the artist and title. Returns `None` if no /// lyrics are found. - pub fn lyrics<'a, S>(&self, artist: S, title: S) -> Result> + pub async fn lyrics<'a, S>(&self, artist: S, title: S) -> Result> where S: Into>, { let args = Query::with("artist", artist.into()) .arg("title", title.into()) .build(); - let res = self.get("getLyrics", args)?; + let res = self.get("getLyrics", args).await?; if res.get("value").is_some() { Ok(Some(serde_json::from_value(res)?)) @@ -339,7 +335,7 @@ impl Client { /// # } /// # fn main() { } /// ``` - pub fn search( + pub async fn search( &self, query: &str, artist_page: SearchPage, @@ -356,16 +352,18 @@ impl Client { .arg("songOffset", song_page.offset) .build(); - let res = self.get("search3", args)?; + let res = self.get("search3", args).await?; Ok(serde_json::from_value::(res)?) } /// Returns a list of all starred artists, albums, and songs. - pub fn starred(&self, folder_id: U) -> Result + pub async fn starred(&self, folder_id: U) -> Result where U: Into>, { - let res = self.get("getStarred", Query::with("musicFolderId", folder_id.into()))?; + let res = self + .get("getStarred", Query::with("musicFolderId", folder_id.into())) + .await?; Ok(serde_json::from_value::(res)?) } } @@ -407,13 +405,15 @@ mod tests { #[test] fn demo_ping() { let cli = test_util::demo_site().unwrap(); - cli.ping().unwrap(); + tokio_test::block_on(async { + cli.ping().await.unwrap(); + }); } #[test] fn demo_license() { let cli = test_util::demo_site().unwrap(); - let license = cli.check_license().unwrap(); + let license = tokio_test::block_on(async { cli.check_license().await.unwrap() }); assert!(license.valid); assert_eq!(license.email, String::from("demo@subsonic.org")); @@ -422,7 +422,7 @@ mod tests { #[test] fn demo_scan_status() { let cli = test_util::demo_site().unwrap(); - let (status, n) = cli.scan_status().unwrap(); + let (status, n) = tokio_test::block_on(async { cli.scan_status().await.unwrap() }); assert!(!status); assert_eq!(n, 525); } @@ -431,7 +431,7 @@ mod tests { fn demo_search() { let cli = test_util::demo_site().unwrap(); let s = SearchPage::new().with_size(1); - let r = cli.search("dada", s, s, s).unwrap(); + let r = tokio_test::block_on(async { cli.search("dada", s, s, s).await.unwrap() }); assert_eq!(r.artists[0].id, 14); assert_eq!(r.artists[0].name, String::from("The Dada Weatherman")); diff --git a/src/collections/album.rs b/src/collections/album.rs index a556b1d..995eb6e 100644 --- a/src/collections/album.rs +++ b/src/collections/album.rs @@ -35,7 +35,7 @@ impl fmt::Display for ListType { Recent => "recent", Starred => "starred", }; - write!(f, "{}", fmt) + write!(f, "{fmt}") } } @@ -74,32 +74,34 @@ impl Album { /// /// Aside from errors the `Client` may cause, the method will error if /// there is no album matching the provided ID. - pub fn get(client: &Client, id: usize) -> Result { - self::get_album(client, id as u64) + pub async fn get(client: &Client, id: usize) -> Result { + self::get_album(client, id as u64).await } /// Lists all albums on the server. Supports paging. - pub fn list( + pub async fn list( client: &Client, list_type: ListType, page: SearchPage, folder: usize, ) -> Result> { - self::get_albums(client, list_type, page.count, page.offset, folder) + self::get_albums(client, list_type, page.count, page.offset, folder).await } /// Returns all songs in the album. - pub fn songs(&self, client: &Client) -> Result> { + pub async fn songs(&self, client: &Client) -> Result> { if self.songs.len() as u64 != self.song_count { - Ok(self::get_album(client, self.id)?.songs) + Ok(self::get_album(client, self.id).await?.songs) } else { Ok(self.songs.clone()) } } /// Returns detailed information about the album. - pub fn info(&self, client: &Client) -> Result { - let res = client.get("getArtistInfo", Query::with("id", self.id))?; + pub async fn info(&self, client: &Client) -> Result { + let res = client + .get("getArtistInfo", Query::with("id", self.id)) + .await?; Ok(serde_json::from_value(res)?) } } @@ -107,7 +109,7 @@ impl Album { impl fmt::Display for Album { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(ref artist) = self.artist { - write!(f, "{} - ", artist)?; + write!(f, "{artist} - ")?; } else { write!(f, "Unknown Artist - ")?; } @@ -115,7 +117,7 @@ impl fmt::Display for Album { write!(f, "{}", self.name)?; if let Some(year) = self.year { - write!(f, " [{}] ", year)?; + write!(f, " [{year}] ")?; } Ok(()) @@ -161,6 +163,7 @@ impl<'de> Deserialize<'de> for Album { } } +#[async_trait::async_trait] impl Media for Album { fn has_cover_art(&self) -> bool { self.cover_id.is_some() @@ -170,11 +173,15 @@ impl Media for Album { self.cover_id.as_deref() } - fn cover_art>>(&self, client: &Client, size: U) -> Result> { + async fn cover_art> + Send>( + &self, + client: &Client, + size: U, + ) -> Result> { let cover = self.cover_id().ok_or(Error::Other("no cover art found"))?; let query = Query::with("id", cover).arg("size", size.into()).build(); - client.get_bytes("getCoverArt", query) + client.get_bytes("getCoverArt", query).await } fn cover_art_url>>(&self, client: &Client, size: U) -> Result { @@ -225,12 +232,12 @@ impl<'de> Deserialize<'de> for AlbumInfo { } } -fn get_album(client: &Client, id: u64) -> Result { - let res = client.get("getAlbum", Query::with("id", id))?; +async fn get_album(client: &Client, id: u64) -> Result { + let res = client.get("getAlbum", Query::with("id", id)).await?; Ok(serde_json::from_value::(res)?) } -fn get_albums( +async fn get_albums( client: &Client, list_type: ListType, size: U, @@ -247,7 +254,7 @@ where .arg("musicFolderId", folder_id.into()) .build(); - let album = client.get("getAlbumList2", args)?; + let album = client.get("getAlbumList2", args).await?; Ok(get_list_as!(album, Album)) } @@ -259,7 +266,11 @@ mod tests { #[test] fn demo_get_albums() { let srv = test_util::demo_site().unwrap(); - let albums = get_albums(&srv, ListType::AlphaByArtist, None, None, None).unwrap(); + let albums = tokio_test::block_on(async { + get_albums(&srv, ListType::AlphaByArtist, None, None, None) + .await + .unwrap() + }); assert!(!albums.is_empty()) } diff --git a/src/collections/artist.rs b/src/collections/artist.rs index 08549a3..33ec6e0 100644 --- a/src/collections/artist.rs +++ b/src/collections/artist.rs @@ -36,22 +36,24 @@ pub struct ArtistInfo { impl Artist { #[allow(missing_docs)] - pub fn get(client: &Client, id: usize) -> Result { - self::get_artist(client, id) + pub async fn get(client: &Client, id: usize) -> Result { + self::get_artist(client, id).await } /// Returns a list of albums released by the artist. - pub fn albums(&self, client: &Client) -> Result> { + pub async fn albums(&self, client: &Client) -> Result> { if self.albums.len() != self.album_count { - Ok(self::get_artist(client, self.id)?.albums) + Ok(self::get_artist(client, self.id).await?.albums) } else { Ok(self.albums.clone()) } } /// Queries last.fm for more information about the artist. - pub fn info(&self, client: &Client) -> Result { - let res = client.get("getArtistInfo", Query::with("id", self.id))?; + pub async fn info(&self, client: &Client) -> Result { + let res = client + .get("getArtistInfo", Query::with("id", self.id)) + .await?; Ok(serde_json::from_value(res)?) } @@ -61,7 +63,7 @@ impl Artist { /// called on. Optionally takes a `count` to specify the maximum number of /// results to return, and whether to only include artists in the Subsonic /// library (defaults to true). - pub fn similar( + pub async fn similar( &self, client: &Client, count: U, @@ -75,12 +77,12 @@ impl Artist { .arg("count", count.into()) .arg("includeNotPresent", include_not_present.into()) .build(); - let res = serde_json::from_value::(client.get("getArtistInfo", args)?)?; + let res = serde_json::from_value::(client.get("getArtistInfo", args).await?)?; Ok(res.similar_artists) } /// Returns the top `count` most played songs released by the artist. - pub fn top_songs(&self, client: &Client, count: U) -> Result> + pub async fn top_songs(&self, client: &Client, count: U) -> Result> where U: Into>, { @@ -88,7 +90,7 @@ impl Artist { .arg("count", count.into()) .build(); - let song = client.get("getTopSongs", args)?; + let song = client.get("getTopSongs", args).await?; Ok(get_list_as!(song, Song)) } } @@ -121,6 +123,7 @@ impl<'de> Deserialize<'de> for Artist { } } +#[async_trait::async_trait] impl Media for Artist { fn has_cover_art(&self) -> bool { self.cover_id.is_some() @@ -130,11 +133,15 @@ impl Media for Artist { self.cover_id.as_deref() } - fn cover_art>>(&self, client: &Client, size: U) -> Result> { + async fn cover_art> + Send>( + &self, + client: &Client, + size: U, + ) -> Result> { let cover = self.cover_id().ok_or(Error::Other("no cover art found"))?; let query = Query::with("id", cover).arg("size", size.into()).build(); - client.get_bytes("getCoverArt", query) + client.get_bytes("getCoverArt", query).await } fn cover_art_url>>(&self, client: &Client, size: U) -> Result { @@ -185,8 +192,8 @@ impl<'de> Deserialize<'de> for ArtistInfo { } /// Fetches an artist from the Subsonic server. -fn get_artist(client: &Client, id: usize) -> Result { - let res = client.get("getArtist", Query::with("id", id))?; +async fn get_artist(client: &Client, id: usize) -> Result { + let res = client.get("getArtist", Query::with("id", id)).await?; Ok(serde_json::from_value::(res)?) } @@ -218,7 +225,7 @@ mod tests { fn remote_artist_album_list() { let srv = test_util::demo_site().unwrap(); let parsed = serde_json::from_value::(raw()).unwrap(); - let albums = parsed.albums(&srv).unwrap(); + let albums = tokio_test::block_on(async { parsed.albums(&srv).await.unwrap() }); assert_eq!(albums[0].id, 1); assert_eq!(albums[0].name, String::from("Bellevue")); @@ -231,7 +238,7 @@ mod tests { let parsed = serde_json::from_value::(raw()).unwrap(); assert_eq!(parsed.cover_id, Some(String::from("ar-1"))); - let cover = parsed.cover_art(&srv, None).unwrap(); + let cover = tokio_test::block_on(async { parsed.cover_art(&srv, None).await.unwrap() }); assert!(!cover.is_empty()) } diff --git a/src/collections/playlist.rs b/src/collections/playlist.rs index 0745618..3441e52 100644 --- a/src/collections/playlist.rs +++ b/src/collections/playlist.rs @@ -22,9 +22,9 @@ pub struct Playlist { impl Playlist { /// Fetches the songs contained in a playlist. - pub fn songs(&self, client: &Client) -> Result> { + pub async fn songs(&self, client: &Client) -> Result> { if self.songs.len() as u64 != self.song_count { - Ok(get_playlist(client, self.id)?.songs) + Ok(get_playlist(client, self.id).await?.songs) } else { Ok(self.songs.clone()) } @@ -66,6 +66,7 @@ impl<'de> Deserialize<'de> for Playlist { } } +#[async_trait::async_trait] impl Media for Playlist { fn has_cover_art(&self) -> bool { !self.cover_id.is_empty() @@ -75,11 +76,15 @@ impl Media for Playlist { Some(self.cover_id.as_ref()) } - fn cover_art>>(&self, client: &Client, size: U) -> Result> { + async fn cover_art> + Send>( + &self, + client: &Client, + size: U, + ) -> Result> { let cover = self.cover_id().ok_or(Error::Other("no cover art found"))?; let query = Query::with("id", cover).arg("size", size.into()).build(); - client.get_bytes("getCoverArt", query) + client.get_bytes("getCoverArt", query).await } fn cover_art_url>>(&self, client: &Client, size: U) -> Result { @@ -91,14 +96,16 @@ impl Media for Playlist { } #[allow(missing_docs)] -pub fn get_playlists(client: &Client, user: Option) -> Result> { - let playlist = client.get("getPlaylists", Query::with("username", user))?; +pub async fn get_playlists(client: &Client, user: Option) -> Result> { + let playlist = client + .get("getPlaylists", Query::with("username", user)) + .await?; Ok(get_list_as!(playlist, Playlist)) } #[allow(missing_docs)] -pub fn get_playlist(client: &Client, id: u64) -> Result { - let res = client.get("getPlaylist", Query::with("id", id))?; +pub async fn get_playlist(client: &Client, id: u64) -> Result { + let res = client.get("getPlaylist", Query::with("id", id)).await?; Ok(serde_json::from_value::(res)?) } @@ -106,13 +113,17 @@ pub fn get_playlist(client: &Client, id: u64) -> Result { /// /// Since API version 1.14.0, the newly created playlist is returned. In earlier /// versions, an empty response is returned. -pub fn create_playlist(client: &Client, name: String, songs: &[u64]) -> Result> { +pub async fn create_playlist( + client: &Client, + name: String, + songs: &[u64], +) -> Result> { let args = Query::new() .arg("name", name) .arg_list("songId", songs) .build(); - let res = client.get("createPlaylist", args)?; + let res = client.get("createPlaylist", args).await?; // TODO API is private // if client.api >= "1.14.0".into() { @@ -123,7 +134,7 @@ pub fn create_playlist(client: &Client, name: String, songs: &[u64]) -> Result( +pub async fn update_playlist<'a, B, S>( client: &Client, id: u64, name: S, @@ -145,13 +156,13 @@ where .arg_list("songIndexToRemove", to_remove) .build(); - client.get("updatePlaylist", args)?; + client.get("updatePlaylist", args).await?; Ok(()) } #[allow(missing_docs)] -pub fn delete_playlist(client: &Client, id: u64) -> Result<()> { - client.get("deletePlaylist", Query::with("id", id))?; +pub async fn delete_playlist(client: &Client, id: u64) -> Result<()> { + client.get("deletePlaylist", Query::with("id", id)).await?; Ok(()) } @@ -165,7 +176,7 @@ mod tests { fn remote_playlist_songs() { let parsed = serde_json::from_value::(raw()).unwrap(); let srv = test_util::demo_site().unwrap(); - let songs = parsed.songs(&srv); + let songs = tokio_test::block_on(parsed.songs(&srv)); assert!(matches!( songs, diff --git a/src/error.rs b/src/error.rs index 7d6d45e..a768938 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,65 +1,65 @@ -use std::convert::From; use std::{fmt, io, num, result}; -use reqwest; use serde::de::{Deserialize, Deserializer}; -use serde_json; /// An alias for `sunk`'s error result type. -pub type Result = result::Result; +pub type Result = result::Result; /// Possible errors that may be returned by a function. -#[derive(Debug, Fail)] +#[derive(Debug, thiserror::Error)] pub enum Error { /// Unable to connect to the Subsonic server. - #[fail(display = "Unable to connect to server: received {}", _0)] + #[error("Unable to connect to server: received {}", _0)] Connection(reqwest::StatusCode), /// Unable to recognize the URL provided in `Client` setup. - #[fail(display = "Invalid URL: {}", _0)] - Url(UrlError), + #[error("Invalid URL: {}", _0)] + Url(#[from] UrlError), /// The Subsonic server returned an error. - #[fail(display = "{}", _0)] - Api(#[cause] ApiError), + #[error("{}", _0)] + Api(#[from] ApiError), - /// A number conversion failed. - #[fail(display = "Failed to parse int: {}", _0)] - Parse(#[cause] num::ParseIntError), + /// A number conversion errored. + #[error("Failed to parse int: {}", _0)] + Parse(#[from] num::ParseIntError), /// An IO issue occurred. - #[fail(display = "IO error: {}", _0)] - Io(#[cause] io::Error), + #[error("IO error: {}", _0)] + Io(#[from] io::Error), /// An error in the web framework occurred. - #[fail(display = "Connection error: {}", _0)] - Reqwest(#[cause] reqwest::Error), + #[error("Connection error: {}", _0)] + Reqwest(#[from] reqwest::Error), /// An error occurred in serialization. - #[fail(display = "Error serialising: {}", _0)] - Serde(#[cause] serde_json::Error), + #[error("Error serialising: {}", _0)] + Serde(#[from] serde_json::Error), /// For general, one-off errors. - #[fail(display = "{}", _0)] + #[error("{}", _0)] Other(&'static str), } /// Possible errors when initializing a `Client`. -#[derive(Debug, Fail)] +#[derive(Debug, thiserror::Error)] pub enum UrlError { /// Unable to parse the URL. - #[fail(display = "{}", _0)] - Reqwest(#[cause] reqwest::UrlError), + #[error("{}", _0)] + Reqwest(#[from] reqwest::Error), /// Unable to determine the scheme of the address. /// /// The provider for the `Client` does not automatically add the HTTP /// scheme like other Rust frameworks. If you encounter this error, /// you probably need to add `http://` or `https://` to your server address. - #[fail(display = "Unable to determine scheme")] + #[error("Unable to determine scheme")] Scheme, /// The server address was not provided. - #[fail(display = "Missing server address")] + #[error("Missing server address")] Address, + /// The URL failed to parse + #[error("{0}")] + ParsingError(#[from] url::ParseError), } /// The possible errors a Subsonic server may return. -#[derive(Debug, Fail, Clone)] +#[derive(Debug, thiserror::Error, Clone)] pub enum ApiError { /// A generic error. Generic(String), @@ -146,43 +146,15 @@ impl fmt::Display for ApiError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::ApiError::*; match *self { - Generic(ref s) => write!(f, "Generic error: {}", s), + Generic(ref s) => write!(f, "Generic error: {s}"), MissingParameter => write!(f, "Missing a required parameter"), ClientMustUpgrade => write!(f, "Incompatible protocol; client must upgrade"), ServerMustUpgrade => write!(f, "Incompatible protocol; server must upgrade"), WrongAuth => write!(f, "Wrong username or password"), Ldap => write!(f, "Token authentication not supported for LDAP users"), - NotAuthorized(ref s) => write!(f, "Not authorized: {}", s), + NotAuthorized(ref s) => write!(f, "Not authorized: {s}"), TrialExpired => write!(f, "Subsonic trial period has expired"), NotFound => write!(f, "Requested data not found"), } } } -macro_rules! box_err { - ($err:ty, $to:ident) => { - impl From<$err> for Error { - fn from(err: $err) -> Error { - Error::$to(err) - } - } - }; -} - -box_err!(reqwest::Error, Reqwest); -box_err!(io::Error, Io); -box_err!(num::ParseIntError, Parse); -box_err!(serde_json::Error, Serde); -box_err!(UrlError, Url); -box_err!(ApiError, Api); - -impl From for UrlError { - fn from(err: reqwest::UrlError) -> UrlError { - UrlError::Reqwest(err) - } -} - -impl From for Error { - fn from(err: reqwest::UrlError) -> Error { - Error::Url(err.into()) - } -} diff --git a/src/jukebox.rs b/src/jukebox.rs index 7c3d5a5..8ac2282 100644 --- a/src/jukebox.rs +++ b/src/jukebox.rs @@ -72,11 +72,16 @@ impl<'de> Deserialize<'de> for JukeboxPlaylist { impl<'a> Jukebox<'a> { /// Creates a new handler to the jukebox of the client. - pub fn start(client: &'a Client) -> Jukebox { + pub async fn start(client: &'a Client) -> Jukebox { Jukebox { client } } - fn send_action_with(&self, action: &str, index: U, ids: &[usize]) -> Result + async fn send_action_with( + &self, + action: &str, + index: U, + ids: &[usize], + ) -> Result where U: Into>, { @@ -84,37 +89,38 @@ impl<'a> Jukebox<'a> { .arg("index", index.into()) .arg_list("id", ids) .build(); - let res = self.client.get("jukeboxControl", args)?; + let res = self.client.get("jukeboxControl", args).await?; Ok(serde_json::from_value(res)?) } - fn send_action(&self, action: &str) -> Result { - self.send_action_with(action, None, &[]) + async fn send_action(&self, action: &str) -> Result { + self.send_action_with(action, None, &[]).await } /// Returns the current playlist of the jukebox, as well as its status. The /// status is also returned as it contains the position of the jukebox /// in its playlist. - pub fn playlist(&self) -> Result { + pub async fn playlist(&self) -> Result { let res = self .client - .get("jukeboxControl", Query::with("action", "get"))?; + .get("jukeboxControl", Query::with("action", "get")) + .await?; Ok(serde_json::from_value::(res)?) } /// Returns the status of the jukebox. - pub fn status(&self) -> Result { - self.send_action("status") + pub async fn status(&self) -> Result { + self.send_action("status").await } /// Tells the jukebox to start playing. - pub fn play(&self) -> Result { - self.send_action("start") + pub async fn play(&self) -> Result { + self.send_action("start").await } /// Tells the jukebox to pause playback. - pub fn stop(&self) -> Result { - self.send_action("stop") + pub async fn stop(&self) -> Result { + self.send_action("stop").await } /// Moves the jukebox's currently playing song to the provided index @@ -122,13 +128,14 @@ impl<'a> Jukebox<'a> { /// /// Using an index outside the range of the jukebox playlist will play the /// last song in the playlist. - pub fn skip_to(&self, n: usize) -> Result { - self.send_action_with("skip", n, &[]) + pub async fn skip_to(&self, n: usize) -> Result { + self.send_action_with("skip", n, &[]).await } /// Adds the song to the jukebox's playlist. - pub fn add(&self, song: &Song) -> Result { + pub async fn add(&self, song: &Song) -> Result { self.send_action_with("add", None, &[song.id as usize]) + .await } /// Adds a song matching the provided ID to the playlist. @@ -137,17 +144,18 @@ impl<'a> Jukebox<'a> { /// /// The method will return an error if a song matching the provided ID /// cannot be found. - pub fn add_id(&self, id: usize) -> Result { - self.send_action_with("add", None, &[id]) + pub async fn add_id(&self, id: usize) -> Result { + self.send_action_with("add", None, &[id]).await } /// Adds all the songs to the jukebox's playlist. - pub fn add_all(&self, songs: &[Song]) -> Result { + pub async fn add_all(&self, songs: &[Song]) -> Result { self.send_action_with( "add", None, &songs.iter().map(|s| s.id as usize).collect::>(), ) + .await } /// Adds multiple songs matching the provided IDs to the playlist. @@ -156,31 +164,31 @@ impl<'a> Jukebox<'a> { /// /// The method will return an error if at least one ID cannot be matched to /// a song. - pub fn add_all_ids(&self, ids: &[usize]) -> Result { - self.send_action_with("add", None, ids) + pub async fn add_all_ids(&self, ids: &[usize]) -> Result { + self.send_action_with("add", None, ids).await } /// Clears the jukebox's playlist. - pub fn clear(&self) -> Result { - self.send_action("clear") + pub async fn clear(&self) -> Result { + self.send_action("clear").await } /// Removes the song at the provided index from the playlist. - pub fn remove_id(&self, idx: usize) -> Result { - self.send_action_with("remove", idx, &[]) + pub async fn remove_id(&self, idx: usize) -> Result { + self.send_action_with("remove", idx, &[]).await } /// Shuffles the jukebox's playlist. - pub fn shuffle(&self) -> Result { - self.send_action("shuffle") + pub async fn shuffle(&self) -> Result { + self.send_action("shuffle").await } /// Sets the jukebox's playback volume. /// /// Seting the volume above `1.0` will have no effect. - pub fn set_volume(&self, volume: f32) -> Result { + pub async fn set_volume(&self, volume: f32) -> Result { let args = Query::with("action", "setGain").arg("gain", volume).build(); - let res = self.client.get("jukeboxControl", args)?; + let res = self.client.get("jukeboxControl", args).await?; Ok(serde_json::from_value(res)?) } } diff --git a/src/lib.rs b/src/lib.rs index e589310..9afa18d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,8 +100,6 @@ #![deny(missing_docs)] -#[macro_use] -extern crate failure; #[macro_use] extern crate log; extern crate md5; diff --git a/src/media/format.rs b/src/media/format.rs index d778853..31805cc 100644 --- a/src/media/format.rs +++ b/src/media/format.rs @@ -31,7 +31,7 @@ pub enum AudioFormat { impl fmt::Display for AudioFormat { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", format!("{:?}", self).to_lowercase()) + write!(f, "{}", format!("{self:?}").to_lowercase()) } } @@ -59,7 +59,7 @@ pub enum VideoFormat { impl fmt::Display for VideoFormat { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", format!("{:?}", self).to_lowercase()) + write!(f, "{}", format!("{self:?}").to_lowercase()) } } diff --git a/src/media/mod.rs b/src/media/mod.rs index 1615ba3..44a0199 100644 --- a/src/media/mod.rs +++ b/src/media/mod.rs @@ -22,6 +22,7 @@ use self::video::Video; // use self::format::{AudioFormat, VideoFormat}; /// A trait for forms of streamable media. +#[async_trait::async_trait] pub trait Streamable { /// Returns the raw bytes of the media. /// @@ -31,7 +32,7 @@ pub trait Streamable { /// /// The method does not provide any information about the encoding of the /// media without evaluating the stream itself. - fn stream(&self, client: &Client) -> Result>; + async fn stream(&self, client: &Client) -> Result>; /// Returns a constructed URL for streaming. /// @@ -47,7 +48,7 @@ pub trait Streamable { /// /// The method does not provide any information about the encoding of the /// media without evaluating the stream itself. - fn download(&self, client: &Client) -> Result>; + async fn download(&self, client: &Client) -> Result>; /// Returns a constructed URL for downloading the song. fn download_url(&self, client: &Client) -> Result; @@ -85,6 +86,7 @@ pub trait Streamable { } /// A trait deriving common methods for any form of media. +#[async_trait::async_trait] pub trait Media { /// Returns whether or not the media has an associated cover. fn has_cover_art(&self) -> bool; @@ -112,7 +114,11 @@ pub trait Media { /// /// Aside from errors that the `Client` may cause, the method will error /// if the media does not have an associated cover art. - fn cover_art>>(&self, client: &Client, size: U) -> Result>; + async fn cover_art> + Send>( + &self, + client: &Client, + size: U, + ) -> Result>; /// Returns the URL pointing to the cover art of the media. /// @@ -152,11 +158,11 @@ impl NowPlaying { /// error if the `NowPlaying` is not a song. /// /// [`Client`]: ../struct.Client.html - pub fn song_info(&self, client: &Client) -> Result { + pub async fn song_info(&self, client: &Client) -> Result { if self.is_video { Err(Error::Other("Now Playing info is not a song")) } else { - Song::get(client, self.id as u64) + Song::get(client, self.id as u64).await } } @@ -168,11 +174,11 @@ impl NowPlaying { /// error if the `NowPlaying` is not a video. /// /// [`Client`]: ../struct.Client.html - pub fn video_info(&self, client: &Client) -> Result