mod srt;
use clap::ValueEnum;
use prettytable::Cell;
use prettytable::Row;
use prettytable::Table;
use serde::Deserialize;
use serde::Serialize;
pub use srt::OverlapFixMode;
pub use srt::SrtFile;
use tokio::io::AsyncBufReadExt;
use tokio::io::BufReader;
use tokio::io::stdin;
use std::io::Cursor;
use std::{fs::File, path::Path};
use crate::common::read_multiple_file_to_string;
use crate::{
common::same_path_with,
ffmpeg::{FfmpegError, FfmpegTool},
};
pub async fn extract_sub_srt<P: AsRef<Path>>(file: P, sub: P) -> Result<(), FfmpegError> {
let options = vec![
"-i",
file.as_ref().to_str().unwrap_or(""),
"-map",
"0:s:0",
"-c",
"copy",
sub.as_ref().to_str().unwrap_or(""),
];
FfmpegTool::Ffmpeg
.exec_with_options(None::<&'static str>, Some(options))
.await?;
Ok(())
}
pub fn update_srt_time<P: AsRef<Path>>(file: P, ms: i64, mode: OverlapFixMode) {
let p = file.as_ref();
let new_file = same_path_with(p, "mod", "_").expect("new file name error");
let f = File::open(p).expect("failed to open the subtitle file");
let mut srt_file = SrtFile::read(f).expect("failed to parse the srt file");
srt_file
.adjust_timestamps(ms, mode)
.expect("failed to adjust the timestamps");
let mut nf = File::create(new_file).expect("failed to create the new srt file");
srt_file
.write(&mut nf)
.expect("failed to write content to new srt file");
}
#[derive(Debug, Deserialize, Serialize)]
struct FfprobeOutput {
streams: Vec<FfprobeStream>,
}
#[derive(Debug, Deserialize, Serialize)]
struct FfprobeStream {
index: u32,
codec_type: String,
codec_name: String,
start_time: String,
duration_ts: u32,
tags: Option<FfprobeTags>,
}
#[derive(Debug, Deserialize, Serialize)]
struct FfprobeTags {
language: Option<String>,
title: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct SubtitleStreamInfo {
pub index: u32,
pub codec_name: String,
pub duration: u32,
pub language: Option<String>,
pub title: Option<String>,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum OutputFormat {
Json,
#[clap(name = "tab")]
Table,
List,
}
pub async fn list_all_subtitle_stream(
file: impl AsRef<Path>,
format: OutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let file_path = file.as_ref();
if !file_path.exists() {
return Err(format!("文件不存在: {}", file_path.display()).into());
}
let mut args: Vec<String> = vec![
"-v".to_string(),
"quiet".to_string(),
"-print_format".to_string(),
"json".to_string(),
"-show_streams".to_string(),
"-select_streams".to_string(),
"s".to_string(),
"--".to_string(), ];
args.push(file_path.to_string_lossy().to_string());
let (stdout, _) = FfmpegTool::Ffprobe
.exec_with_options(None::<&'static str>, Some(args))
.await?;
if stdout.is_empty() {
println!("未找到任何字幕流。");
return Ok(());
}
let output: FfprobeOutput = serde_json::from_str(&stdout)?;
let subtitle_streams: Vec<SubtitleStreamInfo> = output
.streams
.into_iter()
.map(|stream| SubtitleStreamInfo {
index: stream.index,
codec_name: stream.codec_name,
language: stream.tags.as_ref().and_then(|tags| tags.language.clone()),
title: stream.tags.as_ref().and_then(|tags| tags.title.clone()),
duration: stream.duration_ts,
})
.collect();
match format {
OutputFormat::Json => {
let json_output = serde_json::to_string_pretty(&subtitle_streams)?;
println!("{}", json_output);
}
OutputFormat::Table => {
let mut table = Table::new();
table.add_row(Row::new(vec![
Cell::new("Index"),
Cell::new("Codec Name"),
Cell::new("Duration(ms)"),
Cell::new("Language"),
Cell::new("Title"),
]));
for stream in subtitle_streams {
table.add_row(Row::new(vec![
Cell::new(&stream.index.to_string()),
Cell::new(&stream.codec_name),
Cell::new(&stream.duration.to_string()),
Cell::new(&stream.language.unwrap_or_else(|| "N/A".to_string())),
Cell::new(&stream.title.unwrap_or_else(|| "N/A".to_string())),
]));
}
table.printstd();
}
OutputFormat::List => {
for stream in subtitle_streams {
println!(
"Index({}) Codec Name({}) Duration({}ms) Language({}) Title({})",
stream.index,
stream.codec_name,
stream.duration,
stream.language.unwrap_or_else(|| "N/A".to_string()),
stream.title.unwrap_or_else(|| "N/A".to_string())
);
}
}
}
Ok(())
}
pub async fn compare_two_srt_file<P: AsRef<Path> + Send + 'static>(
file_1: P,
file_2: P,
interactive: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let mut file_contents = read_multiple_file_to_string(vec![file_1, file_2]).await?;
let srt_1 = SrtFile::read(Cursor::new(file_contents.remove(0)))?;
let srt_2 = SrtFile::read(Cursor::new(file_contents.remove(0)))?;
let mut print_stream = srt_1.iter().zip(srt_2.iter()).map(|(entry_1, entry_2)| {
let l_str_vec = entry_1.to_entry_str();
let r_str_vec = entry_2.to_entry_str();
let mut table = Table::new();
l_str_vec.iter().zip(r_str_vec.iter()).for_each(|(l, r)| {
table.add_row(Row::new(vec![Cell::new(l), Cell::new(r)]));
});
table
});
let mut reader = BufReader::new(stdin());
let mut input_buf = String::new();
let mut counter = 1;
loop {
while counter > 0 {
if let Some(nxt) = print_stream.next() {
println!("{}", nxt);
if interactive {
counter -= 1;
}
} else {
break;
}
}
if counter > 0 {
break;
}
println!("> n(next) q(quit) 1~9(show next 1~9)");
reader.read_line(&mut input_buf).await?;
let input = input_buf.trim();
match input {
"n" | "1" => {
counter = 1;
}
"q" => {
break;
}
"2" => counter = 2,
"3" => counter = 3,
"4" => counter = 4,
"5" => counter = 5,
"6" => counter = 6,
"7" => counter = 7,
"8" => counter = 8,
"9" => counter = 9,
_ => println!("invalid key input, retry"),
}
input_buf.clear();
}
Ok(())
}