mirror of
https://github.com/crunchy-labs/crunchy-cli.git
synced 2026-01-21 12:12:00 -06:00
Merge pull request #100 from crunchy-labs/feature/multiple-seasons-with-same-number
Interactive choosing on duplicated season numbers
This commit is contained in:
commit
537158cd7b
4 changed files with 134 additions and 7 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::cli::log::tab_info;
|
use crate::cli::log::tab_info;
|
||||||
use crate::cli::utils::{download_segments, find_resolution, FFmpegPreset};
|
use crate::cli::utils::{download_segments, find_resolution, FFmpegPreset, find_multiple_seasons_with_same_number, interactive_season_choosing};
|
||||||
use crate::utils::context::Context;
|
use crate::utils::context::Context;
|
||||||
use crate::utils::format::{format_string, Format};
|
use crate::utils::format::{format_string, Format};
|
||||||
use crate::utils::log::progress;
|
use crate::utils::log::progress;
|
||||||
|
|
@ -120,6 +120,10 @@ pub struct Archive {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
no_subtitle_optimizations: bool,
|
no_subtitle_optimizations: bool,
|
||||||
|
|
||||||
|
#[arg(help = "Ignore interactive input")]
|
||||||
|
#[arg(short, long, default_value_t = false)]
|
||||||
|
yes: bool,
|
||||||
|
|
||||||
#[arg(help = "Crunchyroll series url(s)")]
|
#[arg(help = "Crunchyroll series url(s)")]
|
||||||
urls: Vec<String>,
|
urls: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
@ -378,10 +382,16 @@ async fn formats_from_series(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !archive.yes && !find_multiple_seasons_with_same_number(&seasons).is_empty() {
|
||||||
|
info!(target: "progress_end", "Fetched seasons");
|
||||||
|
seasons = interactive_season_choosing(seasons);
|
||||||
|
info!(target: "progress", "Fetching series details")
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
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 series.seasons().await? {
|
for season in seasons {
|
||||||
if !url_filter.is_season_valid(season.metadata.season_number)
|
if !url_filter.is_season_valid(season.metadata.season_number)
|
||||||
|| !archive
|
|| !archive
|
||||||
.locale
|
.locale
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::cli::log::tab_info;
|
use crate::cli::log::tab_info;
|
||||||
use crate::cli::utils::{download_segments, find_resolution, FFmpegPreset};
|
use crate::cli::utils::{download_segments, find_resolution, FFmpegPreset, interactive_season_choosing, find_multiple_seasons_with_same_number};
|
||||||
use crate::utils::context::Context;
|
use crate::utils::context::Context;
|
||||||
use crate::utils::format::{format_string, Format};
|
use crate::utils::format::{format_string, Format};
|
||||||
use crate::utils::log::progress;
|
use crate::utils::log::progress;
|
||||||
|
|
@ -71,6 +71,10 @@ pub struct Download {
|
||||||
#[arg(value_parser = FFmpegPreset::parse)]
|
#[arg(value_parser = FFmpegPreset::parse)]
|
||||||
ffmpeg_preset: Vec<FFmpegPreset>,
|
ffmpeg_preset: Vec<FFmpegPreset>,
|
||||||
|
|
||||||
|
#[arg(help = "Ignore interactive input")]
|
||||||
|
#[arg(short, long, default_value_t = false)]
|
||||||
|
yes: bool,
|
||||||
|
|
||||||
#[arg(help = "Url(s) to Crunchyroll episodes or series")]
|
#[arg(help = "Url(s) to Crunchyroll episodes or series")]
|
||||||
urls: Vec<String>,
|
urls: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
@ -348,6 +352,12 @@ async fn formats_from_series(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !download.yes && !find_multiple_seasons_with_same_number(&seasons).is_empty() {
|
||||||
|
info!(target: "progress_end", "Fetched seasons");
|
||||||
|
seasons = interactive_season_choosing(seasons);
|
||||||
|
info!(target: "progress", "Fetching series details")
|
||||||
|
}
|
||||||
|
|
||||||
let mut formats = vec![];
|
let mut formats = vec![];
|
||||||
for season in seasons {
|
for season in seasons {
|
||||||
if let Some(fmts) = formats_from_season(download, season, url_filter).await? {
|
if let Some(fmts) = formats_from_season(download, season, url_filter).await? {
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@ use indicatif::{ProgressBar, ProgressFinish, ProgressStyle};
|
||||||
use log::{debug, LevelFilter};
|
use log::{debug, LevelFilter};
|
||||||
use std::borrow::{Borrow, BorrowMut};
|
use std::borrow::{Borrow, BorrowMut};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::io::Write;
|
use std::io::{BufRead, Write};
|
||||||
use std::sync::{mpsc, Arc, Mutex};
|
use std::sync::{mpsc, Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use crunchyroll_rs::{Media, Season};
|
||||||
|
use regex::Regex;
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
|
|
||||||
pub fn find_resolution(
|
pub fn find_resolution(
|
||||||
|
|
@ -327,3 +329,105 @@ impl FFmpegPreset {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn find_multiple_seasons_with_same_number(seasons: &Vec<Media<Season>>) -> Vec<u32> {
|
||||||
|
let mut seasons_map: BTreeMap<u32, u32> = BTreeMap::new();
|
||||||
|
for season in seasons {
|
||||||
|
if let Some(s) = seasons_map.get_mut(&season.metadata.season_number) {
|
||||||
|
*s += 1;
|
||||||
|
} else {
|
||||||
|
seasons_map.insert(season.metadata.season_number, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seasons_map
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(k, v)| if v > 1 { Some(k) } else { None })
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn interactive_season_choosing(seasons: Vec<Media<Season>>) -> Vec<Media<Season>> {
|
||||||
|
let input_regex =
|
||||||
|
Regex::new(r"((?P<single>\d+)|(?P<range_from>\d+)-(?P<range_to>\d+)?)(\s|$)").unwrap();
|
||||||
|
|
||||||
|
let mut seasons_map: BTreeMap<u32, Vec<Media<Season>>> = BTreeMap::new();
|
||||||
|
for season in seasons {
|
||||||
|
if let Some(s) = seasons_map.get_mut(&season.metadata.season_number) {
|
||||||
|
s.push(season);
|
||||||
|
} else {
|
||||||
|
seasons_map.insert(season.metadata.season_number, vec![season]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (num, season_vec) in seasons_map.iter_mut() {
|
||||||
|
if season_vec.len() == 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
println!(":: Found multiple seasons for season number {}", num);
|
||||||
|
println!(":: Select the number of the seasons you want to download (eg \"1 2 4\", \"1-3\", \"1-3 5\"):");
|
||||||
|
for (i, season) in season_vec.iter().enumerate() {
|
||||||
|
println!(":: \t{}. {}", i + 1, season.title)
|
||||||
|
}
|
||||||
|
let mut stdout = std::io::stdout();
|
||||||
|
let _ = write!(stdout, ":: => ");
|
||||||
|
let _ = stdout.flush();
|
||||||
|
let mut user_input = String::new();
|
||||||
|
std::io::stdin().lock()
|
||||||
|
.read_line(&mut user_input)
|
||||||
|
.expect("cannot open stdin");
|
||||||
|
|
||||||
|
let mut nums = vec![];
|
||||||
|
for capture in input_regex.captures_iter(&user_input) {
|
||||||
|
if let Some(single) = capture.name("single") {
|
||||||
|
nums.push(single.as_str().parse().unwrap());
|
||||||
|
} else {
|
||||||
|
let range_from = capture.name("range_from");
|
||||||
|
let range_to = capture.name("range_to");
|
||||||
|
|
||||||
|
// input is '-' which means use all seasons
|
||||||
|
if range_from.is_none() && range_to.is_none() {
|
||||||
|
nums = vec![];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let from = range_from
|
||||||
|
.map(|f| f.as_str().parse::<usize>().unwrap() - 1)
|
||||||
|
.unwrap_or(usize::MIN);
|
||||||
|
let to = range_from
|
||||||
|
.map(|f| f.as_str().parse::<usize>().unwrap() - 1)
|
||||||
|
.unwrap_or(usize::MAX);
|
||||||
|
|
||||||
|
nums.extend(
|
||||||
|
season_vec
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, _)| {
|
||||||
|
if i >= from && i <= to {
|
||||||
|
Some(i)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<usize>>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nums.dedup();
|
||||||
|
|
||||||
|
if !nums.is_empty() {
|
||||||
|
let mut remove_count = 0;
|
||||||
|
for i in 0..season_vec.len() - 1 {
|
||||||
|
if !nums.contains(&i) {
|
||||||
|
season_vec.remove(i - remove_count);
|
||||||
|
remove_count += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seasons_map
|
||||||
|
.into_values()
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<Media<Season>>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,11 @@ pub fn sort_formats_after_seasons(formats: Vec<Format>) -> Vec<Vec<Format>> {
|
||||||
let mut as_map = BTreeMap::new();
|
let mut as_map = BTreeMap::new();
|
||||||
|
|
||||||
for format in formats {
|
for format in formats {
|
||||||
as_map.entry(format.season_number).or_insert_with(Vec::new);
|
// the season title is used as key instead of season number to distinguish duplicated season
|
||||||
as_map.get_mut(&format.season_number).unwrap().push(format);
|
// numbers which are actually two different seasons; season id is not used as this somehow
|
||||||
|
// messes up ordering when duplicated seasons exist
|
||||||
|
as_map.entry(format.season_title.clone()).or_insert_with(Vec::new);
|
||||||
|
as_map.get_mut(&format.season_title).unwrap().push(format);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut sorted = as_map
|
let mut sorted = as_map
|
||||||
|
|
@ -41,7 +44,7 @@ pub fn sort_formats_after_seasons(formats: Vec<Format>) -> Vec<Vec<Format>> {
|
||||||
values
|
values
|
||||||
})
|
})
|
||||||
.collect::<Vec<Vec<Format>>>();
|
.collect::<Vec<Vec<Format>>>();
|
||||||
sorted.sort_by(|a, b| a[0].series_id.cmp(&b[0].series_id));
|
sorted.sort_by(|a, b| a[0].season_number.cmp(&b[0].season_number));
|
||||||
|
|
||||||
sorted
|
sorted
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue