diff --git a/src/iotro.rs b/src/iotro.rs index a4eeccb..98b5a50 100644 --- a/src/iotro.rs +++ b/src/iotro.rs @@ -24,9 +24,7 @@ pub struct Language<'a> { download_videos: &'a str, questions_feedback: &'a str, // metadata - pub(crate) from: &'a str, - // questions - pub(crate) question: &'a str + pub(crate) from: &'a str } pub const GERMAN: Language<'static> = Language { @@ -60,9 +58,7 @@ pub const GERMAN: Language<'static> = Language { download_videos: "Videos herunterladen", questions_feedback: "Fragen, Vorschläge und Feedback", - from: "vom", - - question: "Frage" + from: "vom" }; pub const BRITISH: Language<'static> = Language { @@ -91,7 +87,7 @@ pub const BRITISH: Language<'static> = Language { 3 | 23 => "rd", _ => "th" }; - format!("{}{th} {month} {:04}", d.day, d.year) + format!("{:02}{th} {month} {:04}", d.day, d.year) }, lecture_from: "Lecture from", @@ -102,9 +98,7 @@ pub const BRITISH: Language<'static> = Language { download_videos: "Download videos", questions_feedback: "Questions, Suggestions and Feedback", - from: "from", - - question: "Question" + from: "from" }; impl Default for Language<'static> { diff --git a/src/main.rs b/src/main.rs index 1a5355a..742ecd8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,27 +3,30 @@ #![forbid(elided_lifetimes_in_paths, unsafe_code)] mod iotro; -mod preset; -mod project; mod question; mod render; mod time; use self::{ - project::{Project, ProjectLecture, ProjectSource, Resolution}, - render::Renderer, - time::{parse_date, parse_time, Time} + iotro::Language, + question::Question, + render::{ffmpeg::FfmpegOutputFormat, Renderer}, + time::{parse_date, parse_time, Date, Time} }; -use crate::preset::Preset; use camino::Utf8PathBuf as PathBuf; use clap::Parser; use console::style; +use rational::Rational; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; #[cfg(feature = "mem_limit")] use std::sync::RwLock; use std::{ + collections::BTreeSet, fmt::Display, fs, - io::{self, BufRead as _, Write} + io::{self, BufRead as _, Write}, + str::FromStr }; #[cfg(feature = "mem_limit")] @@ -35,24 +38,34 @@ struct Args { #[clap(short = 'C', long, default_value = ".")] directory: PathBuf, - /// The preset of the lecture. Can be a toml file or a known course slug. - #[clap(short, long)] - preset: String, + /// The slug of the course, e.g. "23ws-malo2". + #[clap(short = 'c', long, default_value = "23ws-malo2")] + course: String, + + /// The label of the course, e.g. "Mathematische Logik II". + #[clap(short, long, default_value = "Mathematische Logik II")] + label: String, + + /// The docent of the course, e.g. "Prof. E. Grädel". + #[clap(short, long, default_value = "Prof. E. Grädel")] + docent: String, + + /// The language of the lecture. Used for the intro and outro frame. + #[clap(short = 'L', long, default_value = "de")] + lang: Language<'static>, #[cfg(feature = "mem_limit")] /// The memory limit for external tools like ffmpeg. #[clap(short, long, default_value = "12G")] mem_limit: String, - /// Transcode the final video clip down to the minimum resolution specified. If not - /// specified, the default value from the preset is used. + /// Transcode the final video clip down to the minimum resolution specified. #[clap(short, long)] transcode: Option, /// Transcode starts at this resolution, or the source resolution, whichever is lower. - /// If not specified, the default value from the preset is used. - #[clap(short = 'T', long)] - transcode_start: Option, + #[clap(short = 'T', long, default_value = "1440p")] + transcode_start: Resolution, /// Treat the audio as stereo. By default, only one channel from the input stereo will /// be used, assuming either the other channel is backup or the same as the used. @@ -60,6 +73,159 @@ struct Args { stereo: bool } +macro_rules! resolutions { + ($($res:ident: $width:literal x $height:literal at $bitrate:literal in $format:ident),+) => { + #[allow(non_camel_case_types, clippy::upper_case_acronyms)] + #[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] + enum Resolution { + $( + #[doc = concat!(stringify!($width), "x", stringify!($height))] + $res + ),+ + } + + const NUM_RESOLUTIONS: usize = { + let mut num = 0; + $(num += 1; stringify!($res);)+ + num + }; + + impl Resolution { + fn values() -> [Self; NUM_RESOLUTIONS] { + [$(Self::$res),+] + } + + fn width(self) -> usize { + match self { + $(Self::$res => $width),+ + } + } + + fn height(self) -> usize { + match self { + $(Self::$res => $height),+ + } + } + + fn bitrate(self) -> u64 { + match self { + $(Self::$res => $bitrate),+ + } + } + + fn format(self) -> FfmpegOutputFormat { + match self { + $(Self::$res => FfmpegOutputFormat::$format),+ + } + } + } + + impl FromStr for Resolution { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + Ok(match s { + $(concat!(stringify!($height), "p") => Self::$res,)+ + _ => anyhow::bail!("Unknown Resolution: {s:?}") + }) + } + } + } +} + +resolutions! { + nHD: 640 x 360 at 500_000 in AvcAac, + HD: 1280 x 720 at 1_000_000 in AvcAac, + FullHD: 1920 x 1080 at 750_000 in Av1Opus, + WQHD: 2560 x 1440 at 1_000_000 in Av1Opus, + // TODO qsx muss mal sagen wieviel bitrate für 4k + UHD: 3840 x 2160 at 2_000_000 in Av1Opus +} + +#[derive(Deserialize, Serialize)] +struct Project { + lecture: ProjectLecture, + source: ProjectSource, + progress: ProjectProgress +} + +#[serde_as] +#[derive(Deserialize, Serialize)] +struct ProjectLecture { + course: String, + label: String, + docent: String, + #[serde_as(as = "DisplayFromStr")] + date: Date, + #[serde(default = "Default::default")] + #[serde_as(as = "DisplayFromStr")] + lang: Language<'static> +} + +#[serde_as] +#[derive(Deserialize, Serialize)] +struct ProjectSource { + files: Vec, + stereo: bool, + + #[serde_as(as = "Option")] + start: Option