Compare commits

..

No commits in common. "master" and "v3.6.6" have entirely different histories.

16 changed files with 872 additions and 562 deletions

28
Cargo.lock generated
View file

@ -349,7 +349,7 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]] [[package]]
name = "crunchy-cli" name = "crunchy-cli"
version = "3.6.7" version = "3.6.6"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
@ -362,7 +362,7 @@ dependencies = [
[[package]] [[package]]
name = "crunchy-cli-core" name = "crunchy-cli-core"
version = "3.6.7" version = "3.6.6"
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.4" version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6e38c223aecf65c9c9bec50764beea5dc70b6c97cd7f767bf6860f2fc8e0a07" checksum = "1d33b8d77c80dea79e66993cb67963b2171dcf0b8fbc87591c58f2dadfea8da2"
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.4" version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144a38040a21aaa456741a9f6749354527bb68ad3bb14210e0bbc40fbd95186c" checksum = "fa51945265f25c45f7d53bd70e5263dd023c0be45e38eaba886a971cb645d797"
dependencies = [ dependencies = [
"darling", "darling",
"quote", "quote",
@ -1125,8 +1125,8 @@ dependencies = [
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.12" version = "0.2.11"
source = "git+https://github.com/crunchy-labs/rust-not-so-native-tls.git?rev=c7ac566#c7ac566559d441bbc3e5e5bd04fb7162c38d88b0" source = "git+https://github.com/crunchy-labs/rust-not-so-native-tls.git?rev=b7969a8#b7969a88210096e0570e29d42fb13533baf62aa6"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
@ -1519,9 +1519,9 @@ checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
[[package]] [[package]]
name = "rsubs-lib" name = "rsubs-lib"
version = "0.3.2" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9f50e3fbcbf1f0bd109954e2dd813d1715c7b4a92a7bf159a85dea49e9d863" checksum = "d01f7609f0b1bc4fe24b352e8d1792c7d71cc43aea797e14b87974cd009ab402"
dependencies = [ dependencies = [
"regex", "regex",
"serde", "serde",
@ -1967,9 +1967,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.38.0" version = "1.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@ -1984,9 +1984,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "2.3.0" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -1,7 +1,7 @@
[package] [package]
name = "crunchy-cli" name = "crunchy-cli"
authors = ["Crunchy Labs Maintainers"] authors = ["Crunchy Labs Maintainers"]
version = "3.6.7" version = "3.6.6"
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.38", features = ["macros", "rt-multi-thread", "time"], default-features = false } tokio = { version = "1.37", features = ["macros", "rt-multi-thread", "time"], default-features = false }
native-tls-crate = { package = "native-tls", version = "0.2.12", optional = true } native-tls-crate = { package = "native-tls", version = "0.2.11", optional = true }
crunchy-cli-core = { path = "./crunchy-cli-core" } crunchy-cli-core = { path = "./crunchy-cli-core" }
@ -34,7 +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 = "c7ac566" } native-tls = { git = "https://github.com/crunchy-labs/rust-not-so-native-tls.git", rev = "b7969a8" }
[profile.release] [profile.release]
strip = true strip = true

View file

@ -1,4 +1,6 @@
# 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

View file

@ -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.7" version = "3.6.6"
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.4", features = ["experimental-stabilizations", "tower"] } crunchyroll-rs = { version = "0.11.3", 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.2" rsubs-lib = "~0.3.1"
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.38", features = ["io-util", "macros", "net", "rt-multi-thread", "time"] } tokio = { version = "1.37", 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 }

View file

@ -1,9 +1,10 @@
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, FilterMediaScope}; use crate::utils::filter::Filter;
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;
@ -283,49 +284,9 @@ 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 = Filter::new( let single_format_collection = ArchiveFilter::new(
url_filter, url_filter,
self.audio.clone(), self.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,

View file

@ -0,0 +1,466 @@
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()
}

View file

@ -1,3 +1,4 @@
mod command; mod command;
mod filter;
pub use command::Archive; pub use command::Archive;

View file

@ -1,7 +1,8 @@
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, FilterMediaScope}; use crate::utils::filter::Filter;
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;
@ -13,7 +14,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, error, warn}; use log::{debug, warn};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
@ -249,53 +250,9 @@ 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 = Filter::new( let single_format_collection = DownloadFilter::new(
url_filter, url_filter,
vec![self.audio.clone()], self.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,

View file

@ -0,0 +1,307 @@
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)
}
}

View file

@ -1,3 +1,4 @@
mod command; mod command;
mod filter;
pub use command::Download; pub use command::Download;

View file

@ -111,10 +111,6 @@ 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],

View file

@ -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: false, is_drm: value.session.uses_stream_limits,
} }
} }
} }
@ -241,6 +241,14 @@ 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>,
@ -413,15 +421,7 @@ impl Format {
}; };
let mut seasons = vec![]; let mut seasons = vec![];
for season in tmp_seasons { for season in tmp_seasons {
seasons.push(season.clone()); seasons.extend(self_and_versions!(season => self.filter_options.audio.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,15 +435,7 @@ 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 mut episodes = vec![episode.clone()]; let episodes = self_and_versions!(episode => self.filter_options.audio.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

View file

@ -322,14 +322,20 @@ 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_offset = u64::MAX; let mut root_format_length = 0;
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_offset = offset.num_milliseconds() as u64; let format_len = format
if format_offset < root_format_offset { .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 {
root_format_idx = i; root_format_idx = i;
root_format_offset = format_offset; root_format_length = format_len;
} }
for _ in &format.audios { for _ in &format.audios {
@ -561,7 +567,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(["-itsoffset".to_string(), format_time_delta(&start_time)]) input.extend(["-ss".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()]);
@ -582,7 +588,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(["-itsoffset".to_string(), format_time_delta(&start_time)]) input.extend(["-ss".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()]);
@ -629,7 +635,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(["-itsoffset".to_string(), format_time_delta(&start_time)]) input.extend(["-ss".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([
@ -648,7 +654,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_human_readable(); let mut title = meta.locale.to_string();
if meta.cc { if meta.cc {
title += " (CC)" title += " (CC)"
} }

View file

@ -1,407 +1,24 @@
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, Locale, MediaCollection, Movie, MovieListing, MusicVideo, Season, Series, Concert, Episode, MediaCollection, Movie, MovieListing, MusicVideo, Season, Series,
}; };
use log::{info, warn};
use std::collections::{BTreeMap, HashMap};
use std::ops::Not;
pub(crate) enum FilterMediaScope<'a> { pub trait Filter {
Series(&'a Series), type T: Send + Sized;
Season(&'a Season), type Output: Send + Sized;
/// 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>),
}
pub(crate) struct Filter { async fn visit_series(&mut self, series: Series) -> Result<Vec<Season>>;
url_filter: UrlFilter, 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>>;
skip_specials: bool, async fn visit(mut self, media_collection: MediaCollection) -> Result<Self::Output>
interactive_input: bool, where
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![];
@ -425,7 +42,9 @@ impl Filter {
.collect::<Vec<MediaCollection>>(), .collect::<Vec<MediaCollection>>(),
), ),
MediaCollection::Episode(episode) => { MediaCollection::Episode(episode) => {
result.push(self.visit_episode(episode).await?) if let Some(t) = 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)
@ -434,12 +53,20 @@ impl Filter {
.map(|m| m.into()) .map(|m| m.into())
.collect::<Vec<MediaCollection>>(), .collect::<Vec<MediaCollection>>(),
), ),
MediaCollection::Movie(movie) => result.push(self.visit_movie(movie).await?), MediaCollection::Movie(movie) => {
if let Some(t) = self.visit_movie(movie).await? {
result.push(t)
}
}
MediaCollection::MusicVideo(music_video) => { MediaCollection::MusicVideo(music_video) => {
result.push(self.visit_music_video(music_video).await?) if let Some(t) = self.visit_music_video(music_video).await? {
result.push(t)
}
} }
MediaCollection::Concert(concert) => { MediaCollection::Concert(concert) => {
result.push(self.visit_concert(concert).await?) if let Some(t) = self.visit_concert(concert).await? {
result.push(t)
}
} }
} }
} }
@ -449,10 +76,8 @@ impl Filter {
self.finish(result).await self.finish(result).await
} }
}
fn missing_locales<'a>(available: &[Locale], searched: &'a [Locale]) -> Vec<&'a Locale> { async fn finish(self, input: Vec<Self::T>) -> Result<Self::Output>;
searched.iter().filter(|p| !available.contains(p)).collect()
} }
/// Remove all duplicates from a [`Vec`]. /// Remove all duplicates from a [`Vec`].

View file

@ -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::{SkipEvents, Stream, StreamData, Subtitle}; use crunchyroll_rs::media::{Resolution, 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,7 +12,6 @@ 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,
@ -348,7 +347,6 @@ impl Iterator for SingleFormatCollectionIterator {
} }
} }
#[allow(dead_code)]
#[derive(Clone)] #[derive(Clone)]
pub struct Format { pub struct Format {
pub title: String, pub title: String,
@ -356,6 +354,8 @@ 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,6 +401,7 @@ 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(),
@ -448,11 +449,11 @@ impl Format {
) )
.replace( .replace(
"{width}", "{width}",
&sanitize(self.width.to_string(), true, universal), &sanitize(self.resolution.width.to_string(), true, universal),
) )
.replace( .replace(
"{height}", "{height}",
&sanitize(self.height.to_string(), true, universal), &sanitize(self.resolution.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(
@ -588,7 +589,7 @@ impl Format {
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(", ") .join(", ")
); );
tab_info!("Resolution: {}x{}", self.height, self.width); tab_info!("Resolution: {}", self.resolution);
tab_info!("FPS: {:.2}", self.fps) tab_info!("FPS: {:.2}", self.fps)
} }

View file

@ -27,11 +27,6 @@ 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());