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

View file

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

View file

@ -98,6 +98,10 @@ pub struct Archive {
#[arg(long, default_value_t = false)]
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)")]
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() {
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)
.await?;

View file

@ -1,10 +1,11 @@
use crate::archive::command::Archive;
use crate::utils::filter::{real_dedup_vec, Filter};
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 anyhow::Result;
use crunchyroll_rs::{Concert, Episode, Locale, Movie, MovieListing, MusicVideo, Season, Series};
use log::warn;
use log::{info, warn};
use std::collections::{BTreeMap, HashMap};
enum Visited {
@ -16,6 +17,7 @@ enum Visited {
pub(crate) struct ArchiveFilter {
url_filter: UrlFilter,
archive: Archive,
interactive_input: bool,
season_episode_count: HashMap<u32, Vec<String>>,
season_subtitles_missing: Vec<u32>,
season_sorting: Vec<String>,
@ -23,10 +25,11 @@ pub(crate) struct 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 {
url_filter,
archive,
interactive_input,
season_episode_count: HashMap::new(),
season_subtitles_missing: vec![],
season_sorting: vec![],
@ -71,7 +74,44 @@ impl Filter for ArchiveFilter {
}
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>> {

View file

@ -73,6 +73,10 @@ pub struct Download {
#[arg(long, default_value_t = false)]
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")]
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() {
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)
.await?;

View file

@ -1,24 +1,27 @@
use crate::download::Download;
use crate::utils::filter::Filter;
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 anyhow::{bail, Result};
use crunchyroll_rs::{Concert, Episode, Movie, MovieListing, MusicVideo, Season, Series};
use log::{error, warn};
use log::{error, info, warn};
use std::collections::HashMap;
pub(crate) struct DownloadFilter {
url_filter: UrlFilter,
download: Download,
interactive_input: bool,
season_episode_count: HashMap<u32, Vec<String>>,
season_subtitles_missing: Vec<u32>,
}
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 {
url_filter,
download,
interactive_input,
season_episode_count: HashMap::new(),
season_subtitles_missing: vec![],
}
@ -43,42 +46,61 @@ impl Filter for DownloadFilter {
}
}
let seasons = series.seasons().await?;
let mut seasons = vec![];
for mut season in series.seasons().await? {
if !self.url_filter.is_season_valid(season.season_number) {
continue;
}
if !season
.audio_locales
.iter()
.any(|l| l == &self.download.audio)
{
if season
.available_versions()
.await?
.iter()
.any(|l| l == &self.download.audio)
{
season = season
.version(vec![self.download.audio.clone()])
.await?
.remove(0)
} else {
error!(
"Season {} - '{}' is not available with {} audio",
season.season_number,
season.title,
self.download.audio.clone(),
);
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, mut season: Season) -> Result<Vec<Episode>> {
if !self.url_filter.is_season_valid(season.season_number) {
return Ok(vec![]);
}
if !season
.audio_locales
.iter()
.any(|l| l == &self.download.audio)
{
if season
.available_versions()
.await?
.iter()
.any(|l| l == &self.download.audio)
{
season = season
.version(vec![self.download.audio.clone()])
.await?
.remove(0)
} else {
error!(
"Season {} - '{}' is not available with {} audio",
season.season_number,
season.title,
self.download.audio.clone(),
);
return Ok(vec![]);
}
}
async fn visit_season(&mut self, season: Season) -> Result<Vec<Episode>> {
let mut episodes = season.episodes().await?;
if Format::has_relative_episodes_fmt(&self.download.output) {

View file

@ -18,6 +18,7 @@ mod search;
mod utils;
pub use archive::Archive;
use dialoguer::console::Term;
pub use download::Download;
pub use login::Login;
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)
})
.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::{
info, set_boxed_logger, set_max_level, Level, LevelFilter, Log, Metadata, Record,
SetLoggerError,
@ -37,6 +37,15 @@ macro_rules! progress {
}
pub(crate) use progress;
macro_rules! progress_pause {
() => {
{
log::info!(target: "progress_pause", "")
}
}
}
pub(crate) use progress_pause;
macro_rules! tab_info {
($($arg:tt)+) => {
if log::max_level() == log::LevelFilter::Debug {
@ -62,6 +71,7 @@ impl Log for CliLogger {
fn log(&self, record: &Record) {
if !self.enabled(record.metadata())
|| (record.target() != "progress"
&& record.target() != "progress_pause"
&& record.target() != "progress_end"
&& !record.target().starts_with("crunchy_cli"))
{
@ -75,6 +85,16 @@ impl Log for CliLogger {
match record.target() {
"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),
_ => {
if self.progress.lock().unwrap().is_some() {
@ -158,6 +178,7 @@ impl CliLogger {
.unwrap()
.tick_strings(&["", "\\", "|", "/", finish_str]),
);
pb.set_draw_target(ProgressDrawTarget::stdout());
pb.enable_steady_tick(Duration::from_millis(200));
pb.set_message(msg);
*progress = Some(pb)

View file

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