rs_plugin_common_interfaces/domain/
book.rs1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::domain::{
5 media::MediaItemReference, other_ids::OtherIds, person::Person, rs_ids::{ApplyRsIds, RsIds}, tag::Tag
6};
7
8#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
9#[serde(rename_all = "camelCase")]
10pub struct Book {
11 #[serde(default)]
12 pub id: String,
13 pub name: String,
14 #[serde(skip_serializing_if = "Option::is_none")]
15 #[serde(rename = "type")]
16 pub kind: Option<String>,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub serie_ref: Option<String>,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub volume: Option<f64>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub chapter: Option<f64>,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub year: Option<u16>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub airdate: Option<i64>,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub overview: Option<String>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub pages: Option<u32>,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub params: Option<Value>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub lang: Option<String>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub original: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub isbn13: Option<String>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub openlibrary_edition_id: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub openlibrary_work_id: Option<String>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub google_books_volume_id: Option<String>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub asin: Option<String>,
47 pub otherids: Option<OtherIds>,
48 #[serde(default)]
49 pub modified: u64,
50 #[serde(default)]
51 pub added: u64,
52}
53
54impl From<Book> for RsIds {
55 fn from(value: Book) -> Self {
56 let mut ids = RsIds::default();
57 if let Some(v) = value.isbn13 { ids.set("isbn13", v); }
58 if let Some(v) = value.openlibrary_edition_id { ids.set("oleid", v); }
59 if let Some(v) = value.openlibrary_work_id { ids.set("olwid", v); }
60 if let Some(v) = value.google_books_volume_id { ids.set("gbvid", v); }
61 if let Some(v) = value.asin { ids.set("asin", v); }
62 if let Some(v) = value.volume { ids.set("volume", v); }
63 if let Some(v) = value.chapter { ids.set("chapter", v); }
64 if let Some(other) = value.otherids {
65 for entry in other.into_vec() {
66 let _ = ids.try_add(entry);
67 }
68 }
69 if ids.try_add(value.id.clone()).is_err() {
70 ids.set("redseat", value.id);
71 }
72 ids
73 }
74}
75
76impl ApplyRsIds for Book {
77 fn apply_rs_ids(&mut self, ids: &RsIds) {
78 if let Some(redseat) = ids.redseat() {
79 self.id = redseat.to_string();
80 }
81 if let Some(isbn13) = ids.isbn13() {
82 self.isbn13 = Some(isbn13.to_string());
83 }
84 if let Some(oleid) = ids.openlibrary_edition_id() {
85 self.openlibrary_edition_id = Some(oleid.to_string());
86 }
87 if let Some(olwid) = ids.openlibrary_work_id() {
88 self.openlibrary_work_id = Some(olwid.to_string());
89 }
90 if let Some(gbvid) = ids.google_books_volume_id() {
91 self.google_books_volume_id = Some(gbvid.to_string());
92 }
93 if let Some(asin) = ids.asin() {
94 self.asin = Some(asin.to_string());
95 }
96 if let Some(volume) = ids.find_detail_f64("volume") {
97 self.volume = Some(volume);
98 }
99 if let Some(chapter) = ids.find_detail_f64("chapter") {
100 self.chapter = Some(chapter);
101 }
102 let known: &[&str] = &["redseat", "isbn13", "oleid", "olwid", "gbvid", "asin"];
103 let mut other = self.otherids.take().unwrap_or_default();
104 for (k, v) in ids.iter() {
105 if !known.contains(&k.as_str()) { other.add(k, v); }
106 }
107 if !other.as_slice().is_empty() { self.otherids = Some(other); }
108 }
109}
110
111#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
112#[serde(rename_all = "camelCase")]
113pub struct BookForUpdate {
114 pub name: Option<String>,
115 #[serde(rename = "type")]
116 pub kind: Option<String>,
117 pub serie_ref: Option<String>,
118 pub volume: Option<f64>,
119 pub chapter: Option<f64>,
120 pub year: Option<u16>,
121 pub airdate: Option<i64>,
122 pub overview: Option<String>,
123 pub pages: Option<u32>,
124 pub params: Option<Value>,
125 pub lang: Option<String>,
126 pub original: Option<String>,
127 pub isbn13: Option<String>,
128 pub openlibrary_edition_id: Option<String>,
129 pub openlibrary_work_id: Option<String>,
130 pub google_books_volume_id: Option<String>,
131 pub asin: Option<String>,
132 pub otherids: Option<OtherIds>,
133
134
135 pub add_tags: Option<Vec<MediaItemReference>>,
136 pub remove_tags: Option<Vec<String>>,
137 pub tags_lookup: Option<Vec<Tag>>,
138
139 pub add_people: Option<Vec<MediaItemReference>>,
140 pub remove_people: Option<Vec<String>>,
141 pub people_lookup: Option<Vec<Person>>,
142}
143
144impl BookForUpdate {
145 pub fn has_update(&self) -> bool {
146 self != &BookForUpdate::default()
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::Book;
153 use crate::domain::{
154 other_ids::OtherIds,
155 rs_ids::{ApplyRsIds, RsIds},
156 };
157 use serde_json::json;
158
159 #[test]
160 fn book_otherids_serializes_as_array_and_rejects_string() {
161 let book = Book {
162 id: "book-1".to_string(),
163 name: "Book 1".to_string(),
164 otherids: Some(OtherIds(vec!["goodreads:321".to_string()])),
165 ..Default::default()
166 };
167 let value = serde_json::to_value(&book).unwrap();
168 assert_eq!(value.get("otherids"), Some(&json!(["goodreads:321"])));
169
170 let parsed: Book = serde_json::from_value(json!({
171 "id": "book-1",
172 "name": "Book 1",
173 "otherids": ["custom:1"]
174 }))
175 .unwrap();
176 assert_eq!(
177 parsed.otherids,
178 Some(OtherIds(vec!["custom:1".to_string()]))
179 );
180
181 let invalid = serde_json::from_value::<Book>(json!({
182 "id": "book-1",
183 "name": "Book 1",
184 "otherids": "custom:1"
185 }));
186 assert!(invalid.is_err());
187 }
188
189 #[test]
190 fn book_apply_rs_ids_updates_only_present_values() {
191 let mut book = Book {
192 id: "book-old".to_string(),
193 name: "Book 1".to_string(),
194 openlibrary_work_id: Some("olw-old".to_string()),
195 chapter: Some(7.0),
196 ..Default::default()
197 };
198 let mut ids = RsIds::default();
199 ids.set("redseat", "book-new");
200 ids.set("isbn13", "9783161484100");
201 ids.set("oleid", "ole-new");
202 ids.set("gbvid", "gb-1");
203 ids.set("asin", "B00TEST");
204 ids.set("volume", "3");
205
206 book.apply_rs_ids(&ids);
207
208 assert_eq!(book.id, "book-new");
209 assert_eq!(book.isbn13.as_deref(), Some("9783161484100"));
210 assert_eq!(book.openlibrary_edition_id.as_deref(), Some("ole-new"));
211 assert_eq!(book.openlibrary_work_id.as_deref(), Some("olw-old"));
212 assert_eq!(book.google_books_volume_id.as_deref(), Some("gb-1"));
213 assert_eq!(book.asin.as_deref(), Some("B00TEST"));
214 assert_eq!(book.volume, Some(3.0));
215 assert_eq!(book.chapter, Some(7.0));
216 }
217}