Add --special-output and --skip-specials flag

This commit is contained in:
bytedream 2023-11-04 15:24:14 +01:00
parent 5a3a304443
commit 787d8ab02c
5 changed files with 106 additions and 10 deletions

View file

@ -54,6 +54,11 @@ pub struct Archive {
{episode_id} ID of the episode")] {episode_id} ID of the episode")]
#[arg(short, long, default_value = "{title}.mkv")] #[arg(short, long, default_value = "{title}.mkv")]
pub(crate) output: String, pub(crate) output: String,
#[arg(help = "Name of the output file if the episode is a special")]
#[arg(long_help = "Name of the output file if the episode is a special. \
If not set, the '-o'/'--output' flag will be used as name template")]
#[arg(long)]
pub(crate) special_output: Option<String>,
#[arg(help = "Video resolution")] #[arg(help = "Video resolution")]
#[arg(long_help = "The video resolution.\ #[arg(long_help = "The video resolution.\
@ -95,6 +100,9 @@ pub struct Archive {
#[arg(help = "Skip files which are already existing")] #[arg(help = "Skip files which are already existing")]
#[arg(long, default_value_t = false)] #[arg(long, default_value_t = false)]
pub(crate) skip_existing: bool, pub(crate) skip_existing: bool,
#[arg(help = "Skip special episodes")]
#[arg(long, default_value_t = false)]
pub(crate) skip_specials: bool,
#[arg(help = "Skip any interactive input")] #[arg(help = "Skip any interactive input")]
#[arg(short, long, default_value_t = false)] #[arg(short, long, default_value_t = false)]
@ -123,6 +131,17 @@ impl Execute for Archive {
&& self.output != "-" && self.output != "-"
{ {
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")
} else if let Some(special_output) = &self.special_output {
if PathBuf::from(special_output)
.extension()
.unwrap_or_default()
.to_string_lossy()
!= "mkv"
&& !is_special_file(special_output)
&& special_output != "-"
{
bail!("File extension for special episodes is not '.mkv'. Currently only matroska / '.mkv' files are supported")
}
} }
self.audio = all_locale_in_locales(self.audio.clone()); self.audio = all_locale_in_locales(self.audio.clone());
@ -147,9 +166,10 @@ impl Execute for Archive {
for (i, (media_collection, url_filter)) in parsed_urls.into_iter().enumerate() { for (i, (media_collection, url_filter)) in parsed_urls.into_iter().enumerate() {
let progress_handler = progress!("Fetching series details"); let progress_handler = progress!("Fetching series details");
let single_format_collection = ArchiveFilter::new(url_filter, self.clone(), !self.yes) let single_format_collection =
.visit(media_collection) ArchiveFilter::new(url_filter, self.clone(), !self.yes, self.skip_specials)
.await?; .visit(media_collection)
.await?;
if single_format_collection.is_empty() { if single_format_collection.is_empty() {
progress_handler.stop(format!("Skipping url {} (no matching videos found)", i + 1)); progress_handler.stop(format!("Skipping url {} (no matching videos found)", i + 1));
@ -175,7 +195,15 @@ impl Execute for Archive {
downloader.add_format(download_format) downloader.add_format(download_format)
} }
let formatted_path = format.format_path((&self.output).into()); let formatted_path = if format.is_special() {
format.format_path(
self.special_output
.as_ref()
.map_or((&self.output).into(), |so| so.into()),
)
} else {
format.format_path((&self.output).into())
};
let (path, changed) = free_file(formatted_path.clone()); let (path, changed) = free_file(formatted_path.clone());
if changed && self.skip_existing { if changed && self.skip_existing {

View file

@ -18,6 +18,7 @@ pub(crate) struct ArchiveFilter {
url_filter: UrlFilter, url_filter: UrlFilter,
archive: Archive, archive: Archive,
interactive_input: bool, interactive_input: bool,
skip_special: bool,
season_episodes: HashMap<String, Vec<Episode>>, season_episodes: HashMap<String, Vec<Episode>>,
season_subtitles_missing: Vec<u32>, season_subtitles_missing: Vec<u32>,
season_sorting: Vec<String>, season_sorting: Vec<String>,
@ -25,11 +26,17 @@ pub(crate) struct ArchiveFilter {
} }
impl ArchiveFilter { impl ArchiveFilter {
pub(crate) fn new(url_filter: UrlFilter, archive: Archive, interactive_input: bool) -> Self { pub(crate) fn new(
url_filter: UrlFilter,
archive: Archive,
interactive_input: bool,
skip_special: bool,
) -> Self {
Self { Self {
url_filter, url_filter,
archive, archive,
interactive_input, interactive_input,
skip_special,
season_episodes: HashMap::new(), season_episodes: HashMap::new(),
season_subtitles_missing: vec![], season_subtitles_missing: vec![],
season_sorting: vec![], season_sorting: vec![],
@ -246,6 +253,13 @@ impl Filter for ArchiveFilter {
return Ok(None); return Ok(None);
} }
// skip the episode if it's a special
if self.skip_special
&& (episode.sequence_number == 0.0 || episode.sequence_number.fract() != 0.0)
{
return Ok(None);
}
let mut episodes = vec![]; let mut episodes = vec![];
if !matches!(self.visited, Visited::Series) && !matches!(self.visited, Visited::Season) { if !matches!(self.visited, Visited::Series) && !matches!(self.visited, Visited::Season) {
if self.archive.audio.contains(&episode.audio_locale) { if self.archive.audio.contains(&episode.audio_locale) {

View file

@ -50,6 +50,11 @@ pub struct Download {
{episode_id} ID of the episode")] {episode_id} ID of the episode")]
#[arg(short, long, default_value = "{title}.mp4")] #[arg(short, long, default_value = "{title}.mp4")]
pub(crate) output: String, pub(crate) output: String,
#[arg(help = "Name of the output file if the episode is a special")]
#[arg(long_help = "Name of the output file if the episode is a special. \
If not set, the '-o'/'--output' flag will be used as name template")]
#[arg(long)]
pub(crate) special_output: Option<String>,
#[arg(help = "Video resolution")] #[arg(help = "Video resolution")]
#[arg(long_help = "The video resolution.\ #[arg(long_help = "The video resolution.\
@ -73,6 +78,9 @@ pub struct Download {
#[arg(help = "Skip files which are already existing")] #[arg(help = "Skip files which are already existing")]
#[arg(long, default_value_t = false)] #[arg(long, default_value_t = false)]
pub(crate) skip_existing: bool, pub(crate) skip_existing: bool,
#[arg(help = "Skip special episodes")]
#[arg(long, default_value_t = false)]
pub(crate) skip_specials: bool,
#[arg(help = "Skip any interactive input")] #[arg(help = "Skip any interactive input")]
#[arg(short, long, default_value_t = false)] #[arg(short, long, default_value_t = false)]
@ -116,6 +124,25 @@ impl Execute for Download {
} }
} }
if let Some(special_output) = &self.special_output {
if Path::new(special_output)
.extension()
.unwrap_or_default()
.is_empty()
&& !is_special_file(special_output)
&& special_output != "-"
{
bail!("No file extension found. Please specify a file extension (via `--special-output`) for the output file")
}
if let Some(ext) = Path::new(special_output).extension() {
if self.force_hardsub {
warn!("Hardsubs are forced for special episodes. Adding subtitles may take a while")
} else if !["mkv", "mov", "mp4"].contains(&ext.to_string_lossy().as_ref()) {
warn!("Detected a container which does not support softsubs. Adding subtitles for special episodes may take a while")
}
}
}
Ok(()) Ok(())
} }
@ -135,9 +162,10 @@ impl Execute for Download {
for (i, (media_collection, url_filter)) in parsed_urls.into_iter().enumerate() { for (i, (media_collection, url_filter)) in parsed_urls.into_iter().enumerate() {
let progress_handler = progress!("Fetching series details"); let progress_handler = progress!("Fetching series details");
let single_format_collection = DownloadFilter::new(url_filter, self.clone(), !self.yes) let single_format_collection =
.visit(media_collection) DownloadFilter::new(url_filter, self.clone(), !self.yes, self.skip_specials)
.await?; .visit(media_collection)
.await?;
if single_format_collection.is_empty() { if single_format_collection.is_empty() {
progress_handler.stop(format!("Skipping url {} (no matching videos found)", i + 1)); progress_handler.stop(format!("Skipping url {} (no matching videos found)", i + 1));
@ -167,7 +195,15 @@ impl Execute for Download {
let mut downloader = download_builder.clone().build(); let mut downloader = download_builder.clone().build();
downloader.add_format(download_format); downloader.add_format(download_format);
let formatted_path = format.format_path((&self.output).into()); let formatted_path = if format.is_special() {
format.format_path(
self.special_output
.as_ref()
.map_or((&self.output).into(), |so| so.into()),
)
} else {
format.format_path((&self.output).into())
};
let (path, changed) = free_file(formatted_path.clone()); let (path, changed) = free_file(formatted_path.clone());
if changed && self.skip_existing { if changed && self.skip_existing {

View file

@ -12,17 +12,24 @@ pub(crate) struct DownloadFilter {
url_filter: UrlFilter, url_filter: UrlFilter,
download: Download, download: Download,
interactive_input: bool, interactive_input: bool,
skip_special: bool,
season_episodes: HashMap<u32, Vec<Episode>>, season_episodes: HashMap<u32, Vec<Episode>>,
season_subtitles_missing: Vec<u32>, season_subtitles_missing: Vec<u32>,
season_visited: bool, season_visited: bool,
} }
impl DownloadFilter { impl DownloadFilter {
pub(crate) fn new(url_filter: UrlFilter, download: Download, interactive_input: bool) -> Self { pub(crate) fn new(
url_filter: UrlFilter,
download: Download,
interactive_input: bool,
skip_special: bool,
) -> Self {
Self { Self {
url_filter, url_filter,
download, download,
interactive_input, interactive_input,
skip_special,
season_episodes: HashMap::new(), season_episodes: HashMap::new(),
season_subtitles_missing: vec![], season_subtitles_missing: vec![],
season_visited: false, season_visited: false,
@ -132,6 +139,13 @@ impl Filter for DownloadFilter {
return Ok(None); return Ok(None);
} }
// skip the episode if it's a special
if self.skip_special
&& (episode.sequence_number == 0.0 || episode.sequence_number.fract() != 0.0)
{
return Ok(None);
}
// check if the audio locale is correct. // check if the audio locale is correct.
// should only be incorrect if the console input was a episode url. otherwise // should only be incorrect if the console input was a episode url. otherwise
// `DownloadFilter::visit_season` returns the correct episodes with matching audio // `DownloadFilter::visit_season` returns the correct episodes with matching audio

View file

@ -473,6 +473,10 @@ impl Format {
tab_info!("FPS: {:.2}", self.fps) tab_info!("FPS: {:.2}", self.fps)
} }
pub fn is_special(&self) -> bool {
self.sequence_number == 0.0 || self.sequence_number.fract() != 0.0
}
pub fn has_relative_fmt<S: AsRef<str>>(s: S) -> bool { pub fn has_relative_fmt<S: AsRef<str>>(s: S) -> bool {
return s.as_ref().contains("{relative_episode_number}") return s.as_ref().contains("{relative_episode_number}")
|| s.as_ref().contains("{relative_sequence_number}"); || s.as_ref().contains("{relative_sequence_number}");