diff --git a/.forgejo/workflows/webhook.yml b/.forgejo/workflows/webhook.yml index 6a5ecc0..6f923dd 100644 --- a/.forgejo/workflows/webhook.yml +++ b/.forgejo/workflows/webhook.yml @@ -2,7 +2,6 @@ name: Trigger quay.io Webhook on: push: - branches: [main] jobs: run: diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index 9320c6f..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! This module contains helper functions for implementing CLI/TUI. - -use crate::time::{parse_time, Time}; -use console::style; -use std::{ - fmt::Display, - io::{self, BufRead as _, Write as _} -}; - -pub fn ask(question: impl Display) -> String { - let mut stdout = io::stdout().lock(); - let mut stdin = io::stdin().lock(); - - write!( - stdout, - "{} {} ", - style(question).bold().magenta(), - style(">").cyan() - ) - .unwrap(); - stdout.flush().unwrap(); - let mut line = String::new(); - stdin.read_line(&mut line).unwrap(); - line.trim().to_owned() -} - -pub fn ask_time(question: impl Display + Copy) -> Time { - let mut stdout = io::stdout().lock(); - let mut stdin = io::stdin().lock(); - - let mut line = String::new(); - loop { - line.clear(); - write!( - stdout, - "{} {} ", - style(question).bold().magenta(), - style(">").cyan() - ) - .unwrap(); - stdout.flush().unwrap(); - stdin.read_line(&mut line).unwrap(); - let line = line.trim(); - match parse_time(line) { - Ok(time) => return time, - Err(err) => writeln!( - stdout, - "{} {line:?}: {err}", - style("Invalid Input").bold().red() - ) - .unwrap() - } - } -} diff --git a/src/iotro.rs b/src/iotro.rs index e37045e..a4eeccb 100644 --- a/src/iotro.rs +++ b/src/iotro.rs @@ -1,9 +1,6 @@ //! A module for writing intros and outros -use crate::{ - project::{ProjectLecture, Resolution}, - time::Date -}; +use crate::{time::Date, ProjectLecture, Resolution}; use anyhow::anyhow; use std::{ fmt::{self, Debug, Display, Formatter}, diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 998054a..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -#![allow(clippy::manual_range_contains)] -#![warn(clippy::unreadable_literal, rust_2018_idioms)] -#![forbid(elided_lifetimes_in_paths, unsafe_code)] - -pub mod cli; -pub mod iotro; -pub mod preset; -pub mod project; -pub mod question; -pub mod render; -pub mod time; - -#[cfg(feature = "mem_limit")] -use std::sync::RwLock; - -#[cfg(feature = "mem_limit")] -pub static MEM_LIMIT: RwLock = RwLock::new(String::new()); diff --git a/src/main.rs b/src/main.rs index a0b32e4..1a5355a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,17 +2,32 @@ #![warn(clippy::unreadable_literal, rust_2018_idioms)] #![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} +}; +use crate::preset::Preset; use camino::Utf8PathBuf as PathBuf; use clap::Parser; use console::style; -use render_video::{ - cli::{ask, ask_time}, - preset::Preset, - project::{Project, ProjectLecture, ProjectSource, Resolution}, - render::Renderer, - time::parse_date +#[cfg(feature = "mem_limit")] +use std::sync::RwLock; +use std::{ + fmt::Display, + fs, + io::{self, BufRead as _, Write} }; -use std::fs; + +#[cfg(feature = "mem_limit")] +static MEM_LIMIT: RwLock = RwLock::new(String::new()); #[derive(Debug, Parser)] struct Args { @@ -45,12 +60,58 @@ struct Args { stereo: bool } +fn ask(question: impl Display) -> String { + let mut stdout = io::stdout().lock(); + let mut stdin = io::stdin().lock(); + + write!( + stdout, + "{} {} ", + style(question).bold().magenta(), + style(">").cyan() + ) + .unwrap(); + stdout.flush().unwrap(); + let mut line = String::new(); + stdin.read_line(&mut line).unwrap(); + line.trim().to_owned() +} + +fn ask_time(question: impl Display + Copy) -> Time { + let mut stdout = io::stdout().lock(); + let mut stdin = io::stdin().lock(); + + let mut line = String::new(); + loop { + line.clear(); + write!( + stdout, + "{} {} ", + style(question).bold().magenta(), + style(">").cyan() + ) + .unwrap(); + stdout.flush().unwrap(); + stdin.read_line(&mut line).unwrap(); + let line = line.trim(); + match parse_time(line) { + Ok(time) => return time, + Err(err) => writeln!( + stdout, + "{} {line:?}: {err}", + style("Invalid Input").bold().red() + ) + .unwrap() + } + } +} + fn main() { let args = Args::parse(); #[cfg(feature = "mem_limit")] { - *(render_video::MEM_LIMIT.write().unwrap()) = args.mem_limit; + *(MEM_LIMIT.write().unwrap()) = args.mem_limit; } // process arguments @@ -74,8 +135,7 @@ fn main() { let lower = name.to_ascii_lowercase(); if (lower.ends_with(".mp4") || lower.ends_with(".mts") - || lower.ends_with(".mkv") - || lower.ends_with(".mov")) + || lower.ends_with(".mkv")) && !entry.file_type().unwrap().is_dir() { files.push(String::from(name)); @@ -122,7 +182,7 @@ fn main() { project }; - let mut renderer = Renderer::new(&directory, &project).unwrap(); + let renderer = Renderer::new(&directory, &project).unwrap(); let recording = renderer.recording_mkv(); // preprocess the video @@ -215,7 +275,7 @@ fn main() { // rescale the video if let Some(lowest_res) = args.transcode.or(preset.transcode) { - for res in Resolution::STANDARD_RESOLUTIONS.into_iter().rev() { + for res in Resolution::values().into_iter().rev() { if res > project.source.metadata.as_ref().unwrap().source_res || res > args.transcode_start.unwrap_or(preset.transcode_start) || res < lowest_res diff --git a/src/preset.rs b/src/preset.rs index 507401a..85f37c6 100644 --- a/src/preset.rs +++ b/src/preset.rs @@ -11,59 +11,57 @@ use std::{fs, io}; #[serde_as] #[derive(Deserialize, Serialize)] -pub struct Preset { +pub(crate) struct Preset { // options for the intro slide - pub course: String, - pub label: String, - pub docent: String, + pub(crate) course: String, + pub(crate) label: String, + pub(crate) docent: String, /// Course language - #[serde(default)] + #[serde(default = "Default::default")] #[serde_as(as = "DisplayFromStr")] - pub lang: Language<'static>, + pub(crate) lang: Language<'static>, // coding options - #[serde_as(as = "DisplayFromStr")] - pub transcode_start: Resolution, - #[serde_as(as = "Option")] - pub transcode: Option + pub(crate) transcode_start: Resolution, + pub(crate) transcode: Option } -pub fn preset_23ws_malo2() -> Preset { +fn preset_23ws_malo2() -> Preset { Preset { course: "23ws-malo2".into(), label: "Mathematische Logik II".into(), docent: "Prof. E. Grädel".into(), lang: GERMAN, - transcode_start: "1440p".parse().unwrap(), - transcode: Some("360p".parse().unwrap()) + transcode_start: Resolution::WQHD, + transcode: Some(Resolution::nHD) } } -pub fn preset_24ss_algomod() -> Preset { +fn preset_24ss_algomod() -> Preset { Preset { course: "24ss-algomod".into(), label: "Algorithmische Modelltheorie".into(), docent: "Prof. E. Grädel".into(), lang: GERMAN, - transcode_start: "1440p".parse().unwrap(), - transcode: Some("720p".parse().unwrap()) + transcode_start: Resolution::WQHD, + transcode: Some(Resolution::HD) } } -pub fn preset_24ss_qc() -> Preset { +fn preset_24ss_qc() -> Preset { Preset { course: "24ss-qc".into(), label: "Introduction to Quantum Computing".into(), docent: "Prof. D. Unruh".into(), lang: BRITISH, - transcode_start: "1440p".parse().unwrap(), - transcode: Some("720p".parse().unwrap()) + transcode_start: Resolution::WQHD, + transcode: Some(Resolution::HD) } } impl Preset { - pub fn find(name: &str) -> anyhow::Result { + pub(crate) fn find(name: &str) -> anyhow::Result { match fs::read(name) { Ok(buf) => return Ok(toml::from_slice(&buf)?), Err(err) if err.kind() == io::ErrorKind::NotFound => {}, diff --git a/src/project.rs b/src/project.rs index 93d7a45..7dbda1f 100644 --- a/src/project.rs +++ b/src/project.rs @@ -8,199 +8,157 @@ use crate::{ use rational::Rational; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; -use std::{ - cmp, - collections::BTreeSet, - fmt::{self, Display, Formatter}, - str::FromStr -}; +use std::{collections::BTreeSet, str::FromStr}; -#[derive(Clone, Copy, Debug, Deserialize, Serialize)] -pub struct Resolution(u32, u32); - -impl Resolution { - pub fn new(width: u32, height: u32) -> Self { - Self(width, height) - } - - pub fn width(self) -> u32 { - self.0 - } - - pub fn height(self) -> u32 { - self.1 - } - - pub(crate) fn bitrate(self) -> u64 { - // 640 * 360: 500k - if self.width() <= 640 { - 500_000 +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)] + pub(crate) enum Resolution { + $( + #[doc = concat!(stringify!($width), "x", stringify!($height))] + $res + ),+ } - // 1280 * 720: 1M - else if self.width() <= 1280 { - 1_000_000 - } - // 1920 * 1080: 2M - else if self.width() <= 1920 { - 2_000_000 - } - // 2560 * 1440: 3M - else if self.width() <= 2560 { - 3_000_000 - } - // 3840 * 2160: 4M - // TODO die bitrate von 4M ist absolut an den haaren herbeigezogen - else if self.width() <= 3840 { - 4_000_000 - } - // we'll cap everything else at 5M for no apparent reason - else { - 5_000_000 - } - } - pub(crate) fn default_codec(self) -> FfmpegOutputFormat { - if self.width() > 1920 { - FfmpegOutputFormat::Av1Opus - } else { - FfmpegOutputFormat::AvcAac + const NUM_RESOLUTIONS: usize = { + let mut num = 0; + $(num += 1; stringify!($res);)+ + num + }; + + impl Resolution { + pub(crate) fn values() -> [Self; NUM_RESOLUTIONS] { + [$(Self::$res),+] + } + + pub(crate) fn width(self) -> usize { + match self { + $(Self::$res => $width),+ + } + } + + pub(crate) fn height(self) -> usize { + match self { + $(Self::$res => $height),+ + } + } + + pub(crate) fn bitrate(self) -> u64 { + match self { + $(Self::$res => $bitrate),+ + } + } + + pub(crate) fn format(self) -> FfmpegOutputFormat { + match self { + $(Self::$res => FfmpegOutputFormat::$format),+ + } + } } - } - pub const STANDARD_RESOLUTIONS: [Self; 5] = [ - Self(640, 360), - Self(1280, 720), - Self(1920, 1080), - Self(2560, 1440), - Self(3840, 2160) - ]; -} + impl FromStr for Resolution { + type Err = anyhow::Error; -impl Display for Resolution { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}p", self.height()) + fn from_str(s: &str) -> anyhow::Result { + Ok(match s { + $(concat!(stringify!($height), "p") => Self::$res,)+ + _ => anyhow::bail!("Unknown Resolution: {s:?}") + }) + } + } } } -impl FromStr for Resolution { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - Ok(match s.to_lowercase().as_str() { - "360p" | "nhd" => Self(640, 360), - "540p" | "qhd" => Self(960, 540), - "720p" | "hd" => Self(1280, 720), - "900p" | "hd+" => Self(1600, 900), - "1080p" | "fhd" | "fullhd" => Self(1920, 1080), - "1440p" | "wqhd" => Self(2560, 1440), - "2160p" | "4k" | "uhd" => Self(3840, 2160), - _ => 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 } -impl Ord for Resolution { - fn cmp(&self, other: &Self) -> cmp::Ordering { - (self.0 * self.1).cmp(&(other.0 * other.1)) - } -} - -impl Eq for Resolution {} - -impl PartialOrd for Resolution { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl PartialEq for Resolution { - fn eq(&self, other: &Self) -> bool { - self.cmp(other) == cmp::Ordering::Equal - } -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct Project { - pub lecture: ProjectLecture, - pub source: ProjectSource, - pub progress: ProjectProgress +#[derive(Deserialize, Serialize)] +pub(crate) struct Project { + pub(crate) lecture: ProjectLecture, + pub(crate) source: ProjectSource, + pub(crate) progress: ProjectProgress } #[serde_as] -#[derive(Debug, Deserialize, Serialize)] -pub struct ProjectLecture { - pub course: String, - pub label: String, - pub docent: String, +#[derive(Deserialize, Serialize)] +pub(crate) struct ProjectLecture { + pub(crate) course: String, + pub(crate) label: String, + pub(crate) docent: String, #[serde_as(as = "DisplayFromStr")] - pub date: Date, + pub(crate) date: Date, #[serde(default = "Default::default")] #[serde_as(as = "DisplayFromStr")] - pub lang: Language<'static> + pub(crate) lang: Language<'static> } #[serde_as] -#[derive(Debug, Deserialize, Serialize)] -pub struct ProjectSource { - pub files: Vec, - pub stereo: bool, +#[derive(Deserialize, Serialize)] +pub(crate) struct ProjectSource { + pub(crate) files: Vec, + pub(crate) stereo: bool, #[serde_as(as = "Option")] - pub start: Option