feat: Update typegen enums and the UI

This commit is contained in:
uttarayan21
2025-11-14 12:05:07 +05:30
parent 94105cdb83
commit df06190c14
9 changed files with 61481 additions and 60426 deletions

2
Cargo.lock generated
View File

@@ -123,7 +123,9 @@ dependencies = [
"serde_json", "serde_json",
"tap", "tap",
"thiserror 2.0.17", "thiserror 2.0.17",
"tokio",
"tokio-test", "tokio-test",
"toml 0.9.8",
] ]
[[package]] [[package]]

View File

@@ -10,6 +10,9 @@ serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145" serde_json = "1.0.145"
tap = "1.0.1" tap = "1.0.1"
thiserror = "2.0.17" thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["fs"] }
toml = "0.9.8"
[dev-dependencies] [dev-dependencies]
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] }
tokio-test = "0.4.4" tokio-test = "0.4.4"

16
api/examples/items.rs Normal file
View File

@@ -0,0 +1,16 @@
use api::*;
#[tokio::main]
pub async fn main() {
let config = std::fs::read_to_string("config.toml").expect("Config.toml");
let config: JellyfinConfig =
toml::from_str(&config).expect("Failed to parse config.toml");
let mut jellyfin = JellyfinClient::new(config);
jellyfin.authenticate_with_cached_token(".session").await.expect("Auth");
let items = jellyfin.raw_items().await.expect("Items");
std::fs::write(
"items.json",
serde_json::to_string_pretty(&items).expect("Serialize items"),
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
pub mod jellyfin; pub mod jellyfin;
use ::tap::*; use ::tap::*;
use reqwest::Method;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@@ -9,6 +10,8 @@ pub enum JellyfinApiError {
ReqwestError(#[from] reqwest::Error), ReqwestError(#[from] reqwest::Error),
#[error("Serialization/Deserialization error: {0}")] #[error("Serialization/Deserialization error: {0}")]
SerdeError(#[from] serde_json::Error), SerdeError(#[from] serde_json::Error),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
} }
type Result<T, E = JellyfinApiError> = std::result::Result<T, E>; type Result<T, E = JellyfinApiError> = std::result::Result<T, E>;
@@ -29,9 +32,9 @@ impl JellyfinClient {
} }
} }
pub fn save_token(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> { pub async fn save_token(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
if let Some(token) = &self.access_token { if let Some(token) = &self.access_token {
std::fs::write(path, token) tokio::fs::write(path, token).await
} else { } else {
Err(std::io::Error::new( Err(std::io::Error::new(
std::io::ErrorKind::Other, std::io::ErrorKind::Other,
@@ -40,17 +43,20 @@ impl JellyfinClient {
} }
} }
pub fn load_token(&mut self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> { pub async fn load_token(&mut self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
let token = std::fs::read_to_string(path)?; let token = tokio::fs::read_to_string(path).await?;
self.access_token = Some(token); self.access_token = Some(token);
Ok(()) Ok(())
} }
pub fn post_builder(&self, uri: impl AsRef<str>) -> reqwest::RequestBuilder { 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()); let url = format!("{}/{}", self.config.server_url.as_str(), uri.as_ref());
self.client.post(&url) self.client.request(method, &url)
.header("X-Emby-Authorization", format!("MediaBrowser Client=\"Jello\", Device=\"Jello\", DeviceId=\"{}\", Version=\"1.0.0\"", self.config.device_id)) .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| { .pipe(|builder| {
if let Some(token) = &self.access_token { if let Some(token) = &self.access_token {
builder.header("X-MediaBrowser-Token", token) builder.header("X-MediaBrowser-Token", token)
@@ -60,18 +66,18 @@ impl JellyfinClient {
}) })
} }
pub fn get_builder(&self, uri: impl AsRef<str>) -> reqwest::RequestBuilder { // pub fn get_builder(&self, uri: impl AsRef<str>) -> reqwest::RequestBuilder {
let url = format!("{}/{}", self.config.server_url.as_str(), uri.as_ref()); // let url = format!("{}/{}", self.config.server_url.as_str(), uri.as_ref());
self.client.get(&url) // self.client.get(&url)
.header("X-Emby-Authorization", format!("MediaBrowser Client=\"Jello\", Device=\"Jello\", DeviceId=\"{}\", Version=\"1.0.0\"", self.config.device_id)) // .header("X-Emby-Authorization", format!("MediaBrowser Client=\"Jello\", Device=\"Jello\", DeviceId=\"{}\", Version=\"1.0.0\"", self.config.device_id))
.pipe(|builder| { // .pipe(|builder| {
if let Some(token) = &self.access_token { // if let Some(token) = &self.access_token {
builder.header("X-MediaBrowser-Token", token) // builder.header("X-MediaBrowser-Token", token)
} else { // } else {
builder // builder
} // }
}) // })
} // }
pub async fn post<T: Serialize + ?Sized, U: serde::de::DeserializeOwned>( pub async fn post<T: Serialize + ?Sized, U: serde::de::DeserializeOwned>(
&self, &self,
@@ -79,7 +85,7 @@ impl JellyfinClient {
body: &T, body: &T,
) -> Result<U> { ) -> Result<U> {
let text = self let text = self
.post_builder(uri) .request_builder(reqwest::Method::POST, uri)
.json(body) .json(body)
.send() .send()
.await? .await?
@@ -92,7 +98,7 @@ impl JellyfinClient {
pub async fn get<U: serde::de::DeserializeOwned>(&self, uri: impl AsRef<str>) -> Result<U> { pub async fn get<U: serde::de::DeserializeOwned>(&self, uri: impl AsRef<str>) -> Result<U> {
let text = self let text = self
.get_builder(uri) .request_builder(reqwest::Method::GET, uri)
.send() .send()
.await? .await?
.error_for_status()? .error_for_status()?
@@ -103,25 +109,34 @@ impl JellyfinClient {
} }
pub async fn authenticate(&mut self) -> Result<jellyfin::AuthenticationResult> { pub async fn authenticate(&mut self) -> Result<jellyfin::AuthenticationResult> {
let out = self let auth_result: jellyfin::AuthenticationResult = self
.post_builder("Users/AuthenticateByName") .post(
.json(&jellyfin::AuthenticateUserByName { "Users/AuthenticateByName",
username: Some(self.config.username.clone()), &jellyfin::AuthenticateUserByName {
pw: Some(self.config.password.clone()), username: Some(self.config.username.clone()),
}) pw: Some(self.config.password.clone()),
.send() },
.await? )
.error_for_status()?
.text()
.await?; .await?;
let auth_result: jellyfin::AuthenticationResult = serde_json::from_str(&out)?;
self.access_token = auth_result.access_token.clone(); self.access_token = auth_result.access_token.clone();
Ok(auth_result) Ok(auth_result)
} }
async fn items(&self) -> Result<jellyfin::BaseItemDtoQueryResult> { pub async fn authenticate_with_cached_token(
&mut self,
path: impl AsRef<std::path::Path>,
) -> Result<()> {
let path = path.as_ref();
if !self.load_token(path).await.is_ok() {
self.authenticate().await?;
self.save_token(path).await?;
}
Ok(())
}
pub async fn raw_items(&self) -> Result<jellyfin::BaseItemDtoQueryResult> {
let text = &self let text = &self
.get_builder("/Items") .request_builder(Method::GET, "Items")
.send() .send()
.await? .await?
.error_for_status()? .error_for_status()?
@@ -130,8 +145,31 @@ impl JellyfinClient {
let out: jellyfin::BaseItemDtoQueryResult = serde_json::from_str(&text)?; let out: jellyfin::BaseItemDtoQueryResult = serde_json::from_str(&text)?;
Ok(out) Ok(out)
} }
pub async fn items(
&self,
root: impl Into<Option<String>>,
) -> 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 trait Item {
// fn id(&self) -> &str;
// fn name(&self) -> &str;
// fn type_(&self) -> jellyfin::BaseItemKind;
// fn media_type(&self) -> &str;
// }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JellyfinConfig { pub struct JellyfinConfig {
pub username: String, pub username: String,

120747
jellyfin.json

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,8 @@
mod errors; mod errors;
mod ui;
use api::{JellyfinClient, JellyfinConfig}; use api::{JellyfinClient, JellyfinConfig};
use errors::*; use errors::*;
use gpui::{
App, Application, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div,
prelude::*, px, rgb, size,
};
#[tokio::main] #[tokio::main]
pub async fn main() -> Result<()> { pub async fn main() -> Result<()> {
dotenvy::dotenv() dotenvy::dotenv()
@@ -22,17 +18,12 @@ pub async fn main() -> Result<()> {
"jello".to_string(), "jello".to_string(),
); );
let mut jellyfin = api::JellyfinClient::new(config); let mut jellyfin = api::JellyfinClient::new(config);
authenticate(&mut jellyfin).await?; jellyfin
.authenticate_with_cached_token(".session")
.await
.change_context(Error)?;
ui::ui(jellyfin);
Ok(()) Ok(())
} }
pub async fn authenticate(client: &mut JellyfinClient) -> Result<()> {
if std::path::PathBuf::from(".session").exists() {
client.load_token(".session").change_context(Error)?;
} else {
client.authenticate().await.change_context(Error)?;
client.save_token(".session").change_context(Error)?;
}
Ok(())
}

165
src/ui.rs
View File

@@ -0,0 +1,165 @@
mod movies;
mod player;
mod series;
use std::collections::BTreeMap;
use gpui::{
App, Application, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, actions,
div, prelude::*, px, rgb, size,
};
#[derive(Clone, Debug)]
pub struct AppState {
pub title: SharedString,
pub items: BTreeMap<SharedString, Item>,
pub current_item: Option<SharedString>,
pub errors: Vec<String>,
pub jellyfin_client: api::JellyfinClient,
}
#[derive(Clone, Debug)]
pub struct Item {
pub id: SharedString,
pub name: SharedString,
pub item_type: SharedString,
pub media_type: SharedString,
}
impl Render for AppState {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.size_full()
.justify_center()
.text_color(rgb(0xffffff))
.child(Self::header())
.child(Self::body(self, window, cx))
.child(Self::footer())
}
}
actions!(jello_actions, [OpenItem, OnLoadItem,]);
impl AppState {
fn new(title: impl AsRef<str>, jellyfin_client: api::JellyfinClient) -> Self {
AppState {
title: SharedString::new(title.as_ref()),
items: BTreeMap::new(),
current_item: None,
errors: Vec::new(),
jellyfin_client,
}
}
// fn on_open_item(&mut self, _: &OpenItem, _: &mut Window, cx: &mut Context<Self>) {
// self.current_item = Some(item.0.clone());
// }
fn header() -> impl IntoElement {
div()
.flex()
.flex_row()
.w_full()
.justify_end()
.h_20()
.border_10()
.bg(rgb(0x333333))
.child(Self::button("Refresh"))
}
fn footer() -> impl IntoElement {
div().flex().flex_row().w_full().h_20().bg(rgb(0x333333))
}
fn body(&mut self, window: &mut Window, cx: &mut Context<AppState>) -> impl IntoElement {
div()
.flex()
.flex_row()
.size_full()
.child(Self::content(self, window, cx))
.child(Self::sidebar(self, window, cx))
}
fn button(label: &str) -> impl IntoElement {
div()
.flex()
.justify_center()
.items_center()
.bg(rgb(0xff00ff))
.text_color(rgb(0xffffff))
.border_5()
.rounded_lg()
.child(label.to_string())
}
fn content(&mut self, window: &mut Window, cx: &mut Context<AppState>) -> impl IntoElement {
div()
.debug_below()
.w_3_4()
// .flex()
// .flex_wrap()
.bg(rgb(0x111111))
.justify_start()
.items_start()
.overflow_hidden()
.child(
div()
.size_full()
.flex()
.flex_wrap()
.justify_start()
.items_start()
.content_start()
.gap_y_10()
.gap_x_10()
.border_t_10()
.p_5()
.child(Self::card())
.child(Self::card())
.child(Self::card())
.child(Self::card())
.child(Self::card())
.child(Self::card())
.child(Self::card())
.child(Self::card())
.child(Self::card()),
)
}
fn sidebar(&mut self, window: &mut Window, cx: &mut Context<AppState>) -> impl IntoElement {
div()
.flex()
.flex_col()
.w_1_4()
.min_w_1_6()
.bg(rgb(0x222222))
.child(div().size_full().bg(gpui::yellow()))
}
fn card() -> impl IntoElement {
div()
.flex()
.flex_col()
.w_48()
.h_64()
.p_10()
.bg(rgb(0xff00ff))
.rounded_lg()
}
}
pub fn ui(jellyfin_client: api::JellyfinClient) {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| AppState::new("Jello Media Browser", jellyfin_client)),
)
.expect("Failed to open window");
})
}

View File

@@ -185,14 +185,20 @@ fn main() {
.expect("Possible oneOf") .expect("Possible oneOf")
.iter() .iter()
.map(|variant| { .map(|variant| {
// let variant_name = modify_keyword(&ref_name.to_pascal_case()); let og_variant = variant.clone();
syn::Ident::new(&variant.to_pascal_case(), proc_macro2::Span::call_site()) let name =
syn::Ident::new(&variant.to_pascal_case(), proc_macro2::Span::call_site());
syn::parse_quote! {
#[serde(rename = #og_variant)]
#name
}
}) })
.collect::<Vec<syn::Ident>>(); .collect::<Vec<syn::Variant>>();
let key = modify_keyword(key); let key = modify_keyword(key);
let key = syn::Ident::new(&key.to_pascal_case(), proc_macro2::Span::call_site()); let key = syn::Ident::new(&key.to_pascal_case(), proc_macro2::Span::call_site());
let tokens = quote::quote! { let tokens = quote::quote! {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum #key { pub enum #key {
#(#variants),* #(#variants),*
} }