feat: Added typegen for jellyfin structs and enums

This commit is contained in:
uttarayan21
2025-11-13 14:57:00 +05:30
parent 07027d6121
commit ffd5562ed3
9 changed files with 9501 additions and 117 deletions

7
Cargo.lock generated
View File

@@ -4106,9 +4106,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.94" version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -5934,7 +5934,10 @@ dependencies = [
name = "typegen" name = "typegen"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"heck 0.5.0",
"indexmap", "indexmap",
"prettyplease",
"proc-macro2",
"quote", "quote",
"serde", "serde",
"serde_json", "serde_json",

View File

@@ -5,7 +5,7 @@ edition = "2024"
[dependencies] [dependencies]
iref = { version = "3.2.2", features = ["serde"] } iref = { version = "3.2.2", features = ["serde"] }
reqwest = "0.12.24" reqwest = { version = "0.12.24", features = ["json"] }
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145" serde_json = "1.0.145"
thiserror = "2.0.17" thiserror = "2.0.17"

5414
api/src/jellyfin.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
pub mod jellyfin;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum JellyfinApiError { pub enum JellyfinApiError {
@@ -28,15 +29,16 @@ impl JellyfinClient {
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
} }
// pub async fn authenticate(&mut self) -> Result<()> { pub async fn authenticate(&mut self) -> Result<()> {
// self.post("Users/AuthenticateByName") self.post("Users/AuthenticateByName")
// .json(AuthenticateUserByName { .json(&jellyfin::AuthenticateUserByName {
// username: self.config.username.clone(), username: Some(self.config.username.clone()),
// pw: self.config.password.clone(), pw: Some(self.config.password.clone()),
// }) })
// .send() .send()
// .await .await?;
// } Ok(())
}
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@@ -46,87 +48,3 @@ pub struct JellyfinConfig {
pub server_url: iref::IriBuf, pub server_url: iref::IriBuf,
pub device_id: String, pub device_id: String,
} }
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct AuthenticationResult {
user: UserDto,
session_info: Option<SessionInfoDto>,
access_token: Option<String>,
server_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct UserDto {}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct SerssionInfoDto {}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct AuthenticateUserByName {
username: String,
pw: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct QuickConnectDto {
secret: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
struct User {
id: String,
configuration: Configuration,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
struct Configuration {
audio_language_preference: Option<String>,
play_default_audio_track: bool,
subtitle_language_preference: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
struct Items {
items: Vec<MediaItem>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct MediaItem {
// #[serde(rename = "Id")]
pub id: String,
// #[serde(rename = "Name")]
pub name: String,
// #[serde(rename = "Type")]
pub type_: String,
// #[serde(rename = "Path")]
pub path: Option<String>,
// #[serde(rename = "CollectionType")]
pub collection_type: Option<String>,
// #[serde(rename = "ProductionYear")]
pub year: Option<i32>,
// #[serde(rename = "Overview")]
pub overview: Option<String>,
// #[serde(rename = "CommunityRating")]
pub imdb_rating: Option<f32>,
// #[serde(rename = "CriticRating")]
pub critic_rating: Option<i32>,
// #[serde(rename = "RunTimeTicks")]
pub runtime_ticks: Option<i64>,
// #[serde(rename = "SeriesId")]
pub series_id: Option<String>,
// #[serde(rename = "SeriesName")]
pub series_name: Option<String>,
// #[serde(rename = "ParentIndexNumber")]
pub parent_index_number: Option<i64>,
// #[serde(rename = "IndexNumber")]
pub index_number: Option<i64>,
}

File diff suppressed because it is too large Load Diff

24
flake.lock generated
View File

@@ -3,11 +3,11 @@
"advisory-db": { "advisory-db": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1761631338, "lastModified": 1762774274,
"narHash": "sha256-F6dlUrDiShwhMfPR+WoVmaQguGdEwjW9SI4nKlkay7c=", "narHash": "sha256-tigj2sBL6S7zmjpt5JdXtvtGrClvja+/LAnmpU6+MV4=",
"owner": "rustsec", "owner": "rustsec",
"repo": "advisory-db", "repo": "advisory-db",
"rev": "2e45336771e36acf5bcefe7c99280ab214719707", "rev": "df17e8c0d170b71c0a4cca3f165c30030a526060",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -18,11 +18,11 @@
}, },
"crane": { "crane": {
"locked": { "locked": {
"lastModified": 1760924934, "lastModified": 1762538466,
"narHash": "sha256-tuuqY5aU7cUkR71sO2TraVKK2boYrdW3gCSXUkF4i44=", "narHash": "sha256-8zrIPl6J+wLm9MH5ksHcW7BUHo7jSNOu0/hA0ohOOaM=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "c6b4d5308293d0d04fcfeee92705017537cad02f", "rev": "0cea393fffb39575c46b7a0318386467272182fe",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -106,11 +106,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1761373498, "lastModified": 1762844143,
"narHash": "sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c=", "narHash": "sha256-SlybxLZ1/e4T2lb1czEtWVzDCVSTvk9WLwGhmxFmBxI=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce", "rev": "9da7f1cf7f8a6e2a7cb3001b048546c92a8258b4",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -138,11 +138,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1761705569, "lastModified": 1763001554,
"narHash": "sha256-dqljv29XldlKvdTwFw8GkxOQHrz3/13yxdwHW8+nzBI=", "narHash": "sha256-wsfhRTuxu6f06RMmP4JWcq3wWRlmYtQaJZ6b3f+EJ94=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "bca7909cb02f5139e0a490b0ff4bae775ea3ebf6", "rev": "315d97eb753cee8e1aa039a5e622b84d32a454bb",
"type": "github" "type": "github"
}, },
"original": { "original": {

6
justfile Normal file
View File

@@ -0,0 +1,6 @@
typegen:
@echo "Generating jellyfin type definitions..."
cd typegen && cargo run
cp typegen/jellyfin.rs api/src/jellyfin.rs
rm typegen/jellyfin.rs

View File

@@ -4,7 +4,10 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
heck = "0.5.0"
indexmap = { version = "2.12.0", features = ["serde"] } indexmap = { version = "2.12.0", features = ["serde"] }
prettyplease = "0.2.37"
proc-macro2 = "1.0.103"
quote = "1.0.41" quote = "1.0.41"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145" serde_json = "1.0.145"

View File

@@ -1,5 +1,10 @@
use heck::*;
use indexmap::IndexMap; use indexmap::IndexMap;
use syn::{FieldsNamed, parse_quote, token::Enum};
const KEYWORDS: &[&str] = &[
"type", "match", "enum", "struct", "fn", "mod", "pub", "use", "crate", "self", "super", "as",
"in", "let", "mut", "ref", "static", "trait", "where",
];
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] #[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub struct JellyfinOpenapi { pub struct JellyfinOpenapi {
@@ -16,21 +21,87 @@ pub struct Schema {
_type: Types, _type: Types,
properties: Option<indexmap::IndexMap<String, Property>>, properties: Option<indexmap::IndexMap<String, Property>>,
#[serde(rename = "oneOf")] #[serde(rename = "oneOf")]
one_of: Option<Vec<EnumVariant>>, one_of: Option<Vec<RefName>>,
#[serde(rename = "enum")]
_enum: Option<Vec<String>>,
description: Option<String>, description: Option<String>,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] #[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub struct EnumVariant { pub struct RefName {
#[serde(rename = "$ref")] #[serde(rename = "$ref")]
_ref: String, _ref: String,
} }
impl RefName {
pub fn get_type_name(&self) -> String {
self._ref.split('/').last().unwrap().to_string()
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] #[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub struct Property { pub struct Property {
#[serde(rename = "type")] #[serde(rename = "type")]
_type: Option<Types>, _type: Option<Types>,
nullable: Option<bool>, nullable: Option<bool>,
format: Option<String>,
items: Option<Box<Property>>,
#[serde(rename = "additionalProperties")]
additional_properties: Option<Box<Property>>,
#[serde(rename = "enum")]
_enum: Option<Vec<String>>,
#[serde(rename = "allOf")]
all_of: Option<Vec<RefName>>,
description: Option<String>,
#[serde(rename = "$ref")]
_ref: Option<String>,
}
impl Property {
pub fn to_string(&self) -> String {
let out = match self._type {
Some(Types::String) => "String".to_string(),
Some(Types::Integer) => match self.format.as_deref() {
Some("int32") => "i32".to_string(),
Some("int64") => "i64".to_string(),
_ => "i32".to_string(),
},
Some(Types::Boolean) => "bool".to_string(),
Some(Types::Number) => "f64".to_string(),
Some(Types::Array) => {
if let Some(ref items) = self.items {
format!("Vec<{}>", items.to_string())
} else {
"Vec<()>".to_string()
}
}
Some(Types::Object) => {
if let Some(properties) = &self.additional_properties {
format!(
"std::collections::HashMap<String, {}>",
properties.to_string()
)
} else {
"std::collections::HashMap<String, serde_json::Value>".to_string()
}
}
None => {
if let Some(ref _ref) = self._ref {
_ref.split('/').last().unwrap().to_string()
} else if let Some(ref all_of) = self.all_of {
all_of[0].get_type_name()
} else {
"()".into()
}
}
};
if let Some(true) = self.nullable {
format!("Option<{}>", out)
} else {
out
}
}
} }
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] #[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
@@ -58,7 +129,7 @@ fn main() {
.components .components
.schemas .schemas
.iter() .iter()
.filter(|(k, v)| v.one_of.is_some()) .filter(|(_, v)| v._enum.is_some())
.map(|(k, v)| (k.clone(), v.clone())) .map(|(k, v)| (k.clone(), v.clone()))
.collect(); .collect();
@@ -67,14 +138,86 @@ fn main() {
.map(|(key, value)| { .map(|(key, value)| {
let fields = value let fields = value
.properties .properties
.unwrap() .as_ref()
.expect("Possible properties")
.iter() .iter()
.map(|(name, _type)| format!("{}:{}", name, _type.is_)); .map(|(name, _type)| {
parse_quote! { let og_name = name.clone();
pub struct #key { let name = modify_keyword(&name.to_snake_case());
let _type = _type.to_string();
} let _type = if _type.contains(key) {
_type.replace(&format!("<{}>", key), format!("<Box<{}>>", key).as_str())
} else {
_type
};
syn::Field {
attrs: syn::parse_quote! {
#[serde(rename = #og_name)]
},
mutability: syn::FieldMutability::None,
vis: syn::Visibility::Public(syn::Token![pub](
proc_macro2::Span::call_site(),
)),
ident: Some(syn::Ident::new(&name, proc_macro2::Span::call_site())),
colon_token: Some(syn::token::Colon(proc_macro2::Span::call_site())),
ty: syn::parse_str(&_type).expect("Failed to parse type"),
} }
}) })
.collect(); .collect::<Vec<syn::Field>>();
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)]
pub struct #key {
#(#fields),*
}
};
syn::parse2(tokens).expect("Failed to parse struct")
})
.collect();
let syn_enums = enums
.iter()
.map(|(key, value)| {
let variants = value
._enum
.as_ref()
.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())
})
.collect::<Vec<syn::Ident>>();
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)]
pub enum #key {
#(#variants),*
}
};
syn::parse2(tokens).expect("Failed to parse enum")
})
.collect::<Vec<syn::ItemEnum>>();
let file = syn::File {
shebang: None,
attrs: vec![],
items: syn_structs
.into_iter()
.map(syn::Item::Struct)
.chain(syn_enums.into_iter().map(syn::Item::Enum))
.collect(),
};
let code = prettyplease::unparse(&file);
std::fs::write("jellyfin.rs", code).expect("Unable to write file");
}
fn modify_keyword(name: &str) -> String {
if KEYWORDS.contains(&name) {
format!("_{}", name)
} else {
name.to_string()
}
} }