You've already forked flix
250 lines
6.2 KiB
Rust
250 lines
6.2 KiB
Rust
use std::process::Command;
|
|
|
|
use anyhow::{Context as _, Result};
|
|
|
|
use crate::model::MediaFile;
|
|
use crate::parser::{Matcher, Selector, StreamFlag, StreamType};
|
|
|
|
#[derive(Default)]
|
|
struct OutputIndex {
|
|
video: usize,
|
|
audio: usize,
|
|
subtitle: usize,
|
|
}
|
|
|
|
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(["-strict", "-2"]);
|
|
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"]);
|
|
|
|
let mut ouput_index = OutputIndex::default();
|
|
for selector in selectors {
|
|
command = command.args(
|
|
make_metadata_args(file, selector, &mut ouput_index)
|
|
.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,
|
|
index: &mut OutputIndex,
|
|
) -> Result<Vec<String>> {
|
|
let stream_type = selector.stream_type.as_ref();
|
|
let stream_language = selector.language();
|
|
|
|
// Bail early if the stream is not selected
|
|
let Some(_) = find_stream_index(file, selector) else {
|
|
if selector.optional {
|
|
return Ok(vec![]);
|
|
} else {
|
|
anyhow::bail!("unsatisfied stream selection");
|
|
}
|
|
};
|
|
|
|
let counter = match selector.stream_type {
|
|
StreamType::Video => &mut index.video,
|
|
StreamType::Audio => &mut index.audio,
|
|
StreamType::Subtitle => &mut index.subtitle,
|
|
};
|
|
let stream_index = *counter;
|
|
*counter = counter.saturating_add(1);
|
|
|
|
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)
|
|
}
|
|
}
|