mirror of
https://github.com/crunchy-labs/crunchy-cli.git
synced 2026-01-21 12:12:00 -06:00
Merge pull request #106 from crunchy-labs/feature/more-episode-number-format-options
Add relative episode number format option
This commit is contained in:
commit
4482d5482f
4 changed files with 160 additions and 106 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>> {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Formats the given string if it has specific pattern in it. It's possible to sanitize it which
|
episode_id: movie.id.clone(),
|
||||||
/// removes characters which can cause failures if the output string is used as a file name.
|
episode_number: 1.0,
|
||||||
pub fn format_path(path: PathBuf, format: &Format, sanitize: bool) -> PathBuf {
|
relative_episode_number: 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>>>();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue