use crate::search::filter::FilterOptions; use anyhow::{bail, Result}; use crunchyroll_rs::media::{Stream, Subtitle}; use crunchyroll_rs::{ Concert, Crunchyroll, 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; use std::sync::Arc; #[derive(Default, Serialize)] struct FormatSeries { pub id: String, pub title: String, pub description: String, pub release_year: u32, } impl From<&Series> for FormatSeries { fn from(value: &Series) -> Self { Self { id: value.id.clone(), title: value.title.clone(), description: value.description.clone(), release_year: value.series_launch_year.unwrap_or_default(), } } } #[derive(Default, Serialize)] struct FormatSeason { pub id: String, pub title: String, pub description: String, pub number: u32, pub episodes: u32, } impl From<&Season> for FormatSeason { fn from(value: &Season) -> Self { Self { id: value.id.clone(), title: value.title.clone(), description: value.description.clone(), number: value.season_number, episodes: value.number_of_episodes, } } } #[derive(Default, Serialize)] struct FormatEpisode { pub id: String, pub title: String, pub description: String, pub locale: Locale, pub number: u32, pub sequence_number: f32, pub duration: i64, pub air_date: i64, pub premium_only: bool, } impl From<&Episode> for FormatEpisode { fn from(value: &Episode) -> Self { Self { id: value.id.clone(), title: value.title.clone(), description: value.description.clone(), locale: value.audio_locale.clone(), number: value.episode_number.unwrap_or_default(), sequence_number: value.sequence_number, duration: value.duration.num_milliseconds(), air_date: value.episode_air_date.timestamp(), premium_only: value.is_premium_only, } } } #[derive(Default, Serialize)] struct FormatMovieListing { pub id: String, pub title: String, pub description: String, } impl From<&MovieListing> for FormatMovieListing { fn from(value: &MovieListing) -> Self { Self { id: value.id.clone(), title: value.title.clone(), description: value.description.clone(), } } } #[derive(Default, Serialize)] struct FormatMovie { pub id: String, pub title: String, pub description: String, pub duration: i64, pub premium_only: bool, } impl From<&Movie> for FormatMovie { fn from(value: &Movie) -> Self { Self { id: value.id.clone(), title: value.title.clone(), description: value.description.clone(), duration: value.duration.num_milliseconds(), premium_only: value.is_premium_only, } } } #[derive(Default, Serialize)] struct FormatMusicVideo { pub id: String, pub title: String, pub description: String, pub duration: i64, pub premium_only: bool, } impl From<&MusicVideo> for FormatMusicVideo { fn from(value: &MusicVideo) -> Self { Self { id: value.id.clone(), title: value.title.clone(), description: value.description.clone(), duration: value.duration.num_milliseconds(), premium_only: value.is_premium_only, } } } #[derive(Default, Serialize)] struct FormatConcert { pub id: String, pub title: String, pub description: String, pub duration: i64, pub premium_only: bool, } impl From<&Concert> for FormatConcert { fn from(value: &Concert) -> Self { Self { id: value.id.clone(), title: value.title.clone(), description: value.description.clone(), duration: value.duration.num_milliseconds(), premium_only: value.is_premium_only, } } } #[derive(Default, Serialize)] struct FormatStream { pub locale: Locale, pub dash_url: String, pub is_drm: bool, } impl From<&Stream> for FormatStream { fn from(value: &Stream) -> Self { Self { locale: value.audio_locale.clone(), dash_url: value.url.clone(), is_drm: false, } } } #[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(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 { 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)] enum Scope { Series, Season, Episode, MovieListing, Movie, MusicVideo, Concert, Stream, Subtitle, Account, } macro_rules! must_match_if_true { ($condition:expr => $media_collection:ident | $field:pat => $expr:expr) => { if $condition { match &$media_collection { $field => Some($expr), _ => panic!(), } } else { None } }; } pub struct Format { pattern: Vec<(Range, Scope, String)>, pattern_count: HashMap, input: String, filter_options: FilterOptions, crunchyroll: Arc, } impl Format { pub fn new( input: String, filter_options: FilterOptions, crunchyroll: Arc, ) -> Result { let scope_regex = Regex::new(r"(?m)\{\{\s*(?P\w+)\.(?P\w+)\s*}}").unwrap(); let mut pattern = vec![]; let mut pattern_count = HashMap::new(); macro_rules! generate_field_check { ($($scope:expr => $struct_:ident)+) => { HashMap::from([ $( ( $scope, serde_json::from_value::>(serde_json::to_value($struct_::default()).unwrap()).unwrap() ) ),+ ]) }; } let field_check = generate_field_check!( Scope::Series => FormatSeries Scope::Season => FormatSeason Scope::Episode => FormatEpisode Scope::MovieListing => FormatMovieListing Scope::Movie => FormatMovie Scope::MusicVideo => FormatMusicVideo Scope::Concert => FormatConcert Scope::Stream => FormatStream Scope::Subtitle => FormatSubtitle Scope::Account => FormatAccount ); 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, "account" => Scope::Account, _ => bail!("'{}.{}' is not a valid keyword", scope, field), }; if field_check .get(&format_pattern_scope) .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, crunchyroll, }) } pub async fn parse(&self, media_collection: MediaCollection) -> Result { match &media_collection { MediaCollection::Series(_) | MediaCollection::Season(_) | MediaCollection::Episode(_) => { self.check_scopes(vec![ Scope::Series, Scope::Season, Scope::Episode, Scope::Stream, Scope::Subtitle, Scope::Account, ])?; self.parse_series(media_collection).await } MediaCollection::MovieListing(_) | MediaCollection::Movie(_) => { self.check_scopes(vec![ Scope::MovieListing, Scope::Movie, Scope::Stream, Scope::Subtitle, Scope::Account, ])?; self.parse_movie_listing(media_collection).await } MediaCollection::MusicVideo(_) => { self.check_scopes(vec![ Scope::MusicVideo, Scope::Stream, Scope::Subtitle, Scope::Account, ])?; self.parse_music_video(media_collection).await } MediaCollection::Concert(_) => { self.check_scopes(vec![ Scope::Concert, Scope::Stream, Scope::Subtitle, Scope::Account, ])?; self.parse_concert(media_collection).await } } } async fn parse_series(&self, media_collection: MediaCollection) -> Result { 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 account_empty = self.check_pattern_count_empty(Scope::Account); #[allow(clippy::type_complexity)] let mut tree: Vec<(Season, Vec<(Episode, Vec)>)> = 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 season in tmp_seasons { seasons.push(season.clone()); for version in season.versions { if season.id == version.id { continue; } if self.filter_options.audio.contains(&version.audio_locale) { seasons.push(version.season().await?) } } } tree.extend( self.filter_options .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![episode.clone()]; for version in &episode.versions { if episode.id == version.id { continue; } if self.filter_options.audio.contains(&version.audio_locale) { episodes.push(version.episode().await?) } } tree.push(( Season::default(), episodes .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 { let stream = episode.stream_maybe_without_drm().await?; stream.clone().invalidate().await?; streams.push(stream) } } } else { for (_, episodes) in tree.iter_mut() { for (_, streams) in episodes { streams.push(Stream::default()) } } } 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)); 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 stream in streams { let stream_map = self.serializable_to_json_map(FormatStream::from(&stream)); output.push( self.replace_all( HashMap::from([ (Scope::Account, &account_map), (Scope::Series, &series_map), (Scope::Season, &season_map), (Scope::Episode, &episode_map), (Scope::Stream, &stream_map), ]), stream, ) .unwrap_or_default(), ) } } } Ok(output.join("\n")) } async fn parse_movie_listing(&self, media_collection: MediaCollection) -> Result { 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)> = 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.stream_maybe_without_drm().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 stream in streams { let stream_map = self.serializable_to_json_map(FormatStream::from(&stream)); output.push( self.replace_all( HashMap::from([ (Scope::MovieListing, &movie_listing_map), (Scope::Movie, &movie_map), (Scope::Stream, &stream_map), ]), stream, ) .unwrap_or_default(), ) } } Ok(output.join("\n")) } async fn parse_music_video(&self, media_collection: MediaCollection) -> Result { let music_video_empty = self.check_pattern_count_empty(Scope::MusicVideo); let stream_empty = self.check_pattern_count_empty(Scope::Stream); let music_video = must_match_if_true!(!music_video_empty => media_collection|MediaCollection::MusicVideo(music_video) => music_video.clone()).unwrap_or_default(); let stream = must_match_if_true!(!stream_empty => media_collection|MediaCollection::MusicVideo(music_video) => music_video.stream_maybe_without_drm().await?).unwrap_or_default(); let music_video_map = self.serializable_to_json_map(FormatMusicVideo::from(&music_video)); let stream_map = self.serializable_to_json_map(FormatStream::from(&stream)); let output = self .replace_all( HashMap::from([ (Scope::MusicVideo, &music_video_map), (Scope::Stream, &stream_map), ]), stream, ) .unwrap_or_default(); Ok(output) } async fn parse_concert(&self, media_collection: MediaCollection) -> Result { let concert_empty = self.check_pattern_count_empty(Scope::Concert); let stream_empty = self.check_pattern_count_empty(Scope::Stream); let concert = must_match_if_true!(!concert_empty => media_collection|MediaCollection::Concert(concert) => concert.clone()).unwrap_or_default(); let stream = must_match_if_true!(!stream_empty => media_collection|MediaCollection::Concert(concert) => concert.stream_maybe_without_drm().await?).unwrap_or_default(); let concert_map = self.serializable_to_json_map(FormatConcert::from(&concert)); let stream_map = self.serializable_to_json_map(FormatStream::from(&stream)); let output = self .replace_all( HashMap::from([(Scope::Concert, &concert_map), (Scope::Stream, &stream_map)]), stream, ) .unwrap_or_default(); Ok(output) } fn serializable_to_json_map(&self, s: S) -> Map { serde_json::from_value(serde_json::to_value(s).unwrap()).unwrap() } fn check_pattern_count_empty(&self, scope: Scope) -> bool { self.pattern_count.get(&scope).cloned().unwrap_or_default() == 0 } fn check_scopes(&self, available_scopes: Vec) -> Result<()> { for (_, scope, field) in self.pattern.iter() { if !available_scopes.contains(scope) { bail!( "'{}.{}' is not a valid keyword", format!("{:?}", scope).to_lowercase(), field ) } } Ok(()) } fn replace_all( &self, values: HashMap>, mut stream: Stream, ) -> Option { if stream.subtitles.is_empty() { if !self.check_pattern_count_empty(Scope::Subtitle) { return None; } stream .subtitles .insert(Locale::Custom("".to_string()), Subtitle::default()); } let mut output = vec![]; for (_, subtitle) in stream.subtitles { let subtitle_map = self.serializable_to_json_map(FormatSubtitle::from(&subtitle)); let mut tmp_values = values.clone(); tmp_values.insert(Scope::Subtitle, &subtitle_map); output.push(self.replace(tmp_values)) } Some(output.join("\n")) } fn replace(&self, values: HashMap>) -> 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 } }