Add option to select seasons when season number is duplicated (#199)

This commit is contained in:
bytedream 2023-06-23 17:19:16 +02:00
parent d75c04fbb6
commit fc44b8af8a
10 changed files with 227 additions and 52 deletions

28
Cargo.lock generated
View file

@ -355,6 +355,7 @@ dependencies = [
"crunchyroll-rs", "crunchyroll-rs",
"ctrlc", "ctrlc",
"derive_setters", "derive_setters",
"dialoguer",
"dirs", "dirs",
"fs2", "fs2",
"indicatif", "indicatif",
@ -370,7 +371,6 @@ dependencies = [
"shlex", "shlex",
"sys-locale", "sys-locale",
"tempfile", "tempfile",
"terminal_size",
"tokio", "tokio",
] ]
@ -501,6 +501,16 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "dialoguer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87"
dependencies = [
"console",
"shell-words",
]
[[package]] [[package]]
name = "dirs" name = "dirs"
version = "5.0.1" version = "5.0.1"
@ -1539,6 +1549,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "shell-words"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.1.0" version = "1.1.0"
@ -1628,16 +1644,6 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "terminal_size"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237"
dependencies = [
"rustix",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.40" version = "1.0.40"

View file

@ -11,6 +11,7 @@ clap = { version = "4.3", features = ["derive", "string"] }
chrono = "0.4" chrono = "0.4"
crunchyroll-rs = { version = "0.3.6", features = ["dash-stream"] } crunchyroll-rs = { version = "0.3.6", features = ["dash-stream"] }
ctrlc = "3.4" ctrlc = "3.4"
dialoguer = { version = "0.10", default-features = false }
dirs = "5.0" dirs = "5.0"
derive_setters = "0.1" derive_setters = "0.1"
fs2 = "0.4" fs2 = "0.4"
@ -26,7 +27,6 @@ serde_json = "1.0"
serde_plain = "1.0" serde_plain = "1.0"
shlex = "1.1" shlex = "1.1"
tempfile = "3.6" tempfile = "3.6"
terminal_size = "0.2"
tokio = { version = "1.28", features = ["macros", "rt-multi-thread", "time"] } tokio = { version = "1.28", features = ["macros", "rt-multi-thread", "time"] }
sys-locale = "0.3" sys-locale = "0.3"

View file

@ -98,6 +98,10 @@ pub struct Archive {
#[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 any interactive input")]
#[arg(short, long, default_value_t = false)]
pub(crate) yes: bool,
#[arg(help = "Crunchyroll series url(s)")] #[arg(help = "Crunchyroll series url(s)")]
pub(crate) urls: Vec<String>, pub(crate) urls: Vec<String>,
} }
@ -149,7 +153,7 @@ 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()) let single_format_collection = ArchiveFilter::new(url_filter, self.clone(), !self.yes)
.visit(media_collection) .visit(media_collection)
.await?; .await?;

View file

