char-slop/tidal-dl
tidal ripper
git clone https://git.t4t.associates/char-slop/tidal-dl
3d04234
main
1use anyhow::{ Context , Result }; 2use serde::{ Deserialize , Serialize }; 3 4use crate :: api_key:: ApiKey ; 5use crate :: pkce:: PkceSession ; 6 7const TOKEN_URL : & str ="https://auth.tidal.com/v1/oauth2/token" ; 8 9# [ derive ( Debug , Clone , Default )] 10pub struct PendingPkce { 11pub verifier : String , 12pub redirect_uri : String , 13pub client_unique_key : String , 14} 15 16impl PendingPkce { 17pub fn from_session ( session : & PkceSession , redirect_uri : & str ) ->Self { 18Self { 19verifier : session. verifier . clone (), 20redirect_uri : redirect_uri. to_string (), 21client_unique_key : session. client_unique_key . clone (), 22} 23} 24} 25 26# [ derive ( Debug , Clone , Deserialize )] 27struct TokenResponse { 28access_token : String , 29refresh_token : Option < String >, 30expires_in : Option < i64 >, 31user : Option < TokenUser >, 32} 33 34# [ derive ( Debug , Clone , Deserialize )] 35# [ serde ( rename_all = "camelCase" )] 36struct TokenUser { 37user_id : u64 , 38country_code : String , 39} 40 41# [ derive ( Debug , Clone , Serialize , Deserialize )] 42pub struct Session { 43pub user_id : u64 , 44pub country_code : String , 45pub access_token : String , 46pub refresh_token : Option < String >, 47pub expires_in : i64 , 48} 49 50pub async fn exchange_code ( 51client : & reqwest:: Client , 52key : & ApiKey , 53pending : & PendingPkce , 54code : & str , 55) ->Result < Session > { 56let redirect_uri = key. redirect_uri . clone (); 57let form =[ 58( "client_id" , key. client_id . as_str ()), 59( "client_unique_key" , pending. client_unique_key . as_str ()), 60( "code" , code), 61( "code_verifier" , pending. verifier . as_str ()), 62( "grant_type" , "authorization_code" ), 63( "redirect_uri" , redirect_uri. as_str ()), 64( "scope" , key. scope . as_str ()), 65]; 66let resp: serde_json:: Value = client 67. post ( TOKEN_URL ) 68. form ( & form) 69. send () 70. await 71. context ( "token exchange request" ) ? 72. json () 73. await 74. context ( "token exchange body" ) ?; 75if resp. get ( "access_token" ). is_none () { 76 anyhow:: bail!( "Get token failed: {resp}" ); 77} 78let token: TokenResponse = serde_json:: from_value ( resp). context ( "token response" ) ?; 79Ok ( Session { 80user_id : token. user . as_ref (). map ( |u| u. user_id ). unwrap_or ( 0 ), 81country_code : token. user . map ( |u| u. country_code ). unwrap_or_default (), 82access_token : token. access_token , 83refresh_token : token. refresh_token , 84expires_in : token. expires_in . unwrap_or ( 0 ), 85}) 86} 87 88pub async fn verify_access_token ( client : & reqwest:: Client , access_token : & str ) ->bool { 89let resp = client 90. get ( "https://api.tidal.com/v1/sessions" ) 91. bearer_auth ( access_token) 92. send () 93. await ; 94match resp{ 95Ok ( r) => r. status (). is_success (), 96Err ( _) =>false , 97} 98} 99 100pub async fn refresh_token ( 101client : & reqwest:: Client , 102key : & ApiKey , 103refresh_token : & str , 104) ->Result < Session > { 105let form =[ 106( "client_id" , key. client_id . as_str ()), 107( "refresh_token" , refresh_token), 108( "grant_type" , "refresh_token" ), 109( "scope" , key. scope . as_str ()), 110]; 111let mut request = client. post ( TOKEN_URL ); 112if !key. client_secret . is_empty () { 113 request = request. basic_auth ( & key. client_id , Some ( & key. client_secret )); 114} 115let resp: serde_json:: Value = request 116. form ( & form) 117. send () 118. await 119. context ( "refresh request" ) ? 120. json () 121. await 122. context ( "refresh body" ) ?; 123if resp. get ( "access_token" ). is_none () { 124 anyhow:: bail!( "Refresh failed: {resp}" ); 125} 126let token: TokenResponse = serde_json:: from_value ( resp). context ( "refresh response" ) ?; 127Ok ( Session { 128user_id : token. user . as_ref (). map ( |u| u. user_id ). unwrap_or ( 0 ), 129country_code : token. user . map ( |u| u. country_code ). unwrap_or_default (), 130access_token : token. access_token , 131refresh_token : token. refresh_token . or ( Some ( refresh_token. to_string ())), 132expires_in : token. expires_in . unwrap_or ( 0 ), 133}) 134} 135 136const DEVICE_AUTH_URL : & str ="https://auth.tidal.com/v1/oauth2/device_authorization" ; 137pub const DEVICE_SCOPE : & str ="r_usr+w_usr+w_sub" ; 138 139# [ derive ( Debug , Clone , serde :: Deserialize )] 140# [ serde ( rename_all = "camelCase" )] 141pub struct DeviceCode { 142pub device_code : String , 143pub user_code : String , 144pub verification_uri : String , 145pub expires_in : u64 , 146pub interval : u64 , 147} 148 149pub async fn start_device_code ( 150client : & reqwest:: Client , 151key : & ApiKey , 152) ->Result < DeviceCode > { 153let resp: serde_json:: Value = client 154. post ( DEVICE_AUTH_URL ) 155. form ( & [( "client_id" , key. client_id . as_str ()), ( "scope" , DEVICE_SCOPE )]) 156. send () 157. await 158. context ( "device authorization request" ) ? 159. json () 160. await 161. context ( "device authorization body" ) ?; 162if let Some ( status) = resp. get ( "status" ) { 163 anyhow:: bail!( "device authorization failed (status {status}) — try another key" ); 164} 165 serde_json:: from_value ( resp). context ( "device authorization response" ) 166} 167 168/// Map one device-poll response: `Ok(None)` means still waiting. 169pub fn device_poll_result ( resp : serde_json:: Value ) ->Result < Option < Session >> { 170if resp. get ( "access_token" ). is_some () { 171let token: TokenResponse = serde_json:: from_value ( resp). context ( "device token response" ) ?; 172return Ok ( Some ( Session { 173user_id : token. user . as_ref (). map ( |u| u. user_id ). unwrap_or ( 0 ), 174country_code : token. user . map ( |u| u. country_code ). unwrap_or_default (), 175access_token : token. access_token , 176refresh_token : token. refresh_token , 177expires_in : token. expires_in . unwrap_or ( 0 ), 178})); 179} 180let status = resp. get ( "status" ). and_then ( |s| s. as_u64 ()). unwrap_or ( 0 ); 181let sub = resp. get ( "sub_status" ). and_then ( |s| s. as_u64 ()). unwrap_or ( 0 ); 182if status ==400 && sub ==1002 { 183return Ok ( None ); 184} 185 anyhow:: bail!( "device authorization failed: {resp}" ); 186} 187 188pub async fn poll_device ( 189client : & reqwest:: Client , 190key : & ApiKey , 191device_code : & str , 192) ->Result < Option < Session >> { 193let resp: serde_json:: Value = client 194. post ( TOKEN_URL ) 195. basic_auth ( & key. client_id , Some ( & key. client_secret )) 196. form ( & [ 197( "client_id" , key. client_id . as_str ()), 198( "device_code" , device_code), 199( "grant_type" , "urn:ietf:params:oauth:grant-type:device_code" ), 200( "scope" , DEVICE_SCOPE ), 201]) 202. send () 203. await 204. context ( "device poll request" ) ? 205. json () 206. await 207. context ( "device poll body" ) ?; 208device_poll_result ( resp) 209} 210 211# [ cfg ( test )] 212mod tests{ 213use super :: * ; 214 215# [ test ] 216fn device_poll_maps_pending_done_and_error () { 217let pending = serde_json:: json!({ "status" : 400 , "sub_status" : 1002 }); 218assert! ( device_poll_result ( pending). unwrap (). is_none ()); 219 220let done = serde_json:: json!({ 221"access_token" : "a" , "refresh_token" : "r" , "expires_in" : 3600 , 222"user" : { "userId" : 7 , "countryCode" : "US" }, 223}); 224let sess =device_poll_result ( done). unwrap (). unwrap (); 225assert_eq! ( sess. user_id , 7 ); 226 227let err = serde_json:: json!({ "status" : 400 , "sub_status" : 1005 }); 228assert! ( device_poll_result ( err). is_err ()); 229} 230}