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

View file

@ -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<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 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<T: Dto>(&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<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<()> {
let response = self
._client

View file

@ -1,5 +1,8 @@
use serde::{Deserialize, Serialize};
pub trait Dto: Serialize {}
impl<T: Serialize> 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<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 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(())