2024-11-01 08:29:59 +00:00
|
|
|
mod schema;
|
|
|
|
|
|
|
|
impl<T> schema::SubsonicResponse<T> {
|
|
|
|
fn fixup(self) -> Result<T, Error> {
|
|
|
|
match self {
|
|
|
|
Self::Ok { inner } => Ok(inner),
|
|
|
|
Self::Failed { error } => Err(Error::SubsonicError(error)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-31 12:16:42 +00:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum Error {
|
2024-11-01 08:29:59 +00:00
|
|
|
UrlParseError(url::ParseError),
|
2024-10-31 12:16:42 +00:00
|
|
|
ReqwestError(reqwest::Error),
|
2024-11-01 08:29:59 +00:00
|
|
|
SubsonicError(schema::Error),
|
|
|
|
OtherError(&'static str),
|
2024-10-31 12:16:42 +00:00
|
|
|
}
|
2024-10-30 09:06:10 +00:00
|
|
|
|
2024-10-31 12:16:42 +00:00
|
|
|
impl From<reqwest::Error> for Error {
|
|
|
|
fn from(err: reqwest::Error) -> Self {
|
2024-11-01 08:32:20 +00:00
|
|
|
// don't print secret salt/token combo
|
|
|
|
Self::ReqwestError(err.without_url())
|
2024-10-31 12:16:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-01 08:29:59 +00:00
|
|
|
pub struct Client {
|
|
|
|
client: reqwest::Client,
|
|
|
|
base_url: reqwest::Url,
|
2024-10-31 12:16:42 +00:00
|
|
|
}
|
|
|
|
|
2024-11-01 08:29:59 +00:00
|
|
|
fn to_hex_str(bytes: &[u8]) -> String {
|
|
|
|
let mut hex = vec![0u8; 2 * bytes.len()];
|
|
|
|
for (i, byte) in bytes.iter().enumerate() {
|
|
|
|
hex[2 * i] = b"0123456789abcdef"[*byte as usize >> 4];
|
|
|
|
hex[2 * i + 1] = b"0123456789abcdef"[*byte as usize & 0xf];
|
|
|
|
}
|
|
|
|
String::from_utf8(hex).unwrap() // is literally just ascii
|
2024-10-31 12:16:42 +00:00
|
|
|
}
|
|
|
|
|
2024-11-01 08:29:59 +00:00
|
|
|
impl Client {
|
|
|
|
pub fn with_password(url: &str, username: &str, password: &[u8]) -> Result<Self, Error> {
|
|
|
|
const SALT_BYTES: usize = 8;
|
2024-10-31 12:16:42 +00:00
|
|
|
|
2024-11-01 08:32:20 +00:00
|
|
|
// subsonic docs say to generate a salt per request, but that's completely unnecessary
|
2024-11-01 08:29:59 +00:00
|
|
|
let mut salt = vec![0u8; SALT_BYTES];
|
|
|
|
openssl::rand::rand_bytes(&mut salt).expect("could not generate random salt");
|
|
|
|
let salt_hex = to_hex_str(&salt);
|
2024-10-31 12:16:42 +00:00
|
|
|
|
2024-11-01 08:29:59 +00:00
|
|
|
let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::md5())
|
|
|
|
.expect("could not create hasher object");
|
|
|
|
hasher
|
|
|
|
.update(password)
|
|
|
|
.expect("could not feed password to hasher");
|
|
|
|
hasher
|
|
|
|
.update(salt_hex.as_bytes())
|
|
|
|
.expect("could not feed random salt to hasher");
|
|
|
|
let token = hasher
|
|
|
|
.finish()
|
|
|
|
.expect("could not finish hashing password with random salt");
|
|
|
|
let token_hex = to_hex_str(&token);
|
2024-10-31 12:16:42 +00:00
|
|
|
|
2024-11-01 08:29:59 +00:00
|
|
|
Self::with_token(url, username, &token_hex, &salt_hex)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn with_token(url: &str, username: &str, token: &str, salt: &str) -> Result<Self, Error> {
|
2024-10-31 20:54:33 +00:00
|
|
|
let base_url = reqwest::Url::parse_with_params(
|
|
|
|
url,
|
|
|
|
&[
|
|
|
|
("u", username),
|
|
|
|
("t", token),
|
|
|
|
("s", salt),
|
|
|
|
("v", "1.16.1"),
|
|
|
|
("c", "eu.callcc.audrey"),
|
|
|
|
("f", "json"),
|
|
|
|
],
|
|
|
|
)
|
2024-11-01 08:29:59 +00:00
|
|
|
.map_err(Error::UrlParseError)?;
|
2024-10-31 20:54:33 +00:00
|
|
|
|
|
|
|
if base_url.scheme() != "http" && base_url.scheme() != "https" {
|
2024-11-01 08:29:59 +00:00
|
|
|
return Err(Error::OtherError("Url scheme is not HTTP(s)"));
|
2024-10-31 20:54:33 +00:00
|
|
|
}
|
|
|
|
|
2024-10-31 12:16:42 +00:00
|
|
|
Ok(Client {
|
|
|
|
client: reqwest::Client::builder()
|
|
|
|
.user_agent("audrey/linux") // Audrey.Const.user_agent
|
|
|
|
.build()?,
|
2024-10-31 20:54:33 +00:00
|
|
|
base_url,
|
2024-10-31 12:16:42 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-11-01 08:29:59 +00:00
|
|
|
async fn get<T: serde::de::DeserializeOwned>(
|
|
|
|
&self,
|
|
|
|
path: &[&str],
|
|
|
|
query: &[&str],
|
|
|
|
) -> Result<T, Error> {
|
2024-10-31 12:16:42 +00:00
|
|
|
let mut url = self.base_url.clone();
|
|
|
|
url.path_segments_mut()
|
2024-10-31 20:41:02 +00:00
|
|
|
// literally can't fail
|
|
|
|
.unwrap_or_else(|_| unsafe { std::hint::unreachable_unchecked() })
|
2024-11-01 08:29:59 +00:00
|
|
|
.extend(path);
|
2024-10-31 12:16:42 +00:00
|
|
|
|
|
|
|
self.client
|
|
|
|
.get(url)
|
2024-11-01 08:29:59 +00:00
|
|
|
.query(query)
|
2024-10-31 12:16:42 +00:00
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.error_for_status()?
|
2024-11-01 08:29:59 +00:00
|
|
|
.json::<schema::SubsonicResponseOuter<T>>()
|
2024-10-31 12:16:42 +00:00
|
|
|
.await?
|
2024-11-01 08:29:59 +00:00
|
|
|
.subsonic_response
|
2024-10-31 12:16:42 +00:00
|
|
|
.fixup()
|
|
|
|
}
|
2024-11-01 08:29:59 +00:00
|
|
|
|
|
|
|
pub async fn ping(&self) -> Result<(), Error> {
|
|
|
|
self.get(&["rest", "ping"], &[]).await
|
|
|
|
}
|
2024-10-31 12:16:42 +00:00
|
|
|
}
|