Skip to main content

ferrex_model/
library.rs

1use std::path::PathBuf;
2
3use crate::chrono::{DateTime, Utc};
4use crate::media::Media;
5
6use super::ids::{LibraryId, MovieReferenceBatchSize};
7
8/// Read-only operations for library-like types
9pub trait LibraryLike {
10    fn needs_scan(&self) -> bool;
11    fn get_id(&self) -> LibraryId;
12    fn get_name(&self) -> &str;
13    fn get_type(&self) -> LibraryType;
14    fn get_paths(&self) -> Vec<PathBuf>; // Returns owned for compatibility with archived types
15    fn get_scan_interval(&self) -> u32;
16    fn get_last_scan(&self) -> Option<DateTime<Utc>>;
17    fn is_enabled(&self) -> bool;
18    fn is_auto_scan(&self) -> bool;
19    fn is_watch_for_changes(&self) -> bool;
20    fn is_analyze_on_scan(&self) -> bool;
21    fn get_max_retry_attempts(&self) -> u32;
22    fn get_movie_ref_batch_size(&self) -> MovieReferenceBatchSize;
23    fn get_created_at(&self) -> DateTime<Utc>;
24    fn get_updated_at(&self) -> DateTime<Utc>;
25    fn get_media_references_clone(&self) -> Option<Vec<Media>>;
26}
27
28/// Mutable operations for library types (only implemented by owned types like Library)
29pub trait LibraryLikeMut: LibraryLike {
30    fn new(
31        name: String,
32        library_type: LibraryType,
33        paths: Vec<PathBuf>,
34    ) -> Self;
35    fn update_last_scan(&mut self);
36    fn set_paths(&mut self, paths: Vec<PathBuf>);
37    fn set_scan_interval(&mut self, interval: u32);
38    fn set_last_scan(&mut self, last_scan: Option<DateTime<Utc>>);
39    fn set_auto_scan(&mut self, auto_scan: bool);
40    fn set_max_retry_attempts(&mut self, max_retry_attempts: u32);
41    fn set_updated_at(&mut self, updated_at: Option<DateTime<Utc>>);
42    fn set_media_references(&mut self, media_references: Vec<Media>);
43}
44
45/// Represents a media library with a specific type
46#[derive(Debug, Clone)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48#[cfg_attr(
49    feature = "rkyv",
50    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
51)]
52#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq)))]
53pub struct Library {
54    pub id: LibraryId,
55    pub name: String,
56    pub library_type: LibraryType,
57    #[cfg_attr(feature = "rkyv", rkyv(with = crate::rkyv_wrappers::VecPathBuf))]
58    pub paths: Vec<PathBuf>,
59    pub scan_interval_minutes: u32,
60    #[cfg_attr(feature = "rkyv", rkyv(with = crate::rkyv_wrappers::OptionDateTime))]
61    pub last_scan: Option<DateTime<Utc>>,
62    pub enabled: bool,
63    pub auto_scan: bool,
64    pub watch_for_changes: bool,
65    pub analyze_on_scan: bool,
66    pub max_retry_attempts: u32,
67    pub movie_ref_batch_size: MovieReferenceBatchSize,
68    #[cfg_attr(feature = "rkyv", rkyv(with = crate::rkyv_wrappers::DateTimeWrapper))]
69    pub created_at: DateTime<Utc>,
70    #[cfg_attr(feature = "rkyv", rkyv(with = crate::rkyv_wrappers::DateTimeWrapper))]
71    pub updated_at: DateTime<Utc>,
72    pub media: Option<Vec<Media>>,
73}
74
75/// The type of content a library contains
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
78#[cfg_attr(
79    feature = "rkyv",
80    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
81)]
82#[cfg_attr(feature = "serde", serde(rename_all = "PascalCase"))]
83#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, Clone, PartialEq, Eq, Hash)))]
84pub enum LibraryType {
85    Movies,
86    Series,
87}
88
89impl std::fmt::Display for LibraryType {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        match self {
92            LibraryType::Movies => write!(f, "Movies"),
93            LibraryType::Series => write!(f, "TV Shows"),
94        }
95    }
96}
97
98impl LibraryLike for Library {
99    fn needs_scan(&self) -> bool {
100        if !self.enabled {
101            return false;
102        }
103
104        match self.last_scan {
105            None => true,
106            Some(last_scan) => {
107                let elapsed = Utc::now().signed_duration_since(last_scan);
108                elapsed.num_minutes() >= self.scan_interval_minutes as i64
109            }
110        }
111    }
112
113    fn get_id(&self) -> LibraryId {
114        self.id
115    }
116
117    fn get_name(&self) -> &str {
118        &self.name
119    }
120
121    fn get_type(&self) -> LibraryType {
122        self.library_type
123    }
124
125    fn get_paths(&self) -> Vec<PathBuf> {
126        self.paths.clone()
127    }
128
129    fn get_scan_interval(&self) -> u32 {
130        self.scan_interval_minutes
131    }
132
133    fn get_last_scan(&self) -> Option<DateTime<Utc>> {
134        self.last_scan
135    }
136
137    fn is_enabled(&self) -> bool {
138        self.enabled
139    }
140
141    fn is_auto_scan(&self) -> bool {
142        self.auto_scan
143    }
144
145    fn is_watch_for_changes(&self) -> bool {
146        self.watch_for_changes
147    }
148
149    fn is_analyze_on_scan(&self) -> bool {
150        self.analyze_on_scan
151    }
152
153    fn get_max_retry_attempts(&self) -> u32 {
154        self.max_retry_attempts
155    }
156
157    fn get_movie_ref_batch_size(&self) -> MovieReferenceBatchSize {
158        self.movie_ref_batch_size
159    }
160
161    fn get_created_at(&self) -> DateTime<Utc> {
162        self.created_at
163    }
164
165    fn get_updated_at(&self) -> DateTime<Utc> {
166        self.updated_at
167    }
168
169    fn get_media_references_clone(&self) -> Option<Vec<Media>> {
170        self.media.clone()
171    }
172}
173
174impl LibraryLikeMut for Library {
175    fn new(
176        name: String,
177        library_type: LibraryType,
178        paths: Vec<PathBuf>,
179    ) -> Self {
180        let now = Utc::now();
181        Self {
182            id: LibraryId::new(),
183            name,
184            library_type,
185            paths,
186            scan_interval_minutes: 60,
187            last_scan: None,
188            enabled: true,
189            auto_scan: true,
190            watch_for_changes: true,
191            analyze_on_scan: false,
192            max_retry_attempts: 3,
193            movie_ref_batch_size: MovieReferenceBatchSize::default(),
194            created_at: now,
195            updated_at: now,
196            media: None,
197        }
198    }
199
200    fn update_last_scan(&mut self) {
201        self.last_scan = Some(Utc::now());
202        self.updated_at = Utc::now();
203    }
204
205    fn set_paths(&mut self, paths: Vec<PathBuf>) {
206        self.paths = paths;
207        self.updated_at = Utc::now();
208    }
209
210    fn set_scan_interval(&mut self, interval: u32) {
211        self.scan_interval_minutes = interval;
212        self.updated_at = Utc::now();
213    }
214
215    fn set_last_scan(&mut self, last_scan: Option<DateTime<Utc>>) {
216        self.last_scan = last_scan;
217        self.updated_at = Utc::now();
218    }
219
220    fn set_auto_scan(&mut self, auto_scan: bool) {
221        self.auto_scan = auto_scan;
222        self.updated_at = Utc::now();
223    }
224
225    fn set_max_retry_attempts(&mut self, max_retry_attempts: u32) {
226        self.max_retry_attempts = max_retry_attempts;
227        self.updated_at = Utc::now();
228    }
229
230    fn set_updated_at(&mut self, updated_at: Option<DateTime<Utc>>) {
231        self.updated_at = updated_at.unwrap_or_else(Utc::now);
232    }
233
234    fn set_media_references(&mut self, media_references: Vec<Media>) {
235        self.media = Some(media_references);
236        self.updated_at = Utc::now();
237    }
238}
239
240// Request types now live under api::types to avoid conflicts
241
242/// Library scan result
243#[derive(Debug, Clone)]
244#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
245pub struct LibraryScanResult {
246    pub library_id: LibraryId,
247    pub library_name: String,
248    pub total_files: usize,
249    pub new_files: usize,
250    pub updated_files: usize,
251    pub deleted_files: usize,
252    pub errors: Vec<String>,
253    pub duration_seconds: f64,
254}
255
256#[cfg(feature = "rkyv")]
257pub use archived::ArchivedLibraryExt;
258
259#[cfg(feature = "rkyv")]
260mod archived {
261    use super::*;
262    use crate::ids::ArchivedLibraryId;
263    use crate::media::{ArchivedMedia, ArchivedMovieReference};
264    use rkyv::{option::ArchivedOption, vec::ArchivedVec};
265
266    pub trait ArchivedLibraryExt {
267        fn media(&self) -> Option<&ArchivedVec<ArchivedMedia>>;
268        fn media_as_slice(&self) -> &[ArchivedMedia];
269        fn get_movie_refs(
270            &self,
271        ) -> impl Iterator<Item = &ArchivedMovieReference>;
272    }
273
274    impl ArchivedLibraryExt for ArchivedLibrary {
275        fn media(&self) -> Option<&ArchivedVec<ArchivedMedia>> {
276            match &self.media {
277                ArchivedOption::Some(media) => Some(media),
278                ArchivedOption::None => None,
279            }
280        }
281
282        fn media_as_slice(&self) -> &[ArchivedMedia] {
283            match &self.media {
284                ArchivedOption::Some(media) => media.as_slice(),
285                ArchivedOption::None => &[],
286            }
287        }
288
289        fn get_movie_refs(
290            &self,
291        ) -> impl Iterator<Item = &ArchivedMovieReference> {
292            self.media_as_slice().iter().filter_map(|m| match m {
293                ArchivedMedia::Movie(movie) => Some(movie.get()),
294                _ => None,
295            })
296        }
297    }
298
299    impl ArchivedLibrary {
300        pub fn get_id(&self) -> ArchivedLibraryId {
301            self.id
302        }
303
304        pub fn get_name(&self) -> &str {
305            &self.name
306        }
307
308        pub fn get_type(&self) -> &ArchivedLibraryType {
309            &self.library_type
310        }
311
312        pub fn is_enabled(&self) -> bool {
313            self.enabled
314        }
315
316        pub fn is_auto_scan(&self) -> bool {
317            self.auto_scan
318        }
319
320        pub fn is_watch_for_changes(&self) -> bool {
321            self.watch_for_changes
322        }
323
324        pub fn is_analyze_on_scan(&self) -> bool {
325            self.analyze_on_scan
326        }
327    }
328}