audrey/src/subsonic.rs

128 lines
3.8 KiB
Rust
Raw Normal View History

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],
2024-11-01 08:43:55 +00:00
query: &[(&str, &str)],
2024-11-01 08:29:59 +00:00
) -> 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-11-01 08:43:55 +00:00
pub async fn get_random_songs(&self, size: u32) -> Result<Vec<schema::Child>, Error> {
self.get::<schema::RandomSongsOuter>(
&["rest", "getRandomSongs"],
&[("size", &size.to_string())],
)
.await
.map(|response| response.random_songs.song)
}
2024-10-31 12:16:42 +00:00
}