refactor(workspace): move crates under crates/

This commit is contained in:
2026-01-30 02:09:10 +05:30
parent 97db96b105
commit 72bf38a7ff
44 changed files with 9 additions and 128 deletions

7832
crates/api/src/jellyfin.rs Normal file

File diff suppressed because it is too large Load Diff

270
crates/api/src/lib.rs Normal file
View File

@@ -0,0 +1,270 @@
pub mod jellyfin;
use std::sync::Arc;
use ::tap::*;
use reqwest::{Method, header::InvalidHeaderValue};
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),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Unknown Jellyfin API error")]
InvalidHeader(#[from] InvalidHeaderValue),
#[error("Unknown Jellyfin API error")]
Unknown,
}
type Result<T, E = JellyfinApiError> = std::result::Result<T, E>;
#[derive(Debug, Clone)]
pub struct JellyfinClient {
client: reqwest::Client,
access_token: Option<Arc<str>>,
pub config: Arc<JellyfinConfig>,
}
impl JellyfinClient {
pub async fn authenticate(
username: impl AsRef<str>,
password: impl AsRef<str>,
config: JellyfinConfig,
) -> Result<Self> {
let url = format!("{}/Users/AuthenticateByName", config.server_url);
let client = reqwest::Client::new();
let token = client
.post(url)
.json(&jellyfin::AuthenticateUserByName {
username: Some(username.as_ref().to_string()),
pw: Some(password.as_ref().to_string()),
})
.send()
.await?
.error_for_status()?
.json::<jellyfin::AuthenticationResult>()
.await?
.access_token
.ok_or_else(|| std::io::Error::other("No field access_token in auth response"))?;
Self::pre_authenticated(token, config)
}
pub fn pre_authenticated(token: impl AsRef<str>, config: JellyfinConfig) -> Result<Self> {
let auth_header = core::iter::once((
reqwest::header::HeaderName::from_static("x-emby-authorization"),
reqwest::header::HeaderValue::from_str(&format!(
"MediaBrowser Client=\"{}\", Device=\"{}\", DeviceId=\"{}\", Version=\"{}\"",
config.client_name, config.device_name, config.device_id, config.version
))?,
))
.collect();
let client = reqwest::Client::builder()
.default_headers(auth_header)
.build()?;
Ok(Self {
client,
access_token: Some(token.as_ref().to_string().into()),
config: Arc::new(config),
})
}
pub fn new_with_config(config: JellyfinConfig) -> Self {
JellyfinClient {
client: reqwest::Client::new(),
access_token: None,
config: Arc::new(config),
}
}
pub async fn save_token(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
if let Some(token) = &self.access_token {
tokio::fs::write(path, &**token).await
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"No access token to save",
))
}
}
pub async fn load_token(
&mut self,
path: impl AsRef<std::path::Path>,
) -> std::io::Result<String> {
let token = tokio::fs::read_to_string(path).await?;
self.access_token = Some(token.clone().into());
Ok(token)
}
pub fn set_token(&mut self, token: impl AsRef<str>) {
self.access_token = Some(token.as_ref().into());
}
pub fn request_builder(
&self,
method: reqwest::Method,
uri: impl AsRef<str>,
) -> reqwest::RequestBuilder {
let url = format!("{}/{}", self.config.server_url.as_str(), uri.as_ref());
self.client.request(method, &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 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
.request_builder(reqwest::Method::POST, 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
.request_builder(reqwest::Method::GET, uri)
.send()
.await?
.error_for_status()?
.text()
.await?;
let out: U = serde_json::from_str(&text)?;
Ok(out)
}
pub async fn raw_items(&self) -> Result<jellyfin::BaseItemDtoQueryResult> {
let text = &self
.request_builder(Method::GET, "Items")
.send()
.await?
.error_for_status()?
.text()
.await?;
let out: jellyfin::BaseItemDtoQueryResult = serde_json::from_str(&text)?;
Ok(out)
}
pub async fn items(
&self,
root: impl Into<Option<uuid::Uuid>>,
) -> Result<Vec<jellyfin::BaseItemDto>> {
let text = &self
.request_builder(Method::GET, "Items")
.query(&[("parentId", root.into())])
.send()
.await?
.error_for_status()?
.text()
.await?;
let out: jellyfin::BaseItemDtoQueryResult = serde_json::from_str(&text)?;
Ok(out.items)
}
pub async fn search(&self, query: impl AsRef<str>) -> Result<Vec<jellyfin::BaseItemDto>> {
let text = &self
.request_builder(Method::GET, "Items/Search")
.query(&[("searchTerm", query.as_ref()), ("recursive", "true")])
.send()
.await?
.error_for_status()?
.text()
.await?;
let out: jellyfin::BaseItemDtoQueryResult = serde_json::from_str(&text)?;
Ok(out.items)
}
pub async fn thumbnail(
&self,
item: uuid::Uuid,
image_type: jellyfin::ImageType,
) -> Result<bytes::Bytes> {
let uri = format!(
"Items/{}/Images/{}",
item,
serde_json::to_string(&image_type).expect("Failed to serialize image type")
);
let bytes = self
.request_builder(Method::GET, uri)
.send()
.await?
.error_for_status()?
.bytes()
.await?;
Ok(bytes)
}
pub async fn playback_info(&self, item: uuid::Uuid) -> Result<jellyfin::PlaybackInfoDto> {
let uri = format!("Items/{}/PlaybackInfo", item);
let text = &self
.request_builder(Method::GET, uri)
.send()
.await?
.error_for_status()?
.text()
.await?;
let out: jellyfin::PlaybackInfoDto = serde_json::from_str(&text)?;
Ok(out)
}
pub async fn user_data(&self, item: uuid::Uuid) -> Result<jellyfin::UserItemDataDto> {
let uri = format!("UserItems/{}/UserData", item);
let text = &self
.request_builder(Method::GET, uri)
.send()
.await?
.error_for_status()?
.text()
.await?;
let out: jellyfin::UserItemDataDto = serde_json::from_str(&text)?;
Ok(out)
}
pub fn stream_url(&self, item: uuid::Uuid) -> Result<url::Url> {
let stream_url = format!(
"{}/Videos/{}/stream?static=true",
self.config.server_url.as_str(),
item,
);
Ok(url::Url::parse(&stream_url).expect("Failed to parse stream URL"))
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JellyfinConfig {
pub server_url: iref::IriBuf,
pub device_id: String,
pub device_name: String,
pub client_name: String,
pub version: String,
}