more subsonic client bullshit
This commit is contained in:
parent
2e4778f2f9
commit
68c256488d
5 changed files with 922 additions and 43 deletions
815
Cargo.lock
generated
815
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
|
|
19
src/main.rs
19
src/main.rs
|
@ -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()
|
||||
}
|
||||
|
|
102
src/subsonic.rs
102
src/subsonic.rs
|
@ -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
25
src/subsonic/schema.rs
Normal 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,
|
||||
}
|
Loading…
Reference in a new issue