mirror of
https://github.com/crunchy-labs/crunchy-cli.git
synced 2026-01-21 12:12:00 -06:00
Merge pull request #78 from crunchy-labs/feature/ffmpeg-optimizations
Add optional ffmpeg optimizations
This commit is contained in:
commit
5c3f49e9f4
3 changed files with 272 additions and 43 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::cli::log::tab_info;
|
use crate::cli::log::tab_info;
|
||||||
use crate::cli::utils::{download_segments, find_resolution};
|
use crate::cli::utils::{download_segments, FFmpegPreset, find_resolution};
|
||||||
use crate::utils::context::Context;
|
use crate::utils::context::Context;
|
||||||
use crate::utils::format::{format_string, Format};
|
use crate::utils::format::{format_string, Format};
|
||||||
use crate::utils::log::progress;
|
use crate::utils::log::progress;
|
||||||
|
|
@ -8,9 +8,10 @@ use crate::utils::parse::{parse_url, UrlFilter};
|
||||||
use crate::utils::sort::{sort_formats_after_seasons, sort_seasons_after_number};
|
use crate::utils::sort::{sort_formats_after_seasons, sort_seasons_after_number};
|
||||||
use crate::Execute;
|
use crate::Execute;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
|
use chrono::NaiveTime;
|
||||||
use crunchyroll_rs::media::{Resolution, StreamSubtitle};
|
use crunchyroll_rs::media::{Resolution, StreamSubtitle};
|
||||||
use crunchyroll_rs::{Locale, Media, MediaCollection, Series};
|
use crunchyroll_rs::{Locale, Media, MediaCollection, Series};
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info, warn};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
@ -25,7 +26,8 @@ pub enum MergeBehavior {
|
||||||
Video,
|
Video,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_merge_behavior(s: &str) -> Result<MergeBehavior, String> {
|
impl MergeBehavior {
|
||||||
|
fn parse(s: &str) -> Result<MergeBehavior, String> {
|
||||||
Ok(match s.to_lowercase().as_str() {
|
Ok(match s.to_lowercase().as_str() {
|
||||||
"auto" => MergeBehavior::Auto,
|
"auto" => MergeBehavior::Auto,
|
||||||
"audio" => MergeBehavior::Audio,
|
"audio" => MergeBehavior::Audio,
|
||||||
|
|
@ -33,6 +35,7 @@ fn parse_merge_behavior(s: &str) -> Result<MergeBehavior, String> {
|
||||||
_ => return Err(format!("'{}' is not a valid merge behavior", s)),
|
_ => return Err(format!("'{}' is not a valid merge behavior", s)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, clap::Parser)]
|
#[derive(Debug, clap::Parser)]
|
||||||
#[clap(about = "Archive a video")]
|
#[clap(about = "Archive a video")]
|
||||||
|
|
@ -87,9 +90,19 @@ pub struct Archive {
|
||||||
Valid options are 'audio' (stores one video and all other languages as audio only), 'video' (stores the video + audio for every language) and 'auto' (detects if videos differ in length: if so, behave like 'video' else like 'audio')"
|
Valid options are 'audio' (stores one video and all other languages as audio only), 'video' (stores the video + audio for every language) and 'auto' (detects if videos differ in length: if so, behave like 'video' else like 'audio')"
|
||||||
)]
|
)]
|
||||||
#[arg(short, long, default_value = "auto")]
|
#[arg(short, long, default_value = "auto")]
|
||||||
#[arg(value_parser = parse_merge_behavior)]
|
#[arg(value_parser = MergeBehavior::parse)]
|
||||||
merge: MergeBehavior,
|
merge: MergeBehavior,
|
||||||
|
|
||||||
|
#[arg(help = format!("Presets for video converting. Can be used multiple times. \
|
||||||
|
Available presets: \n {}", FFmpegPreset::all().into_iter().map(|p| format!("{}: {}", p.to_string(), p.description())).collect::<Vec<String>>().join("\n ")))]
|
||||||
|
#[arg(long_help = format!("Presets for video converting. Can be used multiple times. \
|
||||||
|
Generally used to minify the file size with keeping (nearly) the same quality. \
|
||||||
|
It is recommended to only use this if you archive videos with high resolutions since low resolution videos tend to result in a larger file with any of the provided presets. \
|
||||||
|
Available presets: \n {}", FFmpegPreset::all().into_iter().map(|p| format!("{}: {}", p.to_string(), p.description())).collect::<Vec<String>>().join("\n ")))]
|
||||||
|
#[arg(long)]
|
||||||
|
#[arg(value_parser = FFmpegPreset::parse)]
|
||||||
|
ffmpeg_preset: Vec<FFmpegPreset>,
|
||||||
|
|
||||||
#[arg(
|
#[arg(
|
||||||
help = "Set which subtitle language should be set as default / auto shown when starting a video"
|
help = "Set which subtitle language should be set as default / auto shown when starting a video"
|
||||||
)]
|
)]
|
||||||
|
|
@ -113,9 +126,18 @@ impl Execute for Archive {
|
||||||
fn pre_check(&self) -> Result<()> {
|
fn pre_check(&self) -> Result<()> {
|
||||||
if !has_ffmpeg() {
|
if !has_ffmpeg() {
|
||||||
bail!("FFmpeg is needed to run this command")
|
bail!("FFmpeg is needed to run this command")
|
||||||
} else if PathBuf::from(&self.output).extension().unwrap_or_default().to_string_lossy() != "mkv" {
|
} else if PathBuf::from(&self.output)
|
||||||
|
.extension()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
!= "mkv"
|
||||||
|
{
|
||||||
bail!("File extension is not '.mkv'. Currently only matroska / '.mkv' files are supported")
|
bail!("File extension is not '.mkv'. Currently only matroska / '.mkv' files are supported")
|
||||||
}
|
}
|
||||||
|
let _ = FFmpegPreset::ffmpeg_presets(self.ffmpeg_preset.clone())?;
|
||||||
|
if self.ffmpeg_preset.len() == 1 && self.ffmpeg_preset.get(0).unwrap() == &FFmpegPreset::Nvidia {
|
||||||
|
warn!("Skipping 'nvidia' hardware acceleration preset since no other codec preset was specified")
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -276,9 +298,13 @@ impl Execute for Archive {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (primary_video, _) = video_paths.get(0).unwrap();
|
||||||
|
let primary_video_length = get_video_length(primary_video.to_path_buf()).unwrap();
|
||||||
for subtitle in subtitles {
|
for subtitle in subtitles {
|
||||||
subtitle_paths
|
subtitle_paths.push((
|
||||||
.push((download_subtitle(&self, subtitle.clone()).await?, subtitle))
|
download_subtitle(&self, subtitle.clone(), primary_video_length).await?,
|
||||||
|
subtitle,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
generate_mkv(&self, path, video_paths, audio_paths, subtitle_paths)?
|
generate_mkv(&self, path, video_paths, audio_paths, subtitle_paths)?
|
||||||
|
|
@ -387,7 +413,9 @@ async fn download_video(ctx: &Context, format: &Format, only_audio: bool) -> Res
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
.arg("-y")
|
.arg("-y")
|
||||||
.args(["-f", "mpegts", "-i", "pipe:"])
|
.args(["-f", "mpegts"])
|
||||||
|
.args(["-i", "pipe:"])
|
||||||
|
.args(["-c", "copy"])
|
||||||
.args(if only_audio { vec!["-vn"] } else { vec![] })
|
.args(if only_audio { vec!["-vn"] } else { vec![] })
|
||||||
.arg(path.to_str().unwrap())
|
.arg(path.to_str().unwrap())
|
||||||
.spawn()?;
|
.spawn()?;
|
||||||
|
|
@ -403,15 +431,20 @@ async fn download_video(ctx: &Context, format: &Format, only_audio: bool) -> Res
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download_subtitle(archive: &Archive, subtitle: StreamSubtitle) -> Result<TempPath> {
|
async fn download_subtitle(
|
||||||
|
archive: &Archive,
|
||||||
|
subtitle: StreamSubtitle,
|
||||||
|
max_length: NaiveTime,
|
||||||
|
) -> Result<TempPath> {
|
||||||
let tempfile = tempfile(".ass")?;
|
let tempfile = tempfile(".ass")?;
|
||||||
let (mut file, path) = tempfile.into_parts();
|
let (mut file, path) = tempfile.into_parts();
|
||||||
|
|
||||||
let mut buf = vec![];
|
let mut buf = vec![];
|
||||||
subtitle.write_to(&mut buf).await?;
|
subtitle.write_to(&mut buf).await?;
|
||||||
if !archive.no_subtitle_optimizations {
|
if !archive.no_subtitle_optimizations {
|
||||||
buf = fix_subtitle(buf)
|
buf = fix_subtitle_look_and_feel(buf)
|
||||||
}
|
}
|
||||||
|
buf = fix_subtitle_length(buf, max_length);
|
||||||
|
|
||||||
file.write_all(buf.as_slice())?;
|
file.write_all(buf.as_slice())?;
|
||||||
|
|
||||||
|
|
@ -421,7 +454,7 @@ async fn download_subtitle(archive: &Archive, subtitle: StreamSubtitle) -> Resul
|
||||||
/// Add `ScaledBorderAndShadows: yes` to subtitles; without it they look very messy on some video
|
/// 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)
|
/// players. See [crunchy-labs/crunchy-cli#66](https://github.com/crunchy-labs/crunchy-cli/issues/66)
|
||||||
/// for more information.
|
/// for more information.
|
||||||
fn fix_subtitle(raw: Vec<u8>) -> Vec<u8> {
|
fn fix_subtitle_look_and_feel(raw: Vec<u8>) -> Vec<u8> {
|
||||||
let mut script_info = false;
|
let mut script_info = false;
|
||||||
let mut new = String::new();
|
let mut new = String::new();
|
||||||
|
|
||||||
|
|
@ -439,6 +472,70 @@ fn fix_subtitle(raw: Vec<u8>) -> Vec<u8> {
|
||||||
new.into_bytes()
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
fn generate_mkv(
|
fn generate_mkv(
|
||||||
archive: &Archive,
|
archive: &Archive,
|
||||||
target: PathBuf,
|
target: PathBuf,
|
||||||
|
|
@ -450,8 +547,6 @@ fn generate_mkv(
|
||||||
let mut maps = vec![];
|
let mut maps = vec![];
|
||||||
let mut metadata = vec![];
|
let mut metadata = vec![];
|
||||||
|
|
||||||
let mut video_length = (0, 0, 0, 0);
|
|
||||||
|
|
||||||
for (i, (video_path, format)) in video_paths.iter().enumerate() {
|
for (i, (video_path, format)) in video_paths.iter().enumerate() {
|
||||||
input.extend(["-i".to_string(), video_path.to_string_lossy().to_string()]);
|
input.extend(["-i".to_string(), video_path.to_string_lossy().to_string()]);
|
||||||
maps.extend(["-map".to_string(), i.to_string()]);
|
maps.extend(["-map".to_string(), i.to_string()]);
|
||||||
|
|
@ -471,11 +566,6 @@ fn generate_mkv(
|
||||||
format!("-metadata:s:a:{}", i),
|
format!("-metadata:s:a:{}", i),
|
||||||
format!("title={}", format.audio.to_human_readable()),
|
format!("title={}", format.audio.to_human_readable()),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let vid_len = get_video_length(video_path.to_path_buf())?;
|
|
||||||
if vid_len > video_length {
|
|
||||||
video_length = vid_len
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (i, (audio_path, format)) in audio_paths.iter().enumerate() {
|
for (i, (audio_path, format)) in audio_paths.iter().enumerate() {
|
||||||
input.extend(["-i".to_string(), audio_path.to_string_lossy().to_string()]);
|
input.extend(["-i".to_string(), audio_path.to_string_lossy().to_string()]);
|
||||||
|
|
@ -508,7 +598,11 @@ fn generate_mkv(
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (input_presets, output_presets) =
|
||||||
|
FFmpegPreset::ffmpeg_presets(archive.ffmpeg_preset.clone())?;
|
||||||
|
|
||||||
let mut command_args = vec!["-y".to_string()];
|
let mut command_args = vec!["-y".to_string()];
|
||||||
|
command_args.extend(input_presets);
|
||||||
command_args.extend(input);
|
command_args.extend(input);
|
||||||
command_args.extend(maps);
|
command_args.extend(maps);
|
||||||
command_args.extend(metadata);
|
command_args.extend(metadata);
|
||||||
|
|
@ -528,9 +622,8 @@ fn generate_mkv(
|
||||||
command_args.extend(["-disposition:s:0".to_string(), "0".to_string()])
|
command_args.extend(["-disposition:s:0".to_string(), "0".to_string()])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
command_args.extend(output_presets);
|
||||||
command_args.extend([
|
command_args.extend([
|
||||||
"-c".to_string(),
|
|
||||||
"copy".to_string(),
|
|
||||||
"-f".to_string(),
|
"-f".to_string(),
|
||||||
"matroska".to_string(),
|
"matroska".to_string(),
|
||||||
target.to_string_lossy().to_string(),
|
target.to_string_lossy().to_string(),
|
||||||
|
|
@ -552,11 +645,11 @@ fn generate_mkv(
|
||||||
|
|
||||||
/// Get the length of a video. This is required because sometimes subtitles have an unnecessary entry
|
/// 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.
|
/// 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 with ffmpeg. See
|
/// 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
|
/// [crunchy-labs/crunchy-cli#32](https://github.com/crunchy-labs/crunchy-cli/issues/32) for more
|
||||||
/// information.
|
/// information.
|
||||||
fn get_video_length(path: PathBuf) -> Result<(u32, u32, u32, u32)> {
|
fn get_video_length(path: PathBuf) -> Result<NaiveTime> {
|
||||||
let video_length = Regex::new(r"Duration:\s?(\d+):(\d+):(\d+).(\d+),")?;
|
let video_length = Regex::new(r"Duration:\s(?P<time>\d+:\d+:\d+\.\d+),")?;
|
||||||
|
|
||||||
let ffmpeg = Command::new("ffmpeg")
|
let ffmpeg = Command::new("ffmpeg")
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
|
|
@ -567,10 +660,5 @@ fn get_video_length(path: PathBuf) -> Result<(u32, u32, u32, u32)> {
|
||||||
let ffmpeg_output = String::from_utf8(ffmpeg.stderr)?;
|
let ffmpeg_output = String::from_utf8(ffmpeg.stderr)?;
|
||||||
let caps = video_length.captures(ffmpeg_output.as_str()).unwrap();
|
let caps = video_length.captures(ffmpeg_output.as_str()).unwrap();
|
||||||
|
|
||||||
Ok((
|
Ok(NaiveTime::parse_from_str(caps.name("time").unwrap().as_str(), "%H:%M:%S%.f").unwrap())
|
||||||
caps[1].parse()?,
|
|
||||||
caps[2].parse()?,
|
|
||||||
caps[3].parse()?,
|
|
||||||
caps[4].parse()?,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::cli::log::tab_info;
|
use crate::cli::log::tab_info;
|
||||||
use crate::cli::utils::{download_segments, find_resolution};
|
use crate::cli::utils::{download_segments, FFmpegPreset, find_resolution};
|
||||||
use crate::utils::context::Context;
|
use crate::utils::context::Context;
|
||||||
use crate::utils::format::{format_string, Format};
|
use crate::utils::format::{format_string, Format};
|
||||||
use crate::utils::log::progress;
|
use crate::utils::log::progress;
|
||||||
|
|
@ -12,7 +12,7 @@ use crunchyroll_rs::media::{Resolution, VariantData};
|
||||||
use crunchyroll_rs::{
|
use crunchyroll_rs::{
|
||||||
Episode, Locale, Media, MediaCollection, Movie, MovieListing, Season, Series,
|
Episode, Locale, Media, MediaCollection, Movie, MovieListing, Season, Series,
|
||||||
};
|
};
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info, warn};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
|
@ -59,6 +59,16 @@ pub struct Download {
|
||||||
#[arg(value_parser = crate::utils::clap::clap_parse_resolution)]
|
#[arg(value_parser = crate::utils::clap::clap_parse_resolution)]
|
||||||
resolution: Resolution,
|
resolution: Resolution,
|
||||||
|
|
||||||
|
#[arg(help = format!("Presets for video converting. Can be used multiple times. \
|
||||||
|
Available presets: \n {}", FFmpegPreset::all().into_iter().map(|p| format!("{}: {}", p.to_string(), p.description())).collect::<Vec<String>>().join("\n ")))]
|
||||||
|
#[arg(long_help = format!("Presets for video converting. Can be used multiple times. \
|
||||||
|
Generally used to minify the file size with keeping (nearly) the same quality. \
|
||||||
|
It is recommended to only use this if you download videos with high resolutions since low resolution videos tend to result in a larger file with any of the provided presets. \
|
||||||
|
Available presets: \n {}", FFmpegPreset::all().into_iter().map(|p| format!("{}: {}", p.to_string(), p.description())).collect::<Vec<String>>().join("\n ")))]
|
||||||
|
#[arg(long)]
|
||||||
|
#[arg(value_parser = FFmpegPreset::parse)]
|
||||||
|
ffmpeg_preset: Vec<FFmpegPreset>,
|
||||||
|
|
||||||
#[arg(help = "Url(s) to Crunchyroll episodes or series")]
|
#[arg(help = "Url(s) to Crunchyroll episodes or series")]
|
||||||
urls: Vec<String>,
|
urls: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
@ -75,6 +85,13 @@ impl Execute for Download {
|
||||||
!= "ts"
|
!= "ts"
|
||||||
{
|
{
|
||||||
bail!("File extension is not '.ts'. If you want to use a custom file format, please install ffmpeg")
|
bail!("File extension is not '.ts'. If you want to use a custom file format, please install ffmpeg")
|
||||||
|
} else if !self.ffmpeg_preset.is_empty() {
|
||||||
|
bail!("FFmpeg is required to use (ffmpeg) presets")
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = FFmpegPreset::ffmpeg_presets(self.ffmpeg_preset.clone())?;
|
||||||
|
if self.ffmpeg_preset.len() == 1 && self.ffmpeg_preset.get(0).unwrap() == &FFmpegPreset::Nvidia {
|
||||||
|
warn!("Skipping 'nvidia' hardware acceleration preset since no other codec preset was specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -211,8 +228,8 @@ impl Execute for Download {
|
||||||
tab_info!("Resolution: {}", format.stream.resolution);
|
tab_info!("Resolution: {}", format.stream.resolution);
|
||||||
tab_info!("FPS: {:.2}", format.stream.fps);
|
tab_info!("FPS: {:.2}", format.stream.fps);
|
||||||
|
|
||||||
if path.extension().unwrap_or_default().to_string_lossy() != "ts" {
|
if path.extension().unwrap_or_default().to_string_lossy() != "ts" || !self.ffmpeg_preset.is_empty() {
|
||||||
download_ffmpeg(&ctx, format.stream, path.as_path()).await?;
|
download_ffmpeg(&ctx, &self, format.stream, path.as_path()).await?;
|
||||||
} else if path.to_str().unwrap() == "-" {
|
} else if path.to_str().unwrap() == "-" {
|
||||||
let mut stdout = std::io::stdout().lock();
|
let mut stdout = std::io::stdout().lock();
|
||||||
download_segments(&ctx, &mut stdout, None, format.stream).await?;
|
download_segments(&ctx, &mut stdout, None, format.stream).await?;
|
||||||
|
|
@ -227,15 +244,18 @@ impl Execute for Download {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download_ffmpeg(ctx: &Context, variant_data: VariantData, target: &Path) -> Result<()> {
|
async fn download_ffmpeg(ctx: &Context, download: &Download, variant_data: VariantData, target: &Path) -> Result<()> {
|
||||||
|
let (input_presets, output_presets) =
|
||||||
|
FFmpegPreset::ffmpeg_presets(download.ffmpeg_preset.clone())?;
|
||||||
|
|
||||||
let ffmpeg = Command::new("ffmpeg")
|
let ffmpeg = Command::new("ffmpeg")
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
.arg("-y")
|
.arg("-y")
|
||||||
|
.args(input_presets)
|
||||||
.args(["-f", "mpegts", "-i", "pipe:"])
|
.args(["-f", "mpegts", "-i", "pipe:"])
|
||||||
.args(["-safe", "0"])
|
.args(output_presets)
|
||||||
.args(["-c", "copy"])
|
|
||||||
.arg(target.to_str().unwrap())
|
.arg(target.to_str().unwrap())
|
||||||
.spawn()?;
|
.spawn()?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::utils::context::Context;
|
use crate::utils::context::Context;
|
||||||
use anyhow::Result;
|
use anyhow::{bail, Result};
|
||||||
use crunchyroll_rs::media::{Resolution, VariantData, VariantSegment};
|
use crunchyroll_rs::media::{Resolution, VariantData, VariantSegment};
|
||||||
use isahc::AsyncReadResponseExt;
|
use isahc::AsyncReadResponseExt;
|
||||||
use log::{debug, LevelFilter};
|
use log::{debug, LevelFilter};
|
||||||
|
|
@ -183,3 +183,124 @@ pub async fn download_segments(
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum FFmpegPreset {
|
||||||
|
Nvidia,
|
||||||
|
|
||||||
|
Av1,
|
||||||
|
H265,
|
||||||
|
H264,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for FFmpegPreset {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
&FFmpegPreset::Nvidia => "nvidia",
|
||||||
|
&FFmpegPreset::Av1 => "av1",
|
||||||
|
&FFmpegPreset::H265 => "h265",
|
||||||
|
&FFmpegPreset::H264 => "h264",
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FFmpegPreset {
|
||||||
|
pub(crate) fn all() -> Vec<FFmpegPreset> {
|
||||||
|
vec![
|
||||||
|
FFmpegPreset::Nvidia,
|
||||||
|
FFmpegPreset::Av1,
|
||||||
|
FFmpegPreset::H265,
|
||||||
|
FFmpegPreset::H264,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn description(self) -> String {
|
||||||
|
match self {
|
||||||
|
FFmpegPreset::Nvidia => "If you're have a nvidia card, use hardware / gpu accelerated video processing if available",
|
||||||
|
FFmpegPreset::Av1 => "Encode the video(s) with the av1 codec. Hardware acceleration is currently not possible with this",
|
||||||
|
FFmpegPreset::H265 => "Encode the video(s) with the h265 codec",
|
||||||
|
FFmpegPreset::H264 => "Encode the video(s) with the h264 codec"
|
||||||
|
}.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse(s: &str) -> Result<FFmpegPreset, String> {
|
||||||
|
Ok(match s.to_lowercase().as_str() {
|
||||||
|
"nvidia" => FFmpegPreset::Nvidia,
|
||||||
|
"av1" => FFmpegPreset::Av1,
|
||||||
|
"h265" | "h.265" | "hevc" => FFmpegPreset::H265,
|
||||||
|
"h264" | "h.264" => FFmpegPreset::H264,
|
||||||
|
_ => return Err(format!("'{}' is not a valid ffmpeg preset", s)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn ffmpeg_presets(mut presets: Vec<FFmpegPreset>) -> Result<(Vec<String>, Vec<String>)> {
|
||||||
|
fn preset_check_remove(presets: &mut Vec<FFmpegPreset>, preset: FFmpegPreset) -> bool {
|
||||||
|
if let Some(i) = presets.iter().position(|p| p == &preset) {
|
||||||
|
presets.remove(i);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let nvidia = preset_check_remove(&mut presets, FFmpegPreset::Nvidia);
|
||||||
|
if presets.len() > 1 {
|
||||||
|
bail!(
|
||||||
|
"Can only use one video codec, {} found: {}",
|
||||||
|
presets.len(),
|
||||||
|
presets
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.to_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let (mut input, mut output) = (vec![], vec![]);
|
||||||
|
for preset in presets {
|
||||||
|
if nvidia {
|
||||||
|
match preset {
|
||||||
|
FFmpegPreset::Av1 => bail!("'nvidia' hardware acceleration preset is not available in combination with the 'av1' codec preset"),
|
||||||
|
FFmpegPreset::H265 => {
|
||||||
|
input.extend(["-hwaccel", "cuvid", "-c:v", "h264_cuvid"]);
|
||||||
|
output.extend(["-c:v", "hevc_nvenc"]);
|
||||||
|
}
|
||||||
|
FFmpegPreset::H264 => {
|
||||||
|
input.extend(["-hwaccel", "cuvid", "-c:v", "h264_cuvid"]);
|
||||||
|
output.extend(["-c:v", "h264_nvenc"]);
|
||||||
|
}
|
||||||
|
_ => ()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match preset {
|
||||||
|
FFmpegPreset::Av1 => {
|
||||||
|
output.extend(["-c:v", "libaom-av1"]);
|
||||||
|
}
|
||||||
|
FFmpegPreset::H265 => {
|
||||||
|
output.extend(["-c:v", "libx265"]);
|
||||||
|
}
|
||||||
|
FFmpegPreset::H264 => {
|
||||||
|
output.extend(["-c:v", "libx264"]);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.is_empty() && output.is_empty() {
|
||||||
|
output.extend(["-c", "copy"])
|
||||||
|
} else {
|
||||||
|
if output.is_empty() {
|
||||||
|
output.extend(["-c", "copy"])
|
||||||
|
} else {
|
||||||
|
output.extend(["-c:a", "copy", "-c:s", "copy"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
input.into_iter().map(|i| i.to_string()).collect(),
|
||||||
|
output.into_iter().map(|o| o.to_string()).collect(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue