diff --git a/Cargo.toml b/Cargo.toml index f08f643..d47d162 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,12 +10,14 @@ readme = "README.md" keywords = ["subsonic", "airsonic", "music", "api", "webapi"] categories = ["api-bindings"] license = "Apache-2.0/MIT" +edition = "2021" [dependencies] failure = "0.1.3" log = "0.4.6" md5 = "0.6.0" rand = "0.6.1" +readonly = "0.2" serde = "1.0.80" serde_derive = "1.0.80" serde_json = "1.0.33" diff --git a/src/annotate.rs b/src/annotate.rs index 95d2b60..b1afb58 100644 --- a/src/annotate.rs +++ b/src/annotate.rs @@ -1,5 +1,7 @@ -use query::Query; -use {Album, Artist, Client, Error, Result, Song}; +//! Annotation APIs. + +use crate::query::Query; +use crate::{Album, Artist, Client, Error, Result, Song}; /// Allows starring, rating, and scrobbling media. pub trait Annotatable { diff --git a/src/client.rs b/src/client.rs index 1dac510..78b3836 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,11 +1,17 @@ -use media::NowPlaying; -use query::Query; +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 response::Response; -use search::{SearchPage, SearchResult}; use serde_json; -use {Error, Genre, Hls, Lyrics, MusicFolder, Result, UrlError, Version}; + +use crate::media::NowPlaying; +use crate::query::Query; +use crate::response::Response; +use crate::search::{SearchPage, SearchResult}; +use crate::{Error, Genre, Hls, Lyrics, MusicFolder, Result, UrlError, Version}; const SALT_SIZE: usize = 36; // Minimum 6 characters. @@ -73,11 +79,6 @@ impl SubsonicAuth { fn to_url(&self, ver: Version) -> String { // First md5 support. let auth = if ver >= "1.13.0".into() { - use std::iter; - - use md5; - use rand::{distributions::Alphanumeric, thread_rng, Rng}; - let mut rng = thread_rng(); let salt: String = iter::repeat(()) .map(|()| rng.sample(Alphanumeric)) @@ -205,7 +206,6 @@ impl Client { /// Returns a response as a vector of bytes rather than serialising it. pub(crate) fn get_bytes(&self, query: &str, args: Query) -> Result> { - use std::io::Read; 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()) @@ -213,7 +213,6 @@ impl Client { /// Returns the raw bytes of a HLS slice. pub fn hls_bytes(&self, hls: &Hls) -> Result> { - use std::io::Read; let url: Url = self.url.join(&hls.url)?; let res = self.reqclient.get(url).send()?; Ok(res.bytes().map(|b| b.unwrap()).collect()) @@ -387,9 +386,8 @@ pub struct License { #[cfg(test)] mod tests { - use test_util; - use super::*; + use crate::test_util; #[test] fn test_token_auth() { @@ -424,8 +422,8 @@ mod tests { fn demo_scan_status() { let cli = test_util::demo_site().unwrap(); let (status, n) = cli.scan_status().unwrap(); - assert_eq!(status, false); - assert_eq!(n, 521); + assert!(!status); + assert_eq!(n, 525); } #[test] diff --git a/src/collections/album.rs b/src/collections/album.rs index d975a47..a556b1d 100644 --- a/src/collections/album.rs +++ b/src/collections/album.rs @@ -1,11 +1,15 @@ +//! Album APIs. + use std::{fmt, result}; -use query::{Arg, IntoArg, Query}; -use search::SearchPage; use serde::de::{Deserialize, Deserializer}; use serde_json; -use {Client, Error, Media, Result, Song}; +use crate::query::{Arg, IntoArg, Query}; +use crate::search::SearchPage; +use crate::{Client, Error, Media, Result, Song}; + +#[allow(missing_docs)] #[derive(Debug, Clone, Copy)] pub enum ListType { AlphaByArtist, @@ -47,18 +51,20 @@ impl IntoArg for ListType { } } +#[allow(missing_docs)] #[derive(Debug, Clone)] +#[readonly::make] pub struct Album { pub id: u64, pub name: String, pub artist: Option, - artist_id: Option, - cover_id: Option, + pub artist_id: Option, + pub cover_id: Option, pub duration: u64, pub year: Option, pub genre: Option, pub song_count: u64, - songs: Vec, + pub songs: Vec, } impl Album { @@ -121,7 +127,7 @@ impl<'de> Deserialize<'de> for Album { where D: Deserializer<'de>, { - #[derive(Debug, Deserialize)] + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct _Album { id: String, @@ -131,7 +137,7 @@ impl<'de> Deserialize<'de> for Album { cover_art: Option, song_count: u64, duration: u64, - created: String, + // created: String, year: Option, genre: Option, #[serde(default)] @@ -179,6 +185,7 @@ impl Media for Album { } } +#[allow(missing_docs)] #[derive(Debug)] pub struct AlbumInfo { pub notes: String, @@ -246,14 +253,13 @@ where #[cfg(test)] mod tests { - use test_util; - use super::*; + use crate::test_util; #[test] fn demo_get_albums() { - let mut srv = test_util::demo_site().unwrap(); - let albums = get_albums(&mut srv, ListType::AlphaByArtist, None, None, None).unwrap(); + let srv = test_util::demo_site().unwrap(); + let albums = get_albums(&srv, ListType::AlphaByArtist, None, None, None).unwrap(); assert!(!albums.is_empty()) } diff --git a/src/collections/artist.rs b/src/collections/artist.rs index 96c9286..08549a3 100644 --- a/src/collections/artist.rs +++ b/src/collections/artist.rs @@ -1,11 +1,15 @@ +//! Artist APIs. + use std::{fmt, result}; -use query::Query; use serde::de::{Deserialize, Deserializer}; use serde_json; -use {Album, Client, Error, Media, Result, Song}; + +use crate::query::Query; +use crate::{Album, Client, Error, Media, Result, Song}; /// Basic information about an artist. +#[allow(missing_docs)] #[derive(Debug, Clone)] pub struct Artist { pub id: usize, @@ -31,6 +35,7 @@ pub struct ArtistInfo { } impl Artist { + #[allow(missing_docs)] pub fn get(client: &Client, id: usize) -> Result { self::get_artist(client, id) } @@ -187,9 +192,8 @@ fn get_artist(client: &Client, id: usize) -> Result { #[cfg(test)] mod tests { - use test_util; - use super::*; + use crate::test_util; #[test] fn parse_artist() { @@ -212,9 +216,9 @@ mod tests { #[test] fn remote_artist_album_list() { - let mut srv = test_util::demo_site().unwrap(); + let srv = test_util::demo_site().unwrap(); let parsed = serde_json::from_value::(raw()).unwrap(); - let albums = parsed.albums(&mut srv).unwrap(); + let albums = parsed.albums(&srv).unwrap(); assert_eq!(albums[0].id, 1); assert_eq!(albums[0].name, String::from("Bellevue")); @@ -223,11 +227,11 @@ mod tests { #[test] fn remote_artist_cover_art() { - let mut srv = test_util::demo_site().unwrap(); + let srv = test_util::demo_site().unwrap(); let parsed = serde_json::from_value::(raw()).unwrap(); assert_eq!(parsed.cover_id, Some(String::from("ar-1"))); - let cover = parsed.cover_art(&mut srv, None).unwrap(); + let cover = parsed.cover_art(&srv, None).unwrap(); assert!(!cover.is_empty()) } diff --git a/src/collections/mod.rs b/src/collections/mod.rs index 299c950..7204690 100644 --- a/src/collections/mod.rs +++ b/src/collections/mod.rs @@ -1,10 +1,12 @@ +//! Collections management APIs. + use std::result; use serde::de::{Deserialize, Deserializer}; -mod album; -mod artist; -mod playlist; +pub mod album; +pub mod artist; +pub mod playlist; pub use self::album::{Album, AlbumInfo, ListType}; pub use self::artist::{Artist, ArtistInfo}; diff --git a/src/collections/playlist.rs b/src/collections/playlist.rs index b3121ef..0745618 100644 --- a/src/collections/playlist.rs +++ b/src/collections/playlist.rs @@ -1,18 +1,23 @@ +//! Playlist APIs. + use std::result; -use query::Query; use serde::de::{Deserialize, Deserializer}; use serde_json; -use {Client, Error, Media, Result, Song}; +use crate::query::Query; +use crate::{Client, Error, Media, Result, Song}; + +#[allow(missing_docs)] #[derive(Debug)] +#[readonly::make] pub struct Playlist { - id: u64, - name: String, - duration: u64, - cover_id: String, - song_count: u64, - songs: Vec, + pub id: u64, + pub name: String, + pub duration: u64, + pub cover_id: String, + pub song_count: u64, + pub songs: Vec, } impl Playlist { @@ -31,18 +36,18 @@ impl<'de> Deserialize<'de> for Playlist { where D: Deserializer<'de>, { - #[derive(Debug, Deserialize)] + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct _Playlist { id: String, name: String, - #[serde(default)] - comment: String, - owner: String, + // #[serde(default)] + // comment: String, + // owner: String, song_count: u64, duration: u64, - created: String, - changed: String, + // created: String, + // changed: String, cover_art: String, #[serde(default)] songs: Vec, @@ -85,12 +90,14 @@ impl Media for Playlist { } } -fn get_playlists(client: &Client, user: Option) -> Result> { +#[allow(missing_docs)] +pub fn get_playlists(client: &Client, user: Option) -> Result> { let playlist = client.get("getPlaylists", Query::with("username", user))?; Ok(get_list_as!(playlist, Playlist)) } -fn get_playlist(client: &Client, id: u64) -> Result { +#[allow(missing_docs)] +pub fn get_playlist(client: &Client, id: u64) -> Result { let res = client.get("getPlaylist", Query::with("id", id))?; Ok(serde_json::from_value::(res)?) } @@ -99,7 +106,7 @@ 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. -fn create_playlist(client: &Client, name: String, songs: &[u64]) -> Result> { +pub fn create_playlist(client: &Client, name: String, songs: &[u64]) -> Result> { let args = Query::new() .arg("name", name) .arg_list("songId", songs) @@ -116,7 +123,7 @@ fn create_playlist(client: &Client, name: String, songs: &[u64]) -> Result( +pub fn update_playlist<'a, B, S>( client: &Client, id: u64, name: S, @@ -142,29 +149,30 @@ where Ok(()) } -fn delete_playlist(client: &Client, id: u64) -> Result<()> { +#[allow(missing_docs)] +pub fn delete_playlist(client: &Client, id: u64) -> Result<()> { client.get("deletePlaylist", Query::with("id", id))?; Ok(()) } #[cfg(test)] mod tests { - use test_util; - use super::*; + use crate::test_util; // The demo playlist exists, but can't be accessed #[test] fn remote_playlist_songs() { let parsed = serde_json::from_value::(raw()).unwrap(); - let mut srv = test_util::demo_site().unwrap(); - let songs = parsed.songs(&mut srv); - - match songs { - Err(::error::Error::Api(::error::ApiError::NotAuthorized(_))) => assert!(true), - Err(e) => panic!("unexpected error: {}", e), - Ok(_) => panic!("test should have failed; insufficient privilege"), - } + let srv = test_util::demo_site().unwrap(); + let songs = parsed.songs(&srv); + + assert!(matches!( + songs, + Err(crate::error::Error::Api( + crate::error::ApiError::NotAuthorized(_) + )) + )); } fn raw() -> serde_json::Value { diff --git a/src/jukebox.rs b/src/jukebox.rs index 3bae77f..7c3d5a5 100644 --- a/src/jukebox.rs +++ b/src/jukebox.rs @@ -1,9 +1,12 @@ +//! Jukebox management and control APIs. + use std::result; -use query::Query; use serde::de::{Deserialize, Deserializer}; use serde_json; -use {Client, Result, Song}; + +use crate::query::Query; +use crate::{Client, Result, Song}; /// A wrapper on a `Client` to control just the jukebox. /// @@ -26,6 +29,7 @@ pub struct JukeboxStatus { /// Volume level of the jukebox, from `0` to `1.0`. #[serde(rename = "gain")] pub volume: f32, + #[allow(missing_docs)] pub position: usize, } diff --git a/src/lib.rs b/src/lib.rs index ee2c9d2..e589310 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,3 @@ -#![warn(missing_docs)] -#![doc(html_root_url = "https://docs.rs/sunk/0.1.2")] - //! # sunk //! //! `sunk` provides natural Rust bindings to the [Subsonic] music server API. @@ -101,6 +98,8 @@ //! Bug reports and broken features are encouraged to be reported! **If //! something does not work as reported, it's probably broken.** +#![deny(missing_docs)] + #[macro_use] extern crate failure; #[macro_use] @@ -118,16 +117,15 @@ mod macros; mod client; mod error; -mod collections; -mod media; - -mod annotate; -mod jukebox; -mod query; -mod response; +pub mod annotate; +pub mod collections; +pub mod jukebox; +pub mod media; +pub mod query; +pub mod response; pub mod search; -mod user; -mod version; +pub mod user; +pub mod version; #[cfg(test)] mod test_util; diff --git a/src/media/format.rs b/src/media/format.rs index 241a691..d778853 100644 --- a/src/media/format.rs +++ b/src/media/format.rs @@ -1,9 +1,13 @@ -use query::{Arg, IntoArg}; +//! Audio and video format APIs. + use std::fmt; +use crate::query::{Arg, IntoArg}; + /// Audio encoding format. /// /// Recognises all of Subsonic's default transcoding formats. +#[allow(missing_docs)] #[derive(Debug)] pub enum AudioFormat { Aac, @@ -32,9 +36,12 @@ impl fmt::Display for AudioFormat { } impl IntoArg for AudioFormat { - fn into_arg(self) -> Arg { self.to_string().into_arg() } + fn into_arg(self) -> Arg { + self.to_string().into_arg() + } } +#[allow(missing_docs)] #[derive(Debug)] pub enum VideoFormat { Avi, @@ -57,5 +64,7 @@ impl fmt::Display for VideoFormat { } impl IntoArg for VideoFormat { - fn into_arg(self) -> Arg { self.to_string().into_arg() } + fn into_arg(self) -> Arg { + self.to_string().into_arg() + } } diff --git a/src/media/mod.rs b/src/media/mod.rs index 37cebdf..1615ba3 100644 --- a/src/media/mod.rs +++ b/src/media/mod.rs @@ -1,11 +1,14 @@ +//! Individual media APIs. + use std::ops::Index; use std::result; use std::str::FromStr; use serde::de::{Deserialize, Deserializer}; -use {Client, Error, Result}; -// pub mod format; +use crate::{Client, Error, Result}; + +pub mod format; pub mod podcast; mod radio; pub mod song; @@ -202,6 +205,11 @@ impl HlsPlaylist { self.hls.len() } + /// Returns whether this playlist is empty. + pub fn is_empty(&self) -> bool { + self.hls.is_empty() + } + /// Returns the total duration of the playlist. pub fn duration(&self) -> usize { self.hls.iter().fold(0, |c, h| c + h.inc) @@ -298,18 +306,18 @@ impl<'de> Deserialize<'de> for NowPlaying { minutes_ago: usize, player_id: usize, id: String, - is_dir: bool, - title: String, - size: usize, - content_type: String, - suffix: String, - transcoded_content_type: Option, - transcoded_suffix: Option, - path: String, + // is_dir: bool, + // title: String, + // size: usize, + // content_type: String, + // suffix: String, + // transcoded_content_type: Option, + // transcoded_suffix: Option, + // path: String, is_video: bool, - created: String, - #[serde(rename = "type")] - media_type: String, + // created: String, + // #[serde(rename = "type")] + // media_type: String, } let raw = _NowPlaying::deserialize(de)?; diff --git a/src/media/podcast.rs b/src/media/podcast.rs index a168312..6fd43d8 100644 --- a/src/media/podcast.rs +++ b/src/media/podcast.rs @@ -1,46 +1,53 @@ +//! Podcast APIs. + use std::result; -use query::Query; use serde::de::{Deserialize, Deserializer}; -use {Client, Result}; +use crate::query::Query; +use crate::{Client, Result}; + +#[allow(missing_docs)] #[derive(Debug)] +#[readonly::make] pub struct Podcast { - id: usize, - url: String, - title: String, - description: String, - cover_art: String, - image_url: String, - status: String, - episodes: Vec, - error: Option, + pub id: usize, + pub url: String, + pub title: String, + pub description: String, + pub cover_art: String, + pub image_url: String, + pub status: String, + pub episodes: Vec, + pub error: Option, } +#[allow(missing_docs)] #[derive(Debug)] +#[readonly::make] pub struct Episode { - id: usize, - parent: usize, - is_dir: bool, - title: String, - album: String, - artist: String, - year: usize, - cover_art: String, - size: usize, - content_type: String, - suffix: String, - duration: usize, - bitrate: usize, - is_video: bool, - created: String, - artist_id: String, - media_type: String, - stream_id: String, - channel_id: String, - description: String, - status: String, - publish_date: String, + pub id: usize, + pub parent: usize, + pub is_dir: bool, + pub title: String, + pub album: String, + pub artist: String, + pub year: usize, + pub cover_art: String, + pub size: usize, + pub content_type: String, + pub suffix: String, + pub duration: usize, + pub bitrate: usize, + pub is_video: bool, + pub created: String, + pub artist_id: String, + pub media_type: String, + pub stream_id: String, + pub channel_id: String, + pub description: String, + pub status: String, + pub publish_date: String, } impl Podcast { diff --git a/src/media/radio.rs b/src/media/radio.rs index ec71c96..84b17f2 100644 --- a/src/media/radio.rs +++ b/src/media/radio.rs @@ -1,12 +1,17 @@ +//! Radio APIs. + use std::result; -use query::Query; use serde::de::{Deserialize, Deserializer}; -use {Client, Result}; +use crate::query::Query; +use crate::{Client, Result}; + +#[allow(missing_docs)] #[derive(Debug)] +#[readonly::make] pub struct RadioStation { - id: usize, + pub id: usize, pub name: String, pub stream_url: String, pub homepage_url: Option, @@ -35,6 +40,7 @@ impl<'de> Deserialize<'de> for RadioStation { } } +#[allow(missing_docs)] impl RadioStation { pub fn id(&self) -> usize { self.id diff --git a/src/media/song.rs b/src/media/song.rs index c72f1ba..2d0cc29 100644 --- a/src/media/song.rs +++ b/src/media/song.rs @@ -1,14 +1,18 @@ +//! Song APIs. + use std::fmt; use std::ops::Range; -use query::Query; -use search::SearchPage; use serde::de::{Deserialize, Deserializer}; use serde_json; -use {Client, Error, HlsPlaylist, Media, Result, Streamable}; + +use crate::query::Query; +use crate::search::SearchPage; +use crate::{Client, Error, HlsPlaylist, Media, Result, Streamable}; /// A work of music contained on a Subsonic server. #[derive(Debug, Clone)] +#[readonly::make] pub struct Song { /// Unique identifier for the song. pub id: u64, @@ -18,11 +22,11 @@ pub struct Song { /// Album the song belongs to. Reads from the song's ID3 tags. pub album: Option, /// The ID of the released album. - album_id: Option, + pub album_id: Option, /// Credited artist for the song. Reads from the song's ID3 tags. pub artist: Option, /// The ID of the releasing artist. - artist_id: Option, + pub artist_id: Option, /// Position of the song in the album. pub track: Option, /// Year the song was released. @@ -30,27 +34,27 @@ pub struct Song { /// Genre of the song. pub genre: Option, /// ID of the song's cover art. Defaults to the parent album's cover. - cover_id: Option, + pub cover_id: Option, /// File size of the song, in bytes. pub size: u64, /// An audio MIME type. - content_type: String, + pub content_type: String, /// The file extension of the song. - suffix: String, + pub suffix: String, /// The MIME type that the song will be transcoded to. - transcoded_content_type: Option, + pub transcoded_content_type: Option, /// The file extension that the song will be transcoded to. - transcoded_suffix: Option, + pub transcoded_suffix: Option, /// Duration of the song, in seconds. pub duration: Option, /// The absolute path of the song in the server database. - path: String, + pub path: String, /// Will always be "song". - media_type: String, + pub media_type: String, /// Bit rate the song will be downsampled to. - stream_br: Option, + pub stream_br: Option, /// Format the song will be transcoded to. - stream_tc: Option, + pub stream_tc: Option, } impl Song { @@ -104,7 +108,7 @@ impl Song { /// the builder. /// /// [struct level documentation]: ./struct.RandomSongs.html - pub fn random_with<'a>(client: &Client) -> RandomSongs { + pub fn random_with(client: &Client) -> RandomSongs { RandomSongs::new(client, 10) } @@ -246,12 +250,12 @@ impl<'de> Deserialize<'de> for Song { where D: Deserializer<'de>, { - #[derive(Debug, Deserialize)] + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct _Song { id: String, - parent: String, - is_dir: bool, + // parent: String, + // is_dir: bool, title: String, album: Option, artist: Option, @@ -265,12 +269,12 @@ impl<'de> Deserialize<'de> for Song { transcoded_content_type: Option, transcoded_suffix: Option, duration: Option, - bit_rate: Option, + // bit_rate: Option, path: String, - is_video: Option, - play_count: u64, - disc_number: Option, - created: String, + // is_video: Option, + // play_count: u64, + // disc_number: Option, + // created: String, album_id: Option, artist_id: Option, #[serde(rename = "type")] @@ -444,9 +448,8 @@ impl<'a> RandomSongs<'a> { #[cfg(test)] mod tests { - use test_util; - use super::*; + use crate::test_util; #[test] fn parse_song() { @@ -459,10 +462,10 @@ mod tests { #[test] fn get_hls() { - let mut srv = test_util::demo_site().unwrap(); + let srv = test_util::demo_site().unwrap(); let song = serde_json::from_value::(raw()).unwrap(); - let hls = song.hls(&mut srv, &[]).unwrap(); + let hls = song.hls(&srv, &[]).unwrap(); assert_eq!(hls.len(), 20) } diff --git a/src/media/video.rs b/src/media/video.rs index fc4c636..c021c5c 100644 --- a/src/media/video.rs +++ b/src/media/video.rs @@ -1,40 +1,46 @@ +//! Video APIs. + use std::result; -use query::Query; use serde::de::{Deserialize, Deserializer}; use serde_json; -use {Client, Error, Media, Result, Streamable}; +use crate::query::Query; +use crate::{Client, Error, Media, Result, Streamable}; + +#[allow(missing_docs)] #[derive(Debug)] +#[readonly::make] pub struct Video { pub id: usize, - parent: usize, - is_dir: bool, + pub parent: usize, + pub is_dir: bool, pub title: String, pub album: Option, - cover_id: Option, + pub cover_id: Option, pub size: usize, - content_type: String, - suffix: String, - transcoded_suffix: Option, - transcoded_content_type: Option, + pub content_type: String, + pub suffix: String, + pub transcoded_suffix: Option, + pub transcoded_content_type: Option, pub duration: usize, - bitrate: usize, - path: String, - is_video: bool, - created: String, - play_count: Option, + pub bitrate: usize, + pub path: String, + pub is_video: bool, + pub created: String, + pub play_count: Option, pub media_type: String, - bookmark_position: Option, - original_height: Option, - original_width: Option, - stream_br: Option, - stream_size: Option<(usize, usize)>, - stream_offset: usize, - stream_tc: Option, + pub bookmark_position: Option, + pub original_height: Option, + pub original_width: Option, + pub stream_br: Option, + pub stream_size: Option<(usize, usize)>, + pub stream_offset: usize, + pub stream_tc: Option, } impl Video { + #[allow(missing_docs)] pub fn get(client: &Client, id: usize) -> Result