feat: add config file loading

This commit is contained in:
Alexander Navarro 2025-10-19 20:20:26 -03:00
parent 381c086eba
commit 67be4dc233
7 changed files with 369 additions and 32 deletions

152
Cargo.lock generated
View file

@ -86,6 +86,15 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "atomic"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
dependencies = [
"bytemuck",
]
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@ -116,6 +125,12 @@ version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "bytemuck"
version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.10.1" version = "1.10.1"
@ -282,6 +297,20 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 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]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.4" version = "0.1.4"
@ -682,6 +711,12 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "inlinable_string"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.11.0" version = "2.11.0"
@ -891,6 +926,29 @@ dependencies = [
"windows-link 0.2.1", "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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@ -942,6 +1000,19 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "quote" name = "quote"
version = "1.0.41" version = "1.0.41"
@ -1191,6 +1262,15 @@ dependencies = [
"serde_core", "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]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -1400,6 +1480,47 @@ dependencies = [
"tokio", "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]] [[package]]
name = "tower" name = "tower"
version = "0.5.2" version = "0.5.2"
@ -1477,6 +1598,7 @@ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"clap", "clap",
"figment",
"rand", "rand",
"reqwest", "reqwest",
"serde", "serde",
@ -1485,6 +1607,15 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "uncased"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
dependencies = [
"version_check",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.19" version = "1.0.19"
@ -1538,6 +1669,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
@ -1894,6 +2031,15 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.46.0" version = "0.46.0"
@ -1906,6 +2052,12 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.0" version = "0.8.0"

View file

@ -7,6 +7,7 @@ edition = "2024"
anyhow = "1.0.100" anyhow = "1.0.100"
chrono = { version = "0.4.42", features = ["serde"] } chrono = { version = "0.4.42", features = ["serde"] }
clap = { version = "4.5.49", features = ["derive"] } clap = { version = "4.5.49", features = ["derive"] }
figment = { version = "0.10.19", features = ["toml", "env"] }
rand = "0.9.2" rand = "0.9.2"
reqwest = { version = "0.12.24", features = ["gzip", "json"] } reqwest = { version = "0.12.24", features = ["gzip", "json"] }
serde = "1.0.228" serde = "1.0.228"

11
config.toml Normal file
View file

@ -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" }

View file

@ -1,5 +1,9 @@
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use uuid::Uuid; use figment::{
Figment,
providers::{Env, Format, Toml},
};
use serde::Deserialize;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about)] #[command(version, about)]
@ -17,3 +21,48 @@ pub enum PlayoutMode {
Continue, Continue,
Reset, Reset,
} }
#[derive(Debug, PartialEq, Deserialize)]
pub struct ScheduleConfig {
pub name: String,
pub blocks: Vec<ScheduleBlock>,
}
#[derive(Debug, PartialEq, Deserialize)]
pub struct ScheduleBlock {
pub schedule_type: ScheduleType,
pub content_type: ContentType,
pub custom_title: Option<String>,
pub pre_filler: Option<Box<ScheduleBlock>>,
pub post_filler: Option<Box<ScheduleBlock>>,
}
#[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<String>) -> anyhow::Result<Self> {
let config = Figment::new()
.merge(Toml::file(path.unwrap_or(String::from("config.toml"))))
.merge(Env::prefixed("TV_SDR_"))
.extract()?;
Ok(config)
}
}

View file

@ -1,13 +1,14 @@
use anyhow::anyhow; use anyhow::anyhow;
use uuid::Uuid; use uuid::Uuid;
use crate::ersatz_tv::models::{ContextModel, ScheduleAllContentDto, SearchQueryDto}; use crate::ersatz_tv::models::{ContextModel, Dto, ScheduleAllContentDto};
pub mod models; pub mod models;
pub struct Client { pub struct Client {
_client: reqwest::Client, _client: reqwest::Client,
host: String, host: String,
base_endpoint: String,
build_id: String, build_id: String,
} }
@ -17,6 +18,7 @@ impl Client {
Ok(Self { Ok(Self {
_client, _client,
host: host.clone(), host: host.clone(),
base_endpoint: format!("{}/api/scripted/playout/build/{}", host, build_id),
build_id: build_id.clone(), build_id: build_id.clone(),
}) })
} }
@ -45,14 +47,11 @@ impl Client {
Ok(context) Ok(context)
} }
async fn _load_content(&self, content: &SearchQueryDto) -> anyhow::Result<()> { pub async fn post<T: Dto>(&self, endpoint: &str, payload: &T) -> anyhow::Result<()> {
let response = self let response = self
._client ._client
.post(format!( .post(format!("{}/{}", self.base_endpoint, endpoint))
"{}/api/scripted/playout/build/{}/add_search", .json(payload)
self.host, self.build_id
))
.json(content)
.send() .send()
.await?; .await?;
@ -68,17 +67,6 @@ impl Client {
Ok(()) Ok(())
} }
pub async fn load_content<T: IntoIterator<Item = SearchQueryDto>>(
&self,
content: T,
) -> anyhow::Result<()> {
for item in content {
self._load_content(&item).await?;
}
Ok(())
}
async fn _schedule_content(&self, content: &ScheduleAllContentDto) -> anyhow::Result<()> { async fn _schedule_content(&self, content: &ScheduleAllContentDto) -> anyhow::Result<()> {
let response = self let response = self
._client ._client

View file

@ -1,5 +1,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub trait Dto: Serialize {}
impl<T: Serialize> Dto for T {}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ContextModel { pub struct ContextModel {
@ -9,6 +12,14 @@ pub struct ContextModel {
is_done: bool, 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)] #[derive(Debug, Serialize, Deserialize)]
pub struct SearchQueryDto { pub struct SearchQueryDto {
pub key: String, pub key: String,
@ -47,3 +58,21 @@ pub enum FillerKind {
Midroll, Midroll,
Postroll, 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<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WaitUntilTimeDto {
pub rewind_on_reset: bool,
pub when: String,
}

View file

@ -1,43 +1,150 @@
use clap::Parser; use clap::Parser;
use tv_scheduler::{ use tv_scheduler::{
config::Args, config::{Args, ContentType, ScheduleBlock, ScheduleConfig, ScheduleType},
ersatz_tv::{ ersatz_tv::{
self, self,
models::{ models::{
FillerKind, ScheduleAllContentDto, SearchQueryDto, SearchQueryOrder, SearchQueryValue, FillerKind, LoadSmartCollectionDto, ScheduleAllContentDto, ScheduleContentUntilDto,
SearchQueryDto, SearchQueryOrder, SearchQueryValue, WaitUntilTimeDto,
}, },
}, },
jellyfin, 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] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
let args = Args::parse(); let args = Args::parse();
let config = ScheduleConfig::new(None)?;
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 tv = ersatz_tv::Client::new(&args.host, &args.build_id)?; 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(), key: song.id.clone(),
order: SearchQueryOrder::Chronological, order: SearchQueryOrder::Chronological,
query: SearchQueryValue::SongTitle(format!(r#"title:"{}" AND library_id:9"#, song.name)), 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(), key: song.id.clone(),
custom_title: song.name.clone(), custom_title: song.name.clone(),
disable_watermarks: false, disable_watermarks: false,
filler_kind: FillerKind::None, filler_kind: FillerKind::None,
}); });
tv.schedule_content(schedule_payload).await?;
// println!("{:#?}", songs); // println!("{:#?}", songs);
Ok(()) Ok(())