mirror of
https://github.com/crunchy-labs/crunchy-cli.git
synced 2026-01-21 12:12:00 -06:00
Compare commits
13 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4332b1beef | ||
|
|
2cf9125de3 | ||
|
|
756022b955 | ||
|
|
509683d23a | ||
|
|
8047680799 | ||
|
|
287df84382 | ||
|
|
e7ac6d8874 | ||
|
|
fb8e535644 | ||
|
|
67c267be20 | ||
|
|
a1c7b2069d | ||
|
|
74e5e05b0f | ||
|
|
7d2ae719c8 | ||
|
|
5593046aae |
16 changed files with 579 additions and 896 deletions
29
Cargo.lock
generated
29
Cargo.lock
generated
|
|
@ -349,7 +349,7 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
|||
|
||||
[[package]]
|
||||
name = "crunchy-cli"
|
||||
version = "3.6.4"
|
||||
version = "3.6.7"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
|
|
@ -362,7 +362,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "crunchy-cli-core"
|
||||
version = "3.6.4"
|
||||
version = "3.6.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-speed-limit",
|
||||
|
|
@ -400,9 +400,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "crunchyroll-rs"
|
||||
version = "0.11.1"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58580acc9c0abf96a231ec8b1a4597ea55d9426ea17f684ce3582e2b26437bbb"
|
||||
checksum = "d6e38c223aecf65c9c9bec50764beea5dc70b6c97cd7f767bf6860f2fc8e0a07"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
|
|
@ -426,9 +426,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "crunchyroll-rs-internal"
|
||||
version = "0.11.1"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce3c844dec8a3390f8c9853b5cf1d65c3d38fd0657b8b5d0e008db8945dea326"
|
||||
checksum = "144a38040a21aaa456741a9f6749354527bb68ad3bb14210e0bbc40fbd95186c"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"quote",
|
||||
|
|
@ -1125,8 +1125,8 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.11"
|
||||
source = "git+https://github.com/crunchy-labs/rust-not-so-native-tls.git?rev=b7969a8#b7969a88210096e0570e29d42fb13533baf62aa6"
|
||||
version = "0.2.12"
|
||||
source = "git+https://github.com/crunchy-labs/rust-not-so-native-tls.git?rev=c7ac566#c7ac566559d441bbc3e5e5bd04fb7162c38d88b0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
|
|
@ -1519,8 +1519,9 @@ checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
|
|||
|
||||
[[package]]
|
||||
name = "rsubs-lib"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/crunchy-labs/rsubs-lib.git?rev=1c51f60#1c51f60b8c48f1a8f7b261372b237d89bdc17dd4"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c9f50e3fbcbf1f0bd109954e2dd813d1715c7b4a92a7bf159a85dea49e9d863"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
|
|
@ -1966,9 +1967,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.37.0"
|
||||
version = "1.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
|
||||
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
|
|
@ -1983,9 +1984,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.2.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
||||
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "crunchy-cli"
|
||||
authors = ["Crunchy Labs Maintainers"]
|
||||
version = "3.6.4"
|
||||
version = "3.6.7"
|
||||
edition = "2021"
|
||||
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"]
|
||||
|
||||
[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" }
|
||||
|
||||
|
|
@ -34,8 +34,7 @@ members = ["crunchy-cli-core"]
|
|||
[patch.crates-io]
|
||||
# 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
|
||||
native-tls = { git = "https://github.com/crunchy-labs/rust-not-so-native-tls.git", rev = "b7969a8" }
|
||||
rsubs-lib = { git = "https://github.com/crunchy-labs/rsubs-lib.git", rev = "1c51f60" }
|
||||
native-tls = { git = "https://github.com/crunchy-labs/rust-not-so-native-tls.git", rev = "c7ac566" }
|
||||
|
||||
[profile.release]
|
||||
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).~~
|
||||
>
|
||||
> 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.
|
||||
# This project has been sunset as Crunchyroll moved to a DRM-only system. See [#362](https://github.com/crunchy-labs/crunchy-cli/issues/362).
|
||||
|
||||
# crunchy-cli
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "crunchy-cli-core"
|
||||
authors = ["Crunchy Labs Maintainers"]
|
||||
version = "3.6.4"
|
||||
version = "3.6.7"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ anyhow = "1.0"
|
|||
async-speed-limit = "0.4"
|
||||
clap = { version = "4.5", features = ["derive", "string"] }
|
||||
chrono = "0.4"
|
||||
crunchyroll-rs = { version = "0.11.1", features = ["experimental-stabilizations", "tower"] }
|
||||
crunchyroll-rs = { version = "0.11.4", features = ["experimental-stabilizations", "tower"] }
|
||||
ctrlc = "3.4"
|
||||
dialoguer = { version = "0.11", default-features = false }
|
||||
dirs = "5.0"
|
||||
|
|
@ -30,7 +30,7 @@ log = { version = "0.4", features = ["std"] }
|
|||
num_cpus = "1.16"
|
||||
regex = "1.10"
|
||||
reqwest = { version = "0.12", features = ["socks", "stream"] }
|
||||
rsubs-lib = "0.3"
|
||||
rsubs-lib = "~0.3.2"
|
||||
rusty-chromaprint = "0.2"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
|
|
@ -39,7 +39,7 @@ shlex = "1.3"
|
|||
sys-locale = "0.3"
|
||||
tempfile = "3.10"
|
||||
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"
|
||||
tower-service = "0.3"
|
||||
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::download::{
|
||||
DownloadBuilder, DownloadFormat, DownloadFormatMetadata, MergeBehavior,
|
||||
};
|
||||
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::locale::{all_locale_in_locales, resolve_locales, LanguageTagging};
|
||||
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() {
|
||||
let progress_handler = progress!("Fetching series details");
|
||||
let single_format_collection = ArchiveFilter::new(
|
||||
let single_format_collection = Filter::new(
|
||||
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.skip_specials,
|
||||
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 filter;
|
||||
|
||||
pub use command::Archive;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
use crate::download::filter::DownloadFilter;
|
||||
use crate::utils::context::Context;
|
||||
use crate::utils::download::{DownloadBuilder, DownloadFormat, DownloadFormatMetadata};
|
||||
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::locale::{resolve_locales, LanguageTagging};
|
||||
use crate::utils::log::progress;
|
||||
|
|
@ -14,7 +13,7 @@ use anyhow::bail;
|
|||
use anyhow::Result;
|
||||
use crunchyroll_rs::media::Resolution;
|
||||
use crunchyroll_rs::Locale;
|
||||
use log::{debug, warn};
|
||||
use log::{debug, error, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
|
|
@ -250,9 +249,53 @@ 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(
|
||||
let single_format_collection = Filter::new(
|
||||
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.skip_specials,
|
||||
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 filter;
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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() {
|
||||
match parse_url(&ctx.crunchy, self.input.clone(), true).await {
|
||||
Ok(ok) => vec![ok],
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ impl From<&Stream> for FormatStream {
|
|||
Self {
|
||||
locale: value.audio_locale.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 {
|
||||
pattern: Vec<(Range<usize>, Scope, String)>,
|
||||
pattern_count: HashMap<Scope, u32>,
|
||||
|
|
@ -421,7 +413,15 @@ impl Format {
|
|||
};
|
||||
let mut seasons = vec![];
|
||||
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(
|
||||
self.filter_options
|
||||
|
|
@ -435,7 +435,15 @@ impl Format {
|
|||
if !episode_empty || !stream_empty {
|
||||
match &media_collection {
|
||||
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((
|
||||
Season::default(),
|
||||
episodes
|
||||
|
|
@ -464,7 +472,9 @@ impl Format {
|
|||
if !stream_empty {
|
||||
for (_, episodes) in tree.iter_mut() {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -322,20 +322,14 @@ impl Downloader {
|
|||
|
||||
if let Some(offsets) = offsets {
|
||||
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() {
|
||||
let offset = offsets.get(&i).copied().unwrap_or_default();
|
||||
let format_len = format
|
||||
.video
|
||||
.0
|
||||
.segments()
|
||||
.iter()
|
||||
.map(|s| s.length.as_millis())
|
||||
.sum::<u128>() as u64
|
||||
- offset.num_milliseconds() as u64;
|
||||
if format_len > root_format_length {
|
||||
let format_offset = offset.num_milliseconds() as u64;
|
||||
if format_offset < root_format_offset {
|
||||
root_format_idx = i;
|
||||
root_format_length = format_len;
|
||||
root_format_offset = format_offset;
|
||||
}
|
||||
|
||||
for _ in &format.audios {
|
||||
|
|
@ -567,7 +561,7 @@ impl Downloader {
|
|||
|
||||
for (i, meta) in videos.iter().enumerate() {
|
||||
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()]);
|
||||
maps.extend(["-map".to_string(), i.to_string()]);
|
||||
|
|
@ -588,7 +582,7 @@ impl Downloader {
|
|||
}
|
||||
for (i, meta) in audios.iter().enumerate() {
|
||||
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()]);
|
||||
maps.extend(["-map".to_string(), (i + videos.len()).to_string()]);
|
||||
|
|
@ -635,7 +629,7 @@ impl Downloader {
|
|||
if container_supports_softsubs {
|
||||
for (i, meta) in subtitles.iter().enumerate() {
|
||||
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()]);
|
||||
maps.extend([
|
||||
|
|
@ -654,7 +648,7 @@ impl Downloader {
|
|||
metadata.extend([
|
||||
format!("-metadata:s:s:{}", i),
|
||||
format!("title={}", {
|
||||
let mut title = meta.locale.to_string();
|
||||
let mut title = meta.locale.to_human_readable();
|
||||
if meta.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 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 {
|
||||
type T: Send + Sized;
|
||||
type Output: Send + Sized;
|
||||
pub(crate) enum FilterMediaScope<'a> {
|
||||
Series(&'a Series),
|
||||
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>>;
|
||||
async fn visit_season(&mut self, season: Season) -> Result<Vec<Episode>>;
|
||||
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>>;
|
||||
pub(crate) struct Filter {
|
||||
url_filter: UrlFilter,
|
||||
|
||||
async fn visit(mut self, media_collection: MediaCollection) -> Result<Self::Output>
|
||||
where
|
||||
Self: Send + Sized,
|
||||
{
|
||||
skip_specials: bool,
|
||||
interactive_input: bool,
|
||||
|
||||
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 result = vec![];
|
||||
|
||||
|
|
@ -42,9 +425,7 @@ pub trait Filter {
|
|||
.collect::<Vec<MediaCollection>>(),
|
||||
),
|
||||
MediaCollection::Episode(episode) => {
|
||||
if let Some(t) = self.visit_episode(episode).await? {
|
||||
result.push(t)
|
||||
}
|
||||
result.push(self.visit_episode(episode).await?)
|
||||
}
|
||||
MediaCollection::MovieListing(movie_listing) => new_items.extend(
|
||||
self.visit_movie_listing(movie_listing)
|
||||
|
|
@ -53,20 +434,12 @@ pub trait Filter {
|
|||
.map(|m| m.into())
|
||||
.collect::<Vec<MediaCollection>>(),
|
||||
),
|
||||
MediaCollection::Movie(movie) => {
|
||||
if let Some(t) = self.visit_movie(movie).await? {
|
||||
result.push(t)
|
||||
}
|
||||
}
|
||||
MediaCollection::Movie(movie) => result.push(self.visit_movie(movie).await?),
|
||||
MediaCollection::MusicVideo(music_video) => {
|
||||
if let Some(t) = self.visit_music_video(music_video).await? {
|
||||
result.push(t)
|
||||
}
|
||||
result.push(self.visit_music_video(music_video).await?)
|
||||
}
|
||||
MediaCollection::Concert(concert) => {
|
||||
if let Some(t) = self.visit_concert(concert).await? {
|
||||
result.push(t)
|
||||
}
|
||||
result.push(self.visit_concert(concert).await?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -76,8 +449,10 @@ pub trait Filter {
|
|||
|
||||
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`].
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use crate::utils::log::tab_info;
|
|||
use crate::utils::os::{is_special_file, sanitize};
|
||||
use anyhow::{bail, Result};
|
||||
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 log::{debug, info};
|
||||
use std::cmp::Ordering;
|
||||
|
|
@ -12,6 +12,7 @@ use std::collections::BTreeMap;
|
|||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone)]
|
||||
pub struct SingleFormat {
|
||||
pub identifier: String,
|
||||
|
|
@ -166,29 +167,20 @@ impl SingleFormat {
|
|||
}
|
||||
|
||||
pub async fn stream(&self) -> Result<Stream> {
|
||||
let mut i = 0;
|
||||
loop {
|
||||
let stream = match &self.source {
|
||||
MediaCollection::Episode(e) => e.stream_maybe_without_drm().await,
|
||||
MediaCollection::Movie(m) => m.stream_maybe_without_drm().await,
|
||||
MediaCollection::MusicVideo(mv) => mv.stream_maybe_without_drm().await,
|
||||
MediaCollection::Concert(c) => c.stream_maybe_without_drm().await,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let stream = match &self.source {
|
||||
MediaCollection::Episode(e) => e.stream_maybe_without_drm().await,
|
||||
MediaCollection::Movie(m) => m.stream_maybe_without_drm().await,
|
||||
MediaCollection::MusicVideo(mv) => mv.stream_maybe_without_drm().await,
|
||||
MediaCollection::Concert(c) => c.stream_maybe_without_drm().await,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if let Err(crunchyroll_rs::error::Error::Request { message, .. }) = &stream {
|
||||
// sometimes the request to get streams fails with an 403 and the message
|
||||
// "JWT error", even if the jwt (i guess the auth bearer token is meant by that) is
|
||||
// perfectly valid. it's retried the request 3 times if this specific error occurs
|
||||
if message == "JWT error" && i < 3 {
|
||||
i += 1;
|
||||
continue;
|
||||
} else if message.starts_with("TOO_MANY_ACTIVE_STREAMS") {
|
||||
bail!("Too many active/parallel streams. Please close at least one stream you're watching and try again")
|
||||
}
|
||||
};
|
||||
return Ok(stream?);
|
||||
}
|
||||
if let Err(crunchyroll_rs::error::Error::Request { message, .. }) = &stream {
|
||||
if message.starts_with("TOO_MANY_ACTIVE_STREAMS") {
|
||||
bail!("Too many active/parallel streams. Please close at least one stream you're watching and try again")
|
||||
}
|
||||
};
|
||||
Ok(stream?)
|
||||
}
|
||||
|
||||
pub async fn skip_events(&self) -> Result<Option<SkipEvents>> {
|
||||
|
|
@ -356,6 +348,7 @@ impl Iterator for SingleFormatCollectionIterator {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone)]
|
||||
pub struct Format {
|
||||
pub title: String,
|
||||
|
|
@ -363,8 +356,6 @@ pub struct Format {
|
|||
|
||||
pub locales: Vec<(Locale, Vec<Locale>)>,
|
||||
|
||||
// deprecated
|
||||
pub resolution: Resolution,
|
||||
pub width: u64,
|
||||
pub height: u64,
|
||||
pub fps: f64,
|
||||
|
|
@ -410,7 +401,6 @@ impl Format {
|
|||
title: first_format.title,
|
||||
description: first_format.description,
|
||||
locales,
|
||||
resolution: first_stream.resolution().unwrap(),
|
||||
width: first_stream.resolution().unwrap().width,
|
||||
height: first_stream.resolution().unwrap().height,
|
||||
fps: first_stream.fps().unwrap(),
|
||||
|
|
@ -458,11 +448,11 @@ impl Format {
|
|||
)
|
||||
.replace(
|
||||
"{width}",
|
||||
&sanitize(self.resolution.width.to_string(), true, universal),
|
||||
&sanitize(self.width.to_string(), true, universal),
|
||||
)
|
||||
.replace(
|
||||
"{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(
|
||||
|
|
@ -598,7 +588,7 @@ impl Format {
|
|||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
);
|
||||
tab_info!("Resolution: {}", self.resolution);
|
||||
tab_info!("Resolution: {}x{}", self.height, self.width);
|
||||
tab_info!("FPS: {:.2}", self.fps)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ pub async fn stream_data_from_stream(
|
|||
}
|
||||
}
|
||||
.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());
|
||||
audios.sort_by(|a, b| a.bandwidth.cmp(&b.bandwidth).reverse());
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue