more subsonic client bullshit

This commit is contained in:
Erica Z 2024-11-01 09:29:59 +01:00
parent 2e4778f2f9
commit 68c256488d
5 changed files with 922 additions and 43 deletions

815
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,10 +8,12 @@ adw = { version = "0.7.0", package = "libadwaita", features = ["v1_6"] }
async-channel = "2.3.1"
gettext-rs = { version = "0.7.2", features = ["gettext-system"] }
gtk = { version = "0.9.2", package = "gtk4", features = ["v4_16"] }
oo7 = "0.3.3"
openssl = "0.10.68"
reqwest = { version = "0.12.9", features = ["json"] }
serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.132"
tokio = { version = "1", features = ["rt-multi-thread"] }
url = "2.5.2"
[build-dependencies]
bindgen = "0.70.1"

View file

@ -46,5 +46,24 @@ fn main() -> glib::ExitCode {
setlocale(LocaleCategory::LcAll, "");
setlocale(LocaleCategory::LcNumeric, "C.UTF-8");
let app = Application::new();
// smol test for the subsonic client
runtime().spawn(async {
let keyring = oo7::Keyring::new().await.unwrap();
let attributes = vec![("xdg:schema", "eu.callcc.audrey")];
let items = keyring.search_items(&attributes).await.unwrap();
if !items.is_empty() {
let item = &items[0];
let attributes = item.attributes().await.unwrap();
let client = subsonic::Client::with_password(
&attributes["server-url"],
&attributes["username"],
&item.secret().await.unwrap(),
)
.unwrap();
client.ping().await.unwrap();
}
});
app.run()
}

View file

@ -1,9 +1,20 @@
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)),
}
}
}
#[derive(Debug)]
pub enum Error {
// FIXME: can't even return url's ParseError directly.........
UrlParseError(String),
UrlParseError(url::ParseError),
ReqwestError(reqwest::Error),
SubsonicError(SubsonicError),
SubsonicError(schema::Error),
OtherError(&'static str),
}
impl From<reqwest::Error> for Error {
@ -12,43 +23,45 @@ impl From<reqwest::Error> for Error {
}
}
#[derive(Debug, serde::Deserialize)]
pub struct SubsonicError {
pub code: u32,
pub message: String,
}
#[derive(Debug, serde::Deserialize)]
pub struct SubsonicResponse<T> {
#[serde(rename = "subsonic-response")]
inner: SubsonicResponseInner<T>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(tag = "status")]
pub enum SubsonicResponseInner<T> {
#[serde(rename = "ok")]
Ok(T),
#[serde(rename = "failed")]
Failed { error: SubsonicError },
}
impl<T> SubsonicResponseInner<T> {
fn fixup(self) -> Result<T, Error> {
match self {
Self::Ok(t) => Ok(t),
Self::Failed { error } => Err(Error::SubsonicError(error)),
}
}
}
pub struct Client {
client: reqwest::Client,
base_url: reqwest::Url,
}
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
}
impl Client {
pub fn new(url: &str, username: &str, token: &str, salt: &str) -> Result<Self, Error> {
pub fn with_password(url: &str, username: &str, password: &[u8]) -> Result<Self, Error> {
const SALT_BYTES: usize = 8;
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);
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);
Self::with_token(url, username, &token_hex, &salt_hex)
}
pub fn with_token(url: &str, username: &str, token: &str, salt: &str) -> Result<Self, Error> {
let base_url = reqwest::Url::parse_with_params(
url,
&[
@ -60,10 +73,10 @@ impl Client {
("f", "json"),
],
)
.map_err(|err| Error::UrlParseError(err.to_string()))?;
.map_err(Error::UrlParseError)?;
if base_url.scheme() != "http" && base_url.scheme() != "https" {
return Err(Error::UrlParseError("Url scheme is not HTTP(s)".into()));
return Err(Error::OtherError("Url scheme is not HTTP(s)"));
}
Ok(Client {
@ -74,21 +87,30 @@ impl Client {
})
}
pub async fn ping(&self) -> Result<(), Error> {
async fn get<T: serde::de::DeserializeOwned>(
&self,
path: &[&str],
query: &[&str],
) -> Result<T, Error> {
let mut url = self.base_url.clone();
url.path_segments_mut()
// literally can't fail
.unwrap_or_else(|_| unsafe { std::hint::unreachable_unchecked() })
.extend(&["rest", "ping"]);
.extend(path);
self.client
.get(url)
.query(query)
.send()
.await?
.error_for_status()?
.json::<SubsonicResponse<()>>()
.json::<schema::SubsonicResponseOuter<T>>()
.await?
.inner
.subsonic_response
.fixup()
}
pub async fn ping(&self) -> Result<(), Error> {
self.get(&["rest", "ping"], &[]).await
}
}

25
src/subsonic/schema.rs Normal file
View file

@ -0,0 +1,25 @@
use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct SubsonicResponseOuter<T> {
pub subsonic_response: SubsonicResponse<T>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "status")]
pub enum SubsonicResponse<T> {
Ok {
#[serde(flatten)]
inner: T,
},
Failed {
error: Error,
},
}
#[derive(Debug, Deserialize)]
pub struct Error {
pub code: u32,
pub message: String,
}