commit d59ca4aceb838f4cbbb3de114d2c61eb1e39b678 Author: Dominic Date: Sun Oct 22 22:44:59 2023 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b36ae9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# test files +/2310*/ + +# rust/cargo +/target/ +/Cargo.lock + +# git +*.orig + +# emacs +*~ +\#*# +.#*# + +# intellij +/.idea/ +*.ipr +*.iml diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4f6a213 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +# -*- eval: (cargo-minor-mode 1) -*- + +[package] +name = "render_video" +version = "0.0.0" +publish = false +edition = "2021" + +[dependencies] +anyhow = "1.0" +camino = "1.1" +clap = { version = "4.4", features = ["derive"] } +ffmpeg = { package = "ffmpeg-next", version = "6.0" } +serde = { version = "1.0.188", features = ["derive"] } +serde_with = "3.4" +toml = { package = "basic-toml", version = "0.1.4" } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..62f400e --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,38 @@ +edition = "2021" +max_width = 90 +newline_style = "Unix" +unstable_features = true + +# skip generated files +format_generated_files = false + +# always use tabs. +hard_tabs = true +tab_spaces = 4 + +# commas inbetween but not after +match_block_trailing_comma = true +trailing_comma = "Never" + +# fix my imports for me +imports_granularity = "Crate" +#group_imports = "One" + +# format everything +format_code_in_doc_comments = true +format_macro_matchers = true + +# don't keep outdated syntax +use_field_init_shorthand = true +use_try_shorthand = true + +# condense Struct { _, _ } to Struct { .. } +condense_wildcard_suffixes = true + +# prefer foo(Bar { \n }) over foo(\nBar { \n }\n) +overflow_delimited_expr = true + +# I wish there was a way to allow 0..n but not a + 1..b + 2 +# However, the later looks so terible that I use spaces everywhere +# https://github.com/rust-lang/rustfmt/issues/3367 +spaces_around_ranges = true diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d17dcbf --- /dev/null +++ b/src/main.rs @@ -0,0 +1,122 @@ +#![warn(rust_2018_idioms)] +#![forbid(elided_lifetimes_in_paths, unsafe_code)] + +mod time; + +use crate::time::{parse_date, parse_time, Date, Time}; +use camino::Utf8PathBuf as PathBuf; +use clap::Parser; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; +use std::{ + fmt::Display, + fs, + io::{self, BufRead as _, Write} +}; + +#[derive(Debug, Parser)] +struct Args { + #[clap(short = 'C', long, default_value = ".")] + directory: PathBuf, + + #[clap(short = 'c', long, default_value = "23ws-malo")] + course: String +} + +#[derive(Deserialize, Serialize)] +struct Project { + lecture: ProjectLecture, + source: ProjectSource +} + +#[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 = "DisplayFromStr")] + first_file_start: Time, + #[serde_as(as = "DisplayFromStr")] + last_file_end: Time +} + +fn ask_time(question: impl Display) -> Time { + let mut stdout = io::stdout().lock(); + let mut stdin = io::stdin().lock(); + + writeln!(stdout, "{question}").unwrap(); + let mut line = String::new(); + loop { + line.clear(); + write!(stdout, "> ").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, "Invalid Input {line:?}: {err}").unwrap() + } + } +} + +fn main() { + let args = Args::parse(); + + // process arguments + let directory = args.directory.canonicalize_utf8().unwrap(); + let course = args.course; + + // let's see if we need to initialise the project + let project_path = directory.join("project.toml"); + let project = if project_path.exists() { + toml::from_slice(&fs::read(&project_path).unwrap()).unwrap() + } else { + let dirname = directory.file_name().unwrap(); + let date = + parse_date(dirname).expect("Directory name is not in the expected format"); + + let mut files = Vec::new(); + for entry in directory.read_dir_utf8().unwrap() { + let entry = entry.unwrap(); + let name = entry.file_name(); + let lower = name.to_ascii_lowercase(); + if (lower.ends_with(".mp4") || lower.ends_with(".mts")) + && entry.file_type().unwrap().is_file() + { + files.push(String::from(name)); + } + } + files.sort_unstable(); + assert!(!files.is_empty()); + println!("I found the following source files: {files:?}"); + + let first_file_start = ask_time(format_args!( + "Please take a look at the file {} and tell me the first second you want included", + files.first().unwrap() + )); + let last_file_end = ask_time(format_args!( + "Please take a look at the file {} and tell me the last second you want included", + files.last().unwrap() + )); + + let project = Project { + lecture: ProjectLecture { course, date }, + source: ProjectSource { + files, + first_file_start, + last_file_end + } + }; + fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()).unwrap(); + project + }; + + println!("{}", toml::to_string(&project).unwrap()); +} diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..4b52ea4 --- /dev/null +++ b/src/time.rs @@ -0,0 +1,210 @@ +use anyhow::bail; +use std::{ + fmt::{self, Display, Write as _}, + str::FromStr +}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Date { + year: u16, + month: u8, + day: u8 +} + +impl FromStr for Date { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + parse_date(s) + } +} + +impl Display for Date { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&format_date(*self)) + } +} + +/// Parse a date in YYMMDD format. +pub fn parse_date(s: &str) -> anyhow::Result { + let int: u32 = s.parse()?; + + let year = int / 10000 + 2000; + if year / 10 != 202 { + bail!("Expected a date in 202X"); + } + + let month = int / 100 % 100; + if month < 1 || month > 12 { + bail!("Invalid month: {month}"); + } + + let day = int % 100; + if day < 1 || day > 31 { + bail!("Invalid day: {day}"); + } + + Ok(Date { + year: year as _, + month: month as _, + day: day as _ + }) +} + +/// Format a date in YYMMDD format. +pub fn format_date(d: Date) -> String { + format!("{:02}{:02}{:02}", d.year % 100, d.month, d.day) +} + +/// Format a date in DD. MMMM YYYY format. +pub fn format_date_long(d: Date) -> String { + let month = match d.month { + 1 => "Januar", + 2 => "Februar", + 3 => "März", + 4 => "April", + 5 => "Mai", + 6 => "Juni", + 7 => "Juli", + 8 => "August", + 9 => "September", + 10 => "Oktober", + 11 => "November", + 12 => "Dezember", + _ => unreachable!() + }; + format!("{:02}. {month} {:04}", d.day, d.year) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Time { + seconds: u32, + micros: u32 +} + +impl FromStr for Time { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + parse_time(s) + } +} + +impl Display for Time { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&format_time(*self)) + } +} + +/// Parse a time in hh:mm:ss:sub or hh:mm:ss.micros format. +pub fn parse_time(s: &str) -> anyhow::Result