feat: Update typegen enums and the UI
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -123,7 +123,9 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tap",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tokio-test",
|
||||
"toml 0.9.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -10,6 +10,9 @@ serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
tap = "1.0.1"
|
||||
thiserror = "2.0.17"
|
||||
tokio = { version = "1.48.0", features = ["fs"] }
|
||||
toml = "0.9.8"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] }
|
||||
tokio-test = "0.4.4"
|
||||
|
||||
16
api/examples/items.rs
Normal file
16
api/examples/items.rs
Normal 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
106
api/src/lib.rs
106
api/src/lib.rs
@@ -1,6 +1,7 @@
|
||||
pub mod jellyfin;
|
||||
|
||||
use ::tap::*;
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
@@ -9,6 +10,8 @@ pub enum JellyfinApiError {
|
||||
ReqwestError(#[from] reqwest::Error),
|
||||
#[error("Serialization/Deserialization error: {0}")]
|
||||
SerdeError(#[from] serde_json::Error),
|
||||
#[error("IO error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
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 {
|
||||
std::fs::write(path, token)
|
||||
tokio::fs::write(path, token).await
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
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<()> {
|
||||
let token = std::fs::read_to_string(path)?;
|
||||
pub async fn load_token(&mut self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
|
||||
let token = tokio::fs::read_to_string(path).await?;
|
||||
self.access_token = Some(token);
|
||||
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());
|
||||
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("Content-Type", "application/json")
|
||||
.pipe(|builder| {
|
||||
if let Some(token) = &self.access_token {
|
||||
builder.header("X-MediaBrowser-Token", token)
|
||||
@@ -60,18 +66,18 @@ impl JellyfinClient {
|
||||
})
|
||||
}
|
||||
|
||||
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 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,
|
||||
@@ -79,7 +85,7 @@ impl JellyfinClient {
|
||||
body: &T,
|
||||
) -> Result<U> {
|
||||
let text = self
|
||||
.post_builder(uri)
|
||||
.request_builder(reqwest::Method::POST, uri)
|
||||
.json(body)
|
||||
.send()
|
||||
.await?
|
||||
@@ -92,7 +98,7 @@ impl JellyfinClient {
|
||||
|
||||
pub async fn get<U: serde::de::DeserializeOwned>(&self, uri: impl AsRef<str>) -> Result<U> {
|
||||
let text = self
|
||||
.get_builder(uri)
|
||||
.request_builder(reqwest::Method::GET, uri)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
@@ -103,25 +109,34 @@ impl JellyfinClient {
|
||||
}
|
||||
|
||||
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()
|
||||
let auth_result: jellyfin::AuthenticationResult = self
|
||||
.post(
|
||||
"Users/AuthenticateByName",
|
||||
&jellyfin::AuthenticateUserByName {
|
||||
username: Some(self.config.username.clone()),
|
||||
pw: Some(self.config.password.clone()),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
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> {
|
||||
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
|
||||
.get_builder("/Items")
|
||||
.request_builder(Method::GET, "Items")
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
@@ -130,8 +145,31 @@ impl JellyfinClient {
|
||||
let out: jellyfin::BaseItemDtoQueryResult = serde_json::from_str(&text)?;
|
||||
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)]
|
||||
pub struct JellyfinConfig {
|
||||
pub username: String,
|
||||
|
||||
120747
jellyfin.json
120747
jellyfin.json
File diff suppressed because it is too large
Load Diff
23
src/main.rs
23
src/main.rs
@@ -1,12 +1,8 @@
|
||||
mod errors;
|
||||
mod ui;
|
||||
use api::{JellyfinClient, JellyfinConfig};
|
||||
use errors::*;
|
||||
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div,
|
||||
prelude::*, px, rgb, size,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn main() -> Result<()> {
|
||||
dotenvy::dotenv()
|
||||
@@ -22,17 +18,12 @@ pub async fn main() -> Result<()> {
|
||||
"jello".to_string(),
|
||||
);
|
||||
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(())
|
||||
}
|
||||
|
||||
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
165
src/ui.rs
@@ -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");
|
||||
})
|
||||
}
|
||||
|
||||
@@ -185,14 +185,20 @@ fn main() {
|
||||
.expect("Possible oneOf")
|
||||
.iter()
|
||||
.map(|variant| {
|
||||
// let variant_name = modify_keyword(&ref_name.to_pascal_case());
|
||||
syn::Ident::new(&variant.to_pascal_case(), proc_macro2::Span::call_site())
|
||||
let og_variant = variant.clone();
|
||||
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 = syn::Ident::new(&key.to_pascal_case(), proc_macro2::Span::call_site());
|
||||
let tokens = quote::quote! {
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub enum #key {
|
||||
#(#variants),*
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user