use crate::error::CrunchyrollError;
use crate::{Crunchyroll, Executor, Locale, Request, Result};
use serde::de::{DeserializeOwned, Error};
use serde::{Deserialize, Deserializer};
use serde_json::Value;
use std::collections::HashMap;
use std::fmt::Debug;
use std::io::Write;
use std::sync::Arc;
trait FixStream: DeserializeOwned {
type Variant: DeserializeOwned;
}
fn deserialize_streams<'de, D: Deserializer<'de>, T: FixStream>(
deserializer: D,
) -> Result<HashMap<Locale, T>, D::Error> {
let as_map: HashMap<String, HashMap<Locale, Value>> = HashMap::deserialize(deserializer)?;
let mut raw: HashMap<Locale, HashMap<String, Value>> = HashMap::new();
for (key, value) in as_map {
for (mut locale, data) in value {
if locale == Locale::Custom(":".to_string()) {
locale = Locale::Custom("".to_string());
}
if let Err(e) = T::Variant::deserialize(&data) {
return Err(Error::custom(e.to_string()));
}
if let Some(entry) = raw.get_mut(&locale) {
entry.insert(key.clone(), data.clone());
} else {
raw.insert(locale, HashMap::from([(key.clone(), data)]));
}
}
}
let as_value = serde_json::to_value(raw).map_err(|e| Error::custom(e.to_string()))?;
serde_json::from_value(as_value).map_err(|e| Error::custom(e.to_string()))
}
#[allow(dead_code)]
#[derive(Clone, Debug, Deserialize, smart_default::SmartDefault, Request)]
#[request(executor(subtitles))]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
pub struct VideoStream {
#[serde(skip)]
pub(crate) executor: Arc<Executor>,
pub media_id: String,
pub audio_locale: Locale,
pub subtitles: HashMap<Locale, StreamSubtitle>,
#[serde(rename = "streams")]
#[serde(deserialize_with = "deserialize_streams")]
#[cfg_attr(not(feature = "__test_strict"), default(HashMap::new()))]
pub variants: HashMap<Locale, VideoVariants>,
#[cfg(feature = "__test_strict")]
captions: crate::StrictValue,
#[cfg(feature = "__test_strict")]
bifs: crate::StrictValue,
#[cfg(feature = "__test_strict")]
versions: crate::StrictValue,
}
impl VideoStream {
pub async fn from_id(crunchy: &Crunchyroll, id: String) -> Result<Self> {
let endpoint = format!(
"https://beta.crunchyroll.com/cms/v2/{}/videos/{}/streams",
crunchy.executor.details.bucket, id
);
crunchy
.executor
.get(endpoint)
.apply_media_query()
.apply_locale_query()
.request()
.await
}
}
#[allow(dead_code)]
#[derive(Clone, Debug, Deserialize, smart_default::SmartDefault, Request)]
#[request(executor(subtitles))]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
pub struct PlaybackStream {
#[serde(skip)]
pub(crate) executor: Arc<Executor>,
pub audio_locale: Locale,
pub subtitles: HashMap<Locale, StreamSubtitle>,
#[serde(rename = "streams")]
#[serde(deserialize_with = "deserialize_streams")]
#[default(HashMap::new())]
pub variants: HashMap<Locale, PlaybackVariants>,
#[cfg(feature = "__test_strict")]
#[serde(rename = "QoS")]
qos: crate::StrictValue,
}
#[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 StreamSubtitle {
#[serde(skip)]
executor: Arc<Executor>,
pub locale: Locale,
pub url: String,
pub format: String,
}
impl StreamSubtitle {
pub async fn write_to(self, w: &mut impl Write) -> Result<()> {
let resp = self.executor.client.get(self.url).send().await?;
let body = resp.bytes().await?;
w.write_all(body.as_ref())
.map_err(|e| CrunchyrollError::Input(e.to_string().into()))?;
Ok(())
}
}
#[derive(Clone, Debug, Default, Deserialize)]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
pub struct VideoVariant {
pub hardsub_locale: Locale,
pub url: String,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
pub struct PlaybackVariant {
pub hardsub_locale: Locale,
pub url: String,
pub vcodec: String,
}
#[allow(dead_code)]
#[derive(Clone, Debug, Default, Deserialize)]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
pub struct VideoVariants {
pub adaptive_dash: Option<VideoVariant>,
pub adaptive_hls: Option<VideoVariant>,
pub download_dash: Option<VideoVariant>,
pub download_hls: Option<VideoVariant>,
pub drm_adaptive_dash: Option<VideoVariant>,
pub drm_adaptive_hls: Option<VideoVariant>,
pub drm_download_dash: Option<VideoVariant>,
pub drm_download_hls: Option<VideoVariant>,
pub drm_multitrack_adaptive_hls_v2: Option<VideoVariant>,
pub multitrack_adaptive_hls_v2: Option<VideoVariant>,
pub vo_adaptive_dash: Option<VideoVariant>,
pub vo_adaptive_hls: Option<VideoVariant>,
pub vo_drm_adaptive_dash: Option<VideoVariant>,
pub vo_drm_adaptive_hls: Option<VideoVariant>,
#[cfg(feature = "__test_strict")]
urls: Option<crate::StrictValue>,
}
impl FixStream for VideoVariants {
type Variant = VideoVariant;
}
#[derive(Clone, Debug, Default, Deserialize)]
#[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
#[cfg_attr(not(feature = "__test_strict"), serde(default))]
pub struct PlaybackVariants {
pub adaptive_dash: Option<PlaybackVariant>,
pub adaptive_hls: Option<PlaybackVariant>,
pub download_hls: Option<PlaybackVariant>,
pub drm_adaptive_dash: Option<PlaybackVariant>,
pub drm_adaptive_hls: Option<PlaybackVariant>,
pub drm_download_hls: Option<PlaybackVariant>,
pub trailer_dash: Option<PlaybackVariant>,
pub trailer_hls: Option<PlaybackVariant>,
pub vo_adaptive_dash: Option<PlaybackVariant>,
pub vo_adaptive_hls: Option<PlaybackVariant>,
pub vo_drm_adaptive_dash: Option<PlaybackVariant>,
pub vo_drm_adaptive_hls: Option<PlaybackVariant>,
}
impl FixStream for PlaybackVariants {
type Variant = PlaybackVariant;
}