Merge pull request #106 from crunchy-labs/feature/more-episode-number-format-options

Add relative episode number format option
This commit is contained in:
ByteDream 2023-01-10 20:17:52 +01:00 committed by GitHub
commit 4482d5482f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 160 additions and 106 deletions

View file

@ -4,7 +4,7 @@ use crate::cli::utils::{
interactive_season_choosing, FFmpegPreset, interactive_season_choosing, FFmpegPreset,
}; };
use crate::utils::context::Context; use crate::utils::context::Context;
use crate::utils::format::{format_path, Format}; use crate::utils::format::Format;
use crate::utils::log::progress; use crate::utils::log::progress;
use crate::utils::os::{free_file, has_ffmpeg, is_special_file, tempfile}; use crate::utils::os::{free_file, has_ffmpeg, is_special_file, tempfile};
use crate::utils::parse::{parse_url, UrlFilter}; use crate::utils::parse::{parse_url, UrlFilter};
@ -67,10 +67,9 @@ pub struct Archive {
{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 \
{padded_season_number} Number of the season padded to double digits\n \
{season_number} Number of the season\n \ {season_number} Number of the season\n \
{padded_episode_number} Number of the episode padded to double digits\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\
{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")]
@ -208,7 +207,7 @@ impl Execute for Archive {
format.stream.resolution, format.stream.resolution,
format.stream.fps, format.stream.fps,
format.season_number, format.season_number,
format.number, format.episode_number,
) )
} }
} }
@ -234,7 +233,7 @@ impl Execute for Archive {
format.stream.resolution, format.stream.resolution,
format.stream.fps, format.stream.fps,
format.season_number, format.season_number,
format.number format.episode_number
) )
} }
} }
@ -243,16 +242,17 @@ impl Execute for Archive {
for (formats, mut subtitles) in archive_formats { for (formats, mut subtitles) in archive_formats {
let (primary, additionally) = formats.split_first().unwrap(); let (primary, additionally) = formats.split_first().unwrap();
let path = free_file(format_path( let path = free_file(
primary.format_path(
if self.output.is_empty() { if self.output.is_empty() {
"{title}.mkv" "{title}.mkv"
} else { } else {
&self.output &self.output
} }
.into(), .into(),
&primary,
true, true,
)); ),
);
info!( info!(
"Downloading {} to '{}'", "Downloading {} to '{}'",
@ -266,7 +266,7 @@ impl Execute for Archive {
tab_info!( tab_info!(
"Episode: S{:02}E{:02}", "Episode: S{:02}E{:02}",
primary.season_number, primary.season_number,
primary.number primary.episode_number
); );
tab_info!( tab_info!(
"Audio: {} (primary), {}", "Audio: {} (primary), {}",
@ -318,7 +318,7 @@ impl Execute for Archive {
// Remove subtitles of deleted video // Remove subtitles of deleted video
if only_audio { if only_audio {
subtitles.retain(|s| s.episode_id != additional.id); subtitles.retain(|s| s.episode_id != additional.episode_id);
} }
} }
@ -395,7 +395,9 @@ async fn formats_from_series(
let mut result: BTreeMap<u32, BTreeMap<u32, (Vec<Format>, Vec<Subtitle>)>> = BTreeMap::new(); let mut result: BTreeMap<u32, BTreeMap<u32, (Vec<Format>, Vec<Subtitle>)>> = BTreeMap::new();
let mut primary_season = true; let mut primary_season = true;
for season in seasons { for season in seasons {
for episode in season.episodes().await? { let episodes = season.episodes().await?;
for episode in episodes.iter() {
if !url_filter.is_episode_valid( if !url_filter.is_episode_valid(
episode.metadata.episode_number, episode.metadata.episode_number,
episode.metadata.season_number, episode.metadata.season_number,
@ -434,7 +436,7 @@ async fn formats_from_series(
}; };
Some(subtitle) Some(subtitle)
})); }));
formats.push(Format::new_from_episode(episode, stream)); formats.push(Format::new_from_episode(episode, &episodes, stream));
} }
primary_season = false; primary_season = false;

View file

@ -4,7 +4,7 @@ use crate::cli::utils::{
interactive_season_choosing, FFmpegPreset, interactive_season_choosing, FFmpegPreset,
}; };
use crate::utils::context::Context; use crate::utils::context::Context;
use crate::utils::format::{format_path, Format}; use crate::utils::format::Format;
use crate::utils::log::progress; 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};
use crate::utils::parse::{parse_url, UrlFilter}; use crate::utils::parse::{parse_url, UrlFilter};
@ -16,6 +16,7 @@ use crunchyroll_rs::{
Episode, Locale, Media, MediaCollection, Movie, MovieListing, Season, Series, Episode, Locale, Media, MediaCollection, Movie, MovieListing, Season, Series,
}; };
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use std::borrow::Cow;
use std::fs::File; use std::fs::File;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
@ -44,10 +45,9 @@ pub struct Download {
{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 \
{padded_season_number} Number of the season padded to double digits\n \
{season_number} Number of the season\n \ {season_number} Number of the season\n \
{padded_episode_number} Number of the episode padded to double digits\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\
{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")]
@ -148,7 +148,7 @@ impl Execute for Download {
episode.metadata.season_title, episode.metadata.season_title,
episode.metadata.series_title episode.metadata.series_title
); );
format_from_episode(&self, episode, &url_filter, false) format_from_episode(&self, &episode, &url_filter, None, false)
.await? .await?
.map(|fmt| vec![fmt]) .map(|fmt| vec![fmt])
} }
@ -182,7 +182,7 @@ impl Execute for Download {
format.stream.resolution, format.stream.resolution,
format.stream.fps, format.stream.fps,
format.season_number, format.season_number,
format.number, format.episode_number,
) )
} }
} }
@ -202,23 +202,24 @@ impl Execute for Download {
format.stream.resolution, format.stream.resolution,
format.stream.fps, format.stream.fps,
format.season_number, format.season_number,
format.number format.episode_number
) )
} }
} }
} }
for format in formats { for format in formats {
let path = free_file(format_path( let path = free_file(
format.format_path(
if self.output.is_empty() { if self.output.is_empty() {
"{title}.mkv" "{title}.mkv"
} else { } else {
&self.output &self.output
} }
.into(), .into(),
&format,
true, true,
)); ),
);
info!( info!(
"Downloading {} to '{}'", "Downloading {} to '{}'",
@ -229,7 +230,11 @@ impl Execute for Download {
path.file_name().unwrap().to_str().unwrap() path.file_name().unwrap().to_str().unwrap()
} }
); );
tab_info!("Episode: S{:02}E{:02}", format.season_number, format.number); tab_info!(
"Episode: S{:02}E{:02}",
format.season_number,
format.episode_number
);
tab_info!("Audio: {}", format.audio); tab_info!("Audio: {}", format.audio);
tab_info!( tab_info!(
"Subtitles: {}", "Subtitles: {}",
@ -388,8 +393,11 @@ async fn formats_from_season(
let mut formats = vec![]; let mut formats = vec![];
for episode in season.episodes().await? { let episodes = season.episodes().await?;
if let Some(fmt) = format_from_episode(download, episode, url_filter, true).await? { for episode in episodes.iter() {
if let Some(fmt) =
format_from_episode(download, &episode, url_filter, Some(&episodes), true).await?
{
formats.push(fmt) formats.push(fmt)
} }
} }
@ -399,8 +407,9 @@ async fn formats_from_season(
async fn format_from_episode( async fn format_from_episode(
download: &Download, download: &Download,
episode: Media<Episode>, episode: &Media<Episode>,
url_filter: &UrlFilter, url_filter: &UrlFilter,
season_episodes: Option<&Vec<Media<Episode>>>,
filter_audio: bool, filter_audio: bool,
) -> Result<Option<Format>> { ) -> Result<Option<Format>> {
if filter_audio && episode.metadata.audio_locale != download.audio { if filter_audio && episode.metadata.audio_locale != download.audio {
@ -453,7 +462,21 @@ async fn format_from_episode(
) )
}; };
Ok(Some(Format::new_from_episode(episode, stream))) let season_eps = if Format::has_relative_episodes_fmt(&download.output) {
if let Some(eps) = season_episodes {
Cow::from(eps)
} else {
Cow::from(episode.season().await?.episodes().await?)
}
} else {
Cow::from(vec![])
};
Ok(Some(Format::new_from_episode(
episode,
&season_eps.to_vec(),
stream,
)))
} }
async fn format_from_movie_listing( async fn format_from_movie_listing(
@ -511,7 +534,7 @@ async fn format_from_movie(
} }
}; };
Ok(Some(Format::new_from_movie(movie, stream))) Ok(Some(Format::new_from_movie(&movie, stream)))
} }
fn some_vec_or_none<T>(v: Vec<T>) -> Option<Vec<T>> { fn some_vec_or_none<T>(v: Vec<T>) -> Option<Vec<T>> {

View file

@ -1,14 +1,14 @@
use crunchyroll_rs::media::VariantData; use crunchyroll_rs::media::VariantData;
use crunchyroll_rs::{Episode, Locale, Media, Movie}; use crunchyroll_rs::{Episode, Locale, Media, Movie};
use log::warn;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
#[derive(Clone)] #[derive(Clone)]
pub struct Format { pub struct Format {
pub id: String,
pub title: String, pub title: String,
pub description: String, pub description: String,
pub number: u32,
pub audio: Locale, pub audio: Locale,
pub duration: Duration, pub duration: Duration,
@ -20,35 +20,60 @@ pub struct Format {
pub season_id: String, pub season_id: String,
pub season_title: String, pub season_title: String,
pub season_number: u32, pub season_number: u32,
pub episode_id: String,
pub episode_number: f32,
pub relative_episode_number: f32,
} }
impl Format { impl Format {
pub fn new_from_episode(episode: Media<Episode>, stream: VariantData) -> Self { pub fn new_from_episode(
episode: &Media<Episode>,
season_episodes: &Vec<Media<Episode>>,
stream: VariantData,
) -> Self {
Self { Self {
id: episode.id, title: episode.title.clone(),
title: episode.title, description: episode.description.clone(),
description: episode.description,
number: episode.metadata.episode_number, audio: episode.metadata.audio_locale.clone(),
audio: episode.metadata.audio_locale,
duration: episode.metadata.duration.to_std().unwrap(), duration: episode.metadata.duration.to_std().unwrap(),
stream, stream,
series_id: episode.metadata.series_id, series_id: episode.metadata.series_id.clone(),
series_name: episode.metadata.series_title, series_name: episode.metadata.series_title.clone(),
season_id: episode.metadata.season_id, season_id: episode.metadata.season_id.clone(),
season_title: episode.metadata.season_title, season_title: episode.metadata.season_title.clone(),
season_number: episode.metadata.season_number, season_number: episode.metadata.season_number.clone(),
episode_id: episode.id.clone(),
episode_number: episode
.metadata
.episode
.parse()
.unwrap_or(episode.metadata.sequence_number),
relative_episode_number: season_episodes
.iter()
.enumerate()
.find_map(|(i, e)| if e == episode { Some((i + 1) as f32) } else { None })
.unwrap_or_else(|| {
warn!("Cannot find relative episode number for episode {} ({}) of season {} ({}) of {}, using normal episode number", episode.metadata.episode_number, episode.title, episode.metadata.season_number, episode.metadata.season_title, episode.metadata.series_title);
episode
.metadata
.episode
.parse()
.unwrap_or(episode.metadata.sequence_number)
}),
} }
} }
pub fn new_from_movie(movie: Media<Movie>, stream: VariantData) -> Self { pub fn new_from_movie(movie: &Media<Movie>, stream: VariantData) -> Self {
Self { Self {
id: movie.id, title: movie.title.clone(),
title: movie.title, description: movie.description.clone(),
description: movie.description,
number: 1,
audio: Locale::ja_JP, audio: Locale::ja_JP,
duration: movie.metadata.duration.to_std().unwrap(), duration: movie.metadata.duration.to_std().unwrap(),
@ -57,16 +82,19 @@ impl Format {
series_id: movie.metadata.movie_listing_id.clone(), series_id: movie.metadata.movie_listing_id.clone(),
series_name: movie.metadata.movie_listing_title.clone(), series_name: movie.metadata.movie_listing_title.clone(),
season_id: movie.metadata.movie_listing_id, season_id: movie.metadata.movie_listing_id.clone(),
season_title: movie.metadata.movie_listing_title, season_title: movie.metadata.movie_listing_title.clone(),
season_number: 1, season_number: 1,
}
episode_id: movie.id.clone(),
episode_number: 1.0,
relative_episode_number: 1.0,
} }
} }
/// Formats the given string if it has specific pattern in it. It's possible to sanitize it which /// Formats the given string if it has specific pattern in it. It's possible to sanitize it which
/// removes characters which can cause failures if the output string is used as a file name. /// removes characters which can cause failures if the output string is used as a file name.
pub fn format_path(path: PathBuf, format: &Format, sanitize: bool) -> PathBuf { pub fn format_path(&self, path: PathBuf, sanitize: bool) -> PathBuf {
let sanitize_func = if sanitize { let sanitize_func = if sanitize {
|s: &str| sanitize_filename::sanitize(s) |s: &str| sanitize_filename::sanitize(s)
} else { } else {
@ -78,32 +106,33 @@ pub fn format_path(path: PathBuf, format: &Format, sanitize: bool) -> PathBuf {
PathBuf::from( PathBuf::from(
as_string as_string
.replace("{title}", &sanitize_func(&format.title)) .replace("{title}", &sanitize_func(&self.title))
.replace("{series_name}", &sanitize_func(&format.series_name)) .replace("{audio}", &sanitize_func(&self.audio.to_string()))
.replace("{season_name}", &sanitize_func(&format.season_title))
.replace("{audio}", &sanitize_func(&format.audio.to_string()))
.replace( .replace(
"{resolution}", "{resolution}",
&sanitize_func(&format.stream.resolution.to_string()), &sanitize_func(&self.stream.resolution.to_string()),
)
.replace(
"{padded_season_number}",
&sanitize_func(&format!("{:0>2}", format.season_number.to_string())),
) )
.replace("{series_id}", &sanitize_func(&self.series_id))
.replace("{series_name}", &sanitize_func(&self.series_name))
.replace("{season_id}", &sanitize_func(&self.season_id))
.replace("{season_name}", &sanitize_func(&self.season_title))
.replace( .replace(
"{season_number}", "{season_number}",
&sanitize_func(&format.season_number.to_string()), &sanitize_func(&format!("{:0>2}", self.season_number.to_string())),
)
.replace(
"{padded_episode_number}",
&sanitize_func(&format!("{:0>2}", format.number.to_string())),
) )
.replace("{episode_id}", &sanitize_func(&self.episode_id))
.replace( .replace(
"{episode_number}", "{episode_number}",
&sanitize_func(&format.number.to_string()), &sanitize_func(&format!("{:0>2}", self.episode_number.to_string())),
) )
.replace("{series_id}", &sanitize_func(&format.series_id)) .replace(
.replace("{season_id}", &sanitize_func(&format.season_id)) "{relative_episode_number}",
.replace("{episode_id}", &sanitize_func(&format.id)), &sanitize_func(&format!("{:0>2}", self.relative_episode_number.to_string())),
),
) )
} }
pub fn has_relative_episodes_fmt<S: AsRef<str>>(s: S) -> bool {
return s.as_ref().contains("{relative_episode_number}");
}
}

View file

@ -42,7 +42,7 @@ pub fn sort_formats_after_seasons(formats: Vec<Format>) -> Vec<Vec<Format>> {
let mut sorted = as_map let mut sorted = as_map
.into_iter() .into_iter()
.map(|(_, mut values)| { .map(|(_, mut values)| {
values.sort_by(|a, b| a.number.cmp(&b.number)); values.sort_by(|a, b| a.episode_number.total_cmp(&b.episode_number));
values values
}) })
.collect::<Vec<Vec<Format>>>(); .collect::<Vec<Vec<Format>>>();