use crate::categories::Category;
use crate::common::{BulkResult, Image};
use crate::error::{CrunchyrollError, CrunchyrollErrorContext};
use crate::media::old_media::{OldEpisode, OldMovie, OldSeason};
use crate::media::{PlaybackStream, VideoStream};
use crate::{options, Crunchyroll, Executor, Locale, Request, Result};
use chrono::{DateTime, Duration, Utc};
use serde::de::{DeserializeOwned, IntoDeserializer};
use serde::{Deserialize, Deserializer};
use serde_json::Value;
use std::sync::Arc;
#[async_trait::async_trait]
pub trait Video: Default + DeserializeOwned + Request {
#[cfg(feature = "experimental-stabilizations")]
async fn __apply_fixes(_: Arc<Executor>, _: &mut Media<Self>) {}
}
#[cfg(feature = "experimental-stabilizations")]
pub(crate) fn parse_locale_from_slug_title(mut title: String) -> Locale {
title = title.trim_end_matches("-dub").to_string();
if title.ends_with("-arabic") {
Locale::ar_SA
} else if title.ends_with("-castilian") {
Locale::es_ES
} else if title.ends_with("-english") {
Locale::en_US
} else if title.ends_with("-english-in") {
Locale::en_IN
} else if title.ends_with("-french") {
Locale::fr_FR
} else if title.ends_with("-german") {
Locale::de_DE
} else if title.ends_with("-hindi") {
Locale::hi_IN
} else if title.ends_with("-italian") {
Locale::it_IT
} else if title.ends_with("-portuguese") {
Locale::pt_BR
} else if title.ends_with("-russian") {
Locale::ru_RU
} else if title.ends_with("-spanish") {
Locale::es_419
} else {
Locale::ja_JP
}
}
#[derive(Clone, Debug, Deserialize, smart_default::SmartDefault)]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
pub struct SearchMetadata {
pub last_public: Option<DateTime<Utc>>,
pub rank: Option<u32>,
pub score: f64,
pub popularity_score: Option<f64>,
}
#[allow(dead_code)]
#[derive(Clone, Debug, Default, Deserialize, Request)]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
pub struct Series {
pub extended_description: String,
pub series_launch_year: Option<u32>,
pub episode_count: u32,
pub season_count: u32,
pub is_subbed: bool,
pub is_dubbed: bool,
pub is_simulcast: bool,
pub audio_locales: Vec<Locale>,
pub subtitle_locales: Vec<Locale>,
#[serde(default)]
#[serde(rename = "tenant_categories")]
pub categories: Vec<Category>,
pub maturity_ratings: Vec<String>,
pub is_mature: bool,
pub mature_blocked: bool,
pub availability_notes: String,
#[cfg(feature = "__test_strict")]
pub(crate) extended_maturity_rating: crate::StrictValue,
}
#[async_trait::async_trait]
impl Video for Series {
#[cfg(feature = "experimental-stabilizations")]
async fn __apply_fixes(executor: Arc<Executor>, media: &mut Media<Self>) {
if executor.fixes.locale_name_parsing {
if let Ok(seasons) = media.seasons().await {
let mut locales = seasons
.into_iter()
.flat_map(|s| s.metadata.audio_locales)
.collect::<Vec<Locale>>();
locales.dedup();
media.metadata.audio_locales = locales
}
}
}
}
#[derive(Clone, Debug, Deserialize, smart_default::SmartDefault, Request)]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
struct SeasonProxy {
audio_locale: Locale,
audio_locales: Vec<Locale>,
subtitle_locales: Vec<Locale>,
#[serde(default)]
season_number: u32,
maturity_ratings: Vec<String>,
is_mature: bool,
mature_blocked: bool,
#[cfg(feature = "__test_strict")]
pub(crate) season_display_number: crate::StrictValue,
#[cfg(feature = "__test_strict")]
pub(crate) season_sequence_number: crate::StrictValue,
#[cfg(feature = "__test_strict")]
pub(crate) extended_maturity_rating: crate::StrictValue,
#[cfg(feature = "__test_strict")]
pub(crate) versions: crate::StrictValue,
#[cfg(feature = "__test_strict")]
pub(crate) identifier: crate::StrictValue,
}
#[allow(dead_code)]
#[derive(Clone, Debug, Deserialize, smart_default::SmartDefault, Request)]
#[serde(from = "SeasonProxy")]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
pub struct Season {
pub audio_locales: Vec<Locale>,
pub subtitle_locales: Vec<Locale>,
#[serde(default)]
pub season_number: u32,
pub maturity_ratings: Vec<String>,
pub is_mature: bool,
pub mature_blocked: bool,
#[cfg(feature = "__test_strict")]
pub(crate) season_display_number: crate::StrictValue,
#[cfg(feature = "__test_strict")]
pub(crate) season_sequence_number: crate::StrictValue,
#[cfg(feature = "__test_strict")]
pub(crate) extended_maturity_rating: crate::StrictValue,
#[cfg(feature = "__test_strict")]
pub(crate) versions: crate::StrictValue,
#[cfg(feature = "__test_strict")]
pub(crate) identifier: crate::StrictValue,
}
#[async_trait::async_trait]
impl Video for Season {
#[cfg(feature = "experimental-stabilizations")]
async fn __apply_fixes(executor: Arc<Executor>, media: &mut Media<Self>) {
if executor.fixes.locale_name_parsing {
media.metadata.audio_locales =
vec![parse_locale_from_slug_title(media.slug_title.clone())]
}
}
}
impl From<SeasonProxy> for Season {
fn from(mut season_proxy: SeasonProxy) -> Self {
if season_proxy.audio_locale != Locale::default() {
season_proxy.audio_locales.push(season_proxy.audio_locale);
season_proxy.audio_locales.dedup()
}
Self {
audio_locales: season_proxy.audio_locales,
subtitle_locales: season_proxy.subtitle_locales,
season_number: season_proxy.season_number,
maturity_ratings: season_proxy.maturity_ratings,
is_mature: season_proxy.is_mature,
mature_blocked: season_proxy.mature_blocked,
#[cfg(feature = "__test_strict")]
season_display_number: season_proxy.season_display_number,
#[cfg(feature = "__test_strict")]
season_sequence_number: season_proxy.season_sequence_number,
#[cfg(feature = "__test_strict")]
extended_maturity_rating: season_proxy.extended_maturity_rating,
#[cfg(feature = "__test_strict")]
versions: season_proxy.versions,
#[cfg(feature = "__test_strict")]
identifier: season_proxy.identifier,
}
}
}
#[allow(dead_code)]
#[derive(Clone, Debug, Deserialize, smart_default::SmartDefault, Request)]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
pub struct Episode {
pub series_id: String,
pub series_title: String,
pub series_slug_title: String,
pub season_id: String,
pub season_title: String,
pub season_slug_title: String,
pub season_number: u32,
pub episode: String,
#[serde(deserialize_with = "crate::internal::serde::deserialize_maybe_null_to_default")]
pub episode_number: u32,
pub sequence_number: f32,
#[serde(alias = "duration_ms")]
#[serde(deserialize_with = "crate::internal::serde::deserialize_millis_to_duration")]
#[default(Duration::milliseconds(0))]
pub duration: Duration,
#[default(DateTime::<Utc>::from(std::time::SystemTime::UNIX_EPOCH))]
pub episode_air_date: DateTime<Utc>,
#[default(DateTime::<Utc>::from(std::time::SystemTime::UNIX_EPOCH))]
pub upload_date: DateTime<Utc>,
#[default(DateTime::<Utc>::from(std::time::SystemTime::UNIX_EPOCH))]
pub free_available_date: DateTime<Utc>,
#[default(DateTime::<Utc>::from(std::time::SystemTime::UNIX_EPOCH))]
pub premium_available_date: DateTime<Utc>,
#[default(DateTime::<Utc>::from(std::time::SystemTime::UNIX_EPOCH))]
pub availability_starts: DateTime<Utc>,
#[default(DateTime::<Utc>::from(std::time::SystemTime::UNIX_EPOCH))]
pub availability_ends: DateTime<Utc>,
pub is_subbed: bool,
pub is_dubbed: bool,
pub closed_captions_available: bool,
pub audio_locale: Locale,
pub subtitle_locales: Vec<Locale>,
pub is_clip: bool,
pub is_premium_only: bool,
#[serde(default)]
#[serde(rename = "tenant_categories")]
pub categories: Vec<Category>,
pub maturity_ratings: Vec<String>,
pub is_mature: bool,
pub mature_blocked: bool,
pub available_offline: bool,
pub availability_notes: String,
pub eligible_region: String,
#[cfg(feature = "__test_strict")]
pub(crate) extended_maturity_rating: crate::StrictValue,
#[cfg(feature = "__test_strict")]
pub(crate) available_date: crate::StrictValue,
#[cfg(feature = "__test_strict")]
pub(crate) premium_date: crate::StrictValue,
#[cfg(feature = "__test_strict")]
pub(crate) versions: crate::StrictValue,
#[cfg(feature = "__test_strict")]
pub(crate) identifier: crate::StrictValue,
}
#[async_trait::async_trait]
impl Video for Episode {
#[cfg(feature = "experimental-stabilizations")]
async fn __apply_fixes(executor: Arc<Executor>, media: &mut Media<Self>) {
if executor.fixes.locale_name_parsing {
media.metadata.audio_locale =
parse_locale_from_slug_title(media.metadata.season_slug_title.clone())
}
}
}
#[allow(dead_code)]
#[derive(Clone, Debug, Deserialize, smart_default::SmartDefault, Request)]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
pub struct MovieListing {
pub first_movie_id: String,
pub extended_description: String,
pub movie_release_year: u32,
#[serde(alias = "duration_ms")]
#[serde(deserialize_with = "crate::internal::serde::deserialize_millis_to_duration")]
#[default(Duration::milliseconds(0))]
pub duration: Duration,
pub is_subbed: bool,
pub is_dubbed: bool,
pub subtitle_locales: Vec<Locale>,
pub is_premium_only: bool,
#[default(DateTime::<Utc>::from(std::time::SystemTime::UNIX_EPOCH))]
pub free_available_date: DateTime<Utc>,
#[default(DateTime::<Utc>::from(std::time::SystemTime::UNIX_EPOCH))]
pub premium_available_date: DateTime<Utc>,
#[serde(default)]
#[serde(rename = "tenant_categories")]
pub categories: Vec<Category>,
pub maturity_ratings: Vec<String>,
pub is_mature: bool,
pub mature_blocked: bool,
pub available_offline: bool,
pub availability_notes: String,
#[cfg(feature = "__test_strict")]
pub(crate) extended_maturity_rating: crate::StrictValue,
#[cfg(feature = "__test_strict")]
pub(crate) available_date: crate::StrictValue,
#[cfg(feature = "__test_strict")]
pub(crate) premium_date: crate::StrictValue,
}
#[async_trait::async_trait]
impl Video for MovieListing {}
#[allow(dead_code)]
#[derive(Clone, Debug, Deserialize, smart_default::SmartDefault, Request)]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
pub struct Movie {
pub movie_listing_id: String,
pub movie_listing_title: String,
pub movie_listing_slug_title: String,
#[serde(alias = "duration_ms")]
#[serde(deserialize_with = "crate::internal::serde::deserialize_millis_to_duration")]
#[default(Duration::milliseconds(0))]
pub duration: Duration,
pub is_subbed: bool,
pub is_dubbed: bool,
pub closed_captions_available: bool,
pub is_premium_only: bool,
pub maturity_ratings: Vec<String>,
pub is_mature: bool,
pub mature_blocked: bool,
pub available_offline: bool,
pub availability_notes: String,
#[cfg(feature = "__test_strict")]
pub(crate) extended_maturity_rating: crate::StrictValue,
}
#[async_trait::async_trait]
impl Video for Movie {}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum MediaCollection {
Series(Media<Series>),
Season(Media<Season>),
Episode(Media<Episode>),
MovieListing(Media<MovieListing>),
Movie(Media<Movie>),
}
impl Default for MediaCollection {
fn default() -> Self {
Self::Series(Media::default())
}
}
#[async_trait::async_trait]
impl Request for MediaCollection {
async fn __set_executor(&mut self, executor: Arc<Executor>) {
match self {
MediaCollection::Series(series) => series.__set_executor(executor).await,
MediaCollection::Season(season) => season.__set_executor(executor).await,
MediaCollection::Episode(episode) => episode.__set_executor(executor).await,
MediaCollection::MovieListing(movie_listing) => {
movie_listing.__set_executor(executor).await
}
MediaCollection::Movie(movie) => movie.__set_executor(executor).await,
}
}
}
impl<'de> Deserialize<'de> for MediaCollection {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let as_map = serde_json::Map::deserialize(deserializer)?;
let err_conv = |e: serde_json::Error| serde::de::Error::custom(e.to_string());
if as_map.contains_key("series_metadata") {
Ok(MediaCollection::Series(
Media::deserialize(Value::from(as_map).into_deserializer()).map_err(err_conv)?,
))
} else if as_map.contains_key("season_metadata") {
Ok(MediaCollection::Season(
Media::deserialize(Value::from(as_map).into_deserializer()).map_err(err_conv)?,
))
} else if as_map.contains_key("episode_metadata") {
Ok(MediaCollection::Episode(
Media::deserialize(Value::from(as_map).into_deserializer()).map_err(err_conv)?,
))
} else if as_map.contains_key("movie_listing_metadata") {
Ok(MediaCollection::MovieListing(
Media::deserialize(Value::from(as_map).into_deserializer()).map_err(err_conv)?,
))
} else if as_map.contains_key("movie_metadata") {
Ok(MediaCollection::Movie(
Media::deserialize(Value::from(as_map).into_deserializer()).map_err(err_conv)?,
))
} else {
Err(serde::de::Error::custom(
"no metadata were found".to_string(),
))
}
}
}
impl MediaCollection {
pub async fn from_id(crunchy: &Crunchyroll, id: String) -> Result<MediaCollection> {
let endpoint = format!(
"https://www.crunchyroll.com/cms/v2/{}/objects/{}",
crunchy.executor.details.bucket, &id
);
let result: BulkResult<MediaCollection> = crunchy
.executor
.get(endpoint)
.apply_media_query()
.apply_locale_query()
.request()
.await?;
if result.items.is_empty() {
Err(CrunchyrollError::Input(
format!("no media could be found for id '{}'", id).into(),
))
} else if result.items.len() >= 2 {
Err(CrunchyrollError::Internal(format!("multiple media were found for id '{}'. this shouldn't happen, please report it immediately!", id).into()))
} else {
Ok(result.items.into_iter().next().unwrap())
}
}
}
macro_rules! impl_try_into_media {
($($generic:path = $enum_field:ident)*) => {
$(
impl TryInto<Media<$generic>> for MediaCollection {
type Error = CrunchyrollError;
fn try_into(self) -> std::result::Result<Media<$generic>, Self::Error> {
if let MediaCollection::$enum_field(value) = self {
Ok(value)
} else {
Err(CrunchyrollError::Input(CrunchyrollErrorContext::new(format!("collection is no '{}'", stringify!($generic)).to_string())))
}
}
}
)*
}
}
impl_try_into_media! {
Series = Series
Season = Season
Episode = Episode
MovieListing = MovieListing
Movie = Movie
}
macro_rules! impl_from_media {
($($generic:path = $enum_field:ident)*) => {
$(
impl From<Media<$generic>> for MediaCollection {
fn from(value: Media<$generic>) -> Self {
Self::$enum_field(value)
}
}
)*
}
}
impl_from_media! {
Series = Series
Season = Season
Episode = Episode
MovieListing = MovieListing
Movie = Movie
}
#[derive(Clone, Debug, Default, Deserialize)]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
pub struct MediaImages {
pub thumbnail: Option<Vec<Vec<Image>>>,
pub poster_tall: Option<Vec<Vec<Image>>>,
pub poster_wide: Option<Vec<Vec<Image>>>,
pub promo_image: Option<Vec<Vec<Image>>>,
}
#[allow(dead_code)]
#[derive(Clone, Debug, Deserialize, smart_default::SmartDefault)]
#[serde(bound = "M: Video")]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
pub struct Media<M: Video> {
#[serde(skip)]
pub(crate) executor: Arc<Executor>,
pub id: String,
#[serde(rename = "__links__")]
#[serde(default)]
#[serde(deserialize_with = "crate::internal::serde::deserialize_stream_id_option")]
pub stream_id: Option<String>,
#[serde(rename = "playback")]
pub playback_url: Option<String>,
pub external_id: String,
pub channel_id: String,
pub slug: String,
pub title: String,
pub slug_title: String,
pub promo_title: String,
pub description: String,
pub promo_description: String,
#[serde(alias = "series_metadata")]
#[serde(alias = "season_metadata")]
#[serde(alias = "episode_metadata")]
#[serde(alias = "movie_listing_metadata")]
#[serde(alias = "movie_metadata")]
pub metadata: M,
pub search_metadata: Option<SearchMetadata>,
pub images: Option<MediaImages>,
#[cfg(feature = "__test_strict")]
#[serde(alias = "type")]
pub(crate) collection_type: crate::StrictValue,
#[cfg(feature = "__test_strict")]
pub(crate) new: Option<crate::StrictValue>,
#[cfg(feature = "__test_strict")]
pub(crate) new_content: Option<crate::StrictValue>,
#[cfg(feature = "__test_strict")]
pub(crate) last_public: Option<crate::StrictValue>,
#[cfg(feature = "__test_strict")]
pub(crate) linked_resource_key: crate::StrictValue,
}
#[async_trait::async_trait]
impl<M: Video> Request for Media<M> {
async fn __set_executor(&mut self, executor: Arc<Executor>) {
self.executor = executor.clone();
#[cfg(feature = "experimental-stabilizations")]
M::__apply_fixes(executor, self).await;
}
}
impl<M: Video> PartialEq for Media<M> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl<M: Video> Eq for Media<M> {}
impl<M: Video> Media<M> {
pub async fn from_id(crunchy: &Crunchyroll, id: String) -> Result<Media<M>> {
let endpoint = format!(
"https://www.crunchyroll.com/cms/v2/{}/objects/{}",
crunchy.executor.details.bucket, &id
);
let result: BulkResult<Media<M>> = crunchy
.executor
.get(endpoint)
.apply_media_query()
.apply_locale_query()
.request()
.await?;
if result.items.is_empty() {
Err(CrunchyrollError::Input(
format!("no media could be found for id '{}'", id).into(),
))
} else if result.items.len() >= 2 {
Err(CrunchyrollError::Internal(format!("multiple media were found for id '{}'. this shouldn't happen, please report it immediately!", id).into()))
} else {
Ok(result.items.into_iter().next().unwrap())
}
}
pub async fn playback(&self) -> Result<PlaybackStream> {
if let Some(playback_url) = &self.playback_url {
self.executor.get(playback_url).request().await
} else {
Err(CrunchyrollError::Request("no playback id available".into()))
}
}
}
impl Media<Series> {
pub async fn seasons(&self) -> Result<Vec<Media<Season>>> {
Media::<Season>::from_series_id(
&Crunchyroll {
executor: self.executor.clone(),
},
self.id.clone(),
)
.await
}
}
impl Media<Season> {
pub async fn from_series_id(
crunchy: &Crunchyroll,
series_id: String,
) -> Result<Vec<Media<Season>>> {
let endpoint = format!(
"https://www.crunchyroll.com/cms/v2/{}/seasons",
crunchy.executor.details.bucket
);
let result: BulkResult<OldSeason> = crunchy
.executor
.get(endpoint)
.query(&[("series_id", series_id)])
.apply_media_query()
.apply_locale_query()
.request()
.await?;
Ok(result.items.into_iter().map(|i| i.into()).collect())
}
pub async fn episodes(&self) -> Result<Vec<Media<Episode>>> {
Media::<Episode>::from_season_id(
&Crunchyroll {
executor: self.executor.clone(),
},
self.id.clone(),
)
.await
}
}
impl Media<Episode> {
pub async fn from_season_id(
crunchy: &Crunchyroll,
season_id: String,
) -> Result<Vec<Media<Episode>>> {
let endpoint = format!(
"https://www.crunchyroll.com/cms/v2/{}/episodes",
crunchy.executor.details.bucket
);
let result: BulkResult<OldEpisode> = crunchy
.executor
.get(endpoint)
.query(&[("season_id", season_id)])
.apply_media_query()
.apply_locale_query()
.request()
.await?;
Ok(result.items.into_iter().map(|i| i.into()).collect())
}
pub async fn season(&self) -> Result<Media<Season>> {
Season::from_id(
&Crunchyroll {
executor: self.executor.clone(),
},
self.metadata.season_id.clone(),
)
.await
}
pub async fn series(&self) -> Result<Media<Series>> {
Series::from_id(
&Crunchyroll {
executor: self.executor.clone(),
},
self.metadata.series_id.clone(),
)
.await
}
}
impl Media<MovieListing> {
pub async fn movies(&self) -> Result<Vec<Media<Movie>>> {
Media::<Movie>::from_movie_listing_id(
&Crunchyroll {
executor: self.executor.clone(),
},
self.id.clone(),
)
.await
}
}
impl Media<Movie> {
pub async fn from_movie_listing_id(
crunchy: &Crunchyroll,
movie_listing_id: String,
) -> Result<Vec<Media<Movie>>> {
let endpoint = format!(
"https://www.crunchyroll.com/cms/v2/{}/movies",
crunchy.executor.details.bucket
);
let result: BulkResult<OldMovie> = crunchy
.executor
.get(endpoint)
.query(&[("movie_listing_id", movie_listing_id)])
.apply_media_query()
.apply_locale_query()
.request()
.await?;
Ok(result.items.into_iter().map(|i| i.into()).collect())
}
pub async fn movie_listing(&self) -> Result<Media<MovieListing>> {
MovieListing::from_id(
&Crunchyroll {
executor: self.executor.clone(),
},
self.metadata.movie_listing_id.clone(),
)
.await
}
}
impl Crunchyroll {
pub async fn media_from_id<M: Video>(&self, id: impl AsRef<str>) -> Result<Media<M>> {
Media::from_id(self, id.as_ref().to_string()).await
}
pub async fn media_collection_from_id<S: AsRef<str>>(&self, id: S) -> Result<MediaCollection> {
MediaCollection::from_id(self, id.as_ref().to_string()).await
}
}
options! {
SimilarOptions;
limit(u32, "n") = Some(20),
start(u32, "start") = None
}
macro_rules! impl_from_id {
($($media:ident)*) => {
$(
impl $media {
pub async fn from_id(crunchy: &Crunchyroll, id: String) -> Result<Media<$media>> {
Media::from_id(crunchy, id).await
}
}
)*
}
}
impl_from_id! {
Series Season Episode
MovieListing Movie
}
macro_rules! impl_media_video_collection {
($($media_video:ident)*) => {
$(
impl Media<$media_video> {
pub async fn similar(&self, options: SimilarOptions) -> Result<BulkResult<MediaCollection>> {
let endpoint = format!("https://www.crunchyroll.com/content/v1/{}/similar_to", self.executor.details.account_id.clone()?);
self.executor.get(endpoint)
.query(&[("guid", &self.id)])
.query(&options.into_query())
.apply_locale_query()
.request()
.await
}
}
)*
}
}
impl_media_video_collection! {
Series MovieListing
}
#[allow(dead_code)]
#[derive(Clone, Debug, Deserialize, smart_default::SmartDefault, Request)]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
struct VideoIntroResult {
media_id: String,
#[serde(rename = "startTime")]
start_time: f64,
#[serde(rename = "endTime")]
end_time: f64,
duration: f64,
#[serde(rename = "comparedWith")]
compared_with: String,
ordering: String,
#[default(DateTime::<Utc>::from(std::time::SystemTime::UNIX_EPOCH))]
last_updated: DateTime<Utc>,
}
#[derive(Clone, Debug, Default, Deserialize, Request)]
#[serde(bound = "M: Video")]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
pub struct RelatedMedia<M: Video> {
pub fully_watched: bool,
pub never_watched: bool,
pub playhead: u32,
#[serde(alias = "episode_metadata")]
#[serde(alias = "movie_metadata")]
pub panel: Media<M>,
}
#[allow(dead_code)]
#[derive(Clone, Debug, Deserialize, smart_default::SmartDefault, Request)]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
pub struct PlayheadInformation {
playhead: u32,
content_id: String,
fully_watched: bool,
#[default(DateTime::<Utc>::from(std::time::SystemTime::UNIX_EPOCH))]
last_modified: DateTime<Utc>,
}
macro_rules! impl_media_video {
($($media_video:ident)*) => {
$(
impl Media<$media_video> {
pub async fn streams(&self) -> Result<VideoStream> {
let endpoint = format!(
"https://www.crunchyroll.com/cms/v2/{}/videos/{}/streams",
self.executor.details.bucket, self.stream_id.as_ref().unwrap_or(&self.id)
);
self.executor.get(endpoint)
.apply_media_query()
.apply_locale_query()
.request()
.await
}
pub async fn available(&self) -> bool {
self.executor.details.premium || !self.metadata.is_premium_only
}
pub async fn intro(&self) -> Result<Option<(f64, f64)>> {
let endpoint = format!(
"https://static.crunchyroll.com/datalab-intro-v2/{}.json",
self.id
);
let raw_result = self.executor.get(endpoint)
.request_raw()
.await?;
let result = String::from_utf8_lossy(raw_result.as_slice());
if result.contains("</Error>") {
Ok(None)
} else {
let video_intro_result: VideoIntroResult = serde_json::from_str(&result)?;
Ok(Some((video_intro_result.start_time, video_intro_result.end_time)))
}
}
pub async fn previous(&self) -> Result<Option<RelatedMedia<$media_video>>> {
let endpoint = format!("https://www.crunchyroll.com/content/v1/previous_episode/{}", &self.id);
let result: serde_json::Value = self.executor.get(endpoint)
.apply_locale_query()
.request()
.await?;
let as_map: serde_json::Map<String, serde_json::Value> = serde_json::from_value(result.clone())?;
if as_map.is_empty() {
Ok(None)
} else {
Ok(Some(serde_json::from_value(result)?))
}
}
pub async fn next(&self) -> Result<Option<RelatedMedia<$media_video>>> {
let endpoint = format!("https://www.crunchyroll.com/content/v1/up_next_episode");
let result: serde_json::Value = self.executor.get(endpoint)
.query(&[("episode_id", &self.id)])
.apply_locale_query()
.request()
.await?;
let as_map: serde_json::Map<String, serde_json::Value> = serde_json::from_value(result.clone())?;
if as_map.is_empty() {
Ok(None)
} else {
Ok(Some(serde_json::from_value(result)?))
}
}
pub async fn playhead(&self) -> Result<Option<PlayheadInformation>> {
let endpoint = format!("https://www.crunchyroll.com/content/v1/playheads/{}/{}", self.executor.details.account_id.clone()?, &self.id);
let result: serde_json::Value = self.executor.get(endpoint)
.apply_locale_query()
.request()
.await?;
let as_map: serde_json::Map<String, serde_json::Value> = serde_json::from_value(result.clone())?;
if as_map.is_empty() {
Ok(None)
} else {
Ok(Some(serde_json::from_value(as_map.get(&self.id).unwrap().clone())?))
}
}
pub async fn set_playhead(&self, position: u32) -> Result<()> {
let endpoint = format!("https://www.crunchyroll.com/content/v1/playheads/{}", self.executor.details.account_id.clone()?);
self.executor.post(endpoint)
.apply_locale_query()
.json(&serde_json::json!({"content_id": &self.id, "playhead": position}))
.request::<crate::EmptyJsonProxy>()
.await?;
Ok(())
}
}
)*
}
}
impl_media_video! {
Episode Movie
}