rs-plugin-common-interfaces 0.34.0

Common description for plugin creation
Documentation
use crate::domain::{
    other_ids::OtherIds,
    rs_ids::{ApplyRsIds, RsIds},
    tools::rating_serializer,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Episode {
    pub serie: String,
    pub season: u32,
    pub number: u32,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub abs: Option<u32>,

    pub name: Option<String>,
    pub overview: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub alt: Option<Vec<String>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub airdate: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub duration: Option<u64>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub params: Option<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub imdb: Option<String>,
    pub slug: Option<String>,
    pub tmdb: Option<u64>,
    pub trakt: Option<u64>,
    pub tvdb: Option<u64>,
    pub otherids: Option<OtherIds>,

    #[serde(serialize_with = "rating_serializer")]
    pub imdb_rating: Option<f32>,
    pub imdb_votes: Option<u64>,
    #[serde(serialize_with = "rating_serializer")]
    pub trakt_rating: Option<f32>,
    pub trakt_votes: Option<u64>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub watched: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub progress: Option<u64>,

    #[serde(default)]
    pub modified: u64,
    #[serde(default)]
    pub added: u64,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub serie_name: Option<String>,
}

impl Episode {
    pub fn id(&self) -> String {
        format!("{}x{}x{}", self.serie, self.season, self.number)
    }
}

impl From<Episode> for RsIds {
    fn from(value: Episode) -> Self {
        let id = value.id();
        let mut ids = RsIds::default();
        if let Some(v) = value.trakt { ids.set("trakt", v); }
        if let Some(v) = value.slug { ids.set("slug", v); }
        if let Some(v) = value.tvdb { ids.set("tvdb", v); }
        if let Some(v) = value.imdb { ids.set("imdb", v); }
        if let Some(v) = value.tmdb { ids.set("tmdb", v); }
        if let Some(other) = value.otherids {
            for entry in other.into_vec() {
                let _ = ids.try_add(entry);
            }
        }
        if ids.try_add(id.clone()).is_err() {
            ids.set("redseat", id);
        }
        ids
    }
}

impl ApplyRsIds for Episode {
    fn apply_rs_ids(&mut self, ids: &RsIds) {
        if let Some(trakt) = ids.trakt() {
            self.trakt = Some(trakt);
        }
        if let Some(slug) = ids.slug() {
            self.slug = Some(slug.to_string());
        }
        if let Some(tvdb) = ids.tvdb() {
            self.tvdb = Some(tvdb);
        }
        if let Some(imdb) = ids.imdb() {
            self.imdb = Some(imdb.to_string());
        }
        if let Some(tmdb) = ids.tmdb() {
            self.tmdb = Some(tmdb);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::Episode;
    use crate::domain::{
        other_ids::OtherIds,
        rs_ids::{ApplyRsIds, RsIds},
    };
    use serde_json::json;

    #[test]
    fn episode_otherids_serializes_as_array_and_rejects_string() {
        let episode = Episode {
            serie: "serie-1".to_string(),
            season: 1,
            number: 1,
            otherids: Some(OtherIds(vec!["tvmaze:ep-1".to_string()])),
            ..Default::default()
        };
        let value = serde_json::to_value(&episode).unwrap();
        assert_eq!(value.get("otherids"), Some(&json!(["tvmaze:ep-1"])));

        let parsed: Episode = serde_json::from_value(json!({
            "serie": "serie-1",
            "season": 1,
            "number": 1,
            "otherids": ["foo:bar"]
        }))
        .unwrap();
        assert_eq!(parsed.otherids, Some(OtherIds(vec!["foo:bar".to_string()])));

        let invalid = serde_json::from_value::<Episode>(json!({
            "serie": "serie-1",
            "season": 1,
            "number": 1,
            "otherids": "foo:bar"
        }));
        assert!(invalid.is_err());
    }

    #[test]
    fn episode_apply_rs_ids_updates_only_present_values() {
        let mut episode = Episode {
            serie: "serie".to_string(),
            season: 1,
            number: 1,
            tmdb: Some(22),
            ..Default::default()
        };
        let mut ids = RsIds::default();
        ids.set("trakt", 55u64);
        ids.set("tvdb", 66u64);
        ids.set("imdb", "tt0000066");
        ids.set("slug", "episode-slug");

        episode.apply_rs_ids(&ids);

        assert_eq!(episode.trakt, Some(55));
        assert_eq!(episode.tvdb, Some(66));
        assert_eq!(episode.imdb.as_deref(), Some("tt0000066"));
        assert_eq!(episode.slug.as_deref(), Some("episode-slug"));
        assert_eq!(episode.tmdb, Some(22));
    }
}