Skip to content
Snippets Groups Projects
Unverified Commit d59ca4ac authored by Dominic Meiser's avatar Dominic Meiser
Browse files

initial commit

parents
No related branches found
No related tags found
No related merge requests found
# test files
/2310*/
# rust/cargo
/target/
/Cargo.lock
# git
*.orig
# emacs
*~
\#*#
.#*#
# intellij
/.idea/
*.ipr
*.iml
# -*- 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" }
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
#![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<String>,
#[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());
}
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<Self> {
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<Date> {
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<Self> {
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<Time> {
let split = s.split(':');
let mut seconds = 0;
let mut micros = 0;
for (i, digits) in split.enumerate() {
// we cannot have more than 4 splits
if i > 3 {
bail!("Expected end of input, found ':'")
}
// we cannot process data after we have parsed micros
if micros != 0 {
bail!("Unexpected microsecond notation");
}
// the 4th split is subseconds, converting to micros
if i == 3 {
micros = digits.parse::<u32>()? * 1000000 / 60;
continue;
}
// add to seconds and potentially micros
seconds *= 60;
if let Some(idx) = digits.find('.') {
seconds += digits[0 .. idx].parse::<u32>()?;
let micros_digits = &digits[idx + 1 ..];
micros = micros_digits.parse::<u32>()?
* 10_u32.pow((6 - micros_digits.len()) as _);
} else {
seconds += digits.parse::<u32>()?;
}
}
Ok(Time { seconds, micros })
}
/// Format a time in hh:mm:ss.micros format.
pub fn format_time(t: Time) -> String {
let h = t.seconds / 3600;
let m = (t.seconds / 60) % 60;
let s = t.seconds % 60;
let mut out = String::new();
if h != 0 {
write!(out, "{h}:").unwrap();
}
if h != 0 || m != 0 {
write!(out, "{m:02}:{s:02}").unwrap();
} else {
write!(out, "{s}").unwrap();
}
if t.micros != 0 {
write!(out, ".{:06}", t.micros).unwrap();
out.truncate(out.trim_end_matches('0').len());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn test_time_parse_only(s: &str, t: Time) {
let time = parse_time(s).unwrap();
assert_eq!(time, t);
}
fn test_time(s: &str, t: Time) {
test_time_parse_only(s, t);
assert_eq!(format_time(t), s);
}
#[test]
fn test_time_0() {
test_time("0", Time {
seconds: 0,
micros: 0
});
}
#[test]
fn test_time_2_3() {
test_time("02:03", Time {
seconds: 123,
micros: 0
});
}
#[test]
fn test_time_1_2_3() {
test_time("1:02:03", Time {
seconds: 3723,
micros: 0
});
}
#[test]
fn test_time_subsecs() {
test_time_parse_only("1:02:03:30", Time {
seconds: 3723,
micros: 500000
});
}
#[test]
fn test_time_micros() {
test_time("1:02:03.5", Time {
seconds: 3723,
micros: 500000
});
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment