Add basic search command

This commit is contained in:
bytedream 2023-05-25 18:53:56 +02:00 committed by ByteDream
parent 0beaa99bfd
commit 0aa648b1a5
10 changed files with 1352 additions and 702 deletions

579
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,14 +5,14 @@ version = "3.0.0-dev.13"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
tokio = { version = "1.27", features = ["macros", "rt-multi-thread", "time"], default-features = false } tokio = { version = "1.28", features = ["macros", "rt-multi-thread", "time"], default-features = false }
crunchy-cli-core = { path = "./crunchy-cli-core" } crunchy-cli-core = { path = "./crunchy-cli-core" }
[build-dependencies] [build-dependencies]
chrono = "0.4" chrono = "0.4"
clap = { version = "4.2", features = ["string"] } clap = { version = "4.3", features = ["string"] }
clap_complete = "4.2" clap_complete = "4.3"
clap_mangen = "0.2" clap_mangen = "0.2"
crunchy-cli-core = { path = "./crunchy-cli-core" } crunchy-cli-core = { path = "./crunchy-cli-core" }

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@ anyhow = "1.0"
async-trait = "0.1" async-trait = "0.1"
clap = { version = "4.2", features = ["derive", "string"] } clap = { version = "4.2", features = ["derive", "string"] }
chrono = "0.4" chrono = "0.4"
crunchyroll-rs = { version = "0.3.4", features = ["dash-stream"] } crunchyroll-rs = { version = "0.3.6", features = ["dash-stream"] }
ctrlc = "3.2" ctrlc = "3.2"
dirs = "5.0" dirs = "5.0"
derive_setters = "0.1" derive_setters = "0.1"
@ -23,10 +23,11 @@ reqwest = { version = "0.11", default-features = false, features = ["socks"] }
sanitize-filename = "0.4" sanitize-filename = "0.4"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
serde_plain = "1.0"
shlex = "1.1" shlex = "1.1"
tempfile = "3.5" tempfile = "3.5"
terminal_size = "0.2" terminal_size = "0.2"
tokio = { version = "1.27", features = ["macros", "rt-multi-thread", "time"] } tokio = { version = "1.28", features = ["macros", "rt-multi-thread", "time"] }
sys-locale = "0.3" sys-locale = "0.3"
[build-dependencies] [build-dependencies]

View file

@ -14,11 +14,13 @@ use std::{env, fs};
mod archive; mod archive;
mod download; mod download;
mod login; mod login;
mod search;
mod utils; mod utils;
pub use archive::Archive; pub use archive::Archive;
pub use download::Download; pub use download::Download;
pub use login::Login; pub use login::Login;
pub use search::Search;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
trait Execute { trait Execute {
@ -81,6 +83,7 @@ enum Command {
Archive(Archive), Archive(Archive),
Download(Download), Download(Download),
Login(Login), Login(Login),
Search(Search),
} }
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
@ -128,6 +131,7 @@ pub async fn cli_entrypoint() {
pre_check_executor(login).await pre_check_executor(login).await
} }
} }
Command::Search(search) => pre_check_executor(search).await,
}; };
let ctx = match create_ctx(&mut cli).await { let ctx = match create_ctx(&mut cli).await {
@ -173,6 +177,7 @@ pub async fn cli_entrypoint() {
Command::Archive(archive) => execute_executor(archive, ctx).await, Command::Archive(archive) => execute_executor(archive, ctx).await,
Command::Download(download) => execute_executor(download, ctx).await, Command::Download(download) => execute_executor(download, ctx).await,
Command::Login(login) => execute_executor(login, ctx).await, Command::Login(login) => execute_executor(login, ctx).await,
Command::Search(search) => execute_executor(search, ctx).await,
}; };
} }

View file

@ -0,0 +1,189 @@
use crate::search::filter::FilterOptions;
use crate::search::format::Format;
use crate::utils::context::Context;
use crate::utils::parse::{parse_url, UrlFilter};
use crate::Execute;
use anyhow::{bail, Result};
use crunchyroll_rs::common::StreamExt;
use crunchyroll_rs::search::QueryResults;
use crunchyroll_rs::{Episode, Locale, MediaCollection, MovieListing, MusicVideo, Series};
#[derive(Debug, clap::Parser)]
pub struct Search {
#[arg(help = "Audio languages to include")]
#[arg(long, default_values_t = vec![crate::utils::locale::system_locale()])]
audio: Vec<Locale>,
#[arg(help = "Filter the locale/language of subtitles according to the value of `--audio`")]
#[arg(long, default_value_t = false)]
filter_subtitles: bool,
#[arg(help = "Limit of search top search results")]
#[arg(long, default_value_t = 5)]
search_top_results_limit: u32,
#[arg(help = "Limit of search series results")]
#[arg(long, default_value_t = 0)]
search_series_limit: u32,
#[arg(help = "Limit of search movie listing results")]
#[arg(long, default_value_t = 0)]
search_movie_listing_limit: u32,
#[arg(help = "Limit of search episode results")]
#[arg(long, default_value_t = 0)]
search_episode_limit: u32,
#[arg(help = "Limit of search music results")]
#[arg(long, default_value_t = 0)]
search_music_limit: u32,
/// Format of the output text.
///
/// You can specify keywords in a specific pattern and they will get replaced in the output text.
/// The required pattern for this begins with `{{`, then the keyword, and closes with `}}` (e.g. `{{episode.title}}`).
/// For example, if you want to get the title of an episode, you can use `Title {{episode.title}}` and `{{episode.title}}` will be replaced with the episode title
///
/// See the following list for all keywords and their meaning:
/// series.title → Series title
/// series.description → Series description
///
/// season.title → Season title
/// season.description → Season description
/// season.number → Season number
///
/// episode.title → Episode title
/// episode.description → Episode description
/// episode.locale → Episode locale/language
/// episode.number → Episode number
/// episode.sequence_number → Episode number. This number is unique unlike `episode.number` which sometimes can be duplicated
///
/// movie_listing.title → Movie listing title
/// movie_listing.description → Movie listing description
///
/// movie.title → Movie title
/// movie.description → Movie description
///
/// music_video.title → Music video title
/// music_video.description → Music video description
///
/// concert.title → Concert title
/// concert.description → Concert description
///
/// stream.locale → Stream locale/language
/// stream.dash_url → Stream url in DASH format
/// stream.hls_url → Stream url in HLS format
///
/// subtitle.locale → Subtitle locale/language
/// subtitle.url → Subtitle url
#[arg(short, long, verbatim_doc_comment)]
#[arg(default_value = "S{{season.number}}E{{episode.number}} - {{episode.title}}")]
output: String,
input: String,
}
#[async_trait::async_trait(?Send)]
impl Execute for Search {
async fn execute(self, ctx: Context) -> Result<()> {
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],
Err(e) => bail!("url {} could not be parsed: {}", self.input, e),
}
} else {
let mut output = vec![];
let query = resolve_query(&self, ctx.crunchy.query(&self.input)).await?;
output.extend(query.0.into_iter().map(|m| (m, UrlFilter::default())));
output.extend(
query
.1
.into_iter()
.map(|s| (s.into(), UrlFilter::default())),
);
output.extend(
query
.2
.into_iter()
.map(|m| (m.into(), UrlFilter::default())),
);
output.extend(
query
.3
.into_iter()
.map(|e| (e.into(), UrlFilter::default())),
);
output.extend(
query
.4
.into_iter()
.map(|m| (m.into(), UrlFilter::default())),
);
output
};
for (media_collection, url_filter) in input {
let filter_options = FilterOptions {
audio: self.audio.clone(),
filter_subtitles: self.filter_subtitles,
url_filter,
};
let format = Format::new(self.output.clone(), filter_options)?;
println!("{}", format.parse(media_collection).await?);
}
Ok(())
}
}
macro_rules! resolve_query {
($limit:expr, $vec:expr, $item:expr) => {
if $limit > 0 {
let mut item_results = $item;
while let Some(item) = item_results.next().await {
$vec.push(item?);
if $vec.len() >= $limit as usize {
break;
}
}
}
};
}
async fn resolve_query(
search: &Search,
query_results: QueryResults,
) -> Result<(
Vec<MediaCollection>,
Vec<Series>,
Vec<MovieListing>,
Vec<Episode>,
Vec<MusicVideo>,
)> {
let mut media_collection = vec![];
let mut series = vec![];
let mut movie_listing = vec![];
let mut episode = vec![];
let mut music_video = vec![];
resolve_query!(
search.search_top_results_limit,
media_collection,
query_results.top_results
);
resolve_query!(search.search_series_limit, series, query_results.series);
resolve_query!(
search.search_movie_listing_limit,
movie_listing,
query_results.movie_listing
);
resolve_query!(search.search_episode_limit, episode, query_results.episode);
resolve_query!(search.search_music_limit, music_video, query_results.music);
Ok((
media_collection,
series,
movie_listing,
episode,
music_video,
))
}

