feat(api): enhance Jellyfin client with async requests and token mgmt

This commit is contained in:
uttarayan21
2025-11-13 16:11:08 +05:30
parent ffd5562ed3
commit 5425031994
5 changed files with 238 additions and 8 deletions

View File

@@ -1,9 +1,14 @@
pub mod jellyfin;
use ::tap::*;
use serde::{Deserialize, Serialize};
#[derive(thiserror::Error, Debug)]
pub enum JellyfinApiError {
#[error("Jellyfin API error: {0}")]
ReqwestError(#[from] reqwest::Error),
#[error("Serialization/Deserialization error: {0}")]
SerdeError(#[from] serde_json::Error),
}
type Result<T, E = JellyfinApiError> = std::result::Result<T, E>;
@@ -11,6 +16,7 @@ type Result<T, E = JellyfinApiError> = std::result::Result<T, E>;
#[derive(Debug, Clone)]
pub struct JellyfinClient {
client: reqwest::Client,
access_token: Option<String>,
config: JellyfinConfig,
}
@@ -18,26 +24,111 @@ impl JellyfinClient {
pub fn new(config: JellyfinConfig) -> Self {
JellyfinClient {
client: reqwest::Client::new(),
access_token: None,
config,
}
}
pub fn post(&self, uri: impl AsRef<str>) -> reqwest::RequestBuilder {
pub fn save_token(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
if let Some(token) = &self.access_token {
std::fs::write(path, token)
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"No access token to save",
))
}
}
pub fn load_token(&mut self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
let token = std::fs::read_to_string(path)?;
self.access_token = Some(token);
Ok(())
}
pub fn post_builder(&self, uri: impl AsRef<str>) -> reqwest::RequestBuilder {
let url = format!("{}/{}", self.config.server_url.as_str(), uri.as_ref());
self.client.post(&url)
.header("X-Emby-Authorization", format!("MediaBrowser Client=\"Jello\", Device=\"Jello\", DeviceId=\"{}\", Version=\"1.0.0\"", self.config.device_id))
.header("Content-Type", "application/json")
.pipe(|builder| {
if let Some(token) = &self.access_token {
builder.header("X-MediaBrowser-Token", token)
} else {
builder
}
})
}
pub async fn authenticate(&mut self) -> Result<()> {
self.post("Users/AuthenticateByName")
pub fn get_builder(&self, uri: impl AsRef<str>) -> reqwest::RequestBuilder {
let url = format!("{}/{}", self.config.server_url.as_str(), uri.as_ref());
self.client.get(&url)
.header("X-Emby-Authorization", format!("MediaBrowser Client=\"Jello\", Device=\"Jello\", DeviceId=\"{}\", Version=\"1.0.0\"", self.config.device_id))
.pipe(|builder| {
if let Some(token) = &self.access_token {
builder.header("X-MediaBrowser-Token", token)
} else {
builder
}
})
}
pub async fn post<T: Serialize + ?Sized, U: serde::de::DeserializeOwned>(
&self,
uri: impl AsRef<str>,
body: &T,
) -> Result<U> {
let text = self
.post_builder(uri)
.json(body)
.send()
.await?
.error_for_status()?
.text()
.await?;
let out: U = serde_json::from_str(&text)?;
Ok(out)
}
pub async fn get<U: serde::de::DeserializeOwned>(&self, uri: impl AsRef<str>) -> Result<U> {
let text = self
.get_builder(uri)
.send()
.await?
.error_for_status()?
.text()
.await?;
let out: U = serde_json::from_str(&text)?;
Ok(out)
}
pub async fn authenticate(&mut self) -> Result<jellyfin::AuthenticationResult> {
let out = self
.post_builder("Users/AuthenticateByName")
.json(&jellyfin::AuthenticateUserByName {
username: Some(self.config.username.clone()),
pw: Some(self.config.password.clone()),
})
.send()
.await?
.error_for_status()?
.text()
.await?;
Ok(())
let auth_result: jellyfin::AuthenticationResult = serde_json::from_str(&out)?;
self.access_token = auth_result.access_token.clone();
Ok(auth_result)
}
async fn items(&self) -> Result<jellyfin::BaseItemDtoQueryResult> {
let text = &self
.get_builder("/Items")
.send()
.await?
.error_for_status()?
.text()
.await?;
let out: jellyfin::BaseItemDtoQueryResult = serde_json::from_str(&text)?;
Ok(out)
}
}
@@ -48,3 +139,33 @@ pub struct JellyfinConfig {
pub server_url: iref::IriBuf,
pub device_id: String,
}
impl JellyfinConfig {
pub fn new(
username: String,
password: String,
server_url: impl AsRef<str>,
device_id: String,
) -> Self {
JellyfinConfig {
username,
password,
server_url: iref::IriBuf::new(server_url.as_ref().into())
.expect("Failed to parse server URL"),
device_id,
}
}
}
#[test]
fn test_client_authenticate() {
let config = JellyfinConfig {
username: "servius".to_string(),
password: "nfz6yqr_NZD1nxk!faj".to_string(),
server_url: iref::IriBuf::new("https://jellyfin.tsuba.darksailor.dev".into()).unwrap(),
device_id: "testdeviceid".to_string(),
};
let mut client = JellyfinClient::new(config);
let auth_result = tokio_test::block_on(client.authenticate());
assert!(auth_result.is_ok());
}