@ -1,10 +1,11 @@
use crate::archive::command::Archive; 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::parse::UrlFilter; use crate::utils::parse::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::warn; use log::{info, warn};
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
enum Visited { enum Visited {
@ -16,6 +17,7 @@ enum Visited {
pub(crate) struct ArchiveFilter { pub(crate) struct ArchiveFilter {
url_filter: UrlFilter, url_filter: UrlFilter,
archive: Archive, archive: Archive,
interactive_input: bool,
season_episode_count: HashMap<u32, Vec<String>>, season_episode_count: HashMap<u32, Vec<String>>,
season_subtitles_missing: Vec<u32>, season_subtitles_missing: Vec<u32>,
season_sorting: Vec<String>, season_sorting: Vec<String>,
@ -23,10 +25,11 @@ pub(crate) struct ArchiveFilter {
} }
impl ArchiveFilter { impl ArchiveFilter {
pub(crate) fn new(url_filter: UrlFilter, archive: Archive) -> Self { pub(crate) fn new(url_filter: UrlFilter, archive: Archive, interactive_input: bool) -> Self {
Self { Self {
url_filter, url_filter,
archive, archive,
interactive_input,
season_episode_count: HashMap::new(), season_episode_count: HashMap::new(),
season_subtitles_missing: vec![], season_subtitles_missing: vec![],
season_sorting: vec![], season_sorting: vec![],
@ -71,7 +74,44 @@ impl Filter for ArchiveFilter {
} }
self.visited = Visited::Series self.visited = Visited::Series
} }
Ok(series.seasons().await?)
let mut seasons = series.seasons().await?;
let mut remove_ids = vec![];
for season in seasons.iter_mut() {
if !self.url_filter.is_season_valid(season.season_number)
&& !season
.audio_locales
.iter()
.any(|l| self.archive.audio.contains(l))
&& !season
.available_versions()
.await?
.iter()
.any(|l| self.archive.audio.contains(l))
{
remove_ids.push(season.id.clone());
}
}
seasons.retain(|s| !remove_ids.contains(&s.id));
let duplicated_seasons = get_duplicated_seasons(&seasons);
if duplicated_seasons.len() > 0 {
if self.interactive_input {
check_for_duplicated_seasons(&mut seasons);
} else {
info!(
"Found duplicated seasons: {}",
duplicated_seasons
.iter()
.map(|d| d.to_string())
.collect::<Vec<String>>()
.join(", ")
)
}
}
Ok(seasons)
} }
async fn visit_season(&mut self, mut season: Season) -> Result<Vec<Episode>> { async fn visit_season(&mut self, mut season: Season) -> Result<Vec<Episode>> {

View file

@ -73,6 +73,10 @@ pub struct Download {
#[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 any interactive input")]
#[arg(short, long, default_value_t = false)]
pub(crate) yes: bool,
#[arg(help = "Url(s) to Crunchyroll episodes or series")] #[arg(help = "Url(s) to Crunchyroll episodes or series")]
pub(crate) urls: Vec<String>, pub(crate) urls: Vec<String>,
} }
@ -119,7 +123,7 @@ 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()) let single_format_collection = DownloadFilter::new(url_filter, self.clone(), !self.yes)
.visit(media_collection) .visit(media_collection)
.await?; .await?;

View file

@ -1,24 +1,27 @@
use crate::download::Download; 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::parse::UrlFilter; use crate::utils::parse::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, warn}; use log::{error, info, warn};
use std::collections::HashMap; use std::collections::HashMap;
pub(crate) struct DownloadFilter { pub(crate) struct DownloadFilter {
url_filter: UrlFilter, url_filter: UrlFilter,
download: Download, download: Download,
interactive_input: bool,
season_episode_count: HashMap<u32, Vec<String>>, season_episode_count: HashMap<u32, Vec<String>>,
season_subtitles_missing: Vec<u32>, season_subtitles_missing: Vec<u32>,
} }
impl DownloadFilter { impl DownloadFilter {
pub(crate) fn new(url_filter: UrlFilter, download: Download) -> Self { pub(crate) fn new(url_filter: UrlFilter, download: Download, interactive_input: bool) -> Self {
Self { Self {
url_filter, url_filter,
download, download,
interactive_input,
season_episode_count: HashMap::new(), season_episode_count: HashMap::new(),
season_subtitles_missing: vec![], season_subtitles_missing: vec![],
} }
@ -43,14 +46,10 @@ impl Filter for DownloadFilter {
} }
} }
let seasons = series.seasons().await?; let mut seasons = vec![];
for mut season in series.seasons().await? {
Ok(seasons)
}
async fn visit_season(&mut self, mut season: Season) -> Result<Vec<Episode>> {
if !self.url_filter.is_season_valid(season.season_number) { if !self.url_filter.is_season_valid(season.season_number) {
return Ok(vec![]); continue;
} }
if !season if !season
@ -75,10 +74,33 @@ impl Filter for DownloadFilter {
season.title, season.title,
self.download.audio.clone(), self.download.audio.clone(),
); );
return Ok(vec![]); continue;
} }
} }
seasons.push(season)
}
let duplicated_seasons = get_duplicated_seasons(&seasons);
if duplicated_seasons.len() > 0 {
if self.interactive_input {
check_for_duplicated_seasons(&mut seasons);
} else {
info!(
"Found duplicated seasons: {}",
duplicated_seasons
.iter()
.map(|d| d.to_string())
.collect::<Vec<String>>()
.join(", ")
)
}
}
Ok(seasons)
}
async fn visit_season(&mut self, season: Season) -> Result<Vec<Episode>> {
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_episodes_fmt(&self.download.output) {

View file

@ -18,6 +18,7 @@ mod search;
mod utils; mod utils;
pub use archive::Archive; pub use archive::Archive;
use dialoguer::console::Term;
pub use download::Download; pub use download::Download;
pub use login::Login; pub use login::Login;
pub use search::Search; pub use search::Search;
@ -168,6 +169,9 @@ pub async fn cli_entrypoint() {
} }
} }
} }
// when pressing ctrl-c while interactively choosing seasons the cursor stays hidden, this
// line shows it again
let _ = Term::stdout().show_cursor();
std::process::exit(1) std::process::exit(1)
}) })
.unwrap(); .unwrap();

View file