View file

@ -0,0 +1,56 @@
use crate::utils::parse::UrlFilter;
use crunchyroll_rs::media::Subtitle;
use crunchyroll_rs::{Episode, Locale, MovieListing, Season, Series};
pub struct FilterOptions {
pub audio: Vec<Locale>,
pub filter_subtitles: bool,
pub url_filter: UrlFilter,
}
impl FilterOptions {
pub fn check_series(&self, series: &Series) -> bool {
self.check_audio_language(&series.audio_locales)
}
pub fn filter_seasons(&self, mut seasons: Vec<Season>) -> Vec<Season> {
seasons.retain(|s| {
self.check_audio_language(&s.audio_locales)
&& self.url_filter.is_season_valid(s.season_number)
});
seasons
}
pub fn filter_episodes(&self, mut episodes: Vec<Episode>) -> Vec<Episode> {
episodes.retain(|e| {
self.check_audio_language(&vec![e.audio_locale.clone()])
&& self
.url_filter
.is_episode_valid(e.episode_number, e.season_number)
});
episodes
}
pub fn check_movie_listing(&self, movie_listing: &MovieListing) -> bool {
self.check_audio_language(
&movie_listing
.audio_locale
.clone()
.map_or(vec![], |a| vec![a.clone()]),
)
}
pub fn filter_subtitles(&self, mut subtitles: Vec<Subtitle>) -> Vec<Subtitle> {
if self.filter_subtitles {
subtitles.retain(|s| self.check_audio_language(&vec![s.locale.clone()]))
}
subtitles
}
fn check_audio_language(&self, audio: &Vec<Locale>) -> bool {
if !self.audio.is_empty() {
return self.audio.iter().any(|a| audio.contains(a));
}
true
}
}

View file

@ -0,0 +1,625 @@
use crate::search::filter::FilterOptions;
use anyhow::{bail, Result};
use crunchyroll_rs::media::{Stream, Subtitle};
use crunchyroll_rs::{
Concert, Episode, Locale, MediaCollection, Movie, MovieListing, MusicVideo, Season, Series,
};
use regex::Regex;
use serde::Serialize;
use serde_json::{Map, Value};
use std::collections::HashMap;
use std::ops::Range;
#[derive(Default, Serialize)]
struct FormatSeries {
pub title: String,
pub description: String,
}
impl From<&Series> for FormatSeries {
fn from(value: &Series) -> Self {
Self {
title: value.title.clone(),
description: value.description.clone(),
}
}
}
#[derive(Default, Serialize)]
struct FormatSeason {
pub title: String,
pub description: String,
pub number: u32,
}
impl From<&Season> for FormatSeason {
fn from(value: &Season) -> Self {
Self {
title: value.title.clone(),
description: value.description.clone(),
number: value.season_number,
}
}
}
#[derive(Default, Serialize)]
struct FormatEpisode {
pub title: String,
pub description: String,
pub locale: Locale,
pub number: u32,
pub sequence_number: f32,
}
impl From<&Episode> for FormatEpisode {
fn from(value: &Episode) -> Self {
Self {
title: value.title.clone(),
description: value.description.clone(),
locale: value.audio_locale.clone(),
number: value.episode_number,
sequence_number: value.sequence_number,
}
}
}
#[derive(Default, Serialize)]
struct FormatMovieListing {
pub title: String,
pub description: String,
}
impl From<&MovieListing> for FormatMovieListing {
fn from(value: &MovieListing) -> Self {
Self {
title: value.title.clone(),
description: value.description.clone(),
}
}
}
#[derive(Default, Serialize)]
struct FormatMovie {
pub title: String,
pub description: String,
}
impl From<&Movie> for FormatMovie {
fn from(value: &Movie) -> Self {
Self {
title: value.title.clone(),
description: value.description.clone(),
}
}
}
#[derive(Default, Serialize)]
struct FormatMusicVideo {
pub title: String,
pub description: String,
}
impl From<&MusicVideo> for FormatMusicVideo {
fn from(value: &MusicVideo) -> Self {
Self {
title: value.title.clone(),
description: value.description.clone(),
}
}
}
#[derive(Default, Serialize)]
struct FormatConcert {
pub title: String,
pub description: String,
}
impl From<&Concert> for FormatConcert {
fn from(value: &Concert) -> Self {
Self {
title: value.title.clone(),
description: value.description.clone(),
}
}
}
#[derive(Default, Serialize)]
struct FormatStream {
pub locale: Locale,
pub dash_url: String,
pub hls_url: String,
}
impl From<&Stream> for FormatStream {
fn from(value: &Stream) -> Self {
let (dash_url, hls_url) = value.variants.get(&Locale::Custom("".to_string())).map_or(
("".to_string(), "".to_string()),
|v| {
(
v.adaptive_dash.clone().unwrap_or_default().url,
v.adaptive_hls.clone().unwrap_or_default().url,
)
},
);
Self {
locale: value.audio_locale.clone(),
dash_url,
hls_url,
}
}
}
#[derive(Default, Serialize)]
struct FormatSubtitle {
pub locale: Locale,
pub url: String,
}
impl From<&Subtitle> for FormatSubtitle {
fn from(value: &Subtitle) -> Self {
Self {
locale: value.locale.clone(),
url: value.url.clone(),
}
}
}
#[derive(Clone, Eq, PartialEq, Hash)]
enum Scope {
Series,
Season,
Episode,
MovieListing,
Movie,
MusicVideo,
Concert,
Stream,
Subtitle,
}
pub struct Format {
pattern: Vec<(Range<usize>, Scope, String)>,
pattern_count: HashMap<Scope, u32>,
input: String,
filter_options: FilterOptions,
}
impl Format {
pub fn new(input: String, filter_options: FilterOptions) -> Result<Self> {
let scope_regex = Regex::new(r"(?m)\{\{\s*(?P<scope>\w+)\.(?P<field>\w+)\s*}}").unwrap();
let mut pattern = vec![];
let mut pattern_count = HashMap::new();
let field_check = HashMap::from([
(
Scope::Series,
serde_json::to_value(FormatSeries::default()).unwrap(),
),
(
Scope::Season,
serde_json::to_value(FormatSeason::default()).unwrap(),
),
(
Scope::Episode,
serde_json::to_value(FormatEpisode::default()).unwrap(),
),
(
Scope::MovieListing,
serde_json::to_value(FormatMovieListing::default()).unwrap(),
),
(
Scope::Movie,
serde_json::to_value(FormatMovie::default()).unwrap(),
),
(
Scope::MusicVideo,
serde_json::to_value(FormatMusicVideo::default()).unwrap(),
),
(
Scope::Concert,
serde_json::to_value(FormatConcert::default()).unwrap(),
),
(
Scope::Stream,
serde_json::to_value(FormatStream::default()).unwrap(),
),
(
Scope::Subtitle,
serde_json::to_value(FormatSubtitle::default()).unwrap(),
),
]);
for capture in scope_regex.captures_iter(&input) {
let full = capture.get(0).unwrap();
let scope = capture.name("scope").unwrap().as_str();
let field = capture.name("field").unwrap().as_str();
let format_pattern_scope = match scope {
"series" => Scope::Series,
"season" => Scope::Season,
"episode" => Scope::Episode,
"movie_listing" => Scope::MovieListing,
"movie" => Scope::Movie,
"music_video" => Scope::MusicVideo,
"concert" => Scope::Concert,
"stream" => Scope::Stream,
"subtitle" => Scope::Subtitle,
_ => bail!("'{}.{}' is not a valid keyword", scope, field),
};
if field_check
.get(&format_pattern_scope)
.unwrap()
.as_object()
.unwrap()
.get(field)
.is_none()
{
bail!("'{}.{}' is not a valid keyword", scope, field)
}
pattern.push((
full.start()..full.end(),
format_pattern_scope.clone(),
field.to_string(),
));
*pattern_count.entry(format_pattern_scope).or_default() += 1
}
Ok(Self {
pattern,
pattern_count,
input,
filter_options,
})
}
fn check_pattern_count_empty(&self, scope: Scope) -> bool {
self.pattern_count.get(&scope).cloned().unwrap_or_default() == 0
}
pub async fn parse(&self, media_collection: MediaCollection) -> Result<String> {
match &media_collection {
MediaCollection::Series(_)
| MediaCollection::Season(_)
| MediaCollection::Episode(_) => self.parse_series(media_collection).await,
MediaCollection::MovieListing(_) | MediaCollection::Movie(_) => {
self.parse_movie_listing(media_collection).await
}
MediaCollection::MusicVideo(_) => self.parse_music_video(media_collection).await,
MediaCollection::Concert(_) => self.parse_concert(media_collection).await,
}
}
async fn parse_series(&self, media_collection: MediaCollection) -> Result<String> {
let series_empty = self.check_pattern_count_empty(Scope::Series);
let season_empty = self.check_pattern_count_empty(Scope::Season);
let episode_empty = self.check_pattern_count_empty(Scope::Episode);
let stream_empty = self.check_pattern_count_empty(Scope::Stream)
&& self.check_pattern_count_empty(Scope::Subtitle);
let mut tree: Vec<(Season, Vec<(Episode, Vec<Stream>)>)> = vec![];
let series = if !series_empty {
let series = match &media_collection {
MediaCollection::Series(series) => series.clone(),
MediaCollection::Season(season) => season.series().await?,
MediaCollection::Episode(episode) => episode.series().await?,
_ => panic!(),
};
if !self.filter_options.check_series(&series) {
return Ok("".to_string());
}
series
} else {
Series::default()
};
if !season_empty || !episode_empty || !stream_empty {
let tmp_seasons = match &media_collection {
MediaCollection::Series(series) => series.seasons().await?,
MediaCollection::Season(season) => vec![season.clone()],
MediaCollection::Episode(_) => vec![],
_ => panic!(),
};
let mut seasons = vec![];
for mut season in tmp_seasons {
if self
.filter_options
.audio
.iter()
.any(|a| season.audio_locales.contains(a))
{
seasons.push(season.clone())
}
seasons.extend(season.version(self.filter_options.audio.clone()).await?);
}
tree.extend(
self.filter_options
.filter_seasons(seasons)
.into_iter()
.map(|s| (s, vec![])),
)
} else {
tree.push((Season::default(), vec![]))
}
if !episode_empty || !stream_empty {
match &media_collection {
MediaCollection::Episode(episode) => {
let mut episodes = vec![];
if self.filter_options.audio.contains(&episode.audio_locale) {
episodes.push(episode.clone())
}
episodes.extend(
episode
.clone()
.version(self.filter_options.audio.clone())
.await?,
);
tree.push((
Season::default(),
episodes
.into_iter()
.filter(|e| self.filter_options.audio.contains(&e.audio_locale))
.map(|e| (e, vec![]))
.collect(),
))
}
_ => {
for (season, episodes) in tree.iter_mut() {
episodes.extend(
self.filter_options
.filter_episodes(season.episodes().await?)
.into_iter()
.map(|e| (e, vec![])),
)
}
}
};
} else {
for (_, episodes) in tree.iter_mut() {
episodes.push((Episode::default(), vec![]))
}
}
if !stream_empty {
for (_, episodes) in tree.iter_mut() {
for (episode, streams) in episodes {
streams.push(episode.streams().await?)
}
}
} else {
for (_, episodes) in tree.iter_mut() {
for (_, streams) in episodes {
streams.push(Stream::default())
}
}
}
let mut output = vec![];
let series_map = self.serializable_to_json_map(FormatSeries::from(&series));
for (season, episodes) in tree {
let season_map = self.serializable_to_json_map(FormatSeason::from(&season));
for (episode, streams) in episodes {
let episode_map = self.serializable_to_json_map(FormatEpisode::from(&episode));
for mut stream in streams {
let stream_map = self.serializable_to_json_map(FormatStream::from(&stream));
if stream.subtitles.is_empty() {
if !self.check_pattern_count_empty(Scope::Subtitle) {
continue;
}
stream
.subtitles
.insert(Locale::Custom("".to_string()), Subtitle::default());
}
for subtitle in self
.filter_options
.filter_subtitles(stream.subtitles.into_values().collect())
{
let subtitle_map =
self.serializable_to_json_map(FormatSubtitle::from(&subtitle));
let replace_map = HashMap::from([
(Scope::Series, &series_map),
(Scope::Season, &season_map),
(Scope::Episode, &episode_map),
(Scope::Stream, &stream_map),
(Scope::Subtitle, &subtitle_map),
]);
output.push(self.replace(replace_map))
}
}
}
}
Ok(output.join("\n"))
}
async fn parse_movie_listing(&self, media_collection: MediaCollection) -> Result<String> {
let movie_listing_empty = self.check_pattern_count_empty(Scope::MovieListing);
let movie_empty = self.check_pattern_count_empty(Scope::Movie);
let stream_empty = self.check_pattern_count_empty(Scope::Stream);
let mut tree: Vec<(Movie, Vec<Stream>)> = vec![];
let movie_listing = if !movie_listing_empty {
let movie_listing = match &media_collection {
MediaCollection::MovieListing(movie_listing) => movie_listing.clone(),
MediaCollection::Movie(movie) => movie.movie_listing().await?,
_ => panic!(),
};
if !self.filter_options.check_movie_listing(&movie_listing) {
return Ok("".to_string());
}
movie_listing
} else {
MovieListing::default()
};
if !movie_empty || !stream_empty {
let movies = match &media_collection {
MediaCollection::MovieListing(movie_listing) => movie_listing.movies().await?,
MediaCollection::Movie(movie) => vec![movie.clone()],
_ => panic!(),
};
tree.extend(movies.into_iter().map(|m| (m, vec![])))
}
if !stream_empty {
for (movie, streams) in tree.iter_mut() {
streams.push(movie.streams().await?)
}
} else {
for (_, streams) in tree.iter_mut() {
streams.push(Stream::default())
}
}
let mut output = vec![];
let movie_listing_map =
self.serializable_to_json_map(FormatMovieListing::from(&movie_listing));
for (movie, streams) in tree {
let movie_map = self.serializable_to_json_map(FormatMovie::from(&movie));
for mut stream in streams {
let stream_map = self.serializable_to_json_map(FormatStream::from(&stream));
if stream.subtitles.is_empty() {
if !self.check_pattern_count_empty(Scope::Subtitle) {
continue;
}
stream
.subtitles
.insert(Locale::Custom("".to_string()), Subtitle::default());
}
for subtitle in self
.filter_options
.filter_subtitles(stream.subtitles.into_values().collect())
{
let subtitle_map =
self.serializable_to_json_map(FormatSubtitle::from(&subtitle));
let replace_map = HashMap::from([
(Scope::MovieListing, &movie_listing_map),
(Scope::Movie, &movie_map),
(Scope::Stream, &stream_map),
(Scope::Subtitle, &subtitle_map),
]);
output.push(self.replace(replace_map))
}
}
}
Ok(output.join("\n"))
}
async fn parse_music_video(&self, media_collection: MediaCollection) -> Result<String> {
let music_video_empty = self.check_pattern_count_empty(Scope::MusicVideo);
let stream_empty = self.check_pattern_count_empty(Scope::Stream);
let music_video = if !music_video_empty {
match &media_collection {
MediaCollection::MusicVideo(music_video) => music_video.clone(),
_ => panic!(),
}
} else {
MusicVideo::default()
};
let mut stream = if !stream_empty {
match &media_collection {
MediaCollection::MusicVideo(music_video) => music_video.streams().await?,
_ => panic!(),
}
} else {
Stream::default()
};
let mut output = vec![];
let music_video_map = self.serializable_to_json_map(FormatMusicVideo::from(&music_video));
let stream_map = self.serializable_to_json_map(FormatStream::from(&stream));
if stream.subtitles.is_empty() {
if !self.check_pattern_count_empty(Scope::Subtitle) {
return Ok("".to_string());
}
stream
.subtitles
.insert(Locale::Custom("".to_string()), Subtitle::default());
}
for subtitle in self
.filter_options
.filter_subtitles(stream.subtitles.into_values().collect())
{
let subtitle_map = self.serializable_to_json_map(FormatSubtitle::from(&subtitle));
let replace_map = HashMap::from([
(Scope::MusicVideo, &music_video_map),
(Scope::Stream, &stream_map),
(Scope::Subtitle, &subtitle_map),
]);
output.push(self.replace(replace_map))
}
Ok(output.join("\n"))
}
async fn parse_concert(&self, media_collection: MediaCollection) -> Result<String> {
let concert_empty = self.check_pattern_count_empty(Scope::Concert);
let stream_empty = self.check_pattern_count_empty(Scope::Stream);
let concert = if !concert_empty {
match &media_collection {
MediaCollection::Concert(concert) => concert.clone(),
_ => panic!(),
}
} else {
Concert::default()
};
let mut stream = if !stream_empty {
match &media_collection {
MediaCollection::Concert(concert) => concert.streams().await?,
_ => panic!(),
}
} else {
Stream::default()
};
let mut output = vec![];
let concert_map = self.serializable_to_json_map(FormatConcert::from(&concert));
let stream_map = self.serializable_to_json_map(FormatStream::from(&stream));
if stream.subtitles.is_empty() {
if !self.check_pattern_count_empty(Scope::Subtitle) {
return Ok("".to_string());
}
stream
.subtitles
.insert(Locale::Custom("".to_string()), Subtitle::default());
}
for subtitle in self
.filter_options
.filter_subtitles(stream.subtitles.into_values().collect())
{
let subtitle_map = self.serializable_to_json_map(FormatSubtitle::from(&subtitle));
let replace_map = HashMap::from([
(Scope::MusicVideo, &concert_map),
(Scope::Stream, &stream_map),
(Scope::Subtitle, &subtitle_map),
]);
output.push(self.replace(replace_map))
}
Ok(output.join("\n"))
}
fn serializable_to_json_map<S: Serialize>(&self, s: S) -> Map<String, Value> {
serde_json::from_value(serde_json::to_value(s).unwrap()).unwrap()
}
fn replace(&self, values: HashMap<Scope, &Map<String, Value>>) -> String {
let mut output = self.input.clone();
let mut offset = 0;
for (range, scope, field) in &self.pattern {
let item =
serde_plain::to_string(values.get(scope).unwrap().get(field.as_str()).unwrap())
.unwrap();
let start = (range.start as i32 + offset) as usize;
let end = (range.end as i32 + offset) as usize;
output.replace_range(start..end, &item);
offset += item.len() as i32 - range.len() as i32;
}
output
}
}

