mirror of
https://github.com/crunchy-labs/crunchy-cli.git
synced 2026-01-21 04:02:00 -06:00
Fix empty subtitles if multiple subtitle formats are used (#398)
This commit is contained in:
parent
173292ff32
commit
442173c08c
3 changed files with 43 additions and 92 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -376,6 +376,7 @@ dependencies = [
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rsubs-lib",
|
||||||
"rustls-native-certs",
|
"rustls-native-certs",
|
||||||
"rusty-chromaprint",
|
"rusty-chromaprint",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -1501,6 +1502,16 @@ version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
|
checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rsubs-lib"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0df7559a05635a4132b737c736ee286af83f3969cb98d9028d17d333e6b41cc5"
|
||||||
|
dependencies = [
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rubato"
|
name = "rubato"
|
||||||
version = "0.14.1"
|
version = "0.14.1"
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ log = { version = "0.4", features = ["std"] }
|
||||||
num_cpus = "1.16"
|
num_cpus = "1.16"
|
||||||
regex = "1.10"
|
regex = "1.10"
|
||||||
reqwest = { version = "0.12", features = ["socks", "stream"] }
|
reqwest = { version = "0.12", features = ["socks", "stream"] }
|
||||||
|
rsubs-lib = "0.2"
|
||||||
rusty-chromaprint = "0.2"
|
rusty-chromaprint = "0.2"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use indicatif::{ProgressBar, ProgressDrawTarget, ProgressFinish, ProgressStyle};
|
||||||
use log::{debug, warn, LevelFilter};
|
use log::{debug, warn, LevelFilter};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
use rsubs_lib::{ssa, vtt};
|
||||||
use std::borrow::Borrow;
|
use std::borrow::Borrow;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
|
@ -931,13 +932,38 @@ impl Downloader {
|
||||||
subtitle: Subtitle,
|
subtitle: Subtitle,
|
||||||
max_length: TimeDelta,
|
max_length: TimeDelta,
|
||||||
) -> Result<TempPath> {
|
) -> Result<TempPath> {
|
||||||
|
let buf = subtitle.data().await?;
|
||||||
|
let mut ass = match subtitle.format.as_str() {
|
||||||
|
"ass" => ssa::parse(String::from_utf8_lossy(&buf).to_string()),
|
||||||
|
"vtt" => vtt::parse(String::from_utf8_lossy(&buf).to_string()).to_ass(),
|
||||||
|
_ => bail!("unknown subtitle format: {}", subtitle.format),
|
||||||
|
};
|
||||||
|
// subtitles aren't always correct sorted and video players may have issues with that. to
|
||||||
|
// prevent issues, the subtitles are sorted
|
||||||
|
ass.events
|
||||||
|
.sort_by(|a, b| a.line_start.total_ms().cmp(&b.line_start.total_ms()));
|
||||||
|
// it might be the case that the start and/or end time are greater than the actual video
|
||||||
|
// length. this might also result in issues with video players, thus the times are stripped
|
||||||
|
// to be maxim
|
||||||
|
for i in (0..ass.events.len()).rev() {
|
||||||
|
if ass.events[i].line_end.total_ms() > max_length.num_milliseconds() as u32 {
|
||||||
|
if ass.events[i].line_start.total_ms() > max_length.num_milliseconds() as u32 {
|
||||||
|
ass.events[i]
|
||||||
|
.line_start
|
||||||
|
.set_ms(max_length.num_milliseconds() as u32);
|
||||||
|
}
|
||||||
|
ass.events[i]
|
||||||
|
.line_end
|
||||||
|
.set_ms(max_length.num_milliseconds() as u32);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let tempfile = tempfile(".ass")?;
|
let tempfile = tempfile(".ass")?;
|
||||||
let (mut file, path) = tempfile.into_parts();
|
let path = tempfile.into_temp_path();
|
||||||
|
|
||||||
let mut buf = subtitle.data().await?;
|
ass.to_file(path.to_string_lossy().to_string().as_str())?;
|
||||||
fix_subtitles(&mut buf, max_length);
|
|
||||||
|
|
||||||
file.write_all(buf.as_slice())?;
|
|
||||||
|
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
@ -1301,93 +1327,6 @@ fn get_subtitle_stats(path: &Path) -> Result<Vec<String>> {
|
||||||
Ok(fonts)
|
Ok(fonts)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fix the subtitles in multiple ways as Crunchyroll sometimes delivers them malformed.
|
|
||||||
///
|
|
||||||
/// Look and feel fix: Add `ScaledBorderAndShadows: yes` to subtitles; without it they look very
|
|
||||||
/// messy on some video players. See
|
|
||||||
/// [crunchy-labs/crunchy-cli#66](https://github.com/crunchy-labs/crunchy-cli/issues/66) for more
|
|
||||||
/// information.
|
|
||||||
/// Length fix: Sometimes subtitles have an unnecessary long entry which exceeds the video length,
|
|
||||||
/// some video players can't handle this correctly. To prevent this, the subtitles must be checked
|
|
||||||
/// if any entry is longer than the video length and if so the entry ending must be hard set to not
|
|
||||||
/// exceed the video length. See [crunchy-labs/crunchy-cli#32](https://github.com/crunchy-labs/crunchy-cli/issues/32)
|
|
||||||
/// for more information.
|
|
||||||
/// Sort fix: Sometimes subtitle entries aren't sorted correctly by time which confuses some video
|
|
||||||
/// players. To prevent this, the subtitle entries must be manually sorted. See
|
|
||||||
/// [crunchy-labs/crunchy-cli#208](https://github.com/crunchy-labs/crunchy-cli/issues/208) for more
|
|
||||||
/// information.
|
|
||||||
fn fix_subtitles(raw: &mut Vec<u8>, max_length: TimeDelta) {
|
|
||||||
let re = Regex::new(
|
|
||||||
r"^Dialogue:\s(?P<layer>\d+),(?P<start>\d+:\d+:\d+\.\d+),(?P<end>\d+:\d+:\d+\.\d+),",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut entries = (vec![], vec![]);
|
|
||||||
|
|
||||||
let mut as_lines: Vec<String> = String::from_utf8_lossy(raw.as_slice())
|
|
||||||
.split('\n')
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for (i, line) in as_lines.iter_mut().enumerate() {
|
|
||||||
if line.trim() == "[Script Info]" {
|
|
||||||
line.push_str("\nScaledBorderAndShadow: yes")
|
|
||||||
} else if let Some(capture) = re.captures(line) {
|
|
||||||
let mut start = capture
|
|
||||||
.name("start")
|
|
||||||
.map_or(NaiveTime::default(), |s| {
|
|
||||||
NaiveTime::parse_from_str(s.as_str(), "%H:%M:%S.%f").unwrap()
|
|
||||||
})
|
|
||||||
.signed_duration_since(NaiveTime::MIN);
|
|
||||||
let mut end = capture
|
|
||||||
.name("end")
|
|
||||||
.map_or(NaiveTime::default(), |e| {
|
|
||||||
NaiveTime::parse_from_str(e.as_str(), "%H:%M:%S.%f").unwrap()
|
|
||||||
})
|
|
||||||
.signed_duration_since(NaiveTime::MIN);
|
|
||||||
|
|
||||||
if start > max_length || end > max_length {
|
|
||||||
let layer = capture
|
|
||||||
.name("layer")
|
|
||||||
.map_or(0, |l| i32::from_str(l.as_str()).unwrap());
|
|
||||||
|
|
||||||
if start > max_length {
|
|
||||||
start = max_length;
|
|
||||||
}
|
|
||||||
if start > max_length || end > max_length {
|
|
||||||
end = max_length;
|
|
||||||
}
|
|
||||||
|
|
||||||
*line = re
|
|
||||||
.replace(
|
|
||||||
line,
|
|
||||||
format!(
|
|
||||||
"Dialogue: {},{},{},",
|
|
||||||
layer,
|
|
||||||
format_time_delta(&start),
|
|
||||||
format_time_delta(&end)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
entries.0.push((start, i));
|
|
||||||
entries.1.push(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.0.sort_by(|(a, _), (b, _)| a.cmp(b));
|
|
||||||
for i in 0..entries.0.len() {
|
|
||||||
let (_, original_position) = entries.0[i];
|
|
||||||
let new_position = entries.1[i];
|
|
||||||
|
|
||||||
if original_position != new_position {
|
|
||||||
as_lines.swap(original_position, new_position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*raw = as_lines.join("\n").into_bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_ffmpeg_chapters(
|
fn write_ffmpeg_chapters(
|
||||||
file: &mut fs::File,
|
file: &mut fs::File,
|
||||||
video_len: TimeDelta,
|
video_len: TimeDelta,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue