Skip to content

Commit

Permalink
📖 Add Chapterer
Browse files Browse the repository at this point in the history
Closes #42
  • Loading branch information
tgotwig committed Jan 13, 2024
1 parent e827f1c commit 16f658a
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 17 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## 🎉 [Unreleased]

### Added

- **Chapterer** which creates `output.*` with chapters in it, everything in between the first `-` till the fill extension of the input files will be used as chapter titles 📖. Can be skipped by `--skip-chapterer`.

### Fixed

- Keep subtitles with merger
Expand Down
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
graph LR;
Video_A-->Vidmerger;
Video_B-->Vidmerger;
Vidmerger-->FFmpeg;
FFmpeg-->FPS_Changer;
FPS_Changer-->Video_A+B;
FFmpeg-->Video_A+B;
Vidmerger-->FPS_Changer;
FPS_Changer-->Merger;
Merger-->Chapterer;
Chapterer-->Video_A+B;
Vidmerger-->Merger;
Merger-->Video_A+B;
```

<p align="center"><img src="img/demo.svg" alt="fusion gif"/></p>
Expand All @@ -24,6 +27,9 @@ Vidmerger is a command-line-tool which uses **ffmpeg** to merge multiple video-f
Here is the usage help of vidmerger 🤗

```shell
A wrapper around ffmpeg which simlifies merging multiple videos 🎞 Everything in between the first
`-` till the fill extension of the input files will be used as chapter titles.

USAGE:
vidmerger [OPTIONS] <TARGET_DIR>

Expand All @@ -37,10 +43,10 @@ OPTIONS:
merges them
-h, --help Print help information
--shutdown For doing a shutdown at the end (needs sudo)
--skip-chapterer Skips the chapterer
--skip-fps-changer Skips the fps changer
--skip-wait Skips the wait time for reading
-V, --version Print version information
```
-V, --version Print version information```
## ✨ Installing / Getting started
Expand Down
6 changes: 5 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ impl Cli {
let matches = Command::new("vidmerger")
.version("0.3.1")
.author("Thomas Gotwig")
.about("A wrapper around ffmpeg which simlifies merging multiple videos 🎞")
.about("A wrapper around ffmpeg which simplifies merging multiple videos 🎞 Everything in between the first `-` till the fill extension of the input files will be used as chapter titles 📖.")
.arg(Arg::new("TARGET_DIR")
.help("Sets the input file to use")
.required(true)
Expand All @@ -34,6 +34,10 @@ impl Cli {
.long("skip-fps-changer")
.help("Skips the fps changer")
)
.arg(Arg::new("skip-chapterer")
.long("skip-chapterer")
.help("Skips the chapterer")
)
.arg(Arg::new("skip-wait")
.long("skip-wait")
.help("Skips the wait time for reading")
Expand Down
34 changes: 34 additions & 0 deletions src/commanders/_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@ pub fn merge(input: String, output: String) -> Result<Child, std::io::Error> {
execute_cmd(cmd)
}

pub fn merge_with_chapters(
input_file_for_chapterer: &str,
file_path: PathBuf,
output_file_for_chapterer: &str,
) -> Result<Child, std::io::Error> {
let cmd = format!(
"ffmpeg -y -i {} -i {} -map 0 -map_metadata 1 -codec copy {}",
&input_file_for_chapterer,
file_path.to_str().unwrap(),
output_file_for_chapterer
);

println!("🚀 Calling:\n");
println!("- {}\n", cmd);

execute_cmd(cmd)
}

pub fn run_ffmpeg_info_command(file_to_merge: &PathBuf) -> Result<Output, Error> {
Command::new("ffmpeg")
.args(["-i", &file_to_merge.to_slash().unwrap()])
Expand All @@ -40,6 +58,22 @@ pub fn adjust_fps_by_ffmpeg(
new_file_location
}

pub fn get_media_seconds(media_path: &str) -> Result<f64, Box<Error>> {
let cmd = format!(
"ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 '{}'",
media_path
);

println!("🚀 Calling:\n");
println!("- {}\n", cmd);
let res = execute_cmd(cmd);

let output = res.unwrap().wait_with_output().unwrap();
let output = String::from_utf8(output.stdout).unwrap();
let output = output.trim().parse::<f64>().unwrap();
Ok(output)
}

fn execute_cmd(cmd: String) -> Result<Child, std::io::Error> {
let (interpreter, arg) = if cfg!(target_os = "windows") {
("cmd", "/c")
Expand Down
140 changes: 140 additions & 0 deletions src/commanders/chapterer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use crate::commanders::_cmd;
use path_slash::PathExt;
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::str;

pub fn execute(
files_to_merge_as_strings: Vec<String>,
tmp_dir: PathBuf,
ffmpeg_output_file: PathBuf,
file_format: &String,
) {
println!("----------------------------------------------------------------");
println!("📖 Start Chapterer\n");

let mut start_time = 0;
let mut metadata_string = String::from(";FFMETADATA1\n");

for path in files_to_merge_as_strings {
let title = extract_title(&path, file_format);
let duration = _cmd::get_media_seconds(&path).unwrap() as i64;

metadata_string.push_str(&format!(
"\n[CHAPTER]\nTIMEBASE=1/1\nSTART={}\nEND={}\ntitle={}\n",
start_time,
start_time + duration,
title
));

start_time += duration;
}

let mut file_path = tmp_dir;
file_path.push("chapters.txt");
let mut file = File::create(&file_path).expect("Failed to create file");
file.write_all(metadata_string.as_bytes())
.expect("Failed to write to file");

let input_file_for_chapterer: String = ffmpeg_output_file.to_slash().unwrap().to_string();
let mut output_with_chapters = ffmpeg_output_file.clone();
output_with_chapters.set_file_name(format!("output_with_chapters.{}", file_format));
let output_file_for_chapterer: String = output_with_chapters.to_slash().unwrap().to_string();

_cmd::merge_with_chapters(
&input_file_for_chapterer,
file_path,
&output_file_for_chapterer,
)
.unwrap()
.wait_with_output()
.unwrap();

fs::remove_file(Path::new(&input_file_for_chapterer)).unwrap();
fs::rename(output_file_for_chapterer, ffmpeg_output_file).unwrap();

println!("✅ Video with chapters created successfully.");
}

fn extract_title(path: &str, file_format: &str) -> String {
let file_name = path.split('/').last().unwrap_or("");
let mut parts = file_name.splitn(2, '-');
parts.next(); // Skip the part before the first '-'
let content_with_extension = parts.next().unwrap_or("").trim();

let format_str = format!(".{}", file_format);
content_with_extension
.split(&format_str)
.next()
.unwrap_or("")
.trim()
.to_string()
}

#[cfg(test)]
mod test_extract_title {
use super::extract_title;

#[test]
fn test_extract_title() {
let path = "path/to/video-Title of Video.mp4";
assert_eq!(extract_title(path, "mp4"), "Title of Video");
}

#[test]
fn test_extract_title_with_dot() {
let path = "path/to/video-[1.0] Title of Video.mp4";
assert_eq!(extract_title(path, "mp4"), "[1.0] Title of Video");
}

#[test]
fn test_extract_title_with_no_dash() {
let path = "path/to/videoTitle of Video.mp4";
assert_eq!(extract_title(path, "mp4"), "");
}

#[test]
fn test_extract_title_with_no_extension() {
let path = "path/to/video-Title of Video";
assert_eq!(extract_title(path, "mp4"), "Title of Video");
}

#[test]
fn test_extract_title_with_multiple_dashes() {
let path = "path/to/video-Title-of-Video.mp4";
assert_eq!(extract_title(path, "mp4"), "Title-of-Video");
}

#[test]
fn test_extract_title_with_empty_path() {
let path = "";
assert_eq!(extract_title(path, "mp4"), "");
}

// FAIL
#[test]
fn test_extract_title_with_only_dashes() {
let path = "---";
assert_eq!(extract_title(path, "mp4"), "--");
}

#[test]
fn test_extract_title_with_special_characters() {
let path = "path/to/video-Title_@_of_Video.mp4";
assert_eq!(extract_title(path, "mp4"), "Title_@_of_Video");
}

#[test]
fn test_extract_title_with_different_format() {
let path = "path/to/video-Title of Video.avi";
assert_eq!(extract_title(path, "mp4"), "Title of Video.avi");
}

#[test]
fn test_extract_title_with_nested_path() {
let path = "path/to/some/other/folder/video-Title.mp4";
assert_eq!(extract_title(path, "mp4"), "Title");
}
}
2 changes: 1 addition & 1 deletion src/commanders/merger.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::commanders::_cmd;

pub fn merge(input: String, output: String, file_format: String) {
pub fn merge(input: String, output: String, file_format: &String) {
let child = _cmd::merge(input, output);

let res = child.unwrap().wait_with_output();
Expand Down
1 change: 1 addition & 0 deletions src/commanders/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod _cmd;

pub mod chapterer;
pub mod fps_adjuster;
pub mod fps_reader;
pub mod merger;
10 changes: 5 additions & 5 deletions src/helpers/str_helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub fn split(string: String) -> Vec<String> {
string.split(',').map(|s| s.to_string()).collect()
}

pub fn gen_input_file_content_for_ffmpeg(files_to_merge: Vec<String>) -> String {
pub fn gen_input_file_content_for_ffmpeg(files_to_merge: &Vec<String>) -> String {
let mut ffmpeg_input_content = String::new();

for file_to_merge in files_to_merge {
Expand Down Expand Up @@ -66,7 +66,7 @@ mod tests {
#[test]
fn test_gen_file_to_merge_with_one_input() {
let files_to_merge = vec![String::from("/1.mp4")];
let ffmpeg_input_content = gen_input_file_content_for_ffmpeg(files_to_merge);
let ffmpeg_input_content = gen_input_file_content_for_ffmpeg(&files_to_merge);
assert_eq!(ffmpeg_input_content, "file '/1.mp4'\n")
}

Expand All @@ -77,7 +77,7 @@ mod tests {
String::from("/2.mp4"),
String::from("/3.mp4"),
];
let ffmpeg_input_content = gen_input_file_content_for_ffmpeg(files_to_merge);
let ffmpeg_input_content = gen_input_file_content_for_ffmpeg(&files_to_merge);
assert_eq!(
ffmpeg_input_content,
"file '/1.mp4'\nfile '/2.mp4'\nfile '/3.mp4'\n"
Expand All @@ -87,14 +87,14 @@ mod tests {
#[test]
fn test_gen_file_to_merge_with_empty_input() {
let files_to_merge = vec![];
let ffmpeg_input_content = gen_input_file_content_for_ffmpeg(files_to_merge);
let ffmpeg_input_content = gen_input_file_content_for_ffmpeg(&files_to_merge);
assert_eq!(ffmpeg_input_content, "")
}

#[test]
fn test_gen_file_to_merge_with_empty_string_input() {
let files_to_merge = vec![String::from("")];
let ffmpeg_input_content = gen_input_file_content_for_ffmpeg(files_to_merge);
let ffmpeg_input_content = gen_input_file_content_for_ffmpeg(&files_to_merge);
assert_eq!(ffmpeg_input_content, "")
}

Expand Down
18 changes: 15 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ fn main() -> Result<(), Error> {
.to_string();
let should_shutdown = matches.is_present("shutdown");
let skip_fps_changer = matches.is_present("skip-fps-changer");
let skip_chapterer = matches.is_present("skip-chapterer");
let skip_wait = matches.is_present("skip-wait");
let fps_from_cli = matches
.value_of("fps")
Expand All @@ -50,7 +51,8 @@ fn main() -> Result<(), Error> {
let all_files_on_target_dir: Vec<PathBuf> = read_dir(target_dir).unwrap();
let mut files_to_merge = filter_files(all_files_on_target_dir, &file_format);
let mut files_to_merge_as_strings = path_bufs_to_sorted_strings(&files_to_merge);
let mut ffmpeg_input_content = gen_input_file_content_for_ffmpeg(files_to_merge_as_strings);
let mut ffmpeg_input_content =
gen_input_file_content_for_ffmpeg(&files_to_merge_as_strings);

if !ffmpeg_input_content.is_empty() {
println!("\n----------------------------------------------------------------");
Expand All @@ -66,7 +68,8 @@ fn main() -> Result<(), Error> {
if !skip_fps_changer {
files_to_merge = change_fps(files_to_merge, &tmp_dir, fps_from_cli);
files_to_merge_as_strings = path_bufs_to_sorted_strings(&files_to_merge);
ffmpeg_input_content = gen_input_file_content_for_ffmpeg(files_to_merge_as_strings);
ffmpeg_input_content =
gen_input_file_content_for_ffmpeg(&files_to_merge_as_strings);
}

println!("----------------------------------------------------------------");
Expand All @@ -78,8 +81,17 @@ fn main() -> Result<(), Error> {
commanders::merger::merge(
ffmpeg_input_file.to_slash().unwrap(),
ffmpeg_output_file.to_slash().unwrap().to_string(),
file_format,
&file_format,
);

if !skip_chapterer {
commanders::chapterer::execute(
files_to_merge_as_strings,
tmp_dir,
ffmpeg_output_file,
&file_format,
);
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ mod integration {
.success(),
);

assert!(get_video_info(&format!("data/{}/output.mp4", test_name)).contains("58.41 fps"));
assert!(get_video_info(&format!("data/{}/output.mp4", test_name)).contains("58.19 fps"));
check_for_merged_file(test_name, "output.mp4");
}

Expand Down

0 comments on commit 16f658a

Please sign in to comment.