From 67be4dc23341ecdd96befbcbb235988b5661422a Mon Sep 17 00:00:00 2001 From: aleidk Date: Sun, 19 Oct 2025 20:20:26 -0300 Subject: [PATCH] feat: add config file loading --- Cargo.lock | 152 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + config.toml | 11 +++ src/config.rs | 51 +++++++++++++- src/ersatz_tv.rs | 24 ++----- src/ersatz_tv/models.rs | 29 ++++++++ src/main.rs | 133 +++++++++++++++++++++++++++++++---- 7 files changed, 369 insertions(+), 32 deletions(-) create mode 100644 config.toml diff --git a/Cargo.lock b/Cargo.lock index 639be26..953e617 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,6 +86,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -116,6 +125,12 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + [[package]] name = "bytes" version = "1.10.1" @@ -282,6 +297,20 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "figment" +version = "0.10.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +dependencies = [ + "atomic", + "pear", + "serde", + "toml", + "uncased", + "version_check", +] + [[package]] name = "find-msvc-tools" version = "0.1.4" @@ -682,6 +711,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + [[package]] name = "ipnet" version = "2.11.0" @@ -891,6 +926,29 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -942,6 +1000,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + [[package]] name = "quote" version = "1.0.41" @@ -1191,6 +1262,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1400,6 +1480,47 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.5.2" @@ -1477,6 +1598,7 @@ dependencies = [ "anyhow", "chrono", "clap", + "figment", "rand", "reqwest", "serde", @@ -1485,6 +1607,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-ident" version = "1.0.19" @@ -1538,6 +1669,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" @@ -1894,6 +2031,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -1906,6 +2052,12 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 4b452cf..aa9d96d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" anyhow = "1.0.100" chrono = { version = "0.4.42", features = ["serde"] } clap = { version = "4.5.49", features = ["derive"] } +figment = { version = "0.10.19", features = ["toml", "env"] } rand = "0.9.2" reqwest = { version = "0.12.24", features = ["gzip", "json"] } serde = "1.0.228" diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..c4b7904 --- /dev/null +++ b/config.toml @@ -0,0 +1,11 @@ +name = "Foo" + +[[blocks]] +schedule_type = { type = "StartAt", time = "8:30 PM", until_tomorrow = true } +# VGM Folder +content_type = { type = "RandomAlbum", container_id = "574563ff9a41f2c98c234db69157cc87", album_name_as_title = true } + +[blocks.pre_filler] +custom_title = "VGM" +schedule_type = { type = "All" } +content_type = { type = "SmartCollection", name = "VGM - All" } diff --git a/src/config.rs b/src/config.rs index 279bfa0..45851d3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,9 @@ use clap::{Parser, ValueEnum}; -use uuid::Uuid; +use figment::{ + Figment, + providers::{Env, Format, Toml}, +}; +use serde::Deserialize; #[derive(Parser, Debug)] #[command(version, about)] @@ -17,3 +21,48 @@ pub enum PlayoutMode { Continue, Reset, } + +#[derive(Debug, PartialEq, Deserialize)] +pub struct ScheduleConfig { + pub name: String, + pub blocks: Vec, +} + +#[derive(Debug, PartialEq, Deserialize)] +pub struct ScheduleBlock { + pub schedule_type: ScheduleType, + pub content_type: ContentType, + pub custom_title: Option, + pub pre_filler: Option>, + pub post_filler: Option>, +} + +#[derive(Debug, PartialEq, Deserialize)] +#[serde(tag = "type")] +pub enum ScheduleType { + All, + StartAt { time: String, until_tomorrow: bool }, +} + +#[derive(Debug, PartialEq, Deserialize)] +#[serde(tag = "type")] +pub enum ContentType { + /// Smart Collection of ErsatzTv + SmartCollection { name: String }, + /// Random album picked from a container Item from Jellyfin + RandomAlbum { + container_id: String, + album_name_as_title: bool, + }, +} + +impl ScheduleConfig { + pub fn new(path: Option) -> anyhow::Result { + let config = Figment::new() + .merge(Toml::file(path.unwrap_or(String::from("config.toml")))) + .merge(Env::prefixed("TV_SDR_")) + .extract()?; + + Ok(config) + } +} diff --git a/src/ersatz_tv.rs b/src/ersatz_tv.rs index 8e4a3a5..1ed4f1b 100644 --- a/src/ersatz_tv.rs +++ b/src/ersatz_tv.rs @@ -1,13 +1,14 @@ use anyhow::anyhow; use uuid::Uuid; -use crate::ersatz_tv::models::{ContextModel, ScheduleAllContentDto, SearchQueryDto}; +use crate::ersatz_tv::models::{ContextModel, Dto, ScheduleAllContentDto}; pub mod models; pub struct Client { _client: reqwest::Client, host: String, + base_endpoint: String, build_id: String, } @@ -17,6 +18,7 @@ impl Client { Ok(Self { _client, host: host.clone(), + base_endpoint: format!("{}/api/scripted/playout/build/{}", host, build_id), build_id: build_id.clone(), }) } @@ -45,14 +47,11 @@ impl Client { Ok(context) } - async fn _load_content(&self, content: &SearchQueryDto) -> anyhow::Result<()> { + pub async fn post(&self, endpoint: &str, payload: &T) -> anyhow::Result<()> { let response = self ._client - .post(format!( - "{}/api/scripted/playout/build/{}/add_search", - self.host, self.build_id - )) - .json(content) + .post(format!("{}/{}", self.base_endpoint, endpoint)) + .json(payload) .send() .await?; @@ -68,17 +67,6 @@ impl Client { Ok(()) } - pub async fn load_content>( - &self, - content: T, - ) -> anyhow::Result<()> { - for item in content { - self._load_content(&item).await?; - } - - Ok(()) - } - async fn _schedule_content(&self, content: &ScheduleAllContentDto) -> anyhow::Result<()> { let response = self ._client diff --git a/src/ersatz_tv/models.rs b/src/ersatz_tv/models.rs index afdffe5..7f1a019 100644 --- a/src/ersatz_tv/models.rs +++ b/src/ersatz_tv/models.rs @@ -1,5 +1,8 @@ use serde::{Deserialize, Serialize}; +pub trait Dto: Serialize {} +impl Dto for T {} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ContextModel { @@ -9,6 +12,14 @@ pub struct ContextModel { is_done: bool, } +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LoadSmartCollectionDto { + pub key: String, + pub order: SearchQueryOrder, + pub smart_collection: String, +} + #[derive(Debug, Serialize, Deserialize)] pub struct SearchQueryDto { pub key: String, @@ -47,3 +58,21 @@ pub enum FillerKind { Midroll, Postroll, } + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ScheduleContentUntilDto { + #[serde(rename = "content")] + pub content_key: String, + pub when: String, + pub tomorrow: bool, + pub filler_kind: FillerKind, + pub custom_title: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WaitUntilTimeDto { + pub rewind_on_reset: bool, + pub when: String, +} diff --git a/src/main.rs b/src/main.rs index 4c3c1d0..eeea8a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,43 +1,150 @@ use clap::Parser; use tv_scheduler::{ - config::Args, + config::{Args, ContentType, ScheduleBlock, ScheduleConfig, ScheduleType}, ersatz_tv::{ self, models::{ - FillerKind, ScheduleAllContentDto, SearchQueryDto, SearchQueryOrder, SearchQueryValue, + FillerKind, LoadSmartCollectionDto, ScheduleAllContentDto, ScheduleContentUntilDto, + SearchQueryDto, SearchQueryOrder, SearchQueryValue, WaitUntilTimeDto, }, }, jellyfin, }; +async fn handle_schedule_block( + tv: &ersatz_tv::Client, + jelly: &jellyfin::Client, + block: &ScheduleBlock, +) -> anyhow::Result<()> { + println!("{:#?}", block); + + // add pre content until time + if let Some(content) = &block.pre_filler { + let payload = match &content.content_type { + ContentType::SmartCollection { name } => LoadSmartCollectionDto { + key: name.clone(), + order: SearchQueryOrder::Chronological, + smart_collection: name.clone(), + }, + ContentType::RandomAlbum { + container_id, + album_name_as_title, + } => todo!(), + }; + + println!("Loading PreFiller: {:#?}", payload); + tv.post("add_smart_collection", &payload).await?; + + match &block.schedule_type { + ScheduleType::All => todo!(), + ScheduleType::StartAt { + time, + until_tomorrow, + } => { + tv.post( + "pad_until", + &ScheduleContentUntilDto { + content_key: payload.key, + when: time.clone(), + tomorrow: *until_tomorrow, + filler_kind: FillerKind::Preroll, + custom_title: content.custom_title.clone(), + }, + ) + .await?; + } + } + } else if let ScheduleType::StartAt { + time, + until_tomorrow: _, + } = &block.schedule_type + { + // Fill with nothing until the desire time + tv.post( + "wait_until_exact", + &WaitUntilTimeDto { + when: time.clone(), + rewind_on_reset: false, + }, + ) + .await?; + } + + // add main content + match &block.content_type { + ContentType::SmartCollection { name } => todo!(), + ContentType::RandomAlbum { + container_id, + album_name_as_title, + } => { + let item = jelly.get_random_vgm_album().await?; + println!("Using album: {}", item.name); + let songs = jelly.get_songs(item.id).await?; + + let content_payload = songs.items.iter().map(|song| SearchQueryDto { + key: song.id.clone(), + order: SearchQueryOrder::Shuffle, + query: SearchQueryValue::SongTitle(format!( + r#"title:"{}" AND library_id:9"#, + song.name + )), + }); + + for content in content_payload { + tv.post("add_search", &content).await?; + } + + let schedule_payload = songs.items.iter().map(|song| ScheduleAllContentDto { + key: song.id.clone(), + custom_title: if *album_name_as_title { + item.name.clone() + } else { + "VGM".to_string() + }, + disable_watermarks: false, + filler_kind: FillerKind::None, + }); + + for content in schedule_payload { + tv.post("add_all", &content).await?; + } + } + } + + // add all post content + Ok(()) +} + #[tokio::main] async fn main() -> anyhow::Result<()> { let args = Args::parse(); - - let jelly = jellyfin::Client::new("https://media.hoshikusu.xyz")?; - - let item = jelly.get_random_vgm_album().await?; - let songs = jelly.get_songs(item.id).await?; + let config = ScheduleConfig::new(None)?; let tv = ersatz_tv::Client::new(&args.host, &args.build_id)?; + let jelly = jellyfin::Client::new("https://media.hoshikusu.xyz")?; - let content_payload = songs.items.iter().map(|song| SearchQueryDto { + for block in config.blocks { + handle_schedule_block(&tv, &jelly, &block).await?; + } + + return Ok(()); + + let _item = jelly.get_random_vgm_album().await?; + let _songs = jelly.get_songs(_item.id).await?; + + let _content_payload = _songs.items.iter().map(|song| SearchQueryDto { key: song.id.clone(), order: SearchQueryOrder::Chronological, query: SearchQueryValue::SongTitle(format!(r#"title:"{}" AND library_id:9"#, song.name)), }); - tv.load_content(content_payload).await?; - - let schedule_payload = songs.items.iter().map(|song| ScheduleAllContentDto { + let _schedule_payload = _songs.items.iter().map(|song| ScheduleAllContentDto { key: song.id.clone(), custom_title: song.name.clone(), disable_watermarks: false, filler_kind: FillerKind::None, }); - tv.schedule_content(schedule_payload).await?; - // println!("{:#?}", songs); Ok(())