Merge branch 'master' into feature/all-languages

This commit is contained in:
ByteDream 2023-01-16 15:43:33 +01:00 committed by GitHub
commit 1844a563d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 287 additions and 238 deletions

View file

@ -6,18 +6,19 @@ use crate::cli::utils::{
use crate::utils::context::Context;
use crate::utils::format::Format;
use crate::utils::log::progress;
use crate::utils::os::{free_file, has_ffmpeg, is_special_file};
use crate::utils::os::{free_file, has_ffmpeg, is_special_file, tempfile};
use crate::utils::parse::{parse_url, UrlFilter};
use crate::utils::sort::{sort_formats_after_seasons, sort_seasons_after_number};
use crate::utils::subtitle::download_subtitle;
use crate::utils::video::get_video_length;
use crate::Execute;
use anyhow::{bail, Result};
use crunchyroll_rs::media::{Resolution, VariantData};
use crunchyroll_rs::media::{Resolution, StreamSubtitle, VariantData};
use crunchyroll_rs::{
Episode, Locale, Media, MediaCollection, Movie, MovieListing, Season, Series,
};
use log::{debug, error, info, warn};
use std::borrow::Cow;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
@ -51,7 +52,7 @@ pub struct Download {
{series_id} ID of the series\n \
{season_id} ID of the season\n \
{episode_id} ID of the episode")]
#[arg(short, long, default_value = "{title}.ts")]
#[arg(short, long, default_value = "{title}.mp4")]
output: String,
#[arg(help = "Video resolution")]
@ -74,6 +75,10 @@ pub struct Download {
#[arg(value_parser = FFmpegPreset::parse)]
ffmpeg_preset: Vec<FFmpegPreset>,
#[arg(help = "Skip files which are already existing")]
#[arg(long, default_value_t = false)]
skip_existing: bool,
#[arg(help = "Ignore interactive input")]
#[arg(short, long, default_value_t = false)]
yes: bool,
@ -85,17 +90,23 @@ pub struct Download {
#[async_trait::async_trait(?Send)]
impl Execute for Download {
fn pre_check(&mut self) -> Result<()> {
if has_ffmpeg() {
debug!("FFmpeg detected")
} else if PathBuf::from(&self.output)
if !has_ffmpeg() {
bail!("FFmpeg is needed to run this command")
} else if Path::new(&self.output)
.extension()
.unwrap_or_default()
.to_string_lossy()
!= "ts"
.is_empty()
&& self.output != "-"
{
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")
bail!("No file extension found. Please specify a file extension (via `-o`) for the output file")
}
if self.subtitle.is_some() {
if let Some(ext) = Path::new(&self.output).extension() {
if ext.to_string_lossy() != "mp4" {
warn!("Detected a non mp4 output container. Adding subtitles may take a while")
}
}
}
let _ = FFmpegPreset::ffmpeg_presets(self.ffmpeg_preset.clone())?;
@ -209,17 +220,16 @@ impl Execute for Download {
}
for format in formats {
let path = free_file(
format.format_path(
if self.output.is_empty() {
"{title}.mkv"
} else {
&self.output
}
.into(),
true,
),
);
let formatted_path = format.format_path((&self.output).into(), true);
let (path, changed) = free_file(formatted_path.clone());
if changed && self.skip_existing {
debug!(
"Skipping already existing file '{}'",
formatted_path.to_string_lossy()
);
continue;
}
info!(
"Downloading {} to '{}'",
@ -245,23 +255,14 @@ impl Execute for Download {
tab_info!("Resolution: {}", format.stream.resolution);
tab_info!("FPS: {:.2}", format.stream.fps);
let extension = path.extension().unwrap_or_default().to_string_lossy();
if (!extension.is_empty() && extension != "ts") || !self.ffmpeg_preset.is_empty() {
download_ffmpeg(&ctx, &self, format.stream, path.as_path()).await?;
} else if path.to_str().unwrap() == "-" {
let mut stdout = std::io::stdout().lock();
download_segments(&ctx, &mut stdout, None, format.stream).await?;
} else {
// create parent directory if it does not exist
if let Some(parent) = path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)?
}
}
let mut file = File::options().create(true).write(true).open(&path)?;
download_segments(&ctx, &mut file, None, format.stream).await?
}
download_ffmpeg(
&ctx,
&self,
format.stream,
format.subtitles.get(0).cloned(),
path.to_path_buf(),
)
.await?;
}
}
@ -273,9 +274,10 @@ async fn download_ffmpeg(
ctx: &Context,
download: &Download,
variant_data: VariantData,
target: &Path,
subtitle: Option<StreamSubtitle>,
mut target: PathBuf,
) -> Result<()> {
let (input_presets, output_presets) =
let (input_presets, mut output_presets) =
FFmpegPreset::ffmpeg_presets(download.ffmpeg_preset.clone())?;
// create parent directory if it does not exist
@ -285,35 +287,83 @@ async fn download_ffmpeg(
}
}
let mut video_file = tempfile(".ts")?;
download_segments(ctx, &mut video_file, None, variant_data).await?;
let subtitle_file = if let Some(ref sub) = subtitle {
let video_len = get_video_length(video_file.path().to_path_buf())?;
Some(download_subtitle(sub.clone(), video_len).await?)
} else {
None
};
let stdout_tempfile = if target.to_string_lossy() == "-" {
let file = tempfile(".mp4")?;
target = file.path().to_path_buf();
Some(file)
} else {
None
};
let subtitle_presets = if let Some(sub_file) = &subtitle_file {
if target.extension().unwrap_or_default().to_string_lossy() == "mp4" {
vec![
"-i".to_string(),
sub_file.to_string_lossy().to_string(),
"-movflags".to_string(),
"faststart".to_string(),
"-c:s".to_string(),
"mov_text".to_string(),
"-disposition:s:s:0".to_string(),
"forced".to_string(),
]
} else {
// remove '-c:v copy' and '-c:a copy' from output presets as its causes issues with
// burning subs into the video
let mut last = String::new();
let mut remove_count = 0;
for (i, s) in output_presets.clone().iter().enumerate() {
if (last == "-c:v" || last == "-c:a") && s == "copy" {
// remove last
output_presets.remove(i - remove_count - 1);
remove_count += 1;
output_presets.remove(i - remove_count);
remove_count += 1;
}
last = s.clone();
}
vec![
"-vf".to_string(),
format!("subtitles={}", sub_file.to_string_lossy()),
]
}
} else {
vec![]
};
let mut ffmpeg = Command::new("ffmpeg")
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.arg("-y")
.args(input_presets)
.args(["-f", "mpegts", "-i", "pipe:"])
.args(
if target
.extension()
.unwrap_or_default()
.to_string_lossy()
.is_empty()
{
vec!["-f", "mpegts"]
} else {
vec![]
}
.as_slice(),
)
.args(["-i", video_file.path().to_string_lossy().as_ref()])
.args(subtitle_presets)
.args(output_presets)
.arg(target.to_str().unwrap())
.spawn()?;
download_segments(ctx, &mut ffmpeg.stdin.take().unwrap(), None, variant_data).await?;
let progress_handler = progress!("Generating output file");
if !ffmpeg.wait()?.success() {
bail!("{}", std::io::read_to_string(ffmpeg.stderr.unwrap())?)
}
progress_handler.stop("Output file generated");
let _progress_handler = progress!("Generating output file");
ffmpeg.wait()?;
info!("Output file generated");
if let Some(mut stdout_file) = stdout_tempfile {
let mut stdout = std::io::stdout();
std::io::copy(&mut stdout_file, &mut stdout)?;
}
Ok(())
}
@ -431,8 +481,11 @@ async fn format_from_episode(
}
let streams = episode.streams().await?;
let streaming_data = if let Some(subtitle) = &download.subtitle {
if !streams.subtitles.keys().cloned().any(|x| &x == subtitle) {
let streaming_data = streams.hls_streaming_data(None).await?;
let subtitle = if let Some(subtitle) = &download.subtitle {
if let Some(sub) = streams.subtitles.get(subtitle) {
Some(sub.clone())
} else {
error!(
"Episode {} ({}) of season {} ({}) of {} has no {} subtitles",
episode.metadata.episode_number,
@ -444,9 +497,8 @@ async fn format_from_episode(
);
return Ok(None);
}
streams.hls_streaming_data(Some(subtitle.clone())).await?
} else {
streams.hls_streaming_data(None).await?
None
};
let Some(stream) = find_resolution(streaming_data, &download.resolution) else {
@ -476,6 +528,7 @@ async fn format_from_episode(
episode,
&season_eps.to_vec(),
stream,
subtitle.map_or_else(|| vec![], |s| vec![s]),
)))
}