Skip to main content

rs_plugin_common_interfaces/domain/
media.rs

1use std::str::FromStr;
2
3use crate::{RsRequest, url::RsLink};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use strum_macros::EnumString;
7
8use crate::domain::backup::BackupFile;
9
10pub const DEFAULT_MIME: &str = "application/octet-stream";
11
12#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
13#[serde(rename_all = "camelCase")]
14pub struct FileEpisode {
15    pub id: String,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub season: Option<u32>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub episode: Option<u32>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub episode_to: Option<u32>,
22}
23
24impl FromStr for FileEpisode {
25    type Err = String;
26
27    fn from_str(s: &str) -> Result<Self, Self::Err> {
28        let splitted: Vec<&str> = s.split("|").collect();
29        if splitted.len() == 3 {
30            Ok(FileEpisode {
31                id: splitted[0].to_string(),
32                season: splitted[1].parse::<u32>().ok().and_then(|i| {
33                    if i == 0 {
34                        None
35                    } else {
36                        Some(i)
37                    }
38                }),
39                episode: splitted[2].parse::<u32>().ok().and_then(|i| {
40                    if i == 0 {
41                        None
42                    } else {
43                        Some(i)
44                    }
45                }),
46                episode_to: None,
47            })
48        } else if splitted.len() == 2 {
49            Ok(FileEpisode {
50                id: splitted[0].to_string(),
51                season: splitted[1].parse::<u32>().ok().and_then(|i| {
52                    if i == 0 {
53                        None
54                    } else {
55                        Some(i)
56                    }
57                }),
58                episode: None,
59                episode_to: None,
60            })
61        } else {
62            Ok(FileEpisode {
63                id: splitted[0].to_string(),
64                season: None,
65                episode: None,
66                episode_to: None,
67            })
68        }
69    }
70}
71
72#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
73pub struct MediaItemReference {
74    pub id: String,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub conf: Option<u16>,
77}
78
79impl FromStr for MediaItemReference {
80    type Err = String;
81
82    fn from_str(s: &str) -> Result<Self, Self::Err> {
83        let splitted: Vec<&str> = s.split('|').collect();
84        if splitted.len() == 2 {
85            Ok(MediaItemReference {
86                id: splitted[0].to_string(),
87                conf: splitted[1].parse::<u16>().ok().and_then(|e| {
88                    if e == 100 {
89                        None
90                    } else {
91                        Some(e)
92                    }
93                }),
94            })
95        } else {
96            Ok(MediaItemReference {
97                id: splitted[0].to_string(),
98                conf: None,
99            })
100        }
101    }
102}
103
104#[derive(
105    Debug, Serialize, Deserialize, Clone, PartialEq, strum_macros::Display, EnumString, Default,
106)]
107#[strum(serialize_all = "camelCase")]
108#[serde(rename_all = "camelCase")]
109pub enum FileType {
110    Directory,
111    Photo,
112    Video,
113    Archive,
114    Album,
115    Book,
116    #[default]
117    Other,
118}
119
120#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
121#[serde(rename_all = "camelCase")]
122pub struct Media {
123    pub id: String,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub source: Option<String>,
126    pub name: String,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub description: Option<String>,
129
130    #[serde(rename = "type")]
131    pub kind: FileType,
132    pub mimetype: String,
133    pub size: Option<u64>,
134
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub params: Option<Value>,
137
138    pub added: Option<i64>,
139    pub modified: Option<i64>,
140    pub created: Option<i64>,
141
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub rating: Option<f32>,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub avg_rating: Option<f32>,
146
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub md5: Option<String>,
149
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub width: Option<usize>,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub height: Option<usize>,
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub phash: Option<String>,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub thumbhash: Option<String>,
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub focal: Option<u64>,
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub iso: Option<u64>,
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub color_space: Option<String>,
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub icc: Option<String>,
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub mp: Option<u32>,
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub sspeed: Option<String>,
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub f_number: Option<f64>,
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub orientation: Option<usize>,
174
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub duration: Option<usize>,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub acodecs: Option<Vec<String>>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub achan: Option<Vec<usize>>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub vcodecs: Option<Vec<String>>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub fps: Option<f64>,
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub bitrate: Option<u64>,
187
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub long: Option<f64>,
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub lat: Option<f64>,
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub model: Option<String>,
194
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub pages: Option<usize>,
197
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub progress: Option<usize>,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub faces: Option<Vec<FaceEmbedding>>,
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub backups: Option<Vec<BackupFile>>,
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub thumb: Option<String>,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub thumbv: Option<usize>,
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub thumbsize: Option<u64>,
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub iv: Option<String>,
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub origin: Option<RsLink>,
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub lang: Option<String>,
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub uploader: Option<String>,
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub uploadkey: Option<String>,
220
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub original_hash: Option<String>,
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub original_id: Option<String>,
225
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub face_recognition_error: Option<String>,
228}
229
230impl Media {
231    pub fn max_date(&self) -> i64 {
232        *[
233            self.created.unwrap_or(0),
234            self.added.unwrap_or(0),
235            self.modified.unwrap_or(0),
236        ]
237        .iter()
238        .max()
239        .unwrap_or(&0)
240    }
241
242    pub fn bytes_size(&self) -> Option<u64> {
243        if self.iv.is_none() {
244            self.size
245        } else {
246            //16 Bytes to store IV
247            //4 to store encrypted thumb size = T (can be 0)
248            //4 to store encrypted Info size = I (can be 0)
249            //32 to store thumb mimetype
250            //256 to store file mimetype
251            //T Bytes for the encrypted thumb
252            //I Bytes for the encrypted info
253            if let Some(file_size) = self.size {
254                Some(file_size + 16 + 4 + 4 + 32 + 256 + self.thumbsize.unwrap_or(0) + 0)
255            } else {
256                None
257            }
258        }
259    }
260}
261
262#[derive(Debug, Serialize, Deserialize, Clone, Default)]
263#[serde(rename_all = "camelCase")]
264pub struct RsGpsPosition {
265    pub lat: f64,
266    pub long: f64,
267}
268
269#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
270#[serde(rename_all = "camelCase")]
271pub struct FaceEmbedding {
272    pub id: String,
273    pub embedding: Vec<f32>,
274    pub media_ref: Option<String>,
275    pub bbox: Option<FaceBBox>,
276    pub confidence: Option<f32>,
277    pub pose: Option<(f32, f32, f32)>,
278    pub person_id: Option<String>,
279}
280
281#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
282#[serde(rename_all = "camelCase")]
283pub struct FaceBBox {
284    pub x1: f32,
285    pub y1: f32,
286    pub x2: f32,
287    pub y2: f32,
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub video_s: Option<f32>, // Seconds in video where face was detected
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub video_percent: Option<u32>, // Percent (0-100) of video where face was detected
292}
293
294
295
296
297
298
299
300#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
301#[serde(rename_all = "camelCase")]
302pub struct MediaForUpdate {
303    pub name: Option<String>,
304    pub description: Option<String>,
305    pub mimetype: Option<String>,
306    pub kind: Option<FileType>,
307    pub size: Option<u64>,
308
309    pub md5: Option<String>,
310
311    pub modified: Option<i64>,
312    pub created: Option<i64>,
313
314    pub width: Option<u32>,
315    pub height: Option<u32>,
316    pub orientation: Option<u8>,
317    pub color_space: Option<String>,
318    pub icc: Option<String>,
319    pub mp: Option<u32>,
320    pub vcodecs: Option<Vec<String>>,
321    pub acodecs: Option<Vec<String>>,
322    pub fps: Option<f64>,
323    pub bitrate: Option<u64>,
324    pub focal: Option<u64>,
325    pub iso: Option<u64>,
326    pub model: Option<String>,
327    pub sspeed: Option<String>,
328    pub f_number: Option<f64>,
329
330    pub pages: Option<usize>,
331
332    pub duration: Option<u64>,
333
334    pub progress: Option<usize>,
335
336    pub add_tags: Option<Vec<MediaItemReference>>,
337    pub remove_tags: Option<Vec<String>>,
338    pub tags_lookup: Option<Vec<String>>,
339
340    pub add_series: Option<Vec<FileEpisode>>,
341    pub remove_series: Option<Vec<FileEpisode>>,
342    pub series_lookup: Option<Vec<String>>,
343    pub season: Option<u32>,
344    pub episode: Option<u32>,
345
346    pub add_people: Option<Vec<MediaItemReference>>,
347    pub remove_people: Option<Vec<String>>,
348    pub people_lookup: Option<Vec<String>>,
349
350    pub long: Option<f64>,
351    pub lat: Option<f64>,
352    pub gps: Option<String>,
353
354    pub origin: Option<RsLink>,
355    pub origin_url: Option<String>,
356    #[serde(default)]
357    pub ignore_origin_duplicate: bool,
358
359    pub movie: Option<String>,
360    pub book: Option<String>,
361
362    pub lang: Option<String>,
363
364    pub rating: Option<u16>,
365
366    pub thumbsize: Option<usize>,
367    pub iv: Option<String>,
368
369    pub uploader: Option<String>,
370    pub uploadkey: Option<String>,
371    pub upload_id: Option<String>,
372
373    pub original_hash: Option<String>,
374    pub original_id: Option<String>,
375}
376
377
378impl MediaForUpdate {
379    /// Merge `patch` into `self`.
380    ///
381    /// Semantics:
382    /// - For most `Option<T>` fields: overwrite only when `patch.field` is `Some`.
383    /// - For "list-like update" fields (`add_*`, `remove_*`, `*_lookup`): append vectors.
384    /// - For `ignore_origin_duplicate` (bool): OR (once true, stays true).
385    pub fn merge_from(&mut self, mut patch: Self) {
386        // Overwrite `dst` only if `src` is Some(..)
387        fn overwrite_if_some<T>(dst: &mut Option<T>, src: &mut Option<T>) {
388            if src.is_some() {
389                *dst = src.take();
390            }
391        }
392
393        // Append vectors when both sides are Some(vec).
394        // If `dst` is None, it becomes src (if any).
395        fn append_vec<T>(dst: &mut Option<Vec<T>>, src: &mut Option<Vec<T>>) {
396            match (dst.as_mut(), src.take()) {
397                (Some(d), Some(mut s)) => d.append(&mut s),
398                (None, Some(s)) => *dst = Some(s),
399                _ => {}
400            }
401        }
402
403        // ----- Scalar / metadata fields (overwrite-if-some) -----
404        overwrite_if_some(&mut self.name, &mut patch.name);
405        overwrite_if_some(&mut self.description, &mut patch.description);
406        overwrite_if_some(&mut self.mimetype, &mut patch.mimetype);
407        overwrite_if_some(&mut self.kind, &mut patch.kind);
408        overwrite_if_some(&mut self.size, &mut patch.size);
409
410        overwrite_if_some(&mut self.md5, &mut patch.md5);
411
412        overwrite_if_some(&mut self.modified, &mut patch.modified);
413        overwrite_if_some(&mut self.created, &mut patch.created);
414
415        overwrite_if_some(&mut self.width, &mut patch.width);
416        overwrite_if_some(&mut self.height, &mut patch.height);
417        overwrite_if_some(&mut self.orientation, &mut patch.orientation);
418        overwrite_if_some(&mut self.color_space, &mut patch.color_space);
419        overwrite_if_some(&mut self.icc, &mut patch.icc);
420        overwrite_if_some(&mut self.mp, &mut patch.mp);
421        overwrite_if_some(&mut self.vcodecs, &mut patch.vcodecs);
422        overwrite_if_some(&mut self.acodecs, &mut patch.acodecs);
423        overwrite_if_some(&mut self.fps, &mut patch.fps);
424        overwrite_if_some(&mut self.bitrate, &mut patch.bitrate);
425        overwrite_if_some(&mut self.focal, &mut patch.focal);
426        overwrite_if_some(&mut self.iso, &mut patch.iso);
427        overwrite_if_some(&mut self.model, &mut patch.model);
428        overwrite_if_some(&mut self.sspeed, &mut patch.sspeed);
429        overwrite_if_some(&mut self.f_number, &mut patch.f_number);
430
431        overwrite_if_some(&mut self.pages, &mut patch.pages);
432        overwrite_if_some(&mut self.duration, &mut patch.duration);
433        overwrite_if_some(&mut self.progress, &mut patch.progress);
434
435        overwrite_if_some(&mut self.season, &mut patch.season);
436        overwrite_if_some(&mut self.episode, &mut patch.episode);
437
438        overwrite_if_some(&mut self.long, &mut patch.long);
439        overwrite_if_some(&mut self.lat, &mut patch.lat);
440        overwrite_if_some(&mut self.gps, &mut patch.gps);
441
442        overwrite_if_some(&mut self.origin, &mut patch.origin);
443        overwrite_if_some(&mut self.origin_url, &mut patch.origin_url);
444
445        overwrite_if_some(&mut self.movie, &mut patch.movie);
446        overwrite_if_some(&mut self.book, &mut patch.book);
447
448        overwrite_if_some(&mut self.lang, &mut patch.lang);
449        overwrite_if_some(&mut self.rating, &mut patch.rating);
450
451        overwrite_if_some(&mut self.thumbsize, &mut patch.thumbsize);
452        overwrite_if_some(&mut self.iv, &mut patch.iv);
453
454        overwrite_if_some(&mut self.uploader, &mut patch.uploader);
455        overwrite_if_some(&mut self.uploadkey, &mut patch.uploadkey);
456        overwrite_if_some(&mut self.upload_id, &mut patch.upload_id);
457
458        overwrite_if_some(&mut self.original_hash, &mut patch.original_hash);
459        overwrite_if_some(&mut self.original_id, &mut patch.original_id);
460
461        // ----- List-like update fields (append) -----
462        append_vec(&mut self.add_tags, &mut patch.add_tags);
463        append_vec(&mut self.remove_tags, &mut patch.remove_tags);
464        append_vec(&mut self.tags_lookup, &mut patch.tags_lookup);
465
466        append_vec(&mut self.add_series, &mut patch.add_series);
467        append_vec(&mut self.remove_series, &mut patch.remove_series);
468        append_vec(&mut self.series_lookup, &mut patch.series_lookup);
469
470        append_vec(&mut self.add_people, &mut patch.add_people);
471        append_vec(&mut self.remove_people, &mut patch.remove_people);
472        append_vec(&mut self.people_lookup, &mut patch.people_lookup);
473
474        // ----- Non-Option field -----
475        self.ignore_origin_duplicate |= patch.ignore_origin_duplicate;
476    }
477
478    /// Convenience: returns a merged value without mutating the original.
479    pub fn merged(mut self, patch: Self) -> Self {
480        self.merge_from(patch);
481        self
482    }
483}
484
485
486impl From<Media> for MediaForUpdate {
487    fn from(value: Media) -> Self {
488        MediaForUpdate {
489            description: value.description,
490            add_people: None,
491            add_tags: None,
492            long: value.long,
493            lat: value.lat,
494            created: value.created,
495            origin: value.origin,
496            add_series: None,
497            pages: value.pages,
498            original_hash: value.original_hash.or(value.md5),
499            original_id: Some(value.original_id.unwrap_or(value.id)),
500            book: None,
501            ..Default::default()
502        }
503    }
504}
505
506impl From<RsRequest> for MediaForUpdate {
507    fn from(value: RsRequest) -> Self {
508        // Build add_series from albums (first entry) + season + episode if albums are provided
509        let (add_series, season, episode) = if let Some(albums) = &value.albums {
510            if let Some(serie_id) = albums.first() {
511                // Have direct serie ID - use add_series, don't need season/episode at top level
512                (
513                    Some(vec![FileEpisode {
514                        id: serie_id.clone(),
515                        season: value.season,
516                        episode: value.episode,
517                        episode_to: None,
518                    }]),
519                    None,
520                    None,
521                )
522            } else {
523                (None, value.season, value.episode)
524            }
525        } else {
526            // No direct album IDs - keep season/episode for series_lookup pairing
527            (None, value.season, value.episode)
528        };
529
530        MediaForUpdate {
531            name: value.filename_or_extract_from_url(),
532            description: value.description,
533            ignore_origin_duplicate: value.ignore_origin_duplicate,
534            size: value.size,
535            // Use the new lookup fields for database text search
536            people_lookup: value.people_lookup,
537            tags_lookup: value.tags_lookup,
538            series_lookup: value.albums_lookup,
539            add_series,
540            movie: value.movie,
541            season,
542            episode,
543            ..Default::default()
544        }
545    }
546}
547