mirror of
https://github.com/crunchy-labs/crunchy-cli.git
synced 2026-01-21 20:22:01 -06:00
Add hardsubs manually to download videos (#81)
This commit is contained in:
parent
17233f2fd2
commit
6d1f8d49f6
7 changed files with 233 additions and 212 deletions
|
|
@ -1,4 +1,4 @@
|
|||
use crunchyroll_rs::media::VariantData;
|
||||
use crunchyroll_rs::media::{StreamSubtitle, VariantData};
|
||||
use crunchyroll_rs::{Episode, Locale, Media, Movie};
|
||||
use log::warn;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -10,6 +10,7 @@ pub struct Format {
|
|||
pub description: String,
|
||||
|
||||
pub audio: Locale,
|
||||
pub subtitles: Vec<StreamSubtitle>,
|
||||
|
||||
pub duration: Duration,
|
||||
pub stream: VariantData,
|
||||
|
|
@ -31,12 +32,14 @@ impl Format {
|
|||
episode: &Media<Episode>,
|
||||
season_episodes: &Vec<Media<Episode>>,
|
||||
stream: VariantData,
|
||||
subtitles: Vec<StreamSubtitle>,
|
||||
) -> Self {
|
||||
Self {
|
||||
title: episode.title.clone(),
|
||||
description: episode.description.clone(),
|
||||
|
||||
audio: episode.metadata.audio_locale.clone(),
|
||||
subtitles,
|
||||
|
||||
duration: episode.metadata.duration.to_std().unwrap(),
|
||||
stream,
|
||||
|
|
@ -78,6 +81,7 @@ impl Format {
|
|||
|
||||
duration: movie.metadata.duration.to_std().unwrap(),
|
||||
stream,
|
||||
subtitles: vec![],
|
||||
|
||||
series_id: movie.metadata.movie_listing_id.clone(),
|
||||
series_name: movie.metadata.movie_listing_title.clone(),
|
||||
|
|
|
|||
|
|
@ -7,3 +7,4 @@ pub mod os;
|
|||
pub mod parse;
|
||||
pub mod sort;
|
||||
pub mod subtitle;
|
||||
pub mod video;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
use crate::utils::os::tempfile;
|
||||
use anyhow::Result;
|
||||
use chrono::NaiveTime;
|
||||
use crunchyroll_rs::media::StreamSubtitle;
|
||||
use crunchyroll_rs::Locale;
|
||||
use regex::Regex;
|
||||
use std::io::Write;
|
||||
use tempfile::TempPath;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Subtitle {
|
||||
|
|
@ -9,3 +15,105 @@ pub struct Subtitle {
|
|||
pub forced: bool,
|
||||
pub primary: bool,
|
||||
}
|
||||
|
||||
pub async fn download_subtitle(
|
||||
subtitle: StreamSubtitle,
|
||||
max_length: NaiveTime,
|
||||
) -> Result<TempPath> {
|
||||
let tempfile = tempfile(".ass")?;
|
||||
let (mut file, path) = tempfile.into_parts();
|
||||
|
||||
let mut buf = vec![];
|
||||
subtitle.write_to(&mut buf).await?;
|
||||
buf = fix_subtitle_look_and_feel(buf);
|
||||
buf = fix_subtitle_length(buf, max_length);
|
||||
|
||||
file.write_all(buf.as_slice())?;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn fix_subtitle_look_and_feel(raw: Vec<u8>) -> Vec<u8> {
|
||||
let mut script_info = false;
|
||||
let mut new = String::new();
|
||||
|
||||
for line in String::from_utf8_lossy(raw.as_slice()).split('\n') {
|
||||
if line.trim().starts_with('[') && script_info {
|
||||
new.push_str("ScaledBorderAndShadow: yes\n");
|
||||
script_info = false
|
||||
} else if line.trim() == "[Script Info]" {
|
||||
script_info = true
|
||||
}
|
||||
new.push_str(line);
|
||||
new.push('\n')
|
||||
}
|
||||
|
||||
new.into_bytes()
|
||||
}
|
||||
|
||||
/// Fix the length of subtitles to a specified maximum amount. This is required because sometimes
|
||||
/// subtitles have an unnecessary entry long after the actual video ends with artificially extends
|
||||
/// the video length on some video players. To prevent this, the video length must be hard set. See
|
||||
/// [crunchy-labs/crunchy-cli#32](https://github.com/crunchy-labs/crunchy-cli/issues/32) for more
|
||||
/// information.
|
||||
fn fix_subtitle_length(raw: Vec<u8>, max_length: NaiveTime) -> Vec<u8> {
|
||||
let re =
|
||||
Regex::new(r#"^Dialogue:\s\d+,(?P<start>\d+:\d+:\d+\.\d+),(?P<end>\d+:\d+:\d+\.\d+),"#)
|
||||
.unwrap();
|
||||
|
||||
// chrono panics if we try to format NaiveTime with `%2f` and the nano seconds has more than 2
|
||||
// digits so them have to be reduced manually to avoid the panic
|
||||
fn format_naive_time(native_time: NaiveTime) -> String {
|
||||
let formatted_time = native_time.format("%f").to_string();
|
||||
format!(
|
||||
"{}.{}",
|
||||
native_time.format("%T"),
|
||||
if formatted_time.len() <= 2 {
|
||||
native_time.format("%2f").to_string()
|
||||
} else {
|
||||
formatted_time.split_at(2).0.parse().unwrap()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let length_as_string = format_naive_time(max_length);
|
||||
let mut new = String::new();
|
||||
|
||||
for line in String::from_utf8_lossy(raw.as_slice()).split('\n') {
|
||||
if let Some(capture) = re.captures(line) {
|
||||
let start = capture.name("start").map_or(NaiveTime::default(), |s| {
|
||||
NaiveTime::parse_from_str(s.as_str(), "%H:%M:%S.%f").unwrap()
|
||||
});
|
||||
let end = capture.name("end").map_or(NaiveTime::default(), |s| {
|
||||
NaiveTime::parse_from_str(s.as_str(), "%H:%M:%S.%f").unwrap()
|
||||
});
|
||||
|
||||
if start > max_length {
|
||||
continue;
|
||||
} else if end > max_length {
|
||||
new.push_str(
|
||||
re.replace(
|
||||
line,
|
||||
format!(
|
||||
"Dialogue: {},{},",
|
||||
format_naive_time(start),
|
||||
&length_as_string
|
||||
),
|
||||
)
|
||||
.to_string()
|
||||
.as_str(),
|
||||
)
|
||||
} else {
|
||||
new.push_str(line)
|
||||
}
|
||||
} else {
|
||||
new.push_str(line)
|
||||
}
|
||||
new.push('\n')
|
||||
}
|
||||
|
||||
new.into_bytes()
|
||||
}
|
||||
|
|
|
|||
25
crunchy-cli-core/src/utils/video.rs
Normal file
25
crunchy-cli-core/src/utils/video.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
use anyhow::Result;
|
||||
use chrono::NaiveTime;
|
||||
use regex::Regex;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
/// Get the length of a video. This is required because sometimes subtitles have an unnecessary entry
|
||||
/// long after the actual video ends with artificially extends the video length on some video players.
|
||||
/// To prevent this, the video length must be hard set. See
|
||||
/// [crunchy-labs/crunchy-cli#32](https://github.com/crunchy-labs/crunchy-cli/issues/32) for more
|
||||
/// information.
|
||||
pub fn get_video_length(path: PathBuf) -> Result<NaiveTime> {
|
||||
let video_length = Regex::new(r"Duration:\s(?P<time>\d+:\d+:\d+\.\d+),")?;
|
||||
|
||||
let ffmpeg = Command::new("ffmpeg")
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.arg("-y")
|
||||
.args(["-i", path.to_str().unwrap()])
|
||||
.output()?;
|
||||
let ffmpeg_output = String::from_utf8(ffmpeg.stderr)?;
|
||||
let caps = video_length.captures(ffmpeg_output.as_str()).unwrap();
|
||||
|
||||
Ok(NaiveTime::parse_from_str(caps.name("time").unwrap().as_str(), "%H:%M:%S%.f").unwrap())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue