Skip to main content

ferrex_model/
media.rs

1use super::{
2    files::MediaFile,
3    ids::{EpisodeID, LibraryId, MovieBatchId, MovieID, SeasonID, SeriesID},
4    numbers::{EpisodeNumber, SeasonNumber},
5    titles::{MovieTitle, SeriesTitle},
6    urls::{EpisodeURL, MovieURL, SeasonURL, SeriesURL},
7};
8use std::fmt;
9
10#[cfg(feature = "rkyv")]
11use crate::media_id::ArchivedMediaID;
12#[cfg(feature = "rkyv")]
13use crate::rkyv_wrappers::DateTimeWrapper;
14use crate::{
15    EnhancedMovieDetails, EnhancedSeriesDetails, EpisodeDetails, SeasonDetails,
16    chrono::{DateTime, Utc},
17};
18/// Lightweight movie reference for lists/collections
19#[derive(Clone, PartialEq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21#[cfg_attr(
22    feature = "rkyv",
23    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
24)]
25#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq)))]
26pub enum Media {
27    /// Movie media reference
28    Movie(Box<MovieReference>),
29    /// Series media reference
30    Series(Box<Series>),
31    /// Season media reference
32    Season(Box<SeasonReference>),
33    /// Episode media reference
34    Episode(Box<EpisodeReference>),
35}
36
37/// Lightweight movie reference for lists/collections
38#[derive(Clone, PartialEq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40#[cfg_attr(
41    feature = "rkyv",
42    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
43)]
44#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq)))]
45pub struct MovieReference {
46    pub id: MovieID,
47    pub library_id: LibraryId,
48    #[cfg_attr(
49        feature = "serde",
50        serde(skip_serializing_if = "Option::is_none", default)
51    )]
52    pub batch_id: Option<MovieBatchId>,
53    pub tmdb_id: u64,
54    pub title: MovieTitle,
55    pub details: EnhancedMovieDetails,
56    pub endpoint: MovieURL,
57    pub file: MediaFile,
58    #[cfg_attr(
59        feature = "serde",
60        serde(skip_serializing_if = "Option::is_none")
61    )]
62    pub theme_color: Option<String>, // Hex color e.g. "#2C3E50"
63}
64
65/// Lightweight series reference for lists/collections
66#[derive(Clone, PartialEq)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
68#[cfg_attr(
69    feature = "rkyv",
70    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
71)]
72#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq)))]
73pub struct Series {
74    pub id: SeriesID,
75    pub library_id: LibraryId,
76    pub tmdb_id: u64,
77    pub title: SeriesTitle,
78    pub details: EnhancedSeriesDetails,
79    pub endpoint: SeriesURL,
80    /// When the series was discovered (row creation time)
81    #[cfg_attr(feature = "serde", serde(default = "Utc::now"))]
82    #[cfg_attr(feature = "rkyv", rkyv(with = DateTimeWrapper))]
83    pub discovered_at: DateTime<Utc>,
84    /// When the series folder was created (for date added sorting)
85    #[cfg_attr(feature = "serde", serde(default = "Utc::now"))]
86    #[cfg_attr(feature = "rkyv", rkyv(with = DateTimeWrapper))]
87    pub created_at: DateTime<Utc>,
88    #[cfg_attr(
89        feature = "serde",
90        serde(skip_serializing_if = "Option::is_none")
91    )]
92    pub theme_color: Option<String>, // Hex color e.g. "#2C3E50"
93}
94
95/// Lightweight season reference
96#[derive(Clone, PartialEq)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98#[cfg_attr(
99    feature = "rkyv",
100    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
101)]
102#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq)))]
103pub struct SeasonReference {
104    pub id: SeasonID,
105    pub library_id: LibraryId,
106    pub season_number: SeasonNumber,
107    pub series_id: SeriesID, // Link to parent series
108    pub tmdb_series_id: u64,
109    pub details: SeasonDetails,
110    pub endpoint: SeasonURL,
111    /// When the season was discovered (row creation time)
112    #[cfg_attr(feature = "serde", serde(default = "Utc::now"))]
113    #[cfg_attr(feature = "rkyv", rkyv(with = DateTimeWrapper))]
114    pub discovered_at: DateTime<Utc>,
115    /// When the season folder was created (for date added sorting)
116    #[cfg_attr(feature = "serde", serde(default = "Utc::now"))]
117    #[cfg_attr(feature = "rkyv", rkyv(with = DateTimeWrapper))]
118    pub created_at: DateTime<Utc>,
119    #[cfg_attr(
120        feature = "serde",
121        serde(skip_serializing_if = "Option::is_none")
122    )]
123    pub theme_color: Option<String>, // Hex color e.g. "#2C3E50"
124}
125
126/// Lightweight episode reference
127#[derive(Clone, PartialEq)]
128#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
129#[cfg_attr(
130    feature = "rkyv",
131    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
132)]
133#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq)))]
134pub struct EpisodeReference {
135    pub id: EpisodeID,
136    pub library_id: LibraryId,
137    pub episode_number: EpisodeNumber,
138    pub season_number: SeasonNumber,
139    pub season_id: SeasonID, // Link to parent season
140    pub series_id: SeriesID, // Link to parent series
141    pub tmdb_series_id: u64,
142    pub details: EpisodeDetails,
143    pub endpoint: EpisodeURL,
144    pub file: MediaFile,
145    /// When the episode was discovered (row creation time)
146    #[cfg_attr(feature = "serde", serde(default = "Utc::now"))]
147    #[cfg_attr(feature = "rkyv", rkyv(with = DateTimeWrapper))]
148    pub discovered_at: DateTime<Utc>,
149    /// When the episode was created (for alternate date-based sorting)
150    #[cfg_attr(feature = "serde", serde(default = "Utc::now"))]
151    #[cfg_attr(feature = "rkyv", rkyv(with = DateTimeWrapper))]
152    pub created_at: DateTime<Utc>,
153}
154
155impl fmt::Debug for Media {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        match self {
158            Media::Movie(movie) => {
159                f.debug_tuple("Media::Movie").field(movie).finish()
160            }
161            Media::Series(series) => {
162                f.debug_tuple("Media::Series").field(series).finish()
163            }
164            Media::Season(season) => {
165                f.debug_tuple("Media::Season").field(season).finish()
166            }
167            Media::Episode(episode) => {
168                f.debug_tuple("Media::Episode").field(episode).finish()
169            }
170        }
171    }
172}
173
174impl fmt::Debug for MovieReference {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        f.debug_struct("MovieReference")
177            .field("id", &self.id)
178            .field("library_id", &self.library_id)
179            .field("batch_id", &self.batch_id)
180            .field("tmdb_id", &self.tmdb_id)
181            .field("title", &self.title)
182            .field("endpoint", &self.endpoint)
183            .field("theme_color", &self.theme_color)
184            .field("details", &self.details)
185            .field("file", &self.file)
186            .finish()
187    }
188}
189
190impl fmt::Debug for Series {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        f.debug_struct("SeriesReference")
193            .field("id", &self.id)
194            .field("library_id", &self.library_id)
195            .field("tmdb_id", &self.tmdb_id)
196            .field("title", &self.title)
197            .field("discovered_at", &self.discovered_at)
198            .field("created_at", &self.created_at)
199            .field("endpoint", &self.endpoint)
200            .field("theme_color", &self.theme_color)
201            .field("details", &self.details)
202            .finish()
203    }
204}
205
206impl fmt::Debug for SeasonReference {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        f.debug_struct("SeasonReference")
209            .field("id", &self.id)
210            .field("library_id", &self.library_id)
211            .field("season_number", &self.season_number)
212            .field("series_id", &self.series_id)
213            .field("tmdb_series_id", &self.tmdb_series_id)
214            .field("discovered_at", &self.discovered_at)
215            .field("created_at", &self.created_at)
216            .field("endpoint", &self.endpoint)
217            .field("details", &self.details)
218            .finish()
219    }
220}
221
222impl fmt::Debug for EpisodeReference {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        f.debug_struct("EpisodeReference")
225            .field("id", &self.id)
226            .field("library_id", &self.library_id)
227            .field("season_number", &self.season_number)
228            .field("episode_number", &self.episode_number)
229            .field("season_id", &self.season_id)
230            .field("series_id", &self.series_id)
231            .field("tmdb_series_id", &self.tmdb_series_id)
232            .field("discovered_at", &self.discovered_at)
233            .field("created_at", &self.created_at)
234            .field("endpoint", &self.endpoint)
235            .field("details", &self.details)
236            .field("file", &self.file)
237            .finish()
238    }
239}
240
241impl MovieReference {
242    /// Returns the rating when available.
243    pub fn rating(&self) -> Option<f32> {
244        self.details.vote_average
245    }
246
247    /// Returns the list of genre names when available.
248    pub fn genres(&self) -> Vec<&str> {
249        self.details
250            .genres
251            .iter()
252            .map(|genre| genre.name.as_str())
253            .collect()
254    }
255}
256
257impl Series {
258    /// Returns the first available year across the known TMDB detail variants.
259    pub fn year(&self) -> Option<u16> {
260        self.details
261            .first_air_date
262            .as_ref()
263            .and_then(|date| date.split('-').next())
264            .and_then(|year| year.parse().ok())
265    }
266
267    /// Returns the rating when TMDB details are cached locally.
268    pub fn rating(&self) -> Option<f32> {
269        self.details.vote_average
270    }
271
272    /// Returns the list of genre names when TMDB details are cached locally.
273    pub fn genres(&self) -> Vec<&str> {
274        self.details
275            .genres
276            .iter()
277            .map(|genre| genre.name.as_str())
278            .collect()
279    }
280}
281
282impl SeasonReference {
283    /// Returns the first available year across the known TMDB detail variants.
284    pub fn year(&self) -> Option<u16> {
285        self.details
286            .air_date
287            .as_ref()
288            .and_then(|date| date.split('-').next())
289            .and_then(|year| year.parse().ok())
290    }
291
292    /// Returns the rating when TMDB details are cached locally.
293    pub fn rating(&self) -> Option<f32> {
294        // TODO: Seasons currently have no rating
295        None
296    }
297
298    /// Returns the list of genre names when TMDB details are cached locally.
299    pub fn genres(&self) -> Option<Vec<&str>> {
300        None
301    }
302}
303
304impl EpisodeReference {
305    /// Returns the first available year across the known TMDB detail variants.
306    pub fn year(&self) -> Option<u16> {
307        self.details
308            .air_date
309            .as_ref()
310            .and_then(|date| date.split('-').next())
311            .and_then(|year| year.parse().ok())
312    }
313
314    /// Returns the rating when TMDB details are cached locally.
315    pub fn rating(&self) -> Option<f32> {
316        self.details.vote_average
317    }
318
319    /// Returns the list of genre names when TMDB details are cached locally.
320    pub fn genres(&self) -> Option<Vec<&str>> {
321        // Episodes do not currently have genre information; we'd need to source it from
322        // the parent series or extend the details model to include genres.
323        None
324    }
325}
326
327#[cfg(feature = "rkyv")]
328impl ArchivedMedia {
329    pub fn archived_media_id(&self) -> ArchivedMediaID {
330        match self {
331            Self::Movie(movie) => ArchivedMediaID::Movie(movie.id),
332            Self::Series(series) => ArchivedMediaID::Series(series.id),
333            Self::Season(season) => ArchivedMediaID::Season(season.id),
334            Self::Episode(episode) => ArchivedMediaID::Episode(episode.id),
335        }
336    }
337}