Skip to content
This repository has been archived by the owner on Jul 4, 2024. It is now read-only.

Add flags and option to control special episode behavior (#206, #241, #246) #257

Merged
merged 7 commits into from
Nov 6, 2023
60 changes: 45 additions & 15 deletions crunchy-cli-core/src/archive/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,26 @@ pub struct Archive {
#[arg(help = "Name of the output file")]
#[arg(long_help = "Name of the output file.\
If you use one of the following pattern they will get replaced:\n \
{title} → Title of the video\n \
{series_name} → Name of the series\n \
{season_name} → Name of the season\n \
{audio} → Audio language of the video\n \
{resolution} → Resolution of the video\n \
{season_number} → Number of the season\n \
{episode_number} → Number of the episode\n \
{relative_episode_number} → Number of the episode relative to its season\n \
{series_id} → ID of the series\n \
{season_id} → ID of the season\n \
{episode_id} → ID of the episode")]
{title} → Title of the video\n \
{series_name} → Name of the series\n \
{season_name} → Name of the season\n \
{audio} → Audio language of the video\n \
{resolution} → Resolution of the video\n \
{season_number} → Number of the season\n \
{episode_number} → Number of the episode\n \
{relative_episode_number} → Number of the episode relative to its season\n \
{sequence_number} → Like '{episode_number}' but without possible non-number characters\n \
{relative_sequence_number} → Like '{relative_episode_number}' but with support for episode 0's and .5's\n \
{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}.mkv")]
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) output_specials: Option<String>,

#[arg(help = "Video resolution")]
#[arg(long_help = "The video resolution.\
Expand Down Expand Up @@ -93,6 +100,9 @@ pub struct Archive {
#[arg(help = "Skip files which are already existing")]
#[arg(long, default_value_t = false)]
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(short, long, default_value_t = false)]
Expand Down Expand Up @@ -121,6 +131,17 @@ impl Execute for Archive {
&& self.output != "-"
{
bail!("File extension is not '.mkv'. Currently only matroska / '.mkv' files are supported")
} else if let Some(special_output) = &self.output_specials {
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());
Expand All @@ -145,9 +166,10 @@ impl Execute for Archive {

for (i, (media_collection, url_filter)) in parsed_urls.into_iter().enumerate() {
let progress_handler = progress!("Fetching series details");
let single_format_collection = ArchiveFilter::new(url_filter, self.clone(), !self.yes)
.visit(media_collection)
.await?;
let single_format_collection =
ArchiveFilter::new(url_filter, self.clone(), !self.yes, self.skip_specials)
.visit(media_collection)
.await?;

if single_format_collection.is_empty() {
progress_handler.stop(format!("Skipping url {} (no matching videos found)", i + 1));
Expand All @@ -173,7 +195,15 @@ impl Execute for Archive {
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.output_specials
.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());

if changed && self.skip_existing {
Expand Down
86 changes: 58 additions & 28 deletions crunchy-cli-core/src/archive/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::archive::command::Archive;
use crate::utils::filter::{real_dedup_vec, Filter};
use crate::utils::format::{Format, SingleFormat, SingleFormatCollection};
use crate::utils::interactive_select::{check_for_duplicated_seasons, get_duplicated_seasons};
use crate::utils::parse::UrlFilter;
use crate::utils::parse::{fract, UrlFilter};
use anyhow::Result;
use crunchyroll_rs::{Concert, Episode, Locale, Movie, MovieListing, MusicVideo, Season, Series};
use log::{info, warn};
Expand All @@ -18,19 +18,26 @@ pub(crate) struct ArchiveFilter {
url_filter: UrlFilter,
archive: Archive,
interactive_input: bool,
season_episode_count: HashMap<String, Vec<String>>,
skip_special: bool,
season_episodes: HashMap<String, Vec<Episode>>,
season_subtitles_missing: Vec<u32>,
season_sorting: Vec<String>,
visited: Visited,
}

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 {
url_filter,
archive,
interactive_input,
season_episode_count: HashMap::new(),
skip_special,
season_episodes: HashMap::new(),
season_subtitles_missing: vec![],
season_sorting: vec![],
visited: Visited::None,
Expand Down Expand Up @@ -226,12 +233,12 @@ impl Filter for ArchiveFilter {
episodes.extend(eps)
}

if Format::has_relative_episodes_fmt(&self.archive.output) {
if Format::has_relative_fmt(&self.archive.output) {
for episode in episodes.iter() {
self.season_episode_count
self.season_episodes
.entry(episode.season_id.clone())
.or_insert(vec![])
.push(episode.id.clone())
.push(episode.clone())
}
}

Expand All @@ -241,7 +248,14 @@ impl Filter for ArchiveFilter {
async fn visit_episode(&mut self, mut episode: Episode) -> Result<Option<Self::T>> {
if !self
.url_filter
.is_episode_valid(episode.episode_number, episode.season_number)
.is_episode_valid(episode.sequence_number, episode.season_number)
{
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);
}
Expand Down Expand Up @@ -299,22 +313,36 @@ impl Filter for ArchiveFilter {
episodes.push((episode.clone(), episode.subtitle_locales.clone()))
}

let relative_episode_number = if Format::has_relative_episodes_fmt(&self.archive.output) {
if self.season_episode_count.get(&episode.season_id).is_none() {
let season_episodes = episode.season().await?.episodes().await?;
self.season_episode_count.insert(
episode.season_id.clone(),
season_episodes.into_iter().map(|e| e.id).collect(),
);
let mut relative_episode_number = None;
let mut relative_sequence_number = None;
// get the relative episode number. only done if the output string has the pattern to include
// the relative episode number as this requires some extra fetching
if Format::has_relative_fmt(&self.archive.output) {
let season_eps = match self.season_episodes.get(&episode.season_id) {
Some(eps) => eps,
None => {
self.season_episodes.insert(
episode.season_id.clone(),
episode.season().await?.episodes().await?,
);
self.season_episodes.get(&episode.season_id).unwrap()
}
};
let mut non_integer_sequence_number_count = 0;
for (i, ep) in season_eps.iter().enumerate() {
if ep.sequence_number.fract() != 0.0 || ep.sequence_number == 0.0 {
non_integer_sequence_number_count += 1;
}
if ep.id == episode.id {
relative_episode_number = Some(i + 1);
relative_sequence_number = Some(
(i + 1 - non_integer_sequence_number_count) as f32
+ fract(ep.sequence_number),
);
break;
}
}
let relative_episode_number = self
.season_episode_count
.get(&episode.season_id)
.unwrap()
.iter()
.position(|id| id == &episode.id)
.map(|index| index + 1);
if relative_episode_number.is_none() {
if relative_episode_number.is_none() || relative_sequence_number.is_none() {
warn!(
"Failed to get relative episode number for episode {} ({}) of {} season {}",
episode.episode_number,
Expand All @@ -323,16 +351,18 @@ impl Filter for ArchiveFilter {
episode.season_number,
)
}
relative_episode_number
} else {
None
};
}

Ok(Some(
episodes
.into_iter()
.map(|(e, s)| {
SingleFormat::new_from_episode(e, s, relative_episode_number.map(|n| n as u32))
SingleFormat::new_from_episode(
e,
s,
relative_episode_number.map(|n| n as u32),
relative_sequence_number,
)
})
.collect(),
))
Expand Down
68 changes: 53 additions & 15 deletions crunchy-cli-core/src/download/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,26 @@ pub struct Download {
#[arg(help = "Name of the output file")]
#[arg(long_help = "Name of the output file.\
If you use one of the following pattern they will get replaced:\n \
{title} → Title of the video\n \
{series_name} → Name of the series\n \
{season_name} → Name of the season\n \
{audio} → Audio language of the video\n \
{resolution} → Resolution of the video\n \
{season_number} → Number of the season\n \
{episode_number} → Number of the episode\n \
{relative_episode_number} → Number of the episode relative to its season\n \
{series_id} → ID of the series\n \
{season_id} → ID of the season\n \
{episode_id} → ID of the episode")]
{title} → Title of the video\n \
{series_name} → Name of the series\n \
{season_name} → Name of the season\n \
{audio} → Audio language of the video\n \
{resolution} → Resolution of the video\n \
{season_number} → Number of the season\n \
{episode_number} → Number of the episode\n \
{relative_episode_number} → Number of the episode relative to its season\n \
{sequence_number} → Like '{episode_number}' but without possible non-number characters\n \
{relative_sequence_number} → Like '{relative_episode_number}' but with support for episode 0's and .5's\n \
{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}.mp4")]
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) output_specials: Option<String>,

#[arg(help = "Video resolution")]
#[arg(long_help = "The video resolution.\
Expand All @@ -71,6 +78,9 @@ pub struct Download {
#[arg(help = "Skip files which are already existing")]
#[arg(long, default_value_t = false)]
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(short, long, default_value_t = false)]
Expand Down Expand Up @@ -114,6 +124,25 @@ impl Execute for Download {
}
}

if let Some(special_output) = &self.output_specials {
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 `--output-specials`) 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(())
}

Expand All @@ -133,9 +162,10 @@ impl Execute for Download {

for (i, (media_collection, url_filter)) in parsed_urls.into_iter().enumerate() {
let progress_handler = progress!("Fetching series details");
let single_format_collection = DownloadFilter::new(url_filter, self.clone(), !self.yes)
.visit(media_collection)
.await?;
let single_format_collection =
DownloadFilter::new(url_filter, self.clone(), !self.yes, self.skip_specials)
.visit(media_collection)
.await?;

if single_format_collection.is_empty() {
progress_handler.stop(format!("Skipping url {} (no matching videos found)", i + 1));
Expand Down Expand Up @@ -165,7 +195,15 @@ impl Execute for Download {
let mut downloader = download_builder.clone().build();
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.output_specials
.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());

if changed && self.skip_existing {
Expand Down
Loading