mirror of
https://github.com/crunchy-labs/crunchy-cli.git
synced 2026-01-21 04:02:00 -06:00
Merge pull request #257 from crunchy-labs/feature/relative_sequence_number
Add flags and option to control special episode behavior (#206, #241, #246)
This commit is contained in:
commit
4d01e2a4ec
7 changed files with 267 additions and 107 deletions
|
|
@ -39,19 +39,26 @@ pub struct Archive {
|
||||||
#[arg(help = "Name of the output file")]
|
#[arg(help = "Name of the output file")]
|
||||||
#[arg(long_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 \
|
If you use one of the following pattern they will get replaced:\n \
|
||||||
{title} → Title of the video\n \
|
{title} → Title of the video\n \
|
||||||
{series_name} → Name of the series\n \
|
{series_name} → Name of the series\n \
|
||||||
{season_name} → Name of the season\n \
|
{season_name} → Name of the season\n \
|
||||||
{audio} → Audio language of the video\n \
|
{audio} → Audio language of the video\n \
|
||||||
{resolution} → Resolution of the video\n \
|
{resolution} → Resolution of the video\n \
|
||||||
{season_number} → Number of the season\n \
|
{season_number} → Number of the season\n \
|
||||||
{episode_number} → Number of the episode\n \
|
{episode_number} → Number of the episode\n \
|
||||||
{relative_episode_number} → Number of the episode relative to its season\n \
|
{relative_episode_number} → Number of the episode relative to its season\n \
|
||||||
{series_id} → ID of the series\n \
|
{sequence_number} → Like '{episode_number}' but without possible non-number characters\n \
|
||||||
{season_id} → ID of the season\n \
|
{relative_sequence_number} → Like '{relative_episode_number}' but with support for episode 0's and .5's\n \
|
||||||
{episode_id} → ID of the episode")]
|
{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")]
|
#[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) output_specials: Option<String>,
|
||||||
|
|
||||||
#[arg(help = "Video resolution")]
|
#[arg(help = "Video resolution")]
|
||||||
#[arg(long_help = "The video resolution.\
|
#[arg(long_help = "The video resolution.\
|
||||||
|
|
@ -93,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)]
|
||||||
|
|
@ -121,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.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());
|
self.audio = all_locale_in_locales(self.audio.clone());
|
||||||
|
|
@ -145,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));
|
||||||
|
|
@ -173,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.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());
|
let (path, changed) = free_file(formatted_path.clone());
|
||||||
|
|
||||||
if changed && self.skip_existing {
|
if changed && self.skip_existing {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use crate::archive::command::Archive;
|
||||||
use crate::utils::filter::{real_dedup_vec, Filter};
|
use crate::utils::filter::{real_dedup_vec, Filter};
|
||||||
use crate::utils::format::{Format, SingleFormat, SingleFormatCollection};
|
use crate::utils::format::{Format, SingleFormat, SingleFormatCollection};
|
||||||
use crate::utils::interactive_select::{check_for_duplicated_seasons, get_duplicated_seasons};
|
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 anyhow::Result;
|
||||||
use crunchyroll_rs::{Concert, Episode, Locale, Movie, MovieListing, MusicVideo, Season, Series};
|
use crunchyroll_rs::{Concert, Episode, Locale, Movie, MovieListing, MusicVideo, Season, Series};
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
|
|
@ -18,19 +18,26 @@ pub(crate) struct ArchiveFilter {
|
||||||
url_filter: UrlFilter,
|
url_filter: UrlFilter,
|
||||||
archive: Archive,
|
archive: Archive,
|
||||||
interactive_input: bool,
|
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_subtitles_missing: Vec<u32>,
|
||||||
season_sorting: Vec<String>,
|
season_sorting: Vec<String>,
|
||||||
visited: Visited,
|
visited: Visited,
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
season_episode_count: HashMap::new(),
|
skip_special,
|
||||||
|
season_episodes: HashMap::new(),
|
||||||
season_subtitles_missing: vec![],
|
season_subtitles_missing: vec![],
|
||||||
season_sorting: vec![],
|
season_sorting: vec![],
|
||||||
visited: Visited::None,
|
visited: Visited::None,
|
||||||
|
|
@ -226,12 +233,12 @@ impl Filter for ArchiveFilter {
|
||||||
episodes.extend(eps)
|
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() {
|
for episode in episodes.iter() {
|
||||||
self.season_episode_count
|
self.season_episodes
|
||||||
.entry(episode.season_id.clone())
|
.entry(episode.season_id.clone())
|
||||||
.or_insert(vec![])
|
.or_insert(vec![])
|
||||||
.push(episode.id.clone())
|
.push(episode.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,7 +248,14 @@ impl Filter for ArchiveFilter {
|
||||||
async fn visit_episode(&mut self, mut episode: Episode) -> Result<Option<Self::T>> {
|
async fn visit_episode(&mut self, mut episode: Episode) -> Result<Option<Self::T>> {
|
||||||
if !self
|
if !self
|
||||||
.url_filter
|
.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);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
@ -299,22 +313,36 @@ impl Filter for ArchiveFilter {
|
||||||
episodes.push((episode.clone(), episode.subtitle_locales.clone()))
|
episodes.push((episode.clone(), episode.subtitle_locales.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
let relative_episode_number = if Format::has_relative_episodes_fmt(&self.archive.output) {
|
let mut relative_episode_number = None;
|
||||||
if self.season_episode_count.get(&episode.season_id).is_none() {
|
let mut relative_sequence_number = None;
|
||||||
let season_episodes = episode.season().await?.episodes().await?;
|
// get the relative episode number. only done if the output string has the pattern to include
|
||||||
self.season_episode_count.insert(
|
// the relative episode number as this requires some extra fetching
|
||||||
episode.season_id.clone(),
|
if Format::has_relative_fmt(&self.archive.output) {
|
||||||
season_episodes.into_iter().map(|e| e.id).collect(),
|
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
|
if relative_episode_number.is_none() || relative_sequence_number.is_none() {
|
||||||
.season_episode_count
|
|
||||||
.get(&episode.season_id)
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
|
||||||
.position(|id| id == &episode.id)
|
|
||||||
.map(|index| index + 1);
|
|
||||||
if relative_episode_number.is_none() {
|
|
||||||
warn!(
|
warn!(
|
||||||
"Failed to get relative episode number for episode {} ({}) of {} season {}",
|
"Failed to get relative episode number for episode {} ({}) of {} season {}",
|
||||||
episode.episode_number,
|
episode.episode_number,
|
||||||
|
|
@ -323,16 +351,18 @@ impl Filter for ArchiveFilter {
|
||||||
episode.season_number,
|
episode.season_number,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
relative_episode_number
|
}
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(
|
Ok(Some(
|
||||||
episodes
|
episodes
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(e, s)| {
|
.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(),
|
.collect(),
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -35,19 +35,26 @@ pub struct Download {
|
||||||
#[arg(help = "Name of the output file")]
|
#[arg(help = "Name of the output file")]
|
||||||
#[arg(long_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 \
|
If you use one of the following pattern they will get replaced:\n \
|
||||||
{title} → Title of the video\n \
|
{title} → Title of the video\n \
|
||||||
{series_name} → Name of the series\n \
|
{series_name} → Name of the series\n \
|
||||||
{season_name} → Name of the season\n \
|
{season_name} → Name of the season\n \
|
||||||
{audio} → Audio language of the video\n \
|
{audio} → Audio language of the video\n \
|
||||||
{resolution} → Resolution of the video\n \
|
{resolution} → Resolution of the video\n \
|
||||||
{season_number} → Number of the season\n \
|
{season_number} → Number of the season\n \
|
||||||
{episode_number} → Number of the episode\n \
|
{episode_number} → Number of the episode\n \
|
||||||
{relative_episode_number} → Number of the episode relative to its season\n \
|
{relative_episode_number} → Number of the episode relative to its season\n \
|
||||||
{series_id} → ID of the series\n \
|
{sequence_number} → Like '{episode_number}' but without possible non-number characters\n \
|
||||||
{season_id} → ID of the season\n \
|
{relative_sequence_number} → Like '{relative_episode_number}' but with support for episode 0's and .5's\n \
|
||||||
{episode_id} → ID of the episode")]
|
{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")]
|
#[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) output_specials: Option<String>,
|
||||||
|
|
||||||
#[arg(help = "Video resolution")]
|
#[arg(help = "Video resolution")]
|
||||||
#[arg(long_help = "The video resolution.\
|
#[arg(long_help = "The video resolution.\
|
||||||
|
|
@ -71,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)]
|
||||||
|
|
@ -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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,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));
|
||||||
|
|
@ -165,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.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());
|
let (path, changed) = free_file(formatted_path.clone());
|
||||||
|
|
||||||
if changed && self.skip_existing {
|
if changed && self.skip_existing {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use crate::download::Download;
|
||||||
use crate::utils::filter::Filter;
|
use crate::utils::filter::Filter;
|
||||||
use crate::utils::format::{Format, SingleFormat, SingleFormatCollection};
|
use crate::utils::format::{Format, SingleFormat, SingleFormatCollection};
|
||||||
use crate::utils::interactive_select::{check_for_duplicated_seasons, get_duplicated_seasons};
|
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::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use crunchyroll_rs::{Concert, Episode, Movie, MovieListing, MusicVideo, Season, Series};
|
use crunchyroll_rs::{Concert, Episode, Movie, MovieListing, MusicVideo, Season, Series};
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
|
|
@ -12,18 +12,25 @@ pub(crate) struct DownloadFilter {
|
||||||
url_filter: UrlFilter,
|
url_filter: UrlFilter,
|
||||||
download: Download,
|
download: Download,
|
||||||
interactive_input: bool,
|
interactive_input: bool,
|
||||||
season_episode_count: HashMap<u32, Vec<String>>,
|
skip_special: bool,
|
||||||
|
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,
|
||||||
season_episode_count: HashMap::new(),
|
skip_special,
|
||||||
|
season_episodes: HashMap::new(),
|
||||||
season_subtitles_missing: vec![],
|
season_subtitles_missing: vec![],
|
||||||
season_visited: false,
|
season_visited: false,
|
||||||
}
|
}
|
||||||
|
|
@ -107,18 +114,18 @@ impl Filter for DownloadFilter {
|
||||||
|
|
||||||
let mut episodes = season.episodes().await?;
|
let mut episodes = season.episodes().await?;
|
||||||
|
|
||||||
if Format::has_relative_episodes_fmt(&self.download.output) {
|
if Format::has_relative_fmt(&self.download.output) {
|
||||||
for episode in episodes.iter() {
|
for episode in episodes.iter() {
|
||||||
self.season_episode_count
|
self.season_episodes
|
||||||
.entry(episode.season_number)
|
.entry(episode.season_number)
|
||||||
.or_insert(vec![])
|
.or_insert(vec![])
|
||||||
.push(episode.id.clone())
|
.push(episode.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
episodes.retain(|e| {
|
episodes.retain(|e| {
|
||||||
self.url_filter
|
self.url_filter
|
||||||
.is_episode_valid(e.episode_number, season.season_number)
|
.is_episode_valid(e.sequence_number, season.season_number)
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(episodes)
|
Ok(episodes)
|
||||||
|
|
@ -127,7 +134,14 @@ impl Filter for DownloadFilter {
|
||||||
async fn visit_episode(&mut self, mut episode: Episode) -> Result<Option<Self::T>> {
|
async fn visit_episode(&mut self, mut episode: Episode) -> Result<Option<Self::T>> {
|
||||||
if !self
|
if !self
|
||||||
.url_filter
|
.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);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
@ -189,28 +203,36 @@ impl Filter for DownloadFilter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// 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
|
// the relative episode number as this requires some extra fetching
|
||||||
let relative_episode_number = if Format::has_relative_episodes_fmt(&self.download.output) {
|
if Format::has_relative_fmt(&self.download.output) {
|
||||||
if self
|
let season_eps = match self.season_episodes.get(&episode.season_number) {
|
||||||
.season_episode_count
|
Some(eps) => eps,
|
||||||
.get(&episode.season_number)
|
None => {
|
||||||
.is_none()
|
self.season_episodes.insert(
|
||||||
{
|
episode.season_number,
|
||||||
let season_episodes = episode.season().await?.episodes().await?;
|
episode.season().await?.episodes().await?,
|
||||||
self.season_episode_count.insert(
|
);
|
||||||
episode.season_number,
|
self.season_episodes.get(&episode.season_number).unwrap()
|
||||||
season_episodes.into_iter().map(|e| e.id).collect(),
|
}
|
||||||
);
|
};
|
||||||
|
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
|
if relative_episode_number.is_none() || relative_sequence_number.is_none() {
|
||||||
.season_episode_count
|
|
||||||
.get(&episode.season_number)
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
|
||||||
.position(|id| id == &episode.id)
|
|
||||||
.map(|index| index + 1);
|
|
||||||
if relative_episode_number.is_none() {
|
|
||||||
warn!(
|
warn!(
|
||||||
"Failed to get relative episode number for episode {} ({}) of {} season {}",
|
"Failed to get relative episode number for episode {} ({}) of {} season {}",
|
||||||
episode.episode_number,
|
episode.episode_number,
|
||||||
|
|
@ -219,10 +241,7 @@ impl Filter for DownloadFilter {
|
||||||
episode.season_number,
|
episode.season_number,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
relative_episode_number
|
}
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(SingleFormat::new_from_episode(
|
Ok(Some(SingleFormat::new_from_episode(
|
||||||
episode.clone(),
|
episode.clone(),
|
||||||
|
|
@ -234,6 +253,7 @@ impl Filter for DownloadFilter {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
relative_episode_number.map(|n| n as u32),
|
relative_episode_number.map(|n| n as u32),
|
||||||
|
relative_sequence_number,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ impl FilterOptions {
|
||||||
self.check_audio_language(&vec![e.audio_locale.clone()])
|
self.check_audio_language(&vec![e.audio_locale.clone()])
|
||||||
&& self
|
&& self
|
||||||
.url_filter
|
.url_filter
|
||||||
.is_episode_valid(e.episode_number, e.season_number)
|
.is_episode_valid(e.sequence_number, e.season_number)
|
||||||
});
|
});
|
||||||
episodes
|
episodes
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,9 @@ pub struct SingleFormat {
|
||||||
|
|
||||||
pub episode_id: String,
|
pub episode_id: String,
|
||||||
pub episode_number: String,
|
pub episode_number: String,
|
||||||
pub sequence_number: f32,
|
|
||||||
pub relative_episode_number: Option<u32>,
|
pub relative_episode_number: Option<u32>,
|
||||||
|
pub sequence_number: f32,
|
||||||
|
pub relative_sequence_number: Option<f32>,
|
||||||
|
|
||||||
pub duration: Duration,
|
pub duration: Duration,
|
||||||
|
|
||||||
|
|
@ -42,6 +43,7 @@ impl SingleFormat {
|
||||||
episode: Episode,
|
episode: Episode,
|
||||||
subtitles: Vec<Locale>,
|
subtitles: Vec<Locale>,
|
||||||
relative_episode_number: Option<u32>,
|
relative_episode_number: Option<u32>,
|
||||||
|
relative_sequence_number: Option<f32>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
identifier: if episode.identifier.is_empty() {
|
identifier: if episode.identifier.is_empty() {
|
||||||
|
|
@ -73,6 +75,7 @@ impl SingleFormat {
|
||||||
},
|
},
|
||||||
sequence_number: episode.sequence_number,
|
sequence_number: episode.sequence_number,
|
||||||
relative_episode_number,
|
relative_episode_number,
|
||||||
|
relative_sequence_number,
|
||||||
duration: episode.duration,
|
duration: episode.duration,
|
||||||
source: episode.into(),
|
source: episode.into(),
|
||||||
}
|
}
|
||||||
|
|
@ -92,8 +95,9 @@ impl SingleFormat {
|
||||||
season_number: 1,
|
season_number: 1,
|
||||||
episode_id: movie.id.clone(),
|
episode_id: movie.id.clone(),
|
||||||
episode_number: "1".to_string(),
|
episode_number: "1".to_string(),
|
||||||
sequence_number: 1.0,
|
|
||||||
relative_episode_number: Some(1),
|
relative_episode_number: Some(1),
|
||||||
|
sequence_number: 1.0,
|
||||||
|
relative_sequence_number: Some(1.0),
|
||||||
duration: movie.duration,
|
duration: movie.duration,
|
||||||
source: movie.into(),
|
source: movie.into(),
|
||||||
}
|
}
|
||||||
|
|
@ -113,8 +117,9 @@ impl SingleFormat {
|
||||||
season_number: 1,
|
season_number: 1,
|
||||||
episode_id: music_video.id.clone(),
|
episode_id: music_video.id.clone(),
|
||||||
episode_number: "1".to_string(),
|
episode_number: "1".to_string(),
|
||||||
sequence_number: 1.0,
|
|
||||||
relative_episode_number: Some(1),
|
relative_episode_number: Some(1),
|
||||||
|
sequence_number: 1.0,
|
||||||
|
relative_sequence_number: Some(1.0),
|
||||||
duration: music_video.duration,
|
duration: music_video.duration,
|
||||||
source: music_video.into(),
|
source: music_video.into(),
|
||||||
}
|
}
|
||||||
|
|
@ -134,8 +139,9 @@ impl SingleFormat {
|
||||||
season_number: 1,
|
season_number: 1,
|
||||||
episode_id: concert.id.clone(),
|
episode_id: concert.id.clone(),
|
||||||
episode_number: "1".to_string(),
|
episode_number: "1".to_string(),
|
||||||
sequence_number: 1.0,
|
|
||||||
relative_episode_number: Some(1),
|
relative_episode_number: Some(1),
|
||||||
|
sequence_number: 1.0,
|
||||||
|
relative_sequence_number: Some(1.0),
|
||||||
duration: concert.duration,
|
duration: concert.duration,
|
||||||
source: concert.into(),
|
source: concert.into(),
|
||||||
}
|
}
|
||||||
|
|
@ -328,8 +334,9 @@ pub struct Format {
|
||||||
|
|
||||||
pub episode_id: String,
|
pub episode_id: String,
|
||||||
pub episode_number: String,
|
pub episode_number: String,
|
||||||
pub sequence_number: f32,
|
|
||||||
pub relative_episode_number: Option<u32>,
|
pub relative_episode_number: Option<u32>,
|
||||||
|
pub sequence_number: f32,
|
||||||
|
pub relative_sequence_number: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Format {
|
impl Format {
|
||||||
|
|
@ -363,8 +370,9 @@ impl Format {
|
||||||
season_number: first_format.season_number,
|
season_number: first_format.season_number,
|
||||||
episode_id: first_format.episode_id,
|
episode_id: first_format.episode_id,
|
||||||
episode_number: first_format.episode_number,
|
episode_number: first_format.episode_number,
|
||||||
sequence_number: first_format.sequence_number,
|
|
||||||
relative_episode_number: first_format.relative_episode_number,
|
relative_episode_number: first_format.relative_episode_number,
|
||||||
|
sequence_number: first_format.sequence_number,
|
||||||
|
relative_sequence_number: first_format.relative_sequence_number,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -400,9 +408,28 @@ impl Format {
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"{relative_episode_number}",
|
"{relative_episode_number}",
|
||||||
&sanitize(
|
&format!(
|
||||||
self.relative_episode_number.unwrap_or_default().to_string(),
|
"{:0>2}",
|
||||||
true,
|
sanitize(
|
||||||
|
self.relative_episode_number.unwrap_or_default().to_string(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"{sequence_number}",
|
||||||
|
&format!("{:0>2}", sanitize(self.sequence_number.to_string(), true)),
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"{relative_sequence_number}",
|
||||||
|
&format!(
|
||||||
|
"{:0>2}",
|
||||||
|
sanitize(
|
||||||
|
self.relative_sequence_number
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -446,7 +473,12 @@ impl Format {
|
||||||
tab_info!("FPS: {:.2}", self.fps)
|
tab_info!("FPS: {:.2}", self.fps)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_relative_episodes_fmt<S: AsRef<str>>(s: S) -> bool {
|
pub fn is_special(&self) -> bool {
|
||||||
return s.as_ref().contains("{relative_episode_number}");
|
self.sequence_number == 0.0 || self.sequence_number.fract() != 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_relative_fmt<S: AsRef<str>>(s: S) -> bool {
|
||||||
|
return s.as_ref().contains("{relative_episode_number}")
|
||||||
|
|| s.as_ref().contains("{relative_sequence_number}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ use regex::Regex;
|
||||||
/// If `to_*` is [`None`] they're set to [`u32::MAX`].
|
/// If `to_*` is [`None`] they're set to [`u32::MAX`].
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct InnerUrlFilter {
|
pub struct InnerUrlFilter {
|
||||||
from_episode: Option<u32>,
|
from_episode: Option<f32>,
|
||||||
to_episode: Option<u32>,
|
to_episode: Option<f32>,
|
||||||
from_season: Option<u32>,
|
from_season: Option<u32>,
|
||||||
to_season: Option<u32>,
|
to_season: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
@ -39,10 +39,10 @@ impl UrlFilter {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_episode_valid(&self, episode: u32, season: u32) -> bool {
|
pub fn is_episode_valid(&self, episode: f32, season: u32) -> bool {
|
||||||
self.inner.iter().any(|f| {
|
self.inner.iter().any(|f| {
|
||||||
let from_episode = f.from_episode.unwrap_or(u32::MIN);
|
let from_episode = f.from_episode.unwrap_or(f32::MIN);
|
||||||
let to_episode = f.to_episode.unwrap_or(u32::MAX);
|
let to_episode = f.to_episode.unwrap_or(f32::MAX);
|
||||||
let from_season = f.from_season.unwrap_or(u32::MIN);
|
let from_season = f.from_season.unwrap_or(u32::MIN);
|
||||||
let to_season = f.to_season.unwrap_or(u32::MAX);
|
let to_season = f.to_season.unwrap_or(u32::MAX);
|
||||||
|
|
||||||
|
|
@ -192,3 +192,13 @@ pub fn parse_resolution(mut resolution: String) -> Result<Resolution> {
|
||||||
bail!("Could not find resolution")
|
bail!("Could not find resolution")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dirty implementation of [`f32::fract`] with more accuracy.
|
||||||
|
pub fn fract(input: f32) -> f32 {
|
||||||
|
if input.fract() == 0.0 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
format!("0.{}", input.to_string().split('.').last().unwrap())
|
||||||
|
.parse::<f32>()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue