Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,497 changes: 961 additions & 536 deletions Cargo.lock

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@ description = "Asynchronous client for the Tidal music service API"
version = "0.1.2"
authors = ["4xposed <stokp1@gmail.com>"]
keywords = ["Tidal", "API", "Asynchronous"]
edition = "2018"
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/4xposed/rstidal"
readme = "README.md"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
log = "0.4.11"
reqwest = { version = "0.10", features = ["json"] }
serde = { version = "1.0.115", features = ["derive"] }
serde_json = "1.0.57"
serde_urlencoded = "0.7.0"
thiserror = "1.0"
log = "0.4.17"
reqwest = { version = "0.11.18", features = ["json"] }
serde = { version = "1.0.163", features = ["derive"] }
serde_json = "1.0.96"
serde_urlencoded = "0.7.1"
thiserror = "1.0.40"

[dev-dependencies]
mockito = "0.27.0"
tokio = { version = "0.2", features = ["full"] }
dotenv = { version = "0.15.0" }
mockito = "0.31"
tokio = { version = "1.28", features = ["full"] }
dotenv = "0.15.0"
93 changes: 56 additions & 37 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ use mockito;
// Use built-in library
use std::collections::HashMap;

// Use internal modules
use crate::config::params;
use crate::error::{AuthError, TidalError, TidalResult};

#[derive(Clone, Debug)]
pub struct TidalCredentials {
pub token: String,
Expand All @@ -29,33 +33,25 @@ impl TidalCredentials {
self
}

#[must_use]
pub async fn create_session(self, username: &str, password: &str) -> Self {
/// Create a session with the provided credentials
/// Returns an error if the token is empty or session creation fails
pub async fn create_session(self, username: &str, password: &str) -> TidalResult<Self> {
if self.token.is_empty() {
// A token needs to be set before this function can be called
panic!("Application Token needs to be set")
return Err(TidalError::Authentication(AuthError::MissingToken));
}
let token = self.token.to_owned();
let session = Session::get_session(&token, username, password).await.ok();
self.session(session)

let session = Session::get_session(&self.token, username, password).await?;
Ok(self.session(Some(session)))
}
}

//Tidal session example:
//{
//"userId": 173393989,
//"sessionId": "84df94d0-9t0b-537a-a485-4404e45581ft",
//"countryCode": "DE"
//"userId": 173393989,
//"sessionId": "84df94d0-9t0b-537a-a485-4404e45581ft",
//"countryCode": "DE"
//}

#[derive(thiserror::Error,Debug)]
pub enum AuthError {
#[error("The Authe request Failed")]
AuthRequestFailed { #[from] source: reqwest::Error },
#[error("Fetch session failed")]
CreateSessionFailed
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Session {
Expand All @@ -65,17 +61,16 @@ pub struct Session {
}

impl Session {
pub async fn get_session(token: &str, username: &str, password: &str) -> Result<Self, AuthError> {
let mut payload: HashMap<&str, &str> = HashMap::new();
pub async fn get_session(token: &str, username: &str, password: &str) -> TidalResult<Self> {
let mut payload: HashMap<&str, &str> = HashMap::with_capacity(2);
payload.insert("username", username);
payload.insert("password", password);
Self::fetch_session_data(token, &payload).await
}

async fn fetch_session_data(token: &str, payload: &HashMap<&str, &str>) -> Result<Self, AuthError> {
async fn fetch_session_data(token: &str, payload: &HashMap<&str, &str>) -> TidalResult<Self> {
let client = Client::new();
let token = token.to_owned();
let query = [("token", &token)];
let query = [(params::TOKEN, token)];

#[cfg(not(test))]
let url = "https://api.tidalhifi.com/v1/login/username";
Expand All @@ -86,21 +81,32 @@ impl Session {
let response = client
.post(url)
.query(&query)
.form(&payload)
.form(payload)
.send()
.await?;
.await
.map_err(AuthError::RequestFailed)?;

if response.status().is_success() {
debug!("response content: {:?}", response);
let session: Session = response.json().await?;
debug!("Session creation successful");
let session: Session = response
.json()
.await
.map_err(|e| TidalError::Authentication(AuthError::RequestFailed(e)))?;
Ok(session)
} else {
error!(
"Creating session failed. token: {:?}, form: {:?}",
&token, &payload
"Session creation failed. Status: {}, Token length: {}, Payload: {:?}",
response.status(),
token.len(),
payload.keys().collect::<Vec<_>>()
);
error!("{:?}", response);
Err(AuthError::CreateSessionFailed)

match response.status() {
reqwest::StatusCode::UNAUTHORIZED => {
Err(TidalError::Authentication(AuthError::InvalidCredentials))
}
_ => Err(TidalError::Authentication(AuthError::SessionCreationFailed)),
}
}
}
}
Expand Down Expand Up @@ -131,13 +137,17 @@ mod tests {
async fn test_credential_create_session_w_token() {
let token = "some_token";
let username = "myuser@example.com";
let password = "somepawssowrd";
let password = "somepassword";
let credentials = TidalCredentials::new(token);

// Test scucessful login
// Test successful login
{
let _mock = mock_successful_login();
let credential_w_session = credentials.clone().create_session(username, password).await;
let credential_w_session = credentials
.clone()
.create_session(username, password)
.await
.unwrap();
assert_eq!(
credential_w_session.session.unwrap().session_id,
"session-id-123"
Expand All @@ -146,12 +156,21 @@ mod tests {
// Test failed login
{
let _mock = mock_failed_login();
let credential_wo_session =
credentials.clone().create_session(username, password).await;
assert_eq!(credential_wo_session.session.is_none(), true);
let credential_result = credentials.clone().create_session(username, password).await;
assert!(credential_result.is_err());
}
}

#[tokio::test]
async fn test_create_session_empty_token() {
let credentials = TidalCredentials::new("");
let result = credentials.create_session("user", "pass").await;
assert!(matches!(
result,
Err(TidalError::Authentication(AuthError::MissingToken))
));
}

fn mock_successful_login() -> mockito::Mock {
mock("POST", "/?token=some_token")
.with_status(200)
Expand Down
Loading