@ -0,0 +1,73 @@
use crate::utils::log::progress_pause;
use crunchyroll_rs::Season;
use dialoguer::console::Term;
use dialoguer::MultiSelect;
use std::collections::BTreeMap;
pub fn get_duplicated_seasons(seasons: &Vec<Season>) -> Vec<u32> {
let mut season_number_counter = BTreeMap::<u32, u32>::new();
for season in seasons {
season_number_counter
.entry(season.season_number)
.and_modify(|c| *c += 1)
.or_default();
}
season_number_counter
.into_iter()
.filter_map(|(k, v)| if v > 0 { Some(k) } else { None })
.collect()
}
pub fn check_for_duplicated_seasons(seasons: &mut Vec<Season>) {
let mut as_map = BTreeMap::new();
for season in seasons.iter() {
as_map
.entry(season.season_number)
.or_insert(vec![])
.push(season)
}
let duplicates: Vec<&Season> = as_map
.into_values()
.filter(|s| s.len() > 1)
.flatten()
.collect();
progress_pause!();
let _ = Term::stdout().clear_line();
let keep = select(
"Duplicated seasons were found. Select the one you want to download (space to select/deselect; enter to continue)",
duplicates
.iter()
.map(|s| format!("Season {} ({})", s.season_number, s.title))
.collect(),
);
progress_pause!();
let mut remove_ids = vec![];
for (i, duplicate) in duplicates.into_iter().enumerate() {
if !keep.contains(&i) {
remove_ids.push(duplicate.id.clone())
}
}
seasons.retain(|s| !remove_ids.contains(&s.id));
}
pub fn select(prompt: &str, input: Vec<String>) -> Vec<usize> {
if input.is_empty() {
return vec![];
}
let def: Vec<bool> = (0..input.len()).map(|_| true).collect();
let selection = MultiSelect::new()
.with_prompt(prompt)
.items(&input[..])
.defaults(&def[..])
.clear(false)
.report(false)
.interact_on(&Term::stdout())
.unwrap_or_default();
selection
}

View file

@ -1,4 +1,4 @@
use indicatif::{ProgressBar, ProgressStyle}; use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use log::{ use log::{
info, set_boxed_logger, set_max_level, Level, LevelFilter, Log, Metadata, Record, info, set_boxed_logger, set_max_level, Level, LevelFilter, Log, Metadata, Record,
SetLoggerError, SetLoggerError,
@ -37,6 +37,15 @@ macro_rules! progress {
} }
pub(crate) use progress; pub(crate) use progress;
macro_rules! progress_pause {
() => {
{
log::info!(target: "progress_pause", "")
}
}
}
pub(crate) use progress_pause;
macro_rules! tab_info { macro_rules! tab_info {
($($arg:tt)+) => { ($($arg:tt)+) => {
if log::max_level() == log::LevelFilter::Debug { if log::max_level() == log::LevelFilter::Debug {
@ -62,6 +71,7 @@ impl Log for CliLogger {
fn log(&self, record: &Record) { fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) if !self.enabled(record.metadata())
|| (record.target() != "progress" || (record.target() != "progress"
&& record.target() != "progress_pause"
&& record.target() != "progress_end" && record.target() != "progress_end"
&& !record.target().starts_with("crunchy_cli")) && !record.target().starts_with("crunchy_cli"))
{ {
@ -75,6 +85,16 @@ impl Log for CliLogger {
match record.target() { match record.target() {
"progress" => self.progress(record, false), "progress" => self.progress(record, false),
"progress_pause" => {
let progress = self.progress.lock().unwrap();
if let Some(p) = &*progress {
p.set_draw_target(if p.is_hidden() {
ProgressDrawTarget::stdout()
} else {
ProgressDrawTarget::hidden()
})
}
}
"progress_end" => self.progress(record, true), "progress_end" => self.progress(record, true),
_ => { _ => {
if self.progress.lock().unwrap().is_some() { if self.progress.lock().unwrap().is_some() {
@ -158,6 +178,7 @@ impl CliLogger {
.unwrap() .unwrap()
.tick_strings(&["", "\\", "|", "/", finish_str]), .tick_strings(&["", "\\", "|", "/", finish_str]),
); );
pb.set_draw_target(ProgressDrawTarget::stdout());
pb.enable_steady_tick(Duration::from_millis(200)); pb.enable_steady_tick(Duration::from_millis(200));
pb.set_message(msg); pb.set_message(msg);
*progress = Some(pb) *progress = Some(pb)

View file

@ -4,6 +4,7 @@ pub mod download;
pub mod ffmpeg; pub mod ffmpeg;
pub mod filter; pub mod filter;
pub mod format; pub mod format;
pub mod interactive_select;
pub mod locale; pub mod locale;
pub mod log; pub mod log;
pub mod os; pub mod os;