View file

@ -0,0 +1,5 @@
mod command;
mod filter;
mod format;
pub use command::Search;

View file

@ -8,7 +8,7 @@ use regex::Regex;
/// If a struct instance equals the [`Default::default()`] it's considered that no find is applied. /// If a struct instance equals the [`Default::default()`] it's considered that no find is applied.
/// If `from_*` is [`None`] they're set to [`u32::MIN`]. /// If `from_*` is [`None`] they're set to [`u32::MIN`].
/// If `to_*` is [`None`] they're set to [`u32::MAX`]. /// If `to_*` is [`None`] they're set to [`u32::MAX`].
#[derive(Debug)] #[derive(Debug, Default)]
pub struct InnerUrlFilter { pub struct InnerUrlFilter {
from_episode: Option<u32>, from_episode: Option<u32>,
to_episode: Option<u32>, to_episode: Option<u32>,
@ -16,11 +16,19 @@ pub struct InnerUrlFilter {
to_season: Option<u32>, to_season: Option<u32>,
} }
#[derive(Debug, Default)] #[derive(Debug)]
pub struct UrlFilter { pub struct UrlFilter {
inner: Vec<InnerUrlFilter>, inner: Vec<InnerUrlFilter>,
} }
impl Default for UrlFilter {
fn default() -> Self {
Self {
inner: vec![InnerUrlFilter::default()],
}
}
}
impl UrlFilter { impl UrlFilter {
pub fn is_season_valid(&self, season: u32) -> bool { pub fn is_season_valid(&self, season: u32) -> bool {
self.inner.iter().any(|f| { self.inner.iter().any(|f| {