feat: add base jellyfin and ersatztv sdk

This commit is contained in:
Alexander Navarro 2025-10-18 17:53:06 -03:00
commit 49cf01c668
11 changed files with 2184 additions and 0 deletions

19
src/config.rs Normal file
View file

@ -0,0 +1,19 @@
use clap::{Parser, ValueEnum};
use uuid::Uuid;
#[derive(Parser, Debug)]
#[command(version, about)]
pub struct Args {
/// URL host of the ErsatzTv service
pub host: String,
/// UUID of the build
pub build_id: Uuid,
/// The playout build mode
pub playout_mode: PlayoutMode,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
pub enum PlayoutMode {
Continue,
Reset,
}

42
src/ersatz_tv.rs Normal file
View file

@ -0,0 +1,42 @@
use anyhow::anyhow;
use uuid::Uuid;
use crate::ersatz_tv::models::ContextModel;
pub mod models;
pub struct Client {
_client: reqwest::Client,
host: String,
}
impl Client {
pub fn new(host: String) -> anyhow::Result<Self> {
let _client = reqwest::Client::builder().gzip(true).build()?;
Ok(Self { _client, host })
}
pub async fn get_context(&self, build_id: Uuid) -> anyhow::Result<ContextModel> {
let response = self
._client
.get(format!(
"{}/api/scripted/playout/build/{}/context",
self.host, build_id
))
.send()
.await?;
if let Err(err) = &response.error_for_status_ref() {
return Err(anyhow!(
"Request for {} returned with status {}:\n{}",
err.url().unwrap().path(),
err.status().unwrap_or_default(),
response.text().await?
));
}
let context: ContextModel = response.json().await?;
Ok(context)
}
}

10
src/ersatz_tv/models.rs Normal file
View file

@ -0,0 +1,10 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContextModel {
current_time: chrono::DateTime<chrono::Local>,
start_time: chrono::DateTime<chrono::Local>,
finish_time: chrono::DateTime<chrono::Local>,
is_done: bool,
}

72
src/jellyfin.rs Normal file
View file

@ -0,0 +1,72 @@
use anyhow::anyhow;
use reqwest::header::{HeaderMap, HeaderValue};
use crate::jellyfin::models::GetItemsResponseModel;
pub mod models;
static JELLYFIN_VGM_FOLDER: &str = "";
pub struct Client {
_client: reqwest::Client,
host: String,
user_id: String,
}
impl Client {
pub fn new<T: Into<String>>(host: T) -> anyhow::Result<Self> {
let mut headers = HeaderMap::new();
let auth_str = format!(
r#"MediaBrowser Client="Jellyfin Web", Device="Firefox", DeviceId="TW96aWxsYS81LjAgKFgxMTsgTGludXggeDg2XzY0OyBydjo5NC4wKSBHZWNrby8yMDEwMDEwMSBGaXJlZm94Lzk0LjB8MTYzODA1MzA2OTY4Mw11", Version="10.7.6", Token="{}""#,
"e9d4caa8c94f429280b43ba45a50ba6b"
);
headers.insert("Authorization", HeaderValue::from_str(auth_str.as_str())?);
headers.insert("Accept", HeaderValue::from_static("application/json"));
let _client = reqwest::Client::builder()
.gzip(true)
.default_headers(headers)
.build()?;
Ok(Self {
_client,
host: host.into(),
user_id: String::from("4a80af8c258e417e98f86e870e17d929"),
})
}
pub async fn get_items<T: Into<String> + std::fmt::Display>(
&self,
parent_id: T,
) -> anyhow::Result<GetItemsResponseModel> {
// INFO: needs to be created manually, escaped characters mess with Jellyfin server
let query = format!(
"parentId={parent_id}&sort_by=SortName&fields=Path,RecursiveItemCount&enable_images=false"
);
let response = self
._client
.get(format!(
"{}/Users/{}/items?{}",
self.host, self.user_id, query
))
.send()
.await?;
if let Err(err) = &response.error_for_status_ref() {
return Err(anyhow!(
"Request for {} returned with status {}:\n{}",
err.url().unwrap().path(),
err.status().unwrap_or_default(),
response.text().await?
));
}
println!("{}", response.url());
let context: GetItemsResponseModel = response.json().await?;
Ok(context)
}
}

50
src/jellyfin/models.rs Normal file
View file

@ -0,0 +1,50 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetItemsQueryStringModel {
pub parent_id: String,
pub sort_by: String,
pub fields: String,
pub enable_images: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct GetItemsResponseModel {
pub items: Vec<ItemModel>,
pub total_record_count: u64,
pub start_index: u64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ItemModel {
pub name: String,
pub id: String,
pub path: String,
pub production_year: Option<i64>,
pub is_folder: bool,
#[serde(rename = "Type")]
pub type_field: String,
pub artists: Option<Vec<String>>,
pub artist_items: Option<Vec<ArtistItemModel>>,
pub album_artist: Option<String>,
pub album_artists: Option<Vec<AlbumArtistModel>>,
pub media_type: String,
pub recursive_item_count: u64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ArtistItemModel {
pub name: String,
pub id: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct AlbumArtistModel {
pub name: String,
pub id: String,
}

3
src/lib.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod config;
pub mod ersatz_tv;
pub mod jellyfin;

19
src/main.rs Normal file
View file

@ -0,0 +1,19 @@
use clap::Parser;
use tv_scheduler::{config::Args, ersatz_tv, jellyfin};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
// let tv = ersatz_tv::Client::new(args.host)?;
// let context = tv.get_context(args.build_id).await?;
// println!("{:?}", context);
let jelly = jellyfin::Client::new("https://media.hoshikusu.xyz")?;
let items = jelly.get_items("574563ff9a41f2c98c234db69157cc87").await?;
println!("{:#?}", items);
Ok(())
}