1use crate::{
2 MediaID,
3 error::{ModelError as MediaError, Result},
4};
5use std::fmt;
6use std::path::PathBuf;
7use uuid::Uuid;
8
9use super::LibraryId;
10use crate::chrono::{DateTime, Utc};
11
12#[derive(Clone, PartialEq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[cfg_attr(
15 feature = "rkyv",
16 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
17)]
18#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq)))]
19pub struct MediaFile {
20 pub id: Uuid,
21 pub media_id: MediaID,
22 #[cfg_attr(feature = "rkyv", rkyv(with = crate::rkyv_wrappers::PathBufWrapper))]
23 pub path: PathBuf,
24 pub filename: String,
25 pub size: u64,
26 #[cfg_attr(feature = "rkyv", rkyv(with = crate::rkyv_wrappers::DateTimeWrapper))]
27 pub discovered_at: DateTime<Utc>,
28 #[cfg_attr(feature = "rkyv", rkyv(with = crate::rkyv_wrappers::DateTimeWrapper))]
29 pub created_at: DateTime<Utc>,
30 pub media_file_metadata: Option<MediaFileMetadata>,
31 pub library_id: LibraryId,
32}
33
34#[derive(Clone, PartialEq)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36#[cfg_attr(
37 feature = "rkyv",
38 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
39)]
40#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq)))]
41pub struct MediaFileMetadata {
42 pub duration: Option<f64>,
44 pub width: Option<u32>,
45 pub height: Option<u32>,
46 pub video_codec: Option<String>,
47 pub audio_codec: Option<String>,
48 pub bitrate: Option<u64>,
49 pub framerate: Option<f64>,
50 pub file_size: u64,
51
52 pub color_primaries: Option<String>,
54 pub color_transfer: Option<String>,
55 pub color_space: Option<String>,
56 pub bit_depth: Option<u32>,
57
58 pub parsed_info: Option<ParsedMediaInfo>,
60}
61
62impl fmt::Debug for MediaFile {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 f.debug_struct("MediaFile")
65 .field("id", &self.id)
66 .field("filename", &self.filename)
67 .field("path", &self.path)
68 .field("size", &self.size)
69 .field("discovered_at", &self.discovered_at)
70 .field("created_at", &self.created_at)
71 .field("has_metadata", &self.media_file_metadata.is_some())
72 .field("metadata", &self.media_file_metadata.as_ref())
73 .field("library_id", &self.library_id)
74 .finish()
75 }
76}
77
78impl fmt::Debug for MediaFileMetadata {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 let resolution = self.width.zip(self.height);
81 let parsed_kind = self.parsed_info.as_ref().map(|info| match info {
82 ParsedMediaInfo::Movie(_) => "Movie",
83 ParsedMediaInfo::Episode(_) => "Episode",
84 });
85
86 f.debug_struct("MediaFileMetadata")
87 .field("duration", &self.duration)
88 .field("resolution", &resolution)
89 .field("video_codec", &self.video_codec)
90 .field("audio_codec", &self.audio_codec)
91 .field("bitrate", &self.bitrate)
92 .field("framerate", &self.framerate)
93 .field("file_size", &self.file_size)
94 .field(
95 "hdr",
96 &(
97 &self.color_primaries,
98 &self.color_transfer,
99 &self.color_space,
100 &self.bit_depth,
101 ),
102 )
103 .field("parsed_info_kind", &parsed_kind)
104 .finish()
105 }
106}
107#[derive(Debug, Clone, PartialEq)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
109#[cfg_attr(
110 feature = "rkyv",
111 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
112)]
113#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq)))]
114pub enum ParsedMediaInfo {
115 Movie(ParsedMovieInfo),
116 Episode(ParsedEpisodeInfo),
117}
118
119#[derive(Debug, Clone, PartialEq)]
120#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
121#[cfg_attr(
122 feature = "rkyv",
123 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
124)]
125#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq)))]
126pub struct ParsedMovieInfo {
127 pub title: String,
128 pub year: Option<u16>,
129 pub resolution: Option<String>,
130 pub source: Option<String>,
131 pub release_group: Option<String>,
132}
133
134#[derive(Debug, Clone, PartialEq)]
135#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
136#[cfg_attr(
137 feature = "rkyv",
138 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
139)]
140#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq)))]
141pub struct ParsedEpisodeInfo {
142 pub show_name: String,
143 pub season: u16,
144 pub episode: u16,
145 pub episode_title: Option<String>,
146 pub year: Option<u16>,
147 pub resolution: Option<String>,
148 pub source: Option<String>,
149 pub release_group: Option<String>,
150}
151
152#[derive(Debug, Clone, PartialEq)]
153#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
154#[cfg_attr(
155 feature = "rkyv",
156 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
157)]
158#[cfg_attr(feature = "serde", serde(rename_all = "PascalCase"))]
159#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq)))]
160pub enum ExtraType {
161 BehindTheScenes,
162 DeletedScenes,
163 Featurette,
164 Interview,
165 Scene,
166 Short,
167 Trailer,
168 Other,
169}
170
171impl std::fmt::Display for ExtraType {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 match self {
174 ExtraType::BehindTheScenes => write!(f, "Behind the Scenes"),
175 ExtraType::DeletedScenes => write!(f, "Deleted Scenes"),
176 ExtraType::Featurette => write!(f, "Featurette"),
177 ExtraType::Interview => write!(f, "Interview"),
178 ExtraType::Scene => write!(f, "Scene"),
179 ExtraType::Short => write!(f, "Short"),
180 ExtraType::Trailer => write!(f, "Trailer"),
181 ExtraType::Other => write!(f, "Other"),
182 }
183 }
184}
185
186impl MediaFile {
187 pub fn new(
188 media_id: MediaID,
189 path: PathBuf,
190 library_id: LibraryId,
191 ) -> Result<Self> {
192 Self::new_with_policy(media_id, path, library_id, false)
193 }
194
195 pub fn new_with_policy(
196 media_id: MediaID,
197 path: PathBuf,
198 library_id: LibraryId,
199 allow_zero_length: bool,
200 ) -> Result<Self> {
201 let filename = path
202 .file_name()
203 .ok_or_else(|| {
204 MediaError::InvalidMedia("Invalid file path".to_string())
205 })?
206 .to_string_lossy()
207 .to_string();
208
209 let metadata = path.metadata().map_err(MediaError::Io)?;
210
211 let created_at = metadata
213 .created()
214 .ok()
215 .and_then(|time| {
216 let duration =
218 time.duration_since(std::time::UNIX_EPOCH).ok()?;
219 DateTime::<Utc>::from_timestamp(
220 duration.as_secs() as i64,
221 duration.subsec_nanos(),
222 )
223 })
224 .unwrap_or_else(|| {
225 metadata
227 .modified()
228 .ok()
229 .and_then(|time| {
230 let duration =
231 time.duration_since(std::time::UNIX_EPOCH).ok()?;
232 DateTime::<Utc>::from_timestamp(
233 duration.as_secs() as i64,
234 duration.subsec_nanos(),
235 )
236 })
237 .unwrap_or_else(Utc::now)
238 });
239
240 let size = metadata.len();
241
242 if size == 0 && !allow_zero_length {
243 return Err(MediaError::InvalidMedia(
244 "Zero-length media files are not supported".to_string(),
245 ));
246 }
247
248 Ok(Self {
249 id: Uuid::now_v7(),
250 media_id,
251 path,
252 filename,
253 size,
254 discovered_at: Utc::now(),
257 created_at,
258 media_file_metadata: None,
259 library_id,
260 })
261 }
262
263 pub fn is_video_file(&self) -> bool {
264 let video_extensions =
265 ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv"];
266
267 if let Some(extension) = self.path.extension()
268 && let Some(ext_str) = extension.to_str()
269 {
270 return video_extensions.contains(&ext_str.to_lowercase().as_str());
271 }
272
273 false
274 }
275}