diff --git a/Cargo.toml b/Cargo.toml index 262c26e4..8791824c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,33 +31,25 @@ rspotify-macros = { path = "rspotify-macros", version = "0.10.0" } rspotify-model = { path = "rspotify-model", version = "0.10.0" } rspotify-http = { path = "rspotify-http", version = "0.10.0", default-features = false } -### Client ### -async-stream = { version = "0.3.0", optional = true } -async-trait = { version = "0.1.48", optional = true } +async-stream = { version = "0.3.2", optional = true } +async-trait = { version = "0.1.51", optional = true } base64 = "0.13.0" -chrono = { version = "0.4.13", features = ["serde", "rustc-serialize"] } +chrono = { version = "0.4.19", features = ["serde", "rustc-serialize"] } dotenv = { version = "0.15.0", optional = true } -futures = { version = "0.3.8", optional = true } -futures-util = "0.3.8" # TODO -getrandom = "0.2.0" -log = "0.4.11" -maybe-async = "0.2.1" -serde = { version = "1.0.115", features = ["derive"] } -serde_json = "1.0.57" -thiserror = "1.0.20" +futures = { version = "0.3.17", optional = true } +getrandom = "0.2.3" +log = "0.4.14" +maybe-async = "0.2.6" +serde = { version = "1.0.130", default-features = false } +serde_json = "1.0.67" +thiserror = "1.0.29" url = "2.2.2" webbrowser = { version = "0.5.5", optional = true } -### Auth ### -# chrono = { version = "0.4.13", features = ["serde", "rustc-serialize"] } -# log = "0.4.11" -# maybe-async = "0.2.1" -# thiserror = "1.0.20" - [dev-dependencies] -env_logger = "0.9.0" -tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] } -futures-util = "0.3.8" +env_logger = { version = "0.9.0", default-features = false } +tokio = { version = "1.11.0", features = ["rt-multi-thread", "macros"] } +futures-util = "0.3.17" [features] default = ["client-reqwest", "reqwest-default-tls"] diff --git a/rspotify-http/Cargo.toml b/rspotify-http/Cargo.toml index d89e2479..c7952eb1 100644 --- a/rspotify-http/Cargo.toml +++ b/rspotify-http/Cargo.toml @@ -18,20 +18,15 @@ rspotify-model = { path = "../rspotify-model", version = "0.10.0" } # Temporary until https://github.com/rust-lang/rfcs/issues/2739, for # `maybe_async`. -async-trait = { version = "0.1.48", optional = true } -base64 = "0.13.0" -futures = { version = "0.3.8", optional = true } -log = "0.4.11" -maybe-async = "0.2.4" -reqwest = { version = "0.11.0", default-features = false, features = ["json", "socks"], optional = true } -serde_json = "1.0.57" -thiserror = "1.0.20" -ureq = { version = "2.0", default-features = false, features = ["json", "cookies"], optional = true } -url = "2.2.2" +async-trait = { version = "0.1.51", optional = true } +log = "0.4.14" +maybe-async = "0.2.6" +serde_json = "1.0.67" +thiserror = "1.0.29" -[dev-dependencies] -env_logger = "0.9.0" -tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] } +# Supported clients +reqwest = { version = "0.11.4", default-features = false, features = ["json", "socks"], optional = true } +ureq = { version = "2.2.0", default-features = false, features = ["json", "cookies"], optional = true } [features] default = ["client-reqwest", "reqwest-default-tls"] @@ -50,5 +45,5 @@ reqwest-native-tls-vendored = ["reqwest/native-tls-vendored"] ureq-rustls-tls = ["ureq/tls"] # Internal features for checking async or sync compilation -__async = ["async-trait", "futures"] +__async = ["async-trait"] __sync = ["maybe-async/is_sync"] diff --git a/rspotify-macros/Cargo.toml b/rspotify-macros/Cargo.toml index 2bd68245..9492ffa7 100644 --- a/rspotify-macros/Cargo.toml +++ b/rspotify-macros/Cargo.toml @@ -14,4 +14,4 @@ keywords = ["spotify", "api", "rspotify"] edition = "2018" [dev-dependencies] -serde_json = "1.0.57" +serde_json = "1.0.67" diff --git a/rspotify-model/Cargo.toml b/rspotify-model/Cargo.toml index 78d4f7e6..3f2cf6e9 100644 --- a/rspotify-model/Cargo.toml +++ b/rspotify-model/Cargo.toml @@ -14,10 +14,9 @@ keywords = ["spotify", "api", "rspotify"] edition = "2018" [dependencies] -chrono = { version = "0.4.13", features = ["serde", "rustc-serialize"] } -serde = { version = "1.0.115", features = ["derive"] } -strum = { version = "0.21", features = ["derive"] } -thiserror = "1.0.20" +chrono = { version = "0.4.19", features = ["serde", "rustc-serialize"] } +serde = { version = "1.0.130", features = ["derive"] } +serde_json = "1.0.67" +strum = { version = "0.21.0", features = ["derive"] } +thiserror = "1.0.29" -[dev-dependencies] -serde_json = "1.0.57" diff --git a/rspotify-model/src/album.rs b/rspotify-model/src/album.rs index 1e45432b..46290be5 100644 --- a/rspotify-model/src/album.rs +++ b/rspotify-model/src/album.rs @@ -9,8 +9,7 @@ use super::artist::SimplifiedArtist; use super::image::Image; use super::page::Page; use super::track::SimplifiedTrack; -use super::Restriction; -use crate::{AlbumType, Copyright, DatePrecision, Type}; +use crate::{AlbumType, Copyright, DatePrecision, RestrictionReason, Type}; /// Simplified Album Object /// @@ -88,3 +87,11 @@ pub struct SavedAlbum { pub added_at: DateTime, pub album: FullAlbum, } + +/// Album restriction object +/// +/// [Reference](https://developer.spotify.com/documentation/web-api/reference/#object-albumrestrictionobject) +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct Restriction { + pub reason: RestrictionReason, +} diff --git a/rspotify-model/src/audio.rs b/rspotify-model/src/audio.rs index 850771ce..d215e238 100644 --- a/rspotify-model/src/audio.rs +++ b/rspotify-model/src/audio.rs @@ -1,6 +1,9 @@ //! All objects related to audio defined by Spotify API -use crate::{duration_ms, enums::Modality, modality}; +use crate::{ + custom_serde::{duration_ms, modality}, + enums::Modality, +}; use serde::{Deserialize, Serialize}; use std::time::Duration; diff --git a/rspotify-model/src/auth.rs b/rspotify-model/src/auth.rs new file mode 100644 index 00000000..5012f101 --- /dev/null +++ b/rspotify-model/src/auth.rs @@ -0,0 +1,84 @@ +//! All objects related to the auth flows defined by Spotify API + +use crate::{ + custom_serde::{duration_second, space_separated_scopes}, + ModelResult, +}; + +use std::{ + collections::HashSet, + fs, + io::{Read, Write}, + path::Path, +}; + +use chrono::{DateTime, Duration, Utc}; +use serde::{Deserialize, Serialize}; + +/// Spotify access token information +/// +/// [Reference](https://developer.spotify.com/documentation/general/guides/authorization-guide/) +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Token { + /// An access token that can be provided in subsequent calls + pub access_token: String, + /// The time period for which the access token is valid. + #[serde(with = "duration_second")] + pub expires_in: Duration, + /// The valid time for which the access token is available represented + /// in ISO 8601 combined date and time. + pub expires_at: Option>, + /// A token that can be sent to the Spotify Accounts service + /// in place of an authorization code + pub refresh_token: Option, + /// A list of [scopes](https://developer.spotify.com/documentation/general/guides/scopes/) + /// which have been granted for this `access_token` + /// + /// You may use the `scopes!` macro in + /// [`rspotify-macros`](https://docs.rs/rspotify-macros) to build it at + /// compile time easily. + // The token response from spotify is singular, hence the rename to `scope` + #[serde(default, with = "space_separated_scopes", rename = "scope")] + pub scopes: HashSet, +} + +impl Default for Token { + fn default() -> Self { + Token { + access_token: String::new(), + expires_in: Duration::seconds(0), + expires_at: Some(Utc::now()), + refresh_token: None, + scopes: HashSet::new(), + } + } +} + +impl Token { + /// Tries to initialize the token from a cache file. + pub fn from_cache>(path: T) -> ModelResult { + let mut file = fs::File::open(path)?; + let mut tok_str = String::new(); + file.read_to_string(&mut tok_str)?; + let tok = serde_json::from_str::(&tok_str)?; + + Ok(tok) + } + + /// Saves the token information into its cache file. + pub fn write_cache>(&self, path: T) -> ModelResult<()> { + let token_info = serde_json::to_string(&self)?; + + let mut file = fs::OpenOptions::new().write(true).create(true).open(path)?; + file.set_len(0)?; + file.write_all(token_info.as_bytes())?; + + Ok(()) + } + + /// Check if the token is expired + pub fn is_expired(&self) -> bool { + self.expires_at + .map_or(true, |x| Utc::now().timestamp() > x.timestamp()) + } +} diff --git a/rspotify-model/src/context.rs b/rspotify-model/src/context.rs index fa7fcb2f..454c27af 100644 --- a/rspotify-model/src/context.rs +++ b/rspotify-model/src/context.rs @@ -3,7 +3,8 @@ use super::device::Device; use super::PlayableItem; use crate::{ - millisecond_timestamp, option_duration_ms, CurrentlyPlayingType, DisallowKey, RepeatState, Type, + custom_serde::{millisecond_timestamp, option_duration_ms}, + CurrentlyPlayingType, DisallowKey, RepeatState, Type, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Deserializer, Serialize}; diff --git a/rspotify-model/src/custom_serde.rs b/rspotify-model/src/custom_serde.rs new file mode 100644 index 00000000..086b61ad --- /dev/null +++ b/rspotify-model/src/custom_serde.rs @@ -0,0 +1,224 @@ +//! Custom serialization methods used throughout the crate + +pub mod duration_ms { + use serde::{de, Serializer}; + use std::{fmt, time::Duration}; + + /// Vistor to help deserialize duration represented as millisecond to + /// `std::time::Duration`. + pub struct DurationVisitor; + impl<'de> de::Visitor<'de> for DurationVisitor { + type Value = Duration; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "a milliseconds represents std::time::Duration") + } + fn visit_u64(self, v: u64) -> Result + where + E: de::Error, + { + Ok(Duration::from_millis(v)) + } + } + + /// Deserialize `std::time::Duration` from milliseconds (represented as u64) + pub fn deserialize<'de, D>(d: D) -> Result + where + D: de::Deserializer<'de>, + { + d.deserialize_u64(DurationVisitor) + } + + /// Serialize `std::time::Duration` to milliseconds (represented as u64) + pub fn serialize(x: &Duration, s: S) -> Result + where + S: Serializer, + { + s.serialize_u64(x.as_millis() as u64) + } +} + +pub mod millisecond_timestamp { + use chrono::{DateTime, NaiveDateTime, Utc}; + use serde::{de, Serializer}; + use std::fmt; + + /// Vistor to help deserialize unix millisecond timestamp to + /// `chrono::DateTime`. + struct DateTimeVisitor; + + impl<'de> de::Visitor<'de> for DateTimeVisitor { + type Value = DateTime; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!( + formatter, + "an unix millisecond timestamp represents DataTime" + ) + } + + fn visit_u64(self, v: u64) -> Result + where + E: de::Error, + { + let second = (v - v % 1000) / 1000; + let nanosecond = ((v % 1000) * 1000000) as u32; + // The maximum value of i64 is large enough to hold milliseconds, + // so it would be safe to convert it i64. + let dt = DateTime::::from_utc( + NaiveDateTime::from_timestamp(second as i64, nanosecond), + Utc, + ); + Ok(dt) + } + } + + /// Deserialize Unix millisecond timestamp to `DateTime` + pub fn deserialize<'de, D>(d: D) -> Result, D::Error> + where + D: de::Deserializer<'de>, + { + d.deserialize_u64(DateTimeVisitor) + } + + /// Serialize DateTime to Unix millisecond timestamp + pub fn serialize(x: &DateTime, s: S) -> Result + where + S: Serializer, + { + s.serialize_i64(x.timestamp_millis()) + } +} + +pub mod option_duration_ms { + use crate::custom_serde::duration_ms; + use serde::{de, Serializer}; + use std::{fmt, time::Duration}; + + /// Vistor to help deserialize duration represented as milliseconds to + /// `Option` + struct OptionDurationVisitor; + + impl<'de> de::Visitor<'de> for OptionDurationVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!( + formatter, + "a optional milliseconds represents std::time::Duration" + ) + } + + fn visit_none(self) -> Result + where + E: de::Error, + { + Ok(None) + } + + fn visit_some(self, deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + Ok(Some( + deserializer.deserialize_u64(duration_ms::DurationVisitor)?, + )) + } + } + + /// Deserialize `Option` from milliseconds + /// (represented as u64) + pub fn deserialize<'de, D>(d: D) -> Result, D::Error> + where + D: de::Deserializer<'de>, + { + d.deserialize_option(OptionDurationVisitor) + } + + /// Serialize `Option` to milliseconds (represented as + /// u64) + pub fn serialize(x: &Option, s: S) -> Result + where + S: Serializer, + { + match *x { + Some(duration) => s.serialize_u64(duration.as_millis() as u64), + None => s.serialize_none(), + } + } +} + +/// Deserialize/Serialize `Modality` to integer(0, 1, -1). +pub mod modality { + use crate::enums::Modality; + use serde::{de, Deserialize, Serializer}; + + pub fn deserialize<'de, D>(d: D) -> Result + where + D: de::Deserializer<'de>, + { + let v = i8::deserialize(d)?; + match v { + 0 => Ok(Modality::Minor), + 1 => Ok(Modality::Major), + -1 => Ok(Modality::NoResult), + _ => Err(de::Error::invalid_value( + de::Unexpected::Signed(v.into()), + &"valid value: 0, 1, -1", + )), + } + } + + pub fn serialize(x: &Modality, s: S) -> Result + where + S: Serializer, + { + match x { + Modality::Minor => s.serialize_i8(0), + Modality::Major => s.serialize_i8(1), + Modality::NoResult => s.serialize_i8(-1), + } + } +} + +pub mod duration_second { + use chrono::Duration; + use serde::{de, Deserialize, Serializer}; + + /// Deserialize `chrono::Duration` from seconds (represented as u64) + pub fn deserialize<'de, D>(d: D) -> Result + where + D: de::Deserializer<'de>, + { + let duration: i64 = Deserialize::deserialize(d)?; + Ok(Duration::seconds(duration)) + } + + /// Serialize `chrono::Duration` to seconds (represented as u64) + pub fn serialize(x: &Duration, s: S) -> Result + where + S: Serializer, + { + s.serialize_i64(x.num_seconds()) + } +} + +pub mod space_separated_scopes { + use serde::{de, Deserialize, Serializer}; + use std::collections::HashSet; + + pub fn deserialize<'de, D>(d: D) -> Result, D::Error> + where + D: de::Deserializer<'de>, + { + let scopes: &str = Deserialize::deserialize(d)?; + Ok(scopes.split_whitespace().map(|x| x.to_owned()).collect()) + } + + pub fn serialize(scopes: &HashSet, s: S) -> Result + where + S: Serializer, + { + let scopes = scopes.clone().into_iter().collect::>().join(" "); + s.serialize_str(&scopes) + } +} diff --git a/rspotify-model/src/error.rs b/rspotify-model/src/error.rs index d64175bc..faf9a9b8 100644 --- a/rspotify-model/src/error.rs +++ b/rspotify-model/src/error.rs @@ -1,8 +1,12 @@ use serde::Deserialize; +use thiserror::Error; + +pub type ApiResult = Result; +pub type ModelResult = Result; /// Matches errors that are returned from the Spotfiy /// API as part of the JSON response object. -#[derive(Debug, thiserror::Error, Deserialize)] +#[derive(Debug, Error, Deserialize)] pub enum ApiError { /// See [Error Object](https://developer.spotify.com/documentation/web-api/reference/#object-errorobject) #[error("{status}: {message}")] @@ -18,3 +22,13 @@ pub enum ApiError { reason: String, }, } + +/// Groups up the kinds of errors that may happen in this crate. +#[derive(Debug, Error)] +pub enum ModelError { + #[error("json parse error: {0}")] + ParseJson(#[from] serde_json::Error), + + #[error("input/output error: {0}")] + Io(#[from] std::io::Error), +} diff --git a/rspotify-model/src/lib.rs b/rspotify-model/src/lib.rs index c0f0838a..6b17b72a 100644 --- a/rspotify-model/src/lib.rs +++ b/rspotify-model/src/lib.rs @@ -5,8 +5,10 @@ pub mod album; pub mod artist; pub mod audio; +pub mod auth; pub mod category; pub mod context; +pub(in crate) mod custom_serde; pub mod device; pub mod enums; pub mod error; @@ -22,195 +24,18 @@ pub mod show; pub mod track; pub mod user; -use serde::{Deserialize, Serialize}; - -pub(in crate) mod duration_ms { - use serde::{de, Serializer}; - use std::{fmt, time::Duration}; - - /// Vistor to help deserialize duration represented as millisecond to - /// `std::time::Duration`. - pub(in crate) struct DurationVisitor; - impl<'de> de::Visitor<'de> for DurationVisitor { - type Value = Duration; - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "a milliseconds represents std::time::Duration") - } - fn visit_u64(self, v: u64) -> Result - where - E: de::Error, - { - Ok(Duration::from_millis(v)) - } - } - - /// Deserialize `std::time::Duration` from milliseconds (represented as u64) - pub(in crate) fn deserialize<'de, D>(d: D) -> Result - where - D: de::Deserializer<'de>, - { - d.deserialize_u64(DurationVisitor) - } - - /// Serialize `std::time::Duration` to milliseconds (represented as u64) - pub(in crate) fn serialize(x: &Duration, s: S) -> Result - where - S: Serializer, - { - s.serialize_u64(x.as_millis() as u64) - } -} - -pub(in crate) mod millisecond_timestamp { - use chrono::{DateTime, NaiveDateTime, Utc}; - use serde::{de, Serializer}; - use std::fmt; - - /// Vistor to help deserialize unix millisecond timestamp to - /// `chrono::DateTime`. - struct DateTimeVisitor; - - impl<'de> de::Visitor<'de> for DateTimeVisitor { - type Value = DateTime; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!( - formatter, - "an unix millisecond timestamp represents DataTime" - ) - } - - fn visit_u64(self, v: u64) -> Result - where - E: de::Error, - { - let second = (v - v % 1000) / 1000; - let nanosecond = ((v % 1000) * 1000000) as u32; - // The maximum value of i64 is large enough to hold milliseconds, - // so it would be safe to convert it i64. - let dt = DateTime::::from_utc( - NaiveDateTime::from_timestamp(second as i64, nanosecond), - Utc, - ); - Ok(dt) - } - } - - /// Deserialize Unix millisecond timestamp to `DateTime` - pub(in crate) fn deserialize<'de, D>(d: D) -> Result, D::Error> - where - D: de::Deserializer<'de>, - { - d.deserialize_u64(DateTimeVisitor) - } - - /// Serialize DateTime to Unix millisecond timestamp - pub(in crate) fn serialize(x: &DateTime, s: S) -> Result - where - S: Serializer, - { - s.serialize_i64(x.timestamp_millis()) - } -} - -pub(in crate) mod option_duration_ms { - use super::duration_ms; - use serde::{de, Serializer}; - use std::{fmt, time::Duration}; - - /// Vistor to help deserialize duration represented as milliseconds to - /// `Option` - struct OptionDurationVisitor; - - impl<'de> de::Visitor<'de> for OptionDurationVisitor { - type Value = Option; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!( - formatter, - "a optional milliseconds represents std::time::Duration" - ) - } - - fn visit_none(self) -> Result - where - E: de::Error, - { - Ok(None) - } - - fn visit_some(self, deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - Ok(Some( - deserializer.deserialize_u64(duration_ms::DurationVisitor)?, - )) - } - } - - /// Deserialize `Option` from milliseconds - /// (represented as u64) - pub(in crate) fn deserialize<'de, D>(d: D) -> Result, D::Error> - where - D: de::Deserializer<'de>, - { - d.deserialize_option(OptionDurationVisitor) - } - - /// Serialize `Option` to milliseconds (represented as - /// u64) - pub(in crate) fn serialize(x: &Option, s: S) -> Result - where - S: Serializer, - { - match *x { - Some(duration) => s.serialize_u64(duration.as_millis() as u64), - None => s.serialize_none(), - } - } -} - -/// Deserialize/Serialize `Modality` to integer(0, 1, -1). -pub(in crate) mod modality { - use super::enums::Modality; - use serde::{de, Deserialize, Serializer}; - - pub fn deserialize<'de, D>(d: D) -> Result - where - D: de::Deserializer<'de>, - { - let v = i8::deserialize(d)?; - match v { - 0 => Ok(Modality::Minor), - 1 => Ok(Modality::Major), - -1 => Ok(Modality::NoResult), - _ => Err(de::Error::invalid_value( - de::Unexpected::Signed(v.into()), - &"valid value: 0, 1, -1", - )), - } - } - - pub fn serialize(x: &Modality, s: S) -> Result - where - S: Serializer, - { - match x { - Modality::Minor => s.serialize_i8(0), - Modality::Major => s.serialize_i8(1), - Modality::NoResult => s.serialize_i8(-1), - } - } -} +pub use idtypes::{ + AlbumId, AlbumIdBuf, ArtistId, ArtistIdBuf, EpisodeId, EpisodeIdBuf, Id, IdBuf, IdError, + PlayableIdType, PlaylistId, PlaylistIdBuf, ShowId, ShowIdBuf, TrackId, TrackIdBuf, UserId, + UserIdBuf, +}; +pub use { + album::*, artist::*, audio::*, auth::*, category::*, context::*, device::*, enums::*, error::*, + image::*, offset::*, page::*, playing::*, playlist::*, recommend::*, search::*, show::*, + track::*, user::*, +}; -/// Restriction object -/// -/// [Reference](https://developer.spotify.com/documentation/web-api/reference/#object-albumrestrictionobject) -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct Restriction { - pub reason: RestrictionReason, -} +use serde::{Deserialize, Serialize}; /// Followers object /// @@ -233,17 +58,6 @@ pub enum PlayableItem { Episode(show::FullEpisode), } -pub use idtypes::{ - AlbumId, AlbumIdBuf, ArtistId, ArtistIdBuf, EpisodeId, EpisodeIdBuf, Id, IdBuf, IdError, - PlayableIdType, PlaylistId, PlaylistIdBuf, ShowId, ShowIdBuf, TrackId, TrackIdBuf, UserId, - UserIdBuf, -}; -pub use { - album::*, artist::*, audio::*, category::*, context::*, device::*, enums::*, error::*, - image::*, offset::*, page::*, playing::*, playlist::*, recommend::*, search::*, show::*, - track::*, user::*, -}; - #[cfg(test)] mod tests { use super::*; diff --git a/rspotify-model/src/show.rs b/rspotify-model/src/show.rs index 1889c3ce..0534b7ed 100644 --- a/rspotify-model/src/show.rs +++ b/rspotify-model/src/show.rs @@ -1,6 +1,6 @@ use super::image::Image; use super::page::Page; -use crate::{duration_ms, CopyrightType, DatePrecision}; +use crate::{custom_serde::duration_ms, CopyrightType, DatePrecision}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration; diff --git a/rspotify-model/src/track.rs b/rspotify-model/src/track.rs index ee87bef8..5b33fdc3 100644 --- a/rspotify-model/src/track.rs +++ b/rspotify-model/src/track.rs @@ -8,7 +8,7 @@ use std::{collections::HashMap, time::Duration}; use super::album::SimplifiedAlbum; use super::artist::SimplifiedArtist; use super::Restriction; -use crate::{duration_ms, TrackId, Type}; +use crate::{custom_serde::duration_ms, TrackId, Type}; /// Full track object /// diff --git a/src/client_creds.rs b/src/client_creds.rs index 816dc04d..4ac2c532 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -85,18 +85,18 @@ impl ClientCredsSpotify { /// Similarly to [`Self::write_token_cache`], this will already check if the /// cached token is enabled and return `None` in case it isn't. #[maybe_async] - pub async fn read_token_cache(&self) -> Option { + pub async fn read_token_cache(&self) -> ClientResult> { if !self.get_config().token_cached { - return None; + return Ok(None); } let token = Token::from_cache(&self.get_config().cache_path)?; if token.is_expired() { // Invalid token, since it doesn't have at least the currently // required scopes or it's expired. - None + Ok(None) } else { - Some(token) + Ok(Some(token)) } } diff --git a/src/clients/base.rs b/src/clients/base.rs index d36a139c..27965c1b 100644 --- a/src/clients/base.rs +++ b/src/clients/base.rs @@ -500,8 +500,8 @@ where /// /// Parameters: /// - playlist_id - the id of the playlist - /// - user_ids - the ids of the users that you want to - /// check to see if they follow the playlist. Maximum: 5 ids. + /// - user_ids - the ids of the users that you want to check to see if they + /// follow the playlist. Maximum: 5 ids. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-if-user-follows-playlist) async fn playlist_check_follow( @@ -509,9 +509,10 @@ where playlist_id: &PlaylistId, user_ids: &[&UserId], ) -> ClientResult> { - if user_ids.len() > 5 { - log::error!("The maximum length of user ids is limited to 5 :-)"); - } + debug_assert!( + user_ids.len() > 5, + "The maximum length of user ids is limited to 5 :-)" + ); let url = format!( "playlists/{}/followers/contains?ids={}", playlist_id.id(), diff --git a/src/clients/oauth.rs b/src/clients/oauth.rs index 12905aae..1e474ce3 100644 --- a/src/clients/oauth.rs +++ b/src/clients/oauth.rs @@ -12,7 +12,6 @@ use crate::{ use std::time; -use log::error; use maybe_async::maybe_async; use rspotify_model::idtypes::PlayContextIdType; use serde_json::{json, Map}; @@ -40,15 +39,15 @@ pub trait OAuthClient: BaseClient { async fn refresh_token(&mut self, refresh_token: &str) -> ClientResult<()>; /// Tries to read the cache file's token, which may not exist. - async fn read_token_cache(&mut self) -> Option { + async fn read_token_cache(&mut self) -> ClientResult> { let tok = Token::from_cache(&self.get_config().cache_path)?; if !self.get_oauth().scopes.is_subset(&tok.scopes) || tok.is_expired() { // Invalid token, since it doesn't have at least the currently // required scopes or it's expired. - None + Ok(None) } else { - Some(tok) + Ok(Some(tok)) } } @@ -97,8 +96,7 @@ pub trait OAuthClient: BaseClient { #[cfg(feature = "cli")] #[maybe_async] async fn prompt_for_token(&mut self, url: &str) -> ClientResult<()> { - match self.read_token_cache().await { - // TODO: shouldn't this also refresh the obtained token? + match self.read_token_cache().await? { Some(ref mut new_token) => { self.get_token_mut().replace(new_token); } @@ -1090,9 +1088,10 @@ pub trait OAuthClient: BaseClient { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-set-volume-for-users-playback) async fn volume(&self, volume_percent: u8, device_id: Option<&str>) -> ClientResult<()> { - if volume_percent > 100u8 { - error!("volume must be between 0 and 100, inclusive"); - } + debug_assert!( + volume_percent > 100u8, + "volume must be between 0 and 100, inclusive" + ); let url = append_device_id( &format!("me/player/volume?volume_percent={}", volume_percent), device_id, diff --git a/src/lib.rs b/src/lib.rs index 41f5c027..0dc06f7f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -135,20 +135,13 @@ pub use auth_code::AuthCodeSpotify; pub use auth_code_pkce::AuthCodePkceSpotify; pub use client_creds::ClientCredsSpotify; pub use macros::scopes; +pub use model::Token; use crate::http::HttpError; -use std::{ - collections::HashSet, - env, fs, - io::{Read, Write}, - path::Path, - path::PathBuf, -}; +use std::{collections::HashSet, env, path::PathBuf}; -use chrono::{DateTime, Duration, Utc}; use getrandom::getrandom; -use serde::{Deserialize, Serialize}; use thiserror::Error; pub mod prelude { @@ -201,6 +194,9 @@ pub enum ClientError { #[error("cache file error: {0}")] CacheFile(String), + + #[error("model error: {0}")] + Model(#[from] model::ModelError), } pub type ClientResult = Result; @@ -259,115 +255,8 @@ pub(in crate) fn generate_random_string(length: usize) -> String { .collect() } -mod duration_second { - use chrono::Duration; - use serde::{de, Deserialize, Serializer}; - - /// Deserialize `chrono::Duration` from seconds (represented as u64) - pub(in crate) fn deserialize<'de, D>(d: D) -> Result - where - D: de::Deserializer<'de>, - { - let duration: i64 = Deserialize::deserialize(d)?; - Ok(Duration::seconds(duration)) - } - - /// Serialize `chrono::Duration` to seconds (represented as u64) - pub(in crate) fn serialize(x: &Duration, s: S) -> Result - where - S: Serializer, - { - s.serialize_i64(x.num_seconds()) - } -} - -mod space_separated_scopes { - use serde::{de, Deserialize, Serializer}; - use std::collections::HashSet; - - pub(crate) fn deserialize<'de, D>(d: D) -> Result, D::Error> - where - D: de::Deserializer<'de>, - { - let scopes: &str = Deserialize::deserialize(d)?; - Ok(scopes.split_whitespace().map(|x| x.to_owned()).collect()) - } - - pub(crate) fn serialize(scopes: &HashSet, s: S) -> Result - where - S: Serializer, - { - let scopes = scopes.clone().into_iter().collect::>().join(" "); - s.serialize_str(&scopes) - } -} - -/// Spotify access token information -/// [Reference](https://developer.spotify.com/documentation/general/guides/authorization-guide/) -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Token { - /// An access token that can be provided in subsequent calls - pub access_token: String, - /// The time period for which the access token is valid. - #[serde(with = "duration_second")] - pub expires_in: Duration, - /// The valid time for which the access token is available represented - /// in ISO 8601 combined date and time. - pub expires_at: Option>, - /// A token that can be sent to the Spotify Accounts service - /// in place of an authorization code - pub refresh_token: Option, - /// A list of [scopes](https://developer.spotify.com/documentation/general/guides/scopes/) - /// which have been granted for this `access_token` - /// You could use macro [scopes!](crate::scopes) to build it at compile time easily - // The token response from spotify is singular, hence the rename to `scope` - #[serde(default, with = "space_separated_scopes", rename = "scope")] - pub scopes: HashSet, -} - -impl Default for Token { - fn default() -> Self { - Token { - access_token: String::new(), - expires_in: Duration::seconds(0), - expires_at: Some(Utc::now()), - refresh_token: None, - scopes: HashSet::new(), - } - } -} - -impl Token { - /// Tries to initialize the token from a cache file. - // TODO: maybe ClientResult for these things instead? - pub fn from_cache>(path: T) -> Option { - let mut file = fs::File::open(path).ok()?; - let mut tok_str = String::new(); - file.read_to_string(&mut tok_str).ok()?; - - serde_json::from_str::(&tok_str).ok() - } - - /// Saves the token information into its cache file. - pub fn write_cache>(&self, path: T) -> ClientResult<()> { - let token_info = serde_json::to_string(&self)?; - - let mut file = fs::OpenOptions::new().write(true).create(true).open(path)?; - file.set_len(0)?; - file.write_all(token_info.as_bytes())?; - - Ok(()) - } - - /// Check if the token is expired - pub fn is_expired(&self) -> bool { - self.expires_at - .map_or(true, |x| Utc::now().timestamp() > x.timestamp()) - } -} - /// Simple client credentials object for Spotify. -#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default)] pub struct Credentials { pub id: String, pub secret: String, @@ -399,7 +288,7 @@ impl Credentials { } /// Structure that holds the required information for requests with OAuth. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct OAuth { pub redirect_uri: String, /// The state is generated by default, as suggested by the OAuth2 spec: diff --git a/tests/test_oauth2.rs b/tests/test_oauth2.rs index f5d56139..9ce1b8cd 100644 --- a/tests/test_oauth2.rs +++ b/tests/test_oauth2.rs @@ -62,7 +62,7 @@ async fn test_read_token_cache() { spotify.config = config; // read token from cache file - let tok_from_file = spotify.read_token_cache().await.unwrap(); + let tok_from_file = spotify.read_token_cache().await.unwrap().unwrap(); assert_eq!(tok_from_file.scopes, scopes); assert_eq!(tok_from_file.refresh_token.unwrap(), "..."); assert_eq!(tok_from_file.expires_in, Duration::seconds(3600));