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:
ByteDream 2023-11-06 20:58:55 +00:00 committed by GitHub
commit 4d01e2a4ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 267 additions and 107 deletions

View file

@ -47,11 +47,18 @@ pub struct Archive {
{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 \
{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 \ {series_id} ID of the series\n \
{season_id} ID of the season\n \ {season_id} ID of the season\n \
{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) 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,7 +166,8 @@ 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 =
ArchiveFilter::new(url_filter, self.clone(), !self.yes, self.skip_specials)
.visit(media_collection) .visit(media_collection)
.await?; .await?;
@ -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 {

View file

@ -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
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_id.clone(),
season_episodes.into_iter().map(|e| e.id).collect(), episode.season().await?.episodes().await?,
); );
self.season_episodes.get(&episode.season_id).unwrap()
} }
let relative_episode_number = self };
.season_episode_count let mut non_integer_sequence_number_count = 0;
.get(&episode.season_id) for (i, ep) in season_eps.iter().enumerate() {
.unwrap() if ep.sequence_number.fract() != 0.0 || ep.sequence_number == 0.0 {
.iter() non_integer_sequence_number_count += 1;
.position(|id| id == &episode.id) }
.map(|index| index + 1); if ep.id == episode.id {
if relative_episode_number.is_none() { relative_episode_number = Some(i + 1);
relative_sequence_number = Some(
(i + 1 - non_integer_sequence_number_count) as f32
+ fract(ep.sequence_number),
);
break;
}
}
if relative_episode_number.is_none() || relative_sequence_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(),
)) ))

View file

@ -43,11 +43,18 @@ pub struct Download {
{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 \
{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 \ {series_id} ID of the series\n \
{season_id} ID of the season\n \ {season_id} ID of the season\n \
{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) 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,7 +162,8 @@ 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 =
DownloadFilter::new(url_filter, self.clone(), !self.yes, self.skip_specials)
.visit(media_collection) .visit(media_collection)
.await?; .await?;
@ -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 {

View file

@ -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(
{
let season_episodes = episode.season().await?.episodes().await?;
self.season_episode_count.insert(
episode.season_number, episode.season_number,
season_episodes.into_iter().map(|e| e.id).collect(), episode.season().await?.episodes().await?,
); );
self.season_episodes.get(&episode.season_number).unwrap()
} }
let relative_episode_number = self };
.season_episode_count let mut non_integer_sequence_number_count = 0;
.get(&episode.season_number) for (i, ep) in season_eps.iter().enumerate() {
.unwrap() if ep.sequence_number.fract() != 0.0 || ep.sequence_number == 0.0 {
.iter() non_integer_sequence_number_count += 1;
.position(|id| id == &episode.id) }
.map(|index| index + 1); if ep.id == episode.id {
if relative_episode_number.is_none() { relative_episode_number = Some(i + 1);
relative_sequence_number = Some(
(i + 1 - non_integer_sequence_number_count) as f32
+ fract(ep.sequence_number),
);
break;
}
}
if relative_episode_number.is_none() || relative_sequence_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,
))) )))
} }

View file

@ -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
} }

View file

@ -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!(
"{:0>2}",
sanitize(
self.relative_episode_number.unwrap_or_default().to_string(), self.relative_episode_number.unwrap_or_default().to_string(),
true, 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}");
} }
} }

View file

@ -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()
}