Skip to content

Commit

Permalink
Merge pull request #680 from Stremio/chore/addon-response-null-support
Browse files Browse the repository at this point in the history
chore: Default fields for resources on `null`
  • Loading branch information
elpiel authored May 7, 2024
2 parents edfda69 + 6cafc85 commit 8d882f5
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 103 deletions.
73 changes: 48 additions & 25 deletions src/types/addon/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,28 +86,38 @@ pub struct ResourceResponseCache {
#[serde(untagged)]
#[serde_as]
pub enum ResourceResponse {
/// [`MetaItemPreview`] response without the `videos` key of an item
///
/// # Examples
///
/// ```
/// use stremio_core::types::addon::ResourceResponse;
///
/// let null_metas = serde_json::json!({ "metas": null });
/// let null_metas = serde_json::from_value::<ResourceResponse>(null_metas).expect("Should be valid");
///
/// match null_metas {
/// ResourceResponse::Metas { metas } => assert!(metas.is_empty()),
/// _ => panic!("Whoops, wrong variant!"),
/// }
/// ```
Metas {
#[serde_as(as = "VecSkipError<_>")]
metas: Vec<MetaItemPreview>,
},
#[serde(rename_all = "camelCase")]
MetasDetailed {
#[serde_as(as = "VecSkipError<_>")]
metas_detailed: Vec<MetaItem>,
},
Meta {
meta: MetaItem,
},
Streams {
#[serde_as(as = "VecSkipError<_>")]
streams: Vec<Stream>,
},
Subtitles {
#[serde_as(as = "VecSkipError<_>")]
subtitles: Vec<Subtitles>,
},
Addons {
#[serde_as(as = "VecSkipError<_>")]
addons: Vec<DescriptorPreview>,
},
}
Expand Down Expand Up @@ -162,36 +172,49 @@ impl<'de> Deserialize<'de> for ResourceResponse {
}

if let Some(value) = value.get_mut("metas") {
let skip = serde_json::from_value::<SkipError<_>>(value.take())
.map_err(serde::de::Error::custom)?;

Ok(ResourceResponse::Metas { metas: skip.0 })
Ok(ResourceResponse::Metas {
metas: match value.take() {
serde_json::Value::Null => Ok(vec![]),
value => serde_json::from_value::<SkipError<_>>(value).map(|skip| skip.0),
}
.map_err(serde::de::Error::custom)?,
})
} else if let Some(value) = value.get_mut("metasDetailed") {
let skip = serde_json::from_value::<SkipError<_>>(value.take())
.map_err(serde::de::Error::custom)?;

Ok(ResourceResponse::MetasDetailed {
metas_detailed: skip.0,
metas_detailed: match value.take() {
serde_json::Value::Null => Ok(vec![]),
value => serde_json::from_value::<SkipError<_>>(value).map(|skip| skip.0),
}
.map_err(serde::de::Error::custom)?,
})
} else if let Some(value) = value.get_mut("meta") {
Ok(ResourceResponse::Meta {
meta: serde_json::from_value(value.take()).map_err(serde::de::Error::custom)?,
})
} else if let Some(value) = value.get_mut("streams") {
let skip = serde_json::from_value::<SkipError<_>>(value.take())
.map_err(serde::de::Error::custom)?;

Ok(ResourceResponse::Streams { streams: skip.0 })
Ok(ResourceResponse::Streams {
streams: match value.take() {
serde_json::Value::Null => Ok(vec![]),
value => serde_json::from_value::<SkipError<_>>(value).map(|skip| skip.0),
}
.map_err(serde::de::Error::custom)?,
})
} else if let Some(value) = value.get_mut("subtitles") {
let skip = serde_json::from_value::<SkipError<_>>(value.take())
.map_err(serde::de::Error::custom)?;

Ok(ResourceResponse::Subtitles { subtitles: skip.0 })
Ok(ResourceResponse::Subtitles {
subtitles: match value.take() {
serde_json::Value::Null => Ok(vec![]),
value => serde_json::from_value::<SkipError<_>>(value).map(|skip| skip.0),
}
.map_err(serde::de::Error::custom)?,
})
} else if let Some(value) = value.get_mut("addons") {
let skip = serde_json::from_value::<SkipError<_>>(value.take())
.map_err(serde::de::Error::custom)?;

Ok(ResourceResponse::Addons { addons: skip.0 })
Ok(ResourceResponse::Addons {
addons: match value.take() {
serde_json::Value::Null => Ok(vec![]),
value => serde_json::from_value::<SkipError<_>>(value).map(|skip| skip.0),
}
.map_err(serde::de::Error::custom)?,
})
} else {
// we should never get to this else, as we already check for missing required key
// but we're leaving it to remove the danger of a developer forgetting to add a new key to the list.
Expand Down
97 changes: 74 additions & 23 deletions src/types/resource/meta_item.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
use crate::constants::{
CATALOG_RESOURCE_NAME, CINEMETA_TOP_CATALOG_ID, CINEMETA_URL, GENRES_LINK_CATEGORY,
IMDB_LINK_CATEGORY, IMDB_TITLE_PATH, IMDB_URL, URI_COMPONENT_ENCODE_SET,
};
use crate::deep_links::DiscoverDeepLinks;
use crate::types::addon::{ExtraValue, ResourcePath, ResourceRequest};
use crate::types::resource::{Stream, StreamSource};
use crate::types::{NumberAsString, SortedVec, SortedVecAdapter, UniqueVec, UniqueVecAdapter};
use chrono::{DateTime, Utc};
use core::cmp::Ordering;
use derivative::Derivative;

use std::borrow::Cow;
use std::collections::HashMap;

use chrono::{DateTime, Utc};
use either::Either;
use itertools::Itertools;
use percent_encoding::utf8_percent_encode;
use serde::{Deserialize, Serialize};
use serde_with::formats::PreferMany;
use serde_with::{
serde_as, DefaultOnNull, DeserializeAs, NoneAsEmptyString, OneOrMany, PickFirst,
TimestampMilliSeconds,
formats::PreferMany, serde_as, DefaultOnNull, DeserializeAs, NoneAsEmptyString, OneOrMany,
PickFirst, TimestampMilliSeconds,
};
use std::borrow::Cow;
use std::collections::HashMap;
use url::Url;

use crate::constants::{
CATALOG_RESOURCE_NAME, CINEMETA_TOP_CATALOG_ID, CINEMETA_URL, GENRES_LINK_CATEGORY,
IMDB_LINK_CATEGORY, IMDB_TITLE_PATH, IMDB_URL, URI_COMPONENT_ENCODE_SET,
};
use crate::deep_links::DiscoverDeepLinks;
use crate::types::addon::{ExtraValue, ResourcePath, ResourceRequest};
use crate::types::resource::{Stream, StreamSource};
use crate::types::{NumberAsString, SortedVec, SortedVecAdapter, UniqueVec, UniqueVecAdapter};

/// The [`MetaItem`] Id type to improve the readability of the code.
///
/// For example when using the id as key in a [`HashMap`].
Expand Down Expand Up @@ -58,15 +59,16 @@ struct MetaItemPreviewLegacy {
id: MetaItemId,
r#type: String,
#[serde(default)]
#[serde_as(as = "DefaultOnNull")]
name: String,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnNull<NoneAsEmptyString>")]
#[serde_as(as = "DefaultOnNull<NoneAsEmptyString>")]
poster: Option<Url>,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnNull<NoneAsEmptyString>")]
#[serde_as(as = "DefaultOnNull<NoneAsEmptyString>")]
background: Option<Url>,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnNull<NoneAsEmptyString>")]
#[serde_as(as = "DefaultOnNull<NoneAsEmptyString>")]
logo: Option<Url>,
description: Option<String>,
#[serde(default)]
Expand All @@ -79,22 +81,69 @@ struct MetaItemPreviewLegacy {
#[serde_as(deserialize_as = "PickFirst<(_, Option<TimestampMilliSeconds<i64>>)>")]
released: Option<DateTime<Utc>>,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnNull")]
poster_shape: PosterShape,
#[serde(default)]
#[serde_as(deserialize_as = "Option<NumberAsString>")]
imdb_rating: Option<String>,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnNull")]
genres: Vec<String>,
#[serde(default)]
links: Option<Vec<Link>>,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnNull<Vec<PickFirst<(_, Stream)>>>")]
trailers: Vec<Trailer>,
#[serde(default)]
trailer_streams: Option<Vec<Stream>>,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnNull")]
behavior_hints: MetaItemBehaviorHints,
}

///
/// ```
/// use stremio_core::types::resource::{MetaItemPreview, MetaItemBehaviorHints, PosterShape};
///
/// let expected = MetaItemPreview {
/// id: "tt:1111111".into(),
/// r#type: "movie".into(),
/// name: "Movie name 404".into(),
/// poster: None,
/// background: None,
/// logo: None,
/// description: None,
/// release_info: None,
/// runtime: None,
/// released: None,
/// poster_shape: PosterShape::default(),
/// links: vec![],
/// trailer_streams: vec![],
/// behavior_hints: MetaItemBehaviorHints::default(),
/// };
///
/// let null_fields = serde_json::json!({
/// "id": "tt:1111111",
/// "type": "movie",
/// "name": "Movie name 404",
/// "posterShape": null,
/// "links": null,
/// "trailers": null,
/// "trailerStreams": null,
/// "behaviorHints": null,
/// });
/// let meta_preview = serde_json::from_value::<MetaItemPreview>(null_fields).unwrap();
/// //assert_eq!(expected, meta_preview);
///
/// let no_fields = serde_json::json!({
/// "id": "tt:1111111",
/// "type": "movie",
/// "name": "Movie name 404",
/// });
///
/// let meta_preview = serde_json::from_value::<MetaItemPreview>(no_fields).unwrap();
/// assert_eq!(expected, meta_preview);
/// ```
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
#[cfg_attr(test, derive(Default))]
#[serde(rename_all = "camelCase", try_from = "MetaItemPreviewLegacy")]
Expand Down Expand Up @@ -199,7 +248,7 @@ pub struct MetaItem {
pub preview: MetaItemPreview,
#[serde(default)]
#[serde_as(
deserialize_as = "SortedVec<UniqueVec<Vec<_>, VideoUniqueVecAdapter>, VideoSortedVecAdapter>"
as = "DefaultOnNull<SortedVec<UniqueVec<Vec<_>, VideoUniqueVecAdapter>, VideoSortedVecAdapter>>"
)]
pub videos: Vec<Video>,
}
Expand All @@ -218,13 +267,12 @@ impl MetaItem {
}
}

#[derive(Derivative, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
#[derivative(Default)]
#[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub enum PosterShape {
Square,
Landscape,
#[derivative(Default)]
#[default]
#[serde(other)]
Poster,
}
Expand All @@ -249,13 +297,16 @@ pub struct Video {
pub id: String,
#[serde(default, alias = "name")]
pub title: String,
#[serde(default)]
pub released: Option<DateTime<Utc>>,
#[serde(default)]
pub overview: Option<String>,
#[serde(default)]
pub thumbnail: Option<String>,
#[serde(alias = "stream", default)]
#[serde_as(deserialize_as = "OneOrMany<_, PreferMany>")]
pub streams: Vec<Stream>,
#[serde(flatten)]
#[serde(default, flatten)]
pub series_info: Option<SeriesInfo>,
#[serde(default)]
pub trailer_streams: Vec<Stream>,
Expand Down
Loading

0 comments on commit 8d882f5

Please sign in to comment.