Skip to main content

rs_plugin_common_interfaces/domain/
episode.rs

1use crate::domain::{
2    other_ids::OtherIds,
3    rs_ids::{ApplyRsIds, RsIds},
4    tools::rating_serializer,
5};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
10#[serde(rename_all = "camelCase")]
11pub struct Episode {
12    pub serie: String,
13    pub season: u32,
14    pub number: u32,
15
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub abs: Option<u32>,
18
19    pub name: Option<String>,
20    pub overview: Option<String>,
21
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub alt: Option<Vec<String>>,
24
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub airdate: Option<i64>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub duration: Option<u64>,
29
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub params: Option<Value>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub imdb: Option<String>,
34    pub slug: Option<String>,
35    pub tmdb: Option<u64>,
36    pub trakt: Option<u64>,
37    pub tvdb: Option<u64>,
38    pub otherids: Option<OtherIds>,
39
40    #[serde(serialize_with = "rating_serializer")]
41    pub imdb_rating: Option<f32>,
42    pub imdb_votes: Option<u64>,
43    #[serde(serialize_with = "rating_serializer")]
44    pub trakt_rating: Option<f32>,
45    pub trakt_votes: Option<u64>,
46
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub watched: Option<i64>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub progress: Option<u64>,
51
52    #[serde(default)]
53    pub modified: u64,
54    #[serde(default)]
55    pub added: u64,
56
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub serie_name: Option<String>,
59}
60
61impl Episode {
62    pub fn id(&self) -> String {
63        format!("{}x{}x{}", self.serie, self.season, self.number)
64    }
65}
66
67impl From<Episode> for RsIds {
68    fn from(value: Episode) -> Self {
69        let id = value.id();
70        let mut ids = RsIds::default();
71        if let Some(v) = value.trakt { ids.set("trakt", v); }
72        if let Some(v) = value.slug { ids.set("slug", v); }
73        if let Some(v) = value.tvdb { ids.set("tvdb", v); }
74        if let Some(v) = value.imdb { ids.set("imdb", v); }
75        if let Some(v) = value.tmdb { ids.set("tmdb", v); }
76        if let Some(other) = value.otherids {
77            for entry in other.into_vec() {
78                let _ = ids.try_add(entry);
79            }
80        }
81        if ids.try_add(id.clone()).is_err() {
82            ids.set("redseat", id);
83        }
84        ids
85    }
86}
87
88impl ApplyRsIds for Episode {
89    fn apply_rs_ids(&mut self, ids: &RsIds) {
90        if let Some(trakt) = ids.trakt() {
91            self.trakt = Some(trakt);
92        }
93        if let Some(slug) = ids.slug() {
94            self.slug = Some(slug.to_string());
95        }
96        if let Some(tvdb) = ids.tvdb() {
97            self.tvdb = Some(tvdb);
98        }
99        if let Some(imdb) = ids.imdb() {
100            self.imdb = Some(imdb.to_string());
101        }
102        if let Some(tmdb) = ids.tmdb() {
103            self.tmdb = Some(tmdb);
104        }
105        let known: &[&str] = &["trakt", "slug", "tvdb", "imdb", "tmdb"];
106        let mut other = self.otherids.take().unwrap_or_default();
107        for (k, v) in ids.iter() {
108            if !known.contains(&k.as_str()) { other.add(k, v); }
109        }
110        if !other.as_slice().is_empty() { self.otherids = Some(other); }
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::Episode;
117    use crate::domain::{
118        other_ids::OtherIds,
119        rs_ids::{ApplyRsIds, RsIds},
120    };
121    use serde_json::json;
122
123    #[test]
124    fn episode_otherids_serializes_as_array_and_rejects_string() {
125        let episode = Episode {
126            serie: "serie-1".to_string(),
127            season: 1,
128            number: 1,
129            otherids: Some(OtherIds(vec!["tvmaze:ep-1".to_string()])),
130            ..Default::default()
131        };
132        let value = serde_json::to_value(&episode).unwrap();
133        assert_eq!(value.get("otherids"), Some(&json!(["tvmaze:ep-1"])));
134
135        let parsed: Episode = serde_json::from_value(json!({
136            "serie": "serie-1",
137            "season": 1,
138            "number": 1,
139            "otherids": ["foo:bar"]
140        }))
141        .unwrap();
142        assert_eq!(parsed.otherids, Some(OtherIds(vec!["foo:bar".to_string()])));
143
144        let invalid = serde_json::from_value::<Episode>(json!({
145            "serie": "serie-1",
146            "season": 1,
147            "number": 1,
148            "otherids": "foo:bar"
149        }));
150        assert!(invalid.is_err());
151    }
152
153    #[test]
154    fn episode_apply_rs_ids_updates_only_present_values() {
155        let mut episode = Episode {
156            serie: "serie".to_string(),
157            season: 1,
158            number: 1,
159            tmdb: Some(22),
160            ..Default::default()
161        };
162        let mut ids = RsIds::default();
163        ids.set("trakt", 55u64);
164        ids.set("tvdb", 66u64);
165        ids.set("imdb", "tt0000066");
166        ids.set("slug", "episode-slug");
167
168        episode.apply_rs_ids(&ids);
169
170        assert_eq!(episode.trakt, Some(55));
171        assert_eq!(episode.tvdb, Some(66));
172        assert_eq!(episode.imdb.as_deref(), Some("tt0000066"));
173        assert_eq!(episode.slug.as_deref(), Some("episode-slug"));
174        assert_eq!(episode.tmdb, Some(22));
175    }
176}