mirror of
https://github.com/crunchy-labs/crunchy-cli.git
synced 2026-01-21 12:12:00 -06:00
Compare commits
11 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4332b1beef | ||
|
|
2cf9125de3 | ||
|
|
756022b955 | ||
|
|
509683d23a | ||
|
|
8047680799 | ||
|
|
287df84382 | ||
|
|
e7ac6d8874 | ||
|
|
fb8e535644 | ||
|
|
67c267be20 | ||
|
|
a1c7b2069d | ||
|
|
74e5e05b0f |
16 changed files with 566 additions and 874 deletions
29
Cargo.lock
generated
29
Cargo.lock
generated
|
|
@ -349,7 +349,7 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crunchy-cli"
|
name = "crunchy-cli"
|
||||||
version = "3.6.5"
|
version = "3.6.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
|
@ -362,7 +362,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crunchy-cli-core"
|
name = "crunchy-cli-core"
|
||||||
version = "3.6.5"
|
version = "3.6.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-speed-limit",
|
"async-speed-limit",
|
||||||
|
|
@ -400,9 +400,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crunchyroll-rs"
|
name = "crunchyroll-rs"
|
||||||
version = "0.11.2"
|
version = "0.11.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a6754d10e1890089eb733b71aee6f4cbc18374040aedb04c4ca76020bcd9818"
|
checksum = "d6e38c223aecf65c9c9bec50764beea5dc70b6c97cd7f767bf6860f2fc8e0a07"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
@ -426,9 +426,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crunchyroll-rs-internal"
|
name = "crunchyroll-rs-internal"
|
||||||
version = "0.11.2"
|
version = "0.11.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ca15fa827cca647852b091006f2b592f8727e1082f812b475b3f9ebe3f59d5bf"
|
checksum = "144a38040a21aaa456741a9f6749354527bb68ad3bb14210e0bbc40fbd95186c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -1125,8 +1125,8 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.11"
|
version = "0.2.12"
|
||||||
source = "git+https://github.com/crunchy-labs/rust-not-so-native-tls.git?rev=b7969a8#b7969a88210096e0570e29d42fb13533baf62aa6"
|
source = "git+https://github.com/crunchy-labs/rust-not-so-native-tls.git?rev=c7ac566#c7ac566559d441bbc3e5e5bd04fb7162c38d88b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -1519,8 +1519,9 @@ checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsubs-lib"
|
name = "rsubs-lib"
|
||||||
version = "0.3.0"
|
version = "0.3.2"
|
||||||
source = "git+https://github.com/crunchy-labs/rsubs-lib.git?rev=1c51f60#1c51f60b8c48f1a8f7b261372b237d89bdc17dd4"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8c9f50e3fbcbf1f0bd109954e2dd813d1715c7b4a92a7bf159a85dea49e9d863"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -1966,9 +1967,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.37.0"
|
version = "1.38.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
|
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -1983,9 +1984,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.2.0"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "crunchy-cli"
|
name = "crunchy-cli"
|
||||||
authors = ["Crunchy Labs Maintainers"]
|
authors = ["Crunchy Labs Maintainers"]
|
||||||
version = "3.6.5"
|
version = "3.6.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
|
@ -14,9 +14,9 @@ openssl-tls = ["dep:native-tls-crate", "native-tls-crate/openssl", "crunchy-cli-
|
||||||
openssl-tls-static = ["dep:native-tls-crate", "native-tls-crate/openssl", "crunchy-cli-core/openssl-tls-static"]
|
openssl-tls-static = ["dep:native-tls-crate", "native-tls-crate/openssl", "crunchy-cli-core/openssl-tls-static"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.37", features = ["macros", "rt-multi-thread", "time"], default-features = false }
|
tokio = { version = "1.38", features = ["macros", "rt-multi-thread", "time"], default-features = false }
|
||||||
|
|
||||||
native-tls-crate = { package = "native-tls", version = "0.2.11", optional = true }
|
native-tls-crate = { package = "native-tls", version = "0.2.12", optional = true }
|
||||||
|
|
||||||
crunchy-cli-core = { path = "./crunchy-cli-core" }
|
crunchy-cli-core = { path = "./crunchy-cli-core" }
|
||||||
|
|
||||||
|
|
@ -34,8 +34,7 @@ members = ["crunchy-cli-core"]
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# fork of the `native-tls` crate which can use openssl as backend on every platform. this is done as `reqwest` only
|
# fork of the `native-tls` crate which can use openssl as backend on every platform. this is done as `reqwest` only
|
||||||
# supports `rustls` and `native-tls` as tls backend
|
# supports `rustls` and `native-tls` as tls backend
|
||||||
native-tls = { git = "https://github.com/crunchy-labs/rust-not-so-native-tls.git", rev = "b7969a8" }
|
native-tls = { git = "https://github.com/crunchy-labs/rust-not-so-native-tls.git", rev = "c7ac566" }
|
||||||
rsubs-lib = { git = "https://github.com/crunchy-labs/rsubs-lib.git", rev = "1c51f60" }
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
strip = true
|
strip = true
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
> ~~This project has been sunset as Crunchyroll moved to a DRM-only system. See [#362](https://github.com/crunchy-labs/crunchy-cli/issues/362).~~
|
# This project has been sunset as Crunchyroll moved to a DRM-only system. See [#362](https://github.com/crunchy-labs/crunchy-cli/issues/362).
|
||||||
>
|
|
||||||
> Well there is one endpoint which still has DRM-free streams, I guess I still have a bit time until (finally) everything is DRM-only.
|
|
||||||
|
|
||||||
# crunchy-cli
|
# crunchy-cli
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "crunchy-cli-core"
|
name = "crunchy-cli-core"
|
||||||
authors = ["Crunchy Labs Maintainers"]
|
authors = ["Crunchy Labs Maintainers"]
|
||||||
version = "3.6.5"
|
version = "3.6.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
|
@ -16,7 +16,7 @@ anyhow = "1.0"
|
||||||
async-speed-limit = "0.4"
|
async-speed-limit = "0.4"
|
||||||
clap = { version = "4.5", features = ["derive", "string"] }
|
clap = { version = "4.5", features = ["derive", "string"] }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
crunchyroll-rs = { version = "0.11.2", features = ["experimental-stabilizations", "tower"] }
|
crunchyroll-rs = { version = "0.11.4", features = ["experimental-stabilizations", "tower"] }
|
||||||
ctrlc = "3.4"
|
ctrlc = "3.4"
|
||||||
dialoguer = { version = "0.11", default-features = false }
|
dialoguer = { version = "0.11", default-features = false }
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
|
|
@ -30,7 +30,7 @@ log = { version = "0.4", features = ["std"] }
|
||||||
num_cpus = "1.16"
|
num_cpus = "1.16"
|
||||||
regex = "1.10"
|
regex = "1.10"
|
||||||
reqwest = { version = "0.12", features = ["socks", "stream"] }
|
reqwest = { version = "0.12", features = ["socks", "stream"] }
|
||||||
rsubs-lib = "0.3"
|
rsubs-lib = "~0.3.2"
|
||||||
rusty-chromaprint = "0.2"
|
rusty-chromaprint = "0.2"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|
@ -39,7 +39,7 @@ shlex = "1.3"
|
||||||
sys-locale = "0.3"
|
sys-locale = "0.3"
|
||||||
tempfile = "3.10"
|
tempfile = "3.10"
|
||||||
time = "0.3"
|
time = "0.3"
|
||||||
tokio = { version = "1.37", features = ["io-util", "macros", "net", "rt-multi-thread", "time"] }
|
tokio = { version = "1.38", features = ["io-util", "macros", "net", "rt-multi-thread", "time"] }
|
||||||
tokio-util = "0.7"
|
tokio-util = "0.7"
|
||||||
tower-service = "0.3"
|
tower-service = "0.3"
|
||||||
rustls-native-certs = { version = "0.7", optional = true }
|
rustls-native-certs = { version = "0.7", optional = true }
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
use crate::archive::filter::ArchiveFilter;
|
|
||||||
use crate::utils::context::Context;
|
use crate::utils::context::Context;
|
||||||
use crate::utils::download::{
|
use crate::utils::download::{
|
||||||
DownloadBuilder, DownloadFormat, DownloadFormatMetadata, MergeBehavior,
|
DownloadBuilder, DownloadFormat, DownloadFormatMetadata, MergeBehavior,
|
||||||
};
|
};
|
||||||
use crate::utils::ffmpeg::FFmpegPreset;
|
use crate::utils::ffmpeg::FFmpegPreset;
|
||||||
use crate::utils::filter::Filter;
|
use crate::utils::filter::{Filter, FilterMediaScope};
|
||||||
use crate::utils::format::{Format, SingleFormat};
|
use crate::utils::format::{Format, SingleFormat};
|
||||||
use crate::utils::locale::{all_locale_in_locales, resolve_locales, LanguageTagging};
|
use crate::utils::locale::{all_locale_in_locales, resolve_locales, LanguageTagging};
|
||||||
use crate::utils::log::progress;
|
use crate::utils::log::progress;
|
||||||
|
|
@ -284,9 +283,49 @@ 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(
|
let single_format_collection = Filter::new(
|
||||||
url_filter,
|
url_filter,
|
||||||
self.clone(),
|
self.audio.clone(),
|
||||||
|
self.subtitle.clone(),
|
||||||
|
|scope, locales| {
|
||||||
|
let audios = locales.into_iter().map(|l| l.to_string()).collect::<Vec<String>>().join(", ");
|
||||||
|
match scope {
|
||||||
|
FilterMediaScope::Series(series) => warn!("Series {} is not available with {} audio", series.title, audios),
|
||||||
|
FilterMediaScope::Season(season) => warn!("Season {} is not available with {} audio", season.season_number, audios),
|
||||||
|
FilterMediaScope::Episode(episodes) => {
|
||||||
|
if episodes.len() == 1 {
|
||||||
|
warn!("Episode {} is not available with {} audio", episodes[0].sequence_number, audios)
|
||||||
|
} else if episodes.len() == 2 {
|
||||||
|
warn!("Season {} is only available with {} audio from episode {} to {}", episodes[0].season_number, audios, episodes[0].sequence_number, episodes[1].sequence_number)
|
||||||
|
} else {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
},
|
||||||
|
|scope, locales| {
|
||||||
|
let subtitles = locales.into_iter().map(|l| l.to_string()).collect::<Vec<String>>().join(", ");
|
||||||
|
match scope {
|
||||||
|
FilterMediaScope::Series(series) => warn!("Series {} is not available with {} subtitles", series.title, subtitles),
|
||||||
|
FilterMediaScope::Season(season) => warn!("Season {} is not available with {} subtitles", season.season_number, subtitles),
|
||||||
|
FilterMediaScope::Episode(episodes) => {
|
||||||
|
if episodes.len() == 1 {
|
||||||
|
warn!("Episode {} of season {} is not available with {} subtitles", episodes[0].sequence_number, episodes[0].season_title, subtitles)
|
||||||
|
} else if episodes.len() == 2 {
|
||||||
|
warn!("Season {} of season {} is only available with {} subtitles from episode {} to {}", episodes[0].season_number, episodes[0].season_title, subtitles, episodes[0].sequence_number, episodes[1].sequence_number)
|
||||||
|
} else {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
},
|
||||||
|
|season| {
|
||||||
|
warn!("Skipping premium episodes in season {season}");
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
Format::has_relative_fmt(&self.output),
|
||||||
!self.yes,
|
!self.yes,
|
||||||
self.skip_specials,
|
self.skip_specials,
|
||||||
ctx.crunchy.premium().await,
|
ctx.crunchy.premium().await,
|
||||||
|
|
|
||||||
|
|
@ -1,466 +0,0 @@
|
||||||
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::{fract, UrlFilter};
|
|
||||||
use anyhow::Result;
|
|
||||||
use crunchyroll_rs::{Concert, Episode, Locale, Movie, MovieListing, MusicVideo, Season, Series};
|
|
||||||
use log::{info, warn};
|
|
||||||
use std::collections::{BTreeMap, HashMap};
|
|
||||||
use std::ops::Not;
|
|
||||||
|
|
||||||
enum Visited {
|
|
||||||
Series,
|
|
||||||
Season,
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct ArchiveFilter {
|
|
||||||
url_filter: UrlFilter,
|
|
||||||
archive: Archive,
|
|
||||||
interactive_input: bool,
|
|
||||||
skip_special: bool,
|
|
||||||
season_episodes: HashMap<String, Vec<Episode>>,
|
|
||||||
season_subtitles_missing: Vec<u32>,
|
|
||||||
seasons_with_premium: Option<Vec<u32>>,
|
|
||||||
season_sorting: Vec<String>,
|
|
||||||
visited: Visited,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ArchiveFilter {
|
|
||||||
pub(crate) fn new(
|
|
||||||
url_filter: UrlFilter,
|
|
||||||
archive: Archive,
|
|
||||||
interactive_input: bool,
|
|
||||||
skip_special: bool,
|
|
||||||
is_premium: bool,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
url_filter,
|
|
||||||
archive,
|
|
||||||
interactive_input,
|
|
||||||
skip_special,
|
|
||||||
season_episodes: HashMap::new(),
|
|
||||||
season_subtitles_missing: vec![],
|
|
||||||
seasons_with_premium: is_premium.not().then_some(vec![]),
|
|
||||||
season_sorting: vec![],
|
|
||||||
visited: Visited::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Filter for ArchiveFilter {
|
|
||||||
type T = Vec<SingleFormat>;
|
|
||||||
type Output = SingleFormatCollection;
|
|
||||||
|
|
||||||
async fn visit_series(&mut self, series: Series) -> Result<Vec<Season>> {
|
|
||||||
// `series.audio_locales` isn't always populated b/c of crunchyrolls api. so check if the
|
|
||||||
// audio is matching only if the field is populated
|
|
||||||
if !series.audio_locales.is_empty() {
|
|
||||||
let missing_audio = missing_locales(&series.audio_locales, &self.archive.audio);
|
|
||||||
if !missing_audio.is_empty() {
|
|
||||||
warn!(
|
|
||||||
"Series {} is not available with {} audio",
|
|
||||||
series.title,
|
|
||||||
missing_audio
|
|
||||||
.into_iter()
|
|
||||||
.map(|l| l.to_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", ")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
let missing_subtitle =
|
|
||||||
missing_locales(&series.subtitle_locales, &self.archive.subtitle);
|
|
||||||
if !missing_subtitle.is_empty() {
|
|
||||||
warn!(
|
|
||||||
"Series {} is not available with {} subtitles",
|
|
||||||
series.title,
|
|
||||||
missing_subtitle
|
|
||||||
.into_iter()
|
|
||||||
.map(|l| l.to_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", ")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
self.visited = Visited::Series
|
|
||||||
}
|
|
||||||
|
|
||||||
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.is_empty() {
|
|
||||||
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![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut seasons = season.version(self.archive.audio.clone()).await?;
|
|
||||||
if self
|
|
||||||
.archive
|
|
||||||
.audio
|
|
||||||
.iter()
|
|
||||||
.any(|l| season.audio_locales.contains(l))
|
|
||||||
{
|
|
||||||
seasons.insert(0, season.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !matches!(self.visited, Visited::Series) {
|
|
||||||
let mut audio_locales: Vec<Locale> = seasons
|
|
||||||
.iter()
|
|
||||||
.flat_map(|s| s.audio_locales.clone())
|
|
||||||
.collect();
|
|
||||||
real_dedup_vec(&mut audio_locales);
|
|
||||||
let missing_audio = missing_locales(&audio_locales, &self.archive.audio);
|
|
||||||
if !missing_audio.is_empty() {
|
|
||||||
warn!(
|
|
||||||
"Season {} is not available with {} audio",
|
|
||||||
season.season_number,
|
|
||||||
missing_audio
|
|
||||||
.into_iter()
|
|
||||||
.map(|l| l.to_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", ")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let subtitle_locales: Vec<Locale> = seasons
|
|
||||||
.iter()
|
|
||||||
.flat_map(|s| s.subtitle_locales.clone())
|
|
||||||
.collect();
|
|
||||||
let missing_subtitle = missing_locales(&subtitle_locales, &self.archive.subtitle);
|
|
||||||
if !missing_subtitle.is_empty() {
|
|
||||||
warn!(
|
|
||||||
"Season {} is not available with {} subtitles",
|
|
||||||
season.season_number,
|
|
||||||
missing_subtitle
|
|
||||||
.into_iter()
|
|
||||||
.map(|l| l.to_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", ")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
self.visited = Visited::Season
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut episodes = vec![];
|
|
||||||
for season in seasons {
|
|
||||||
self.season_sorting.push(season.id.clone());
|
|
||||||
let season_locale = if season.audio_locales.len() < 2 {
|
|
||||||
Some(
|
|
||||||
season
|
|
||||||
.audio_locales
|
|
||||||
.first()
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or(Locale::ja_JP),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let mut eps = season.episodes().await?;
|
|
||||||
let before_len = eps.len();
|
|
||||||
|
|
||||||
for mut ep in eps.clone() {
|
|
||||||
if let Some(l) = &season_locale {
|
|
||||||
if &ep.audio_locale == l {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
eps.remove(eps.iter().position(|p| p.id == ep.id).unwrap());
|
|
||||||
} else {
|
|
||||||
let mut requested_locales = self.archive.audio.clone();
|
|
||||||
if let Some(idx) = requested_locales.iter().position(|p| p == &ep.audio_locale)
|
|
||||||
{
|
|
||||||
requested_locales.remove(idx);
|
|
||||||
} else {
|
|
||||||
eps.remove(eps.iter().position(|p| p.id == ep.id).unwrap());
|
|
||||||
}
|
|
||||||
eps.extend(ep.version(self.archive.audio.clone()).await?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if eps.len() < before_len {
|
|
||||||
if eps.is_empty() {
|
|
||||||
if matches!(self.visited, Visited::Series) {
|
|
||||||
warn!(
|
|
||||||
"Season {} is not available with {} audio",
|
|
||||||
season.season_number,
|
|
||||||
season_locale.unwrap_or(Locale::ja_JP)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let last_episode = eps.last().unwrap();
|
|
||||||
warn!(
|
|
||||||
"Season {} is only available with {} audio until episode {} ({})",
|
|
||||||
season.season_number,
|
|
||||||
season_locale.unwrap_or(Locale::ja_JP),
|
|
||||||
last_episode.sequence_number,
|
|
||||||
last_episode.title
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
episodes.extend(eps)
|
|
||||||
}
|
|
||||||
|
|
||||||
if Format::has_relative_fmt(&self.archive.output) {
|
|
||||||
for episode in episodes.iter() {
|
|
||||||
self.season_episodes
|
|
||||||
.entry(episode.season_id.clone())
|
|
||||||
.or_default()
|
|
||||||
.push(episode.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(episodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn visit_episode(&mut self, mut episode: Episode) -> Result<Option<Self::T>> {
|
|
||||||
if !self
|
|
||||||
.url_filter
|
|
||||||
.is_episode_valid(episode.sequence_number, episode.season_number)
|
|
||||||
{
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip the episode if it's a special
|
|
||||||
if self.skip_special
|
|
||||||
&& (episode.sequence_number == 0.0 || episode.sequence_number.fract() != 0.0)
|
|
||||||
{
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut episodes = vec![];
|
|
||||||
if !matches!(self.visited, Visited::Series) && !matches!(self.visited, Visited::Season) {
|
|
||||||
if self.archive.audio.contains(&episode.audio_locale) {
|
|
||||||
episodes.push((episode.clone(), episode.subtitle_locales.clone()))
|
|
||||||
}
|
|
||||||
episodes.extend(
|
|
||||||
episode
|
|
||||||
.version(self.archive.audio.clone())
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|e| (e.clone(), e.subtitle_locales.clone())),
|
|
||||||
);
|
|
||||||
let audio_locales: Vec<Locale> = episodes
|
|
||||||
.iter()
|
|
||||||
.map(|(e, _)| e.audio_locale.clone())
|
|
||||||
.collect();
|
|
||||||
let missing_audio = missing_locales(&audio_locales, &self.archive.audio);
|
|
||||||
if !missing_audio.is_empty() {
|
|
||||||
warn!(
|
|
||||||
"Episode {} is not available with {} audio",
|
|
||||||
episode.sequence_number,
|
|
||||||
missing_audio
|
|
||||||
.into_iter()
|
|
||||||
.map(|l| l.to_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", ")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut subtitle_locales: Vec<Locale> =
|
|
||||||
episodes.iter().flat_map(|(_, s)| s.clone()).collect();
|
|
||||||
real_dedup_vec(&mut subtitle_locales);
|
|
||||||
let missing_subtitles = missing_locales(&subtitle_locales, &self.archive.subtitle);
|
|
||||||
if !missing_subtitles.is_empty()
|
|
||||||
&& !self
|
|
||||||
.season_subtitles_missing
|
|
||||||
.contains(&episode.season_number)
|
|
||||||
{
|
|
||||||
warn!(
|
|
||||||
"Episode {} is not available with {} subtitles",
|
|
||||||
episode.sequence_number,
|
|
||||||
missing_subtitles
|
|
||||||
.into_iter()
|
|
||||||
.map(|l| l.to_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", ")
|
|
||||||
);
|
|
||||||
self.season_subtitles_missing.push(episode.season_number)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
episodes.push((episode.clone(), episode.subtitle_locales.clone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.seasons_with_premium.is_some() {
|
|
||||||
let episode_len_before = episodes.len();
|
|
||||||
episodes.retain(|(e, _)| !e.is_premium_only);
|
|
||||||
if episode_len_before < episodes.len()
|
|
||||||
&& !self
|
|
||||||
.seasons_with_premium
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.contains(&episode.season_number)
|
|
||||||
{
|
|
||||||
warn!(
|
|
||||||
"Skipping premium episodes in season {}",
|
|
||||||
episode.season_number
|
|
||||||
);
|
|
||||||
self.seasons_with_premium
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.push(episode.season_number)
|
|
||||||
}
|
|
||||||
|
|
||||||
if episodes.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut relative_episode_number = None;
|
|
||||||
let mut relative_sequence_number = None;
|
|
||||||
// get the relative episode number. only done if the output string has the pattern to include
|
|
||||||
// the relative episode number as this requires some extra fetching
|
|
||||||
if Format::has_relative_fmt(&self.archive.output) {
|
|
||||||
let season_eps = match self.season_episodes.get(&episode.season_id) {
|
|
||||||
Some(eps) => eps,
|
|
||||||
None => {
|
|
||||||
self.season_episodes.insert(
|
|
||||||
episode.season_id.clone(),
|
|
||||||
episode.season().await?.episodes().await?,
|
|
||||||
);
|
|
||||||
self.season_episodes.get(&episode.season_id).unwrap()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut non_integer_sequence_number_count = 0;
|
|
||||||
for (i, ep) in season_eps.iter().enumerate() {
|
|
||||||
if ep.sequence_number.fract() != 0.0 || ep.sequence_number == 0.0 {
|
|
||||||
non_integer_sequence_number_count += 1;
|
|
||||||
}
|
|
||||||
if ep.id == episode.id {
|
|
||||||
relative_episode_number = Some(i + 1);
|
|
||||||
relative_sequence_number = Some(
|
|
||||||
(i + 1 - non_integer_sequence_number_count) as f32
|
|
||||||
+ fract(ep.sequence_number),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if relative_episode_number.is_none() || relative_sequence_number.is_none() {
|
|
||||||
warn!(
|
|
||||||
"Failed to get relative episode number for episode {} ({}) of {} season {}",
|
|
||||||
episode.sequence_number,
|
|
||||||
episode.title,
|
|
||||||
episode.series_title,
|
|
||||||
episode.season_number,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(
|
|
||||||
episodes
|
|
||||||
.into_iter()
|
|
||||||
.map(|(e, s)| {
|
|
||||||
SingleFormat::new_from_episode(
|
|
||||||
e,
|
|
||||||
s,
|
|
||||||
relative_episode_number.map(|n| n as u32),
|
|
||||||
relative_sequence_number,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn visit_movie_listing(&mut self, movie_listing: MovieListing) -> Result<Vec<Movie>> {
|
|
||||||
Ok(movie_listing.movies().await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn visit_movie(&mut self, movie: Movie) -> Result<Option<Self::T>> {
|
|
||||||
Ok(Some(vec![SingleFormat::new_from_movie(movie, vec![])]))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn visit_music_video(&mut self, music_video: MusicVideo) -> Result<Option<Self::T>> {
|
|
||||||
Ok(Some(vec![SingleFormat::new_from_music_video(music_video)]))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn visit_concert(&mut self, concert: Concert) -> Result<Option<Self::T>> {
|
|
||||||
Ok(Some(vec![SingleFormat::new_from_concert(concert)]))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn finish(self, input: Vec<Self::T>) -> Result<Self::Output> {
|
|
||||||
let flatten_input: Self::T = input.into_iter().flatten().collect();
|
|
||||||
|
|
||||||
let mut single_format_collection = SingleFormatCollection::new();
|
|
||||||
|
|
||||||
let mut pre_sorted: BTreeMap<String, Self::T> = BTreeMap::new();
|
|
||||||
for data in flatten_input {
|
|
||||||
pre_sorted
|
|
||||||
.entry(data.identifier.clone())
|
|
||||||
.or_insert(vec![])
|
|
||||||
.push(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut sorted: Vec<(String, Self::T)> = pre_sorted.into_iter().collect();
|
|
||||||
sorted.sort_by(|(_, a), (_, b)| {
|
|
||||||
self.season_sorting
|
|
||||||
.iter()
|
|
||||||
.position(|p| p == &a.first().unwrap().season_id)
|
|
||||||
.unwrap()
|
|
||||||
.cmp(
|
|
||||||
&self
|
|
||||||
.season_sorting
|
|
||||||
.iter()
|
|
||||||
.position(|p| p == &b.first().unwrap().season_id)
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
for (_, mut data) in sorted {
|
|
||||||
data.sort_by(|a, b| {
|
|
||||||
self.archive
|
|
||||||
.audio
|
|
||||||
.iter()
|
|
||||||
.position(|p| p == &a.audio)
|
|
||||||
.unwrap_or(usize::MAX)
|
|
||||||
.cmp(
|
|
||||||
&self
|
|
||||||
.archive
|
|
||||||
.audio
|
|
||||||
.iter()
|
|
||||||
.position(|p| p == &b.audio)
|
|
||||||
.unwrap_or(usize::MAX),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
single_format_collection.add_single_formats(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(single_format_collection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn missing_locales<'a>(available: &[Locale], searched: &'a [Locale]) -> Vec<&'a Locale> {
|
|
||||||
searched.iter().filter(|p| !available.contains(p)).collect()
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
mod command;
|
mod command;
|
||||||
mod filter;
|
|
||||||
|
|
||||||
pub use command::Archive;
|
pub use command::Archive;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
use crate::download::filter::DownloadFilter;
|
|
||||||
use crate::utils::context::Context;
|
use crate::utils::context::Context;
|
||||||
use crate::utils::download::{DownloadBuilder, DownloadFormat, DownloadFormatMetadata};
|
use crate::utils::download::{DownloadBuilder, DownloadFormat, DownloadFormatMetadata};
|
||||||
use crate::utils::ffmpeg::{FFmpegPreset, SOFTSUB_CONTAINERS};
|
use crate::utils::ffmpeg::{FFmpegPreset, SOFTSUB_CONTAINERS};
|
||||||
use crate::utils::filter::Filter;
|
use crate::utils::filter::{Filter, FilterMediaScope};
|
||||||
use crate::utils::format::{Format, SingleFormat};
|
use crate::utils::format::{Format, SingleFormat};
|
||||||
use crate::utils::locale::{resolve_locales, LanguageTagging};
|
use crate::utils::locale::{resolve_locales, LanguageTagging};
|
||||||
use crate::utils::log::progress;
|
use crate::utils::log::progress;
|
||||||
|
|
@ -14,7 +13,7 @@ use anyhow::bail;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crunchyroll_rs::media::Resolution;
|
use crunchyroll_rs::media::Resolution;
|
||||||
use crunchyroll_rs::Locale;
|
use crunchyroll_rs::Locale;
|
||||||
use log::{debug, warn};
|
use log::{debug, error, warn};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
|
@ -250,9 +249,53 @@ 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(
|
let single_format_collection = Filter::new(
|
||||||
url_filter,
|
url_filter,
|
||||||
self.clone(),
|
vec![self.audio.clone()],
|
||||||
|
self.subtitle.as_ref().map_or(vec![], |s| vec![s.clone()]),
|
||||||
|
|scope, locales| {
|
||||||
|
match scope {
|
||||||
|
FilterMediaScope::Series(series) => bail!("Series {} is not available with {} audio", series.title, locales[0]),
|
||||||
|
FilterMediaScope::Season(season) => {
|
||||||
|
error!("Season {} is not available with {} audio", season.season_number, locales[0]);
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
FilterMediaScope::Episode(episodes) => {
|
||||||
|
if episodes.len() == 1 {
|
||||||
|
warn!("Episode {} of season {} is not available with {} audio", episodes[0].sequence_number, episodes[0].season_title, locales[0])
|
||||||
|
} else if episodes.len() == 2 {
|
||||||
|
warn!("Season {} is only available with {} audio from episode {} to {}", episodes[0].season_number, locales[0], episodes[0].sequence_number, episodes[1].sequence_number)
|
||||||
|
} else {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|scope, locales| {
|
||||||
|
match scope {
|
||||||
|
FilterMediaScope::Series(series) => bail!("Series {} is not available with {} subtitles", series.title, locales[0]),
|
||||||
|
FilterMediaScope::Season(season) => {
|
||||||
|
warn!("Season {} is not available with {} subtitles", season.season_number, locales[0]);
|
||||||
|
Ok(false)
|
||||||
|
},
|
||||||
|
FilterMediaScope::Episode(episodes) => {
|
||||||
|
if episodes.len() == 1 {
|
||||||
|
warn!("Episode {} of season {} is not available with {} subtitles", episodes[0].sequence_number, episodes[0].season_title, locales[0])
|
||||||
|
} else if episodes.len() == 2 {
|
||||||
|
warn!("Season {} is only available with {} subtitles from episode {} to {}", episodes[0].season_number, locales[0], episodes[0].sequence_number, episodes[1].sequence_number)
|
||||||
|
} else {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|season| {
|
||||||
|
warn!("Skipping premium episodes in season {season}");
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
Format::has_relative_fmt(&self.output),
|
||||||
!self.yes,
|
!self.yes,
|
||||||
self.skip_specials,
|
self.skip_specials,
|
||||||
ctx.crunchy.premium().await,
|
ctx.crunchy.premium().await,
|
||||||
|
|
|
||||||
|
|
@ -1,307 +0,0 @@
|
||||||
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::{fract, UrlFilter};
|
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use crunchyroll_rs::{Concert, Episode, Movie, MovieListing, MusicVideo, Season, Series};
|
|
||||||
use log::{error, info, warn};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::ops::Not;
|
|
||||||
|
|
||||||
pub(crate) struct DownloadFilter {
|
|
||||||
url_filter: UrlFilter,
|
|
||||||
download: Download,
|
|
||||||
interactive_input: bool,
|
|
||||||
skip_special: bool,
|
|
||||||
season_episodes: HashMap<u32, Vec<Episode>>,
|
|
||||||
season_subtitles_missing: Vec<u32>,
|
|
||||||
seasons_with_premium: Option<Vec<u32>>,
|
|
||||||
season_visited: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DownloadFilter {
|
|
||||||
pub(crate) fn new(
|
|
||||||
url_filter: UrlFilter,
|
|
||||||
download: Download,
|
|
||||||
interactive_input: bool,
|
|
||||||
skip_special: bool,
|
|
||||||
is_premium: bool,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
url_filter,
|
|
||||||
download,
|
|
||||||
interactive_input,
|
|
||||||
skip_special,
|
|
||||||
season_episodes: HashMap::new(),
|
|
||||||
season_subtitles_missing: vec![],
|
|
||||||
seasons_with_premium: is_premium.not().then_some(vec![]),
|
|
||||||
season_visited: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Filter for DownloadFilter {
|
|
||||||
type T = SingleFormat;
|
|
||||||
type Output = SingleFormatCollection;
|
|
||||||
|
|
||||||
async fn visit_series(&mut self, series: Series) -> Result<Vec<Season>> {
|
|
||||||
// `series.audio_locales` isn't always populated b/c of crunchyrolls api. so check if the
|
|
||||||
// audio is matching only if the field is populated
|
|
||||||
if !series.audio_locales.is_empty() && !series.audio_locales.contains(&self.download.audio)
|
|
||||||
{
|
|
||||||
error!(
|
|
||||||
"Series {} is not available with {} audio",
|
|
||||||
series.title, self.download.audio
|
|
||||||
);
|
|
||||||
return Ok(vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.is_empty() {
|
|
||||||
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>> {
|
|
||||||
self.season_visited = true;
|
|
||||||
|
|
||||||
let mut episodes = season.episodes().await?;
|
|
||||||
|
|
||||||
if Format::has_relative_fmt(&self.download.output) {
|
|
||||||
for episode in episodes.iter() {
|
|
||||||
self.season_episodes
|
|
||||||
.entry(episode.season_number)
|
|
||||||
.or_default()
|
|
||||||
.push(episode.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
episodes.retain(|e| {
|
|
||||||
self.url_filter
|
|
||||||
.is_episode_valid(e.sequence_number, season.season_number)
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(episodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn visit_episode(&mut self, mut episode: Episode) -> Result<Option<Self::T>> {
|
|
||||||
if !self
|
|
||||||
.url_filter
|
|
||||||
.is_episode_valid(episode.sequence_number, episode.season_number)
|
|
||||||
{
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip the episode if it's a special
|
|
||||||
if self.skip_special
|
|
||||||
&& (episode.sequence_number == 0.0 || episode.sequence_number.fract() != 0.0)
|
|
||||||
{
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the audio locale is correct.
|
|
||||||
// should only be incorrect if the console input was a episode url. otherwise
|
|
||||||
// `DownloadFilter::visit_season` returns the correct episodes with matching audio
|
|
||||||
if episode.audio_locale != self.download.audio {
|
|
||||||
// check if any other version (same episode, other language) of this episode is available
|
|
||||||
// with the requested audio. if not, return an error
|
|
||||||
if !episode
|
|
||||||
.available_versions()
|
|
||||||
.await?
|
|
||||||
.contains(&self.download.audio)
|
|
||||||
{
|
|
||||||
let error_message = format!(
|
|
||||||
"Episode {} ({}) of {} season {} is not available with {} audio",
|
|
||||||
episode.sequence_number,
|
|
||||||
episode.title,
|
|
||||||
episode.series_title,
|
|
||||||
episode.season_number,
|
|
||||||
self.download.audio
|
|
||||||
);
|
|
||||||
// sometimes a series randomly has episode in an other language. if this is the case,
|
|
||||||
// only error if the input url was a episode url
|
|
||||||
if self.season_visited {
|
|
||||||
warn!("{}", error_message);
|
|
||||||
return Ok(None);
|
|
||||||
} else {
|
|
||||||
bail!("{}", error_message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// overwrite the current episode with the other version episode
|
|
||||||
episode = episode
|
|
||||||
.version(vec![self.download.audio.clone()])
|
|
||||||
.await?
|
|
||||||
.remove(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the subtitles are supported
|
|
||||||
if let Some(subtitle_locale) = &self.download.subtitle {
|
|
||||||
if !episode.subtitle_locales.contains(subtitle_locale) {
|
|
||||||
// if the episode doesn't have the requested subtitles, print a error. to print this
|
|
||||||
// error only once per season, it's checked if an error got printed before by looking
|
|
||||||
// up if the season id is present in `self.season_subtitles_missing`. if not, print
|
|
||||||
// the error and add the season id to `self.season_subtitles_missing`. if it is
|
|
||||||
// present, skip the error printing
|
|
||||||
if !self
|
|
||||||
.season_subtitles_missing
|
|
||||||
.contains(&episode.season_number)
|
|
||||||
{
|
|
||||||
self.season_subtitles_missing.push(episode.season_number);
|
|
||||||
error!(
|
|
||||||
"{} season {} is not available with {} subtitles",
|
|
||||||
episode.series_title, episode.season_number, subtitle_locale
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.seasons_with_premium.is_some() && episode.is_premium_only {
|
|
||||||
if !self
|
|
||||||
.seasons_with_premium
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.contains(&episode.season_number)
|
|
||||||
{
|
|
||||||
warn!(
|
|
||||||
"Skipping premium episodes in season {}",
|
|
||||||
episode.season_number
|
|
||||||
);
|
|
||||||
self.seasons_with_premium
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.push(episode.season_number)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut relative_episode_number = None;
|
|
||||||
let mut relative_sequence_number = None;
|
|
||||||
// get the relative episode number. only done if the output string has the pattern to include
|
|
||||||
// the relative episode number as this requires some extra fetching
|
|
||||||
if Format::has_relative_fmt(&self.download.output) {
|
|
||||||
let season_eps = match self.season_episodes.get(&episode.season_number) {
|
|
||||||
Some(eps) => eps,
|
|
||||||
None => {
|
|
||||||
self.season_episodes.insert(
|
|
||||||
episode.season_number,
|
|
||||||
episode.season().await?.episodes().await?,
|
|
||||||
);
|
|
||||||
self.season_episodes.get(&episode.season_number).unwrap()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut non_integer_sequence_number_count = 0;
|
|
||||||
for (i, ep) in season_eps.iter().enumerate() {
|
|
||||||
if ep.sequence_number.fract() != 0.0 || ep.sequence_number == 0.0 {
|
|
||||||
non_integer_sequence_number_count += 1;
|
|
||||||
}
|
|
||||||
if ep.id == episode.id {
|
|
||||||
relative_episode_number = Some(i + 1);
|
|
||||||
relative_sequence_number = Some(
|
|
||||||
(i + 1 - non_integer_sequence_number_count) as f32
|
|
||||||
+ fract(ep.sequence_number),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if relative_episode_number.is_none() || relative_sequence_number.is_none() {
|
|
||||||
warn!(
|
|
||||||
"Failed to get relative episode number for episode {} ({}) of {} season {}",
|
|
||||||
episode.sequence_number,
|
|
||||||
episode.title,
|
|
||||||
episode.series_title,
|
|
||||||
episode.season_number,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(SingleFormat::new_from_episode(
|
|
||||||
episode.clone(),
|
|
||||||
self.download.subtitle.clone().map_or(vec![], |s| {
|
|
||||||
if episode.subtitle_locales.contains(&s) {
|
|
||||||
vec![s]
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
relative_episode_number.map(|n| n as u32),
|
|
||||||
relative_sequence_number,
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn visit_movie_listing(&mut self, movie_listing: MovieListing) -> Result<Vec<Movie>> {
|
|
||||||
Ok(movie_listing.movies().await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn visit_movie(&mut self, movie: Movie) -> Result<Option<Self::T>> {
|
|
||||||
Ok(Some(SingleFormat::new_from_movie(movie, vec![])))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn visit_music_video(&mut self, music_video: MusicVideo) -> Result<Option<Self::T>> {
|
|
||||||
Ok(Some(SingleFormat::new_from_music_video(music_video)))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn visit_concert(&mut self, concert: Concert) -> Result<Option<Self::T>> {
|
|
||||||
Ok(Some(SingleFormat::new_from_concert(concert)))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn finish(self, input: Vec<Self::T>) -> Result<Self::Output> {
|
|
||||||
let mut single_format_collection = SingleFormatCollection::new();
|
|
||||||
|
|
||||||
for data in input {
|
|
||||||
single_format_collection.add_single_formats(vec![data])
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(single_format_collection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
mod command;
|
mod command;
|
||||||
mod filter;
|
|
||||||
|
|
||||||
pub use command::Download;
|
pub use command::Download;
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,10 @@ impl Execute for Search {
|
||||||
warn!("Using `search` anonymously or with a non-premium account may return incomplete results")
|
warn!("Using `search` anonymously or with a non-premium account may return incomplete results")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.output.contains("{{stream.is_drm}}") {
|
||||||
|
warn!("The `{{{{stream.is_drm}}}}` option is deprecated as it isn't reliable anymore and will be removed soon")
|
||||||
|
}
|
||||||
|
|
||||||
let input = if crunchyroll_rs::parse::parse_url(&self.input).is_some() {
|
let input = if crunchyroll_rs::parse::parse_url(&self.input).is_some() {
|
||||||
match parse_url(&ctx.crunchy, self.input.clone(), true).await {
|
match parse_url(&ctx.crunchy, self.input.clone(), true).await {
|
||||||
Ok(ok) => vec![ok],
|
Ok(ok) => vec![ok],
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@ impl From<&Stream> for FormatStream {
|
||||||
Self {
|
Self {
|
||||||
locale: value.audio_locale.clone(),
|
locale: value.audio_locale.clone(),
|
||||||
dash_url: value.url.clone(),
|
dash_url: value.url.clone(),
|
||||||
is_drm: value.session.uses_stream_limits,
|
is_drm: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -241,14 +241,6 @@ macro_rules! must_match_if_true {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! self_and_versions {
|
|
||||||
($var:expr => $audio:expr) => {{
|
|
||||||
let mut items = vec![$var.clone()];
|
|
||||||
items.extend($var.clone().version($audio).await?);
|
|
||||||
items
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Format {
|
pub struct Format {
|
||||||
pattern: Vec<(Range<usize>, Scope, String)>,
|
pattern: Vec<(Range<usize>, Scope, String)>,
|
||||||
pattern_count: HashMap<Scope, u32>,
|
pattern_count: HashMap<Scope, u32>,
|
||||||
|
|
@ -421,7 +413,15 @@ impl Format {
|
||||||
};
|
};
|
||||||
let mut seasons = vec![];
|
let mut seasons = vec![];
|
||||||
for season in tmp_seasons {
|
for season in tmp_seasons {
|
||||||
seasons.extend(self_and_versions!(season => self.filter_options.audio.clone()))
|
seasons.push(season.clone());
|
||||||
|
for version in season.versions {
|
||||||
|
if season.id == version.id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.filter_options.audio.contains(&version.audio_locale) {
|
||||||
|
seasons.push(version.season().await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tree.extend(
|
tree.extend(
|
||||||
self.filter_options
|
self.filter_options
|
||||||
|
|
@ -435,7 +435,15 @@ impl Format {
|
||||||
if !episode_empty || !stream_empty {
|
if !episode_empty || !stream_empty {
|
||||||
match &media_collection {
|
match &media_collection {
|
||||||
MediaCollection::Episode(episode) => {
|
MediaCollection::Episode(episode) => {
|
||||||
let episodes = self_and_versions!(episode => self.filter_options.audio.clone());
|
let mut episodes = vec![episode.clone()];
|
||||||
|
for version in &episode.versions {
|
||||||
|
if episode.id == version.id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.filter_options.audio.contains(&version.audio_locale) {
|
||||||
|
episodes.push(version.episode().await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
tree.push((
|
tree.push((
|
||||||
Season::default(),
|
Season::default(),
|
||||||
episodes
|
episodes
|
||||||
|
|
@ -464,7 +472,9 @@ impl Format {
|
||||||
if !stream_empty {
|
if !stream_empty {
|
||||||
for (_, episodes) in tree.iter_mut() {
|
for (_, episodes) in tree.iter_mut() {
|
||||||
for (episode, streams) in episodes {
|
for (episode, streams) in episodes {
|
||||||
streams.push(episode.stream_maybe_without_drm().await?)
|
let stream = episode.stream_maybe_without_drm().await?;
|
||||||
|
stream.clone().invalidate().await?;
|
||||||
|
streams.push(stream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -322,20 +322,14 @@ impl Downloader {
|
||||||
|
|
||||||
if let Some(offsets) = offsets {
|
if let Some(offsets) = offsets {
|
||||||
let mut root_format_idx = 0;
|
let mut root_format_idx = 0;
|
||||||
let mut root_format_length = 0;
|
let mut root_format_offset = u64::MAX;
|
||||||
|
|
||||||
for (i, format) in self.formats.iter().enumerate() {
|
for (i, format) in self.formats.iter().enumerate() {
|
||||||
let offset = offsets.get(&i).copied().unwrap_or_default();
|
let offset = offsets.get(&i).copied().unwrap_or_default();
|
||||||
let format_len = format
|
let format_offset = offset.num_milliseconds() as u64;
|
||||||
.video
|
if format_offset < root_format_offset {
|
||||||
.0
|
|
||||||
.segments()
|
|
||||||
.iter()
|
|
||||||
.map(|s| s.length.as_millis())
|
|
||||||
.sum::<u128>() as u64
|
|
||||||
- offset.num_milliseconds() as u64;
|
|
||||||
if format_len > root_format_length {
|
|
||||||
root_format_idx = i;
|
root_format_idx = i;
|
||||||
root_format_length = format_len;
|
root_format_offset = format_offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
for _ in &format.audios {
|
for _ in &format.audios {
|
||||||
|
|
@ -567,7 +561,7 @@ impl Downloader {
|
||||||
|
|
||||||
for (i, meta) in videos.iter().enumerate() {
|
for (i, meta) in videos.iter().enumerate() {
|
||||||
if let Some(start_time) = meta.start_time {
|
if let Some(start_time) = meta.start_time {
|
||||||
input.extend(["-ss".to_string(), format_time_delta(&start_time)])
|
input.extend(["-itsoffset".to_string(), format_time_delta(&start_time)])
|
||||||
}
|
}
|
||||||
input.extend(["-i".to_string(), meta.path.to_string_lossy().to_string()]);
|
input.extend(["-i".to_string(), meta.path.to_string_lossy().to_string()]);
|
||||||
maps.extend(["-map".to_string(), i.to_string()]);
|
maps.extend(["-map".to_string(), i.to_string()]);
|
||||||
|
|
@ -588,7 +582,7 @@ impl Downloader {
|
||||||
}
|
}
|
||||||
for (i, meta) in audios.iter().enumerate() {
|
for (i, meta) in audios.iter().enumerate() {
|
||||||
if let Some(start_time) = meta.start_time {
|
if let Some(start_time) = meta.start_time {
|
||||||
input.extend(["-ss".to_string(), format_time_delta(&start_time)])
|
input.extend(["-itsoffset".to_string(), format_time_delta(&start_time)])
|
||||||
}
|
}
|
||||||
input.extend(["-i".to_string(), meta.path.to_string_lossy().to_string()]);
|
input.extend(["-i".to_string(), meta.path.to_string_lossy().to_string()]);
|
||||||
maps.extend(["-map".to_string(), (i + videos.len()).to_string()]);
|
maps.extend(["-map".to_string(), (i + videos.len()).to_string()]);
|
||||||
|
|
@ -635,7 +629,7 @@ impl Downloader {
|
||||||
if container_supports_softsubs {
|
if container_supports_softsubs {
|
||||||
for (i, meta) in subtitles.iter().enumerate() {
|
for (i, meta) in subtitles.iter().enumerate() {
|
||||||
if let Some(start_time) = meta.start_time {
|
if let Some(start_time) = meta.start_time {
|
||||||
input.extend(["-ss".to_string(), format_time_delta(&start_time)])
|
input.extend(["-itsoffset".to_string(), format_time_delta(&start_time)])
|
||||||
}
|
}
|
||||||
input.extend(["-i".to_string(), meta.path.to_string_lossy().to_string()]);
|
input.extend(["-i".to_string(), meta.path.to_string_lossy().to_string()]);
|
||||||
maps.extend([
|
maps.extend([
|
||||||
|
|
@ -654,7 +648,7 @@ impl Downloader {
|
||||||
metadata.extend([
|
metadata.extend([
|
||||||
format!("-metadata:s:s:{}", i),
|
format!("-metadata:s:s:{}", i),
|
||||||
format!("title={}", {
|
format!("title={}", {
|
||||||
let mut title = meta.locale.to_string();
|
let mut title = meta.locale.to_human_readable();
|
||||||
if meta.cc {
|
if meta.cc {
|
||||||
title += " (CC)"
|
title += " (CC)"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,407 @@
|
||||||
|
use crate::utils::format::{SingleFormat, SingleFormatCollection};
|
||||||
|
use crate::utils::interactive_select::{check_for_duplicated_seasons, get_duplicated_seasons};
|
||||||
|
use crate::utils::parse::{fract, UrlFilter};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crunchyroll_rs::{
|
use crunchyroll_rs::{
|
||||||
Concert, Episode, MediaCollection, Movie, MovieListing, MusicVideo, Season, Series,
|
Concert, Episode, Locale, MediaCollection, Movie, MovieListing, MusicVideo, Season, Series,
|
||||||
};
|
};
|
||||||
|
use log::{info, warn};
|
||||||
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
use std::ops::Not;
|
||||||
|
|
||||||
pub trait Filter {
|
pub(crate) enum FilterMediaScope<'a> {
|
||||||
type T: Send + Sized;
|
Series(&'a Series),
|
||||||
type Output: Send + Sized;
|
Season(&'a Season),
|
||||||
|
/// Always contains 1 or 2 episodes.
|
||||||
|
/// - 1: The episode's audio is completely missing
|
||||||
|
/// - 2: The requested audio is only available from first entry to last entry
|
||||||
|
Episode(Vec<&'a Episode>),
|
||||||
|
}
|
||||||
|
|
||||||
async fn visit_series(&mut self, series: Series) -> Result<Vec<Season>>;
|
pub(crate) struct Filter {
|
||||||
async fn visit_season(&mut self, season: Season) -> Result<Vec<Episode>>;
|
url_filter: UrlFilter,
|
||||||
async fn visit_episode(&mut self, episode: Episode) -> Result<Option<Self::T>>;
|
|
||||||
async fn visit_movie_listing(&mut self, movie_listing: MovieListing) -> Result<Vec<Movie>>;
|
|
||||||
async fn visit_movie(&mut self, movie: Movie) -> Result<Option<Self::T>>;
|
|
||||||
async fn visit_music_video(&mut self, music_video: MusicVideo) -> Result<Option<Self::T>>;
|
|
||||||
async fn visit_concert(&mut self, concert: Concert) -> Result<Option<Self::T>>;
|
|
||||||
|
|
||||||
async fn visit(mut self, media_collection: MediaCollection) -> Result<Self::Output>
|
skip_specials: bool,
|
||||||
where
|
interactive_input: bool,
|
||||||
Self: Send + Sized,
|
|
||||||
{
|
relative_episode_number: bool,
|
||||||
|
|
||||||
|
audio_locales: Vec<Locale>,
|
||||||
|
subtitle_locales: Vec<Locale>,
|
||||||
|
|
||||||
|
audios_missing: fn(FilterMediaScope, Vec<&Locale>) -> Result<bool>,
|
||||||
|
subtitles_missing: fn(FilterMediaScope, Vec<&Locale>) -> Result<bool>,
|
||||||
|
no_premium: fn(u32) -> Result<()>,
|
||||||
|
|
||||||
|
is_premium: bool,
|
||||||
|
|
||||||
|
series_visited: bool,
|
||||||
|
season_episodes: HashMap<String, Vec<Episode>>,
|
||||||
|
season_with_premium: Option<Vec<u32>>,
|
||||||
|
season_sorting: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(crate) fn new(
|
||||||
|
url_filter: UrlFilter,
|
||||||
|
audio_locales: Vec<Locale>,
|
||||||
|
subtitle_locales: Vec<Locale>,
|
||||||
|
audios_missing: fn(FilterMediaScope, Vec<&Locale>) -> Result<bool>,
|
||||||
|
subtitles_missing: fn(FilterMediaScope, Vec<&Locale>) -> Result<bool>,
|
||||||
|
no_premium: fn(u32) -> Result<()>,
|
||||||
|
relative_episode_number: bool,
|
||||||
|
interactive_input: bool,
|
||||||
|
skip_specials: bool,
|
||||||
|
is_premium: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
url_filter,
|
||||||
|
audio_locales,
|
||||||
|
subtitle_locales,
|
||||||
|
relative_episode_number,
|
||||||
|
interactive_input,
|
||||||
|
audios_missing,
|
||||||
|
subtitles_missing,
|
||||||
|
no_premium,
|
||||||
|
is_premium,
|
||||||
|
series_visited: false,
|
||||||
|
season_episodes: HashMap::new(),
|
||||||
|
skip_specials,
|
||||||
|
season_with_premium: is_premium.not().then_some(vec![]),
|
||||||
|
season_sorting: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn visit_series(&mut self, series: Series) -> Result<Vec<Season>> {
|
||||||
|
// the audio locales field isn't always populated
|
||||||
|
if !series.audio_locales.is_empty() {
|
||||||
|
let missing_audios = missing_locales(&series.audio_locales, &self.audio_locales);
|
||||||
|
if !missing_audios.is_empty()
|
||||||
|
&& !(self.audios_missing)(FilterMediaScope::Series(&series), missing_audios)?
|
||||||
|
{
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
let missing_subtitles =
|
||||||
|
missing_locales(&series.subtitle_locales, &self.subtitle_locales);
|
||||||
|
if !missing_subtitles.is_empty()
|
||||||
|
&& !(self.subtitles_missing)(FilterMediaScope::Series(&series), missing_subtitles)?
|
||||||
|
{
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut seasons = vec![];
|
||||||
|
for season in series.seasons().await? {
|
||||||
|
if !self.url_filter.is_season_valid(season.season_number) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let missing_audios = missing_locales(
|
||||||
|
&season
|
||||||
|
.versions
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.audio_locale.clone())
|
||||||
|
.collect::<Vec<Locale>>(),
|
||||||
|
&self.audio_locales,
|
||||||
|
);
|
||||||
|
if !missing_audios.is_empty()
|
||||||
|
&& !(self.audios_missing)(FilterMediaScope::Season(&season), missing_audios)?
|
||||||
|
{
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
seasons.push(season)
|
||||||
|
}
|
||||||
|
|
||||||
|
let duplicated_seasons = get_duplicated_seasons(&seasons);
|
||||||
|
if !duplicated_seasons.is_empty() {
|
||||||
|
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(", ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.series_visited = true;
|
||||||
|
|
||||||
|
Ok(seasons)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn visit_season(&mut self, season: Season) -> Result<Vec<Episode>> {
|
||||||
|
if !self.url_filter.is_season_valid(season.season_number) {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut seasons = vec![];
|
||||||
|
if self
|
||||||
|
.audio_locales
|
||||||
|
.iter()
|
||||||
|
.any(|l| season.audio_locales.contains(l))
|
||||||
|
{
|
||||||
|
seasons.push(season.clone())
|
||||||
|
}
|
||||||
|
for version in season.versions {
|
||||||
|
if season.id == version.id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.audio_locales.contains(&version.audio_locale) {
|
||||||
|
seasons.push(version.season().await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut episodes = vec![];
|
||||||
|
for season in seasons {
|
||||||
|
self.season_sorting.push(season.id.clone());
|
||||||
|
let mut eps = season.episodes().await?;
|
||||||
|
|
||||||
|
// removes any episode that does not have the audio locale of the season. yes, this is
|
||||||
|
// the case sometimes
|
||||||
|
if season.audio_locales.len() < 2 {
|
||||||
|
let season_locale = season
|
||||||
|
.audio_locales
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(Locale::ja_JP);
|
||||||
|
eps.retain(|e| e.audio_locale == season_locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::if_same_then_else)]
|
||||||
|
if eps.len() < season.number_of_episodes as usize {
|
||||||
|
if eps.is_empty()
|
||||||
|
&& !(self.audios_missing)(
|
||||||
|
FilterMediaScope::Season(&season),
|
||||||
|
season.audio_locales.iter().collect(),
|
||||||
|
)?
|
||||||
|
{
|
||||||
|
return Ok(vec![]);
|
||||||
|
} else if !eps.is_empty()
|
||||||
|
&& !(self.audios_missing)(
|
||||||
|
FilterMediaScope::Episode(vec![eps.first().unwrap(), eps.last().unwrap()]),
|
||||||
|
vec![&eps.first().unwrap().audio_locale],
|
||||||
|
)?
|
||||||
|
{
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
episodes.extend(eps)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.relative_episode_number {
|
||||||
|
for episode in &episodes {
|
||||||
|
self.season_episodes
|
||||||
|
.entry(episode.season_id.clone())
|
||||||
|
.or_default()
|
||||||
|
.push(episode.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(episodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn visit_episode(&mut self, episode: Episode) -> Result<Vec<SingleFormat>> {
|
||||||
|
if !self
|
||||||
|
.url_filter
|
||||||
|
.is_episode_valid(episode.sequence_number, episode.season_number)
|
||||||
|
{
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip the episode if it's a special
|
||||||
|
if self.skip_specials
|
||||||
|
&& (episode.sequence_number == 0.0 || episode.sequence_number.fract() != 0.0)
|
||||||
|
{
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut episodes = vec![];
|
||||||
|
if !self.series_visited {
|
||||||
|
if self.audio_locales.contains(&episode.audio_locale) {
|
||||||
|
episodes.push(episode.clone())
|
||||||
|
}
|
||||||
|
for version in &episode.versions {
|
||||||
|
// `episode` is also a version of itself. the if block above already adds the
|
||||||
|
// episode if it matches the requested audio, so it doesn't need to be requested
|
||||||
|
// here again
|
||||||
|
if version.id == episode.id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.audio_locales.contains(&version.audio_locale) {
|
||||||
|
episodes.push(version.episode().await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let audio_locales: Vec<Locale> =
|
||||||
|
episodes.iter().map(|e| e.audio_locale.clone()).collect();
|
||||||
|
let missing_audios = missing_locales(&audio_locales, &self.audio_locales);
|
||||||
|
if !missing_audios.is_empty()
|
||||||
|
&& !(self.audios_missing)(
|
||||||
|
FilterMediaScope::Episode(vec![&episode]),
|
||||||
|
missing_audios,
|
||||||
|
)?
|
||||||
|
{
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut subtitle_locales: Vec<Locale> = episodes
|
||||||
|
.iter()
|
||||||
|
.flat_map(|e| e.subtitle_locales.clone())
|
||||||
|
.collect();
|
||||||
|
subtitle_locales.sort();
|
||||||
|
subtitle_locales.dedup();
|
||||||
|
let missing_subtitles = missing_locales(&subtitle_locales, &self.subtitle_locales);
|
||||||
|
if !missing_subtitles.is_empty()
|
||||||
|
&& !(self.subtitles_missing)(
|
||||||
|
FilterMediaScope::Episode(vec![&episode]),
|
||||||
|
missing_subtitles,
|
||||||
|
)?
|
||||||
|
{
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
episodes.push(episode.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(seasons_with_premium) = &mut self.season_with_premium {
|
||||||
|
let episodes_len_before = episodes.len();
|
||||||
|
episodes.retain(|e| !e.is_premium_only && !self.is_premium);
|
||||||
|
if episodes_len_before < episodes.len()
|
||||||
|
&& !seasons_with_premium.contains(&episode.season_number)
|
||||||
|
{
|
||||||
|
(self.no_premium)(episode.season_number)?;
|
||||||
|
seasons_with_premium.push(episode.season_number)
|
||||||
|
}
|
||||||
|
|
||||||
|
if episodes.is_empty() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut relative_episode_number = None;
|
||||||
|
let mut relative_sequence_number = None;
|
||||||
|
if self.relative_episode_number {
|
||||||
|
let season_eps = match self.season_episodes.get(&episode.season_id) {
|
||||||
|
Some(eps) => eps,
|
||||||
|
None => {
|
||||||
|
self.season_episodes.insert(
|
||||||
|
episode.season_id.clone(),
|
||||||
|
episode.season().await?.episodes().await?,
|
||||||
|
);
|
||||||
|
self.season_episodes.get(&episode.season_id).unwrap()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut non_integer_sequence_number_count = 0;
|
||||||
|
for (i, ep) in season_eps.iter().enumerate() {
|
||||||
|
if ep.sequence_number != 0.0 || ep.sequence_number.fract() == 0.0 {
|
||||||
|
non_integer_sequence_number_count += 1
|
||||||
|
}
|
||||||
|
if ep.id == episode.id {
|
||||||
|
relative_episode_number = Some(i + 1);
|
||||||
|
relative_sequence_number = Some(
|
||||||
|
(i + 1 - non_integer_sequence_number_count) as f32
|
||||||
|
+ fract(ep.sequence_number),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if relative_episode_number.is_none() || relative_sequence_number.is_none() {
|
||||||
|
warn!(
|
||||||
|
"Failed to get relative episode number for episode {} ({}) of {} season {}",
|
||||||
|
episode.sequence_number,
|
||||||
|
episode.title,
|
||||||
|
episode.series_title,
|
||||||
|
episode.season_number,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(episodes
|
||||||
|
.into_iter()
|
||||||
|
.map(|e| {
|
||||||
|
SingleFormat::new_from_episode(
|
||||||
|
e.clone(),
|
||||||
|
e.subtitle_locales,
|
||||||
|
relative_episode_number.map(|n| n as u32),
|
||||||
|
relative_sequence_number,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn visit_movie_listing(&mut self, movie_listing: MovieListing) -> Result<Vec<Movie>> {
|
||||||
|
Ok(movie_listing.movies().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn visit_movie(&mut self, movie: Movie) -> Result<Vec<SingleFormat>> {
|
||||||
|
Ok(vec![SingleFormat::new_from_movie(movie, vec![])])
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn visit_music_video(&mut self, music_video: MusicVideo) -> Result<Vec<SingleFormat>> {
|
||||||
|
Ok(vec![SingleFormat::new_from_music_video(music_video)])
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn visit_concert(&mut self, concert: Concert) -> Result<Vec<SingleFormat>> {
|
||||||
|
Ok(vec![SingleFormat::new_from_concert(concert)])
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn finish(self, input: Vec<Vec<SingleFormat>>) -> Result<SingleFormatCollection> {
|
||||||
|
let flatten_input: Vec<SingleFormat> = input.into_iter().flatten().collect();
|
||||||
|
|
||||||
|
let mut single_format_collection = SingleFormatCollection::new();
|
||||||
|
|
||||||
|
let mut pre_sorted: BTreeMap<String, Vec<SingleFormat>> = BTreeMap::new();
|
||||||
|
for data in flatten_input {
|
||||||
|
pre_sorted
|
||||||
|
.entry(data.identifier.clone())
|
||||||
|
.or_default()
|
||||||
|
.push(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sorted: Vec<(String, Vec<SingleFormat>)> = pre_sorted.into_iter().collect();
|
||||||
|
sorted.sort_by(|(_, a), (_, b)| {
|
||||||
|
self.season_sorting
|
||||||
|
.iter()
|
||||||
|
.position(|p| p == &a.first().unwrap().season_id)
|
||||||
|
.unwrap()
|
||||||
|
.cmp(
|
||||||
|
&self
|
||||||
|
.season_sorting
|
||||||
|
.iter()
|
||||||
|
.position(|p| p == &b.first().unwrap().season_id)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
for (_, mut data) in sorted {
|
||||||
|
data.sort_by(|a, b| {
|
||||||
|
self.audio_locales
|
||||||
|
.iter()
|
||||||
|
.position(|p| p == &a.audio)
|
||||||
|
.unwrap_or(usize::MAX)
|
||||||
|
.cmp(
|
||||||
|
&self
|
||||||
|
.audio_locales
|
||||||
|
.iter()
|
||||||
|
.position(|p| p == &b.audio)
|
||||||
|
.unwrap_or(usize::MAX),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
single_format_collection.add_single_formats(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(single_format_collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn visit(
|
||||||
|
mut self,
|
||||||
|
media_collection: MediaCollection,
|
||||||
|
) -> Result<SingleFormatCollection> {
|
||||||
let mut items = vec![media_collection];
|
let mut items = vec![media_collection];
|
||||||
let mut result = vec![];
|
let mut result = vec![];
|
||||||
|
|
||||||
|
|
@ -42,9 +425,7 @@ pub trait Filter {
|
||||||
.collect::<Vec<MediaCollection>>(),
|
.collect::<Vec<MediaCollection>>(),
|
||||||
),
|
),
|
||||||
MediaCollection::Episode(episode) => {
|
MediaCollection::Episode(episode) => {
|
||||||
if let Some(t) = self.visit_episode(episode).await? {
|
result.push(self.visit_episode(episode).await?)
|
||||||
result.push(t)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
MediaCollection::MovieListing(movie_listing) => new_items.extend(
|
MediaCollection::MovieListing(movie_listing) => new_items.extend(
|
||||||
self.visit_movie_listing(movie_listing)
|
self.visit_movie_listing(movie_listing)
|
||||||
|
|
@ -53,20 +434,12 @@ pub trait Filter {
|
||||||
.map(|m| m.into())
|
.map(|m| m.into())
|
||||||
.collect::<Vec<MediaCollection>>(),
|
.collect::<Vec<MediaCollection>>(),
|
||||||
),
|
),
|
||||||
MediaCollection::Movie(movie) => {
|
MediaCollection::Movie(movie) => result.push(self.visit_movie(movie).await?),
|
||||||
if let Some(t) = self.visit_movie(movie).await? {
|
|
||||||
result.push(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MediaCollection::MusicVideo(music_video) => {
|
MediaCollection::MusicVideo(music_video) => {
|
||||||
if let Some(t) = self.visit_music_video(music_video).await? {
|
result.push(self.visit_music_video(music_video).await?)
|
||||||
result.push(t)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
MediaCollection::Concert(concert) => {
|
MediaCollection::Concert(concert) => {
|
||||||
if let Some(t) = self.visit_concert(concert).await? {
|
result.push(self.visit_concert(concert).await?)
|
||||||
result.push(t)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -76,8 +449,10 @@ pub trait Filter {
|
||||||
|
|
||||||
self.finish(result).await
|
self.finish(result).await
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn finish(self, input: Vec<Self::T>) -> Result<Self::Output>;
|
fn missing_locales<'a>(available: &[Locale], searched: &'a [Locale]) -> Vec<&'a Locale> {
|
||||||
|
searched.iter().filter(|p| !available.contains(p)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove all duplicates from a [`Vec`].
|
/// Remove all duplicates from a [`Vec`].
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use crate::utils::log::tab_info;
|
||||||
use crate::utils::os::{is_special_file, sanitize};
|
use crate::utils::os::{is_special_file, sanitize};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use chrono::{Datelike, Duration};
|
use chrono::{Datelike, Duration};
|
||||||
use crunchyroll_rs::media::{Resolution, SkipEvents, Stream, StreamData, Subtitle};
|
use crunchyroll_rs::media::{SkipEvents, Stream, StreamData, Subtitle};
|
||||||
use crunchyroll_rs::{Concert, Episode, Locale, MediaCollection, Movie, MusicVideo};
|
use crunchyroll_rs::{Concert, Episode, Locale, MediaCollection, Movie, MusicVideo};
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
@ -12,6 +12,7 @@ use std::collections::BTreeMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SingleFormat {
|
pub struct SingleFormat {
|
||||||
pub identifier: String,
|
pub identifier: String,
|
||||||
|
|
@ -347,6 +348,7 @@ impl Iterator for SingleFormatCollectionIterator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Format {
|
pub struct Format {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
|
@ -354,8 +356,6 @@ pub struct Format {
|
||||||
|
|
||||||
pub locales: Vec<(Locale, Vec<Locale>)>,
|
pub locales: Vec<(Locale, Vec<Locale>)>,
|
||||||
|
|
||||||
// deprecated
|
|
||||||
pub resolution: Resolution,
|
|
||||||
pub width: u64,
|
pub width: u64,
|
||||||
pub height: u64,
|
pub height: u64,
|
||||||
pub fps: f64,
|
pub fps: f64,
|
||||||
|
|
@ -401,7 +401,6 @@ impl Format {
|
||||||
title: first_format.title,
|
title: first_format.title,
|
||||||
description: first_format.description,
|
description: first_format.description,
|
||||||
locales,
|
locales,
|
||||||
resolution: first_stream.resolution().unwrap(),
|
|
||||||
width: first_stream.resolution().unwrap().width,
|
width: first_stream.resolution().unwrap().width,
|
||||||
height: first_stream.resolution().unwrap().height,
|
height: first_stream.resolution().unwrap().height,
|
||||||
fps: first_stream.fps().unwrap(),
|
fps: first_stream.fps().unwrap(),
|
||||||
|
|
@ -449,11 +448,11 @@ impl Format {
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"{width}",
|
"{width}",
|
||||||
&sanitize(self.resolution.width.to_string(), true, universal),
|
&sanitize(self.width.to_string(), true, universal),
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"{height}",
|
"{height}",
|
||||||
&sanitize(self.resolution.height.to_string(), true, universal),
|
&sanitize(self.height.to_string(), true, universal),
|
||||||
)
|
)
|
||||||
.replace("{series_id}", &sanitize(&self.series_id, true, universal))
|
.replace("{series_id}", &sanitize(&self.series_id, true, universal))
|
||||||
.replace(
|
.replace(
|
||||||
|
|
@ -589,7 +588,7 @@ impl Format {
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join(", ")
|
.join(", ")
|
||||||
);
|
);
|
||||||
tab_info!("Resolution: {}", self.resolution);
|
tab_info!("Resolution: {}x{}", self.height, self.width);
|
||||||
tab_info!("FPS: {:.2}", self.fps)
|
tab_info!("FPS: {:.2}", self.fps)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,11 @@ pub async fn stream_data_from_stream(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
if videos.iter().any(|v| v.drm.is_some()) || audios.iter().any(|v| v.drm.is_some()) {
|
||||||
|
bail!("Stream is DRM protected")
|
||||||
|
}
|
||||||
|
|
||||||
videos.sort_by(|a, b| a.bandwidth.cmp(&b.bandwidth).reverse());
|
videos.sort_by(|a, b| a.bandwidth.cmp(&b.bandwidth).reverse());
|
||||||
audios.sort_by(|a, b| a.bandwidth.cmp(&b.bandwidth).reverse());
|
audios.sort_by(|a, b| a.bandwidth.cmp(&b.bandwidth).reverse());
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue