You've already forked flix
Model updates, bugfixes, and a new muxing cli tool
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use crate::model::MediaFile;
|
||||
use crate::parser::{Matcher, Selector, StreamFlag, StreamType};
|
||||
|
||||
pub fn mux_files<T>(
|
||||
dry_run: bool,
|
||||
files: &[MediaFile],
|
||||
selectors: &[Selector],
|
||||
fixed_length_start: impl FnOnce(usize) -> T,
|
||||
fixed_length_update: impl Fn(&mut T),
|
||||
fixed_length_end: impl FnOnce(T),
|
||||
print_fn: impl Fn(&T, &str),
|
||||
) {
|
||||
let mut progress = fixed_length_start(files.len());
|
||||
for file in files {
|
||||
if let Err(err) = mux(dry_run, file, selectors) {
|
||||
print_fn(&progress, &format!("{:?}", err));
|
||||
}
|
||||
fixed_length_update(&mut progress);
|
||||
}
|
||||
fixed_length_end(progress);
|
||||
}
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
fn mux(dry_run: bool, file: &MediaFile, selectors: &[Selector]) -> Result<()> {
|
||||
let mut command = Command::new("ffmpeg");
|
||||
let mut command = command.args(["-v", "error"]);
|
||||
|
||||
command = command.arg("-i");
|
||||
command = command.arg(file.path.as_os_str());
|
||||
|
||||
for selector in selectors {
|
||||
command = command.args(
|
||||
make_map_args(file, selector)
|
||||
.with_context(|| format!("Failed to mux {:?}", file.path))?,
|
||||
);
|
||||
}
|
||||
|
||||
command = command.args(["-c:v", "copy", "-c:a", "copy", "-c:s", "mov_text"]);
|
||||
command = command.args(["-movflags", "faststart+disable_chpl+write_colr"]);
|
||||
command = command.args(["-map_chapters", "-1"]);
|
||||
command = command.args(["-map_metadata", "-1"]);
|
||||
command = command.args(["-metadata:g", "encoding_tool=Skrundzflix"]);
|
||||
|
||||
for selector in selectors {
|
||||
command = command.args(
|
||||
make_metadata_args(file, selector)
|
||||
.with_context(|| format!("Failed to mux {:?}", file.path))?,
|
||||
);
|
||||
}
|
||||
|
||||
let temp_path = file.path.with_extension("mp4");
|
||||
command = command.arg(temp_path.file_name().expect("file name exists"));
|
||||
|
||||
if dry_run {
|
||||
print_command(command);
|
||||
} else {
|
||||
let output = command
|
||||
.output()
|
||||
.with_context(|| format!("Failed to mux {:?}", file.path))?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!(
|
||||
"ffmpeg failed for {:?}:\n\n{}",
|
||||
file.path,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn make_map_args(file: &MediaFile, selector: &Selector) -> Result<Vec<String>> {
|
||||
let source_index = 0;
|
||||
let stream_type = selector.stream_type.as_ref();
|
||||
let Some(stream_index) = find_stream_index(file, selector) else {
|
||||
if selector.optional {
|
||||
return Ok(vec![]);
|
||||
} else {
|
||||
anyhow::bail!("unsatisfied stream selection");
|
||||
}
|
||||
};
|
||||
|
||||
Ok(vec![
|
||||
String::from("-map"),
|
||||
format!("{}:{}:{}", source_index, stream_type, stream_index),
|
||||
])
|
||||
}
|
||||
|
||||
fn make_metadata_args(file: &MediaFile, selector: &Selector) -> Result<Vec<String>> {
|
||||
let stream_type = selector.stream_type.as_ref();
|
||||
let Some(stream_index) = find_stream_index(file, selector) else {
|
||||
if selector.optional {
|
||||
return Ok(vec![]);
|
||||
} else {
|
||||
anyhow::bail!("unsatisfied stream selection");
|
||||
}
|
||||
};
|
||||
|
||||
let stream_language = selector.language();
|
||||
|
||||
let mut args = vec![
|
||||
format!("-metadata:s:{}:{}", stream_type, stream_index),
|
||||
format!("language={}", stream_language),
|
||||
];
|
||||
|
||||
if selector.stream_type == StreamType::Subtitle {
|
||||
let sub_title = match selector.flag {
|
||||
Some(StreamFlag::Forced) => "Forced",
|
||||
Some(StreamFlag::Sdh) => "SDH",
|
||||
None => match stream_language {
|
||||
"eng" => "English",
|
||||
"jpn" => "Japanese",
|
||||
_ => anyhow::bail!("Unhandled subtitle language: {}", stream_language),
|
||||
},
|
||||
};
|
||||
|
||||
args.push(format!("-metadata:s:s:{}", stream_index));
|
||||
args.push(format!("title={}", sub_title));
|
||||
}
|
||||
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
fn find_stream_index(file: &MediaFile, selector: &Selector) -> Option<usize> {
|
||||
let needs_forced = selector.flag == Some(StreamFlag::Forced);
|
||||
let needs_sdh = selector.flag == Some(StreamFlag::Sdh);
|
||||
|
||||
match selector.stream_type {
|
||||
StreamType::Video => match selector.matcher {
|
||||
Matcher::Index(index) => (file.streams.video.len() > index).then_some(index),
|
||||
Matcher::Language(ref language) => file
|
||||
.streams
|
||||
.video
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| c.language() == Some(language.as_str()))
|
||||
.map(|(i, _)| i)
|
||||
.next(),
|
||||
Matcher::Codec(ref codec) => file
|
||||
.streams
|
||||
.video
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| c.codec() == codec)
|
||||
.map(|(i, _)| i)
|
||||
.next(),
|
||||
},
|
||||
StreamType::Audio => match selector.matcher {
|
||||
Matcher::Index(index) => (file.streams.audio.len() > index).then_some(index),
|
||||
Matcher::Language(ref language) => file
|
||||
.streams
|
||||
.audio
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| c.language() == Some(language.as_str()))
|
||||
.map(|(i, _)| i)
|
||||
.next(),
|
||||
Matcher::Codec(ref codec) => file
|
||||
.streams
|
||||
.audio
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| c.codec() == codec)
|
||||
.map(|(i, _)| i)
|
||||
.next(),
|
||||
},
|
||||
StreamType::Subtitle => match selector.matcher {
|
||||
Matcher::Index(index) => (file.streams.subtitle.len() > index).then_some(index),
|
||||
Matcher::Language(ref language) => {
|
||||
file.streams
|
||||
.subtitle
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| c.language() == Some(language.as_str()))
|
||||
.filter(|(_, c)| {
|
||||
c.title()
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase()
|
||||
.contains("forced") == needs_forced
|
||||
})
|
||||
.filter(|(_, c)| {
|
||||
c.title()
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase()
|
||||
.contains("sdh") == needs_sdh
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.next()
|
||||
}
|
||||
Matcher::Codec(ref codec) => file
|
||||
.streams
|
||||
.subtitle
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| c.codec() == codec)
|
||||
.map(|(i, _)| i)
|
||||
.next(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn print_command(cmd: &Command) {
|
||||
let program = cmd.get_program().to_string_lossy();
|
||||
|
||||
let args = cmd
|
||||
.get_args()
|
||||
.map(|a| shell_escape(a.to_string_lossy().as_ref()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
println!("{} {}", program, args);
|
||||
}
|
||||
|
||||
fn shell_escape(s: &str) -> String {
|
||||
if s.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || "-_./".contains(c))
|
||||
{
|
||||
s.to_string()
|
||||
} else {
|
||||
format!("{:?}", s)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user