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"
|
async-channel = "2.3.1"
|
||||||
gettext-rs = { version = "0.7.2", features = ["gettext-system"] }
|
gettext-rs = { version = "0.7.2", features = ["gettext-system"] }
|
||||||
gtk = { version = "0.9.2", package = "gtk4", features = ["v4_16"] }
|
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"] }
|
reqwest = { version = "0.12.9", features = ["json"] }
|
||||||
serde = { version = "1.0.214", features = ["derive"] }
|
serde = { version = "1.0.214", features = ["derive"] }
|
||||||
serde_json = "1.0.132"
|
|
||||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||||
|
url = "2.5.2"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
bindgen = "0.70.1"
|
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::LcAll, "");
|
||||||
setlocale(LocaleCategory::LcNumeric, "C.UTF-8");
|
setlocale(LocaleCategory::LcNumeric, "C.UTF-8");
|
||||||
let app = Application::new();
|
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()
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
// FIXME: can't even return url's ParseError directly.........
|
UrlParseError(url::ParseError),
|
||||||
UrlParseError(String),
|
|
||||||
ReqwestError(reqwest::Error),
|
ReqwestError(reqwest::Error),
|
||||||
SubsonicError(SubsonicError),
|
SubsonicError(schema::Error),
|
||||||
|
OtherError(&'static str),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<reqwest::Error> for Error {
|
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 {
|
pub struct Client {
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
base_url: reqwest::Url,
|
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 {
|
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(
|
let base_url = reqwest::Url::parse_with_params(
|
||||||
url,
|
url,
|
||||||
&[
|
&[
|
||||||
|
@ -60,10 +73,10 @@ impl Client {
|
||||||
("f", "json"),
|
("f", "json"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.map_err(|err| Error::UrlParseError(err.to_string()))?;
|
.map_err(Error::UrlParseError)?;
|
||||||
|
|
||||||
if base_url.scheme() != "http" && base_url.scheme() != "https" {
|
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 {
|
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();
|
let mut url = self.base_url.clone();
|
||||||
url.path_segments_mut()
|
url.path_segments_mut()
|
||||||
// literally can't fail
|
// literally can't fail
|
||||||
.unwrap_or_else(|_| unsafe { std::hint::unreachable_unchecked() })
|
.unwrap_or_else(|_| unsafe { std::hint::unreachable_unchecked() })
|
||||||
.extend(&["rest", "ping"]);
|
.extend(path);
|
||||||
|
|
||||||
self.client
|
self.client
|
||||||
.get(url)
|
.get(url)
|
||||||
|
.query(query)
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
.error_for_status()?
|
.error_for_status()?
|
||||||
.json::<SubsonicResponse<()>>()
|
.json::<schema::SubsonicResponseOuter<T>>()
|
||||||
.await?
|
.await?
|
||||||
.inner
|
.subsonic_response
|
||||||
.fixup()
|
.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