mirror of
https://github.com/crunchy-labs/crunchy-cli.git
synced 2026-01-21 04:02:00 -06:00
Add account scope for search command
This commit is contained in:
parent
4b74299733
commit
25cde6163c
3 changed files with 65 additions and 9 deletions
|
|
@ -225,8 +225,6 @@ async fn execute_executor(executor: impl Execute, ctx: Context) {
|
||||||
if let Some(crunchy_error) = err.downcast_mut::<Error>() {
|
if let Some(crunchy_error) = err.downcast_mut::<Error>() {
|
||||||
if let Error::Block { message, .. } = crunchy_error {
|
if let Error::Block { message, .. } = crunchy_error {
|
||||||
*message = "Triggered Cloudflare bot protection. Try again later or use a VPN or proxy to spoof your location".to_string()
|
*message = "Triggered Cloudflare bot protection. Try again later or use a VPN or proxy to spoof your location".to_string()
|
||||||
} else if let Error::Request { message, .. } = crunchy_error {
|
|
||||||
*message = "You've probably hit a rate limit. Try again later, generally after 10-20 minutes the rate limit is over and you can continue to use the cli".to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
error!("An error occurred: {}", crunchy_error)
|
error!("An error occurred: {}", crunchy_error)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use crunchyroll_rs::common::StreamExt;
|
||||||
use crunchyroll_rs::search::QueryResults;
|
use crunchyroll_rs::search::QueryResults;
|
||||||
use crunchyroll_rs::{Episode, Locale, MediaCollection, MovieListing, MusicVideo, Series};
|
use crunchyroll_rs::{Episode, Locale, MediaCollection, MovieListing, MusicVideo, Series};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Debug, clap::Parser)]
|
#[derive(Debug, clap::Parser)]
|
||||||
#[clap(about = "Search in videos")]
|
#[clap(about = "Search in videos")]
|
||||||
|
|
@ -87,11 +88,16 @@ pub struct Search {
|
||||||
/// concert.premium_only → If the concert is only available with Crunchyroll premium
|
/// concert.premium_only → If the concert is only available with Crunchyroll premium
|
||||||
///
|
///
|
||||||
/// stream.locale → Stream locale/language
|
/// stream.locale → Stream locale/language
|
||||||
/// stream.dash_url → Stream url in DASH format
|
/// stream.dash_url → Stream url in DASH format. You need to set the `Authorization` header to `Bearer <account.token>` when requesting this url
|
||||||
/// stream.is_drm → If `stream.is_drm` is DRM encrypted
|
/// stream.is_drm → If `stream.dash_url` is DRM encrypted
|
||||||
///
|
///
|
||||||
/// subtitle.locale → Subtitle locale/language
|
/// subtitle.locale → Subtitle locale/language
|
||||||
/// subtitle.url → Url to the subtitle
|
/// subtitle.url → Url to the subtitle
|
||||||
|
///
|
||||||
|
/// account.token → Access token to make request to restricted endpoints. This token is only valid for a max. of 5 minutes
|
||||||
|
/// account.id → Internal ID of the user account
|
||||||
|
/// account.profile_name → Profile name of the account
|
||||||
|
/// account.email → Email address of the account
|
||||||
#[arg(short, long, verbatim_doc_comment)]
|
#[arg(short, long, verbatim_doc_comment)]
|
||||||
#[arg(default_value = "S{{season.number}}E{{episode.number}} - {{episode.title}}")]
|
#[arg(default_value = "S{{season.number}}E{{episode.number}} - {{episode.title}}")]
|
||||||
output: String,
|
output: String,
|
||||||
|
|
@ -143,13 +149,14 @@ impl Execute for Search {
|
||||||
output
|
output
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let crunchy_arc = Arc::new(ctx.crunchy);
|
||||||
for (media_collection, url_filter) in input {
|
for (media_collection, url_filter) in input {
|
||||||
let filter_options = FilterOptions {
|
let filter_options = FilterOptions {
|
||||||
audio: self.audio.clone(),
|
audio: self.audio.clone(),
|
||||||
url_filter,
|
url_filter,
|
||||||
};
|
};
|
||||||
|
|
||||||
let format = Format::new(self.output.clone(), filter_options)?;
|
let format = Format::new(self.output.clone(), filter_options, crunchy_arc.clone())?;
|
||||||
println!("{}", format.parse(media_collection).await?);
|
println!("{}", format.parse(media_collection).await?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,15 @@ use crate::search::filter::FilterOptions;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use crunchyroll_rs::media::{Stream, Subtitle};
|
use crunchyroll_rs::media::{Stream, Subtitle};
|
||||||
use crunchyroll_rs::{
|
use crunchyroll_rs::{
|
||||||
Concert, Episode, Locale, MediaCollection, Movie, MovieListing, MusicVideo, Season, Series,
|
Concert, Crunchyroll, Episode, Locale, MediaCollection, Movie, MovieListing, MusicVideo,
|
||||||
|
Season, Series,
|
||||||
};
|
};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Default, Serialize)]
|
#[derive(Default, Serialize)]
|
||||||
struct FormatSeries {
|
struct FormatSeries {
|
||||||
|
|
@ -191,6 +193,27 @@ impl From<&Subtitle> for FormatSubtitle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize)]
|
||||||
|
struct FormatAccount {
|
||||||
|
pub token: String,
|
||||||
|
pub id: String,
|
||||||
|
pub profile_name: String,
|
||||||
|
pub email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FormatAccount {
|
||||||
|
pub async fn async_from(value: &Crunchyroll) -> Result<Self> {
|
||||||
|
let account = value.account().await?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
token: value.access_token().await,
|
||||||
|
id: account.account_id,
|
||||||
|
profile_name: account.profile_name,
|
||||||
|
email: account.email,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
enum Scope {
|
enum Scope {
|
||||||
Series,
|
Series,
|
||||||
|
|
@ -202,6 +225,7 @@ enum Scope {
|
||||||
Concert,
|
Concert,
|
||||||
Stream,
|
Stream,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
|
Account,
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! must_match_if_true {
|
macro_rules! must_match_if_true {
|
||||||
|
|
@ -230,10 +254,15 @@ pub struct Format {
|
||||||
pattern_count: HashMap<Scope, u32>,
|
pattern_count: HashMap<Scope, u32>,
|
||||||
input: String,
|
input: String,
|
||||||
filter_options: FilterOptions,
|
filter_options: FilterOptions,
|
||||||
|
crunchyroll: Arc<Crunchyroll>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Format {
|
impl Format {
|
||||||
pub fn new(input: String, filter_options: FilterOptions) -> Result<Self> {
|
pub fn new(
|
||||||
|
input: String,
|
||||||
|
filter_options: FilterOptions,
|
||||||
|
crunchyroll: Arc<Crunchyroll>,
|
||||||
|
) -> Result<Self> {
|
||||||
let scope_regex = Regex::new(r"(?m)\{\{\s*(?P<scope>\w+)\.(?P<field>\w+)\s*}}").unwrap();
|
let scope_regex = Regex::new(r"(?m)\{\{\s*(?P<scope>\w+)\.(?P<field>\w+)\s*}}").unwrap();
|
||||||
let mut pattern = vec![];
|
let mut pattern = vec![];
|
||||||
let mut pattern_count = HashMap::new();
|
let mut pattern_count = HashMap::new();
|
||||||
|
|
@ -260,6 +289,7 @@ impl Format {
|
||||||
Scope::Concert => FormatConcert
|
Scope::Concert => FormatConcert
|
||||||
Scope::Stream => FormatStream
|
Scope::Stream => FormatStream
|
||||||
Scope::Subtitle => FormatSubtitle
|
Scope::Subtitle => FormatSubtitle
|
||||||
|
Scope::Account => FormatAccount
|
||||||
);
|
);
|
||||||
|
|
||||||
for capture in scope_regex.captures_iter(&input) {
|
for capture in scope_regex.captures_iter(&input) {
|
||||||
|
|
@ -277,6 +307,7 @@ impl Format {
|
||||||
"concert" => Scope::Concert,
|
"concert" => Scope::Concert,
|
||||||
"stream" => Scope::Stream,
|
"stream" => Scope::Stream,
|
||||||
"subtitle" => Scope::Subtitle,
|
"subtitle" => Scope::Subtitle,
|
||||||
|
"account" => Scope::Account,
|
||||||
_ => bail!("'{}.{}' is not a valid keyword", scope, field),
|
_ => bail!("'{}.{}' is not a valid keyword", scope, field),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -302,6 +333,7 @@ impl Format {
|
||||||
pattern_count,
|
pattern_count,
|
||||||
input,
|
input,
|
||||||
filter_options,
|
filter_options,
|
||||||
|
crunchyroll,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,6 +348,7 @@ impl Format {
|
||||||
Scope::Episode,
|
Scope::Episode,
|
||||||
Scope::Stream,
|
Scope::Stream,
|
||||||
Scope::Subtitle,
|
Scope::Subtitle,
|
||||||
|
Scope::Account,
|
||||||
])?;
|
])?;
|
||||||
|
|
||||||
self.parse_series(media_collection).await
|
self.parse_series(media_collection).await
|
||||||
|
|
@ -326,17 +359,28 @@ impl Format {
|
||||||
Scope::Movie,
|
Scope::Movie,
|
||||||
Scope::Stream,
|
Scope::Stream,
|
||||||
Scope::Subtitle,
|
Scope::Subtitle,
|
||||||
|
Scope::Account,
|
||||||
])?;
|
])?;
|
||||||
|
|
||||||
self.parse_movie_listing(media_collection).await
|
self.parse_movie_listing(media_collection).await
|
||||||
}
|
}
|
||||||
MediaCollection::MusicVideo(_) => {
|
MediaCollection::MusicVideo(_) => {
|
||||||
self.check_scopes(vec![Scope::MusicVideo, Scope::Stream, Scope::Subtitle])?;
|
self.check_scopes(vec![
|
||||||
|
Scope::MusicVideo,
|
||||||
|
Scope::Stream,
|
||||||
|
Scope::Subtitle,
|
||||||
|
Scope::Account,
|
||||||
|
])?;
|
||||||
|
|
||||||
self.parse_music_video(media_collection).await
|
self.parse_music_video(media_collection).await
|
||||||
}
|
}
|
||||||
MediaCollection::Concert(_) => {
|
MediaCollection::Concert(_) => {
|
||||||
self.check_scopes(vec![Scope::Concert, Scope::Stream, Scope::Subtitle])?;
|
self.check_scopes(vec![
|
||||||
|
Scope::Concert,
|
||||||
|
Scope::Stream,
|
||||||
|
Scope::Subtitle,
|
||||||
|
Scope::Account,
|
||||||
|
])?;
|
||||||
|
|
||||||
self.parse_concert(media_collection).await
|
self.parse_concert(media_collection).await
|
||||||
}
|
}
|
||||||
|
|
@ -349,6 +393,7 @@ impl Format {
|
||||||
let episode_empty = self.check_pattern_count_empty(Scope::Episode);
|
let episode_empty = self.check_pattern_count_empty(Scope::Episode);
|
||||||
let stream_empty = self.check_pattern_count_empty(Scope::Stream)
|
let stream_empty = self.check_pattern_count_empty(Scope::Stream)
|
||||||
&& self.check_pattern_count_empty(Scope::Subtitle);
|
&& self.check_pattern_count_empty(Scope::Subtitle);
|
||||||
|
let account_empty = self.check_pattern_count_empty(Scope::Account);
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
let mut tree: Vec<(Season, Vec<(Episode, Vec<Stream>)>)> = vec![];
|
let mut tree: Vec<(Season, Vec<(Episode, Vec<Stream>)>)> = vec![];
|
||||||
|
|
@ -431,6 +476,11 @@ impl Format {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut output = vec![];
|
let mut output = vec![];
|
||||||
|
let account_map = if !account_empty {
|
||||||
|
self.serializable_to_json_map(FormatAccount::async_from(&self.crunchyroll).await?)
|
||||||
|
} else {
|
||||||
|
Map::default()
|
||||||
|
};
|
||||||
let series_map = self.serializable_to_json_map(FormatSeries::from(&series));
|
let series_map = self.serializable_to_json_map(FormatSeries::from(&series));
|
||||||
for (season, episodes) in tree {
|
for (season, episodes) in tree {
|
||||||
let season_map = self.serializable_to_json_map(FormatSeason::from(&season));
|
let season_map = self.serializable_to_json_map(FormatSeason::from(&season));
|
||||||
|
|
@ -442,6 +492,7 @@ impl Format {
|
||||||
output.push(
|
output.push(
|
||||||
self.replace_all(
|
self.replace_all(
|
||||||
HashMap::from([
|
HashMap::from([
|
||||||
|
(Scope::Account, &account_map),
|
||||||
(Scope::Series, &series_map),
|
(Scope::Series, &series_map),
|
||||||
(Scope::Season, &season_map),
|
(Scope::Season, &season_map),
|
||||||
(Scope::Episode, &episode_map),
|
(Scope::Episode, &episode_map),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue