#![allow(clippy::manual_range_contains)] #![warn(clippy::unreadable_literal, rust_2018_idioms)] #![forbid(elided_lifetimes_in_paths, unsafe_code)] mod iotro; mod render; mod time; use crate::{ render::{ffmpeg::FfmpegOutputFormat, Renderer}, time::{parse_date, parse_time, Date, Time} }; use camino::Utf8PathBuf as PathBuf; use clap::Parser; use rational::Rational; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; use std::{ collections::BTreeSet, fmt::Display, fs, io::{self, BufRead as _, Write}, str::FromStr, sync::RwLock }; static MEM_LIMIT: RwLock = RwLock::new(String::new()); #[derive(Debug, Parser)] struct Args { /// The root directory of the project. It should contain the raw video file(s). #[clap(short = 'C', long, default_value = ".")] directory: PathBuf, /// The slug of the course, e.g. "23ws-malo2". #[clap(short = 'c', long, default_value = "23ws-malo2")] course: String, /// The memory limit for external tools like ffmpeg. #[clap(short, long, default_value = "8G")] mem_limit: String, /// Transcode the final video clip down to the minimum resolution specified. #[clap(short, long)] transcode: Option } 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 2_000_000 in AvcAac, WQHD: 2560 x 1440 at 3_000_000 in Av1Opus, // TODO qsx muss mal sagen wieviel bitrate für 4k UHD: 3840 x 2160 at 4_000_000 in Av1Opus } #[derive(Deserialize, Serialize)] struct Project { lecture: ProjectLecture, source: ProjectSource, progress: ProjectProgress } #[serde_as] #[derive(Deserialize, Serialize)] struct ProjectLecture { course: String, #[serde_as(as = "DisplayFromStr")] date: Date } #[serde_as] #[derive(Deserialize, Serialize)] struct ProjectSource { files: Vec, #[serde_as(as = "Option")] start: Option