1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
6#[serde(rename_all = "lowercase")]
7pub enum MediaType {
8 Statusbar,
10 Background,
12 Border,
14 Font,
16 Sound,
18}
19
20impl MediaType {
21 pub fn folder_name(&self) -> &'static str {
23 match self {
24 Self::Statusbar => "statusbar",
25 Self::Background => "background",
26 Self::Border => "border",
27 Self::Font => "font",
28 Self::Sound => "sound",
29 }
30 }
31
32 pub fn lsm_type(&self) -> &'static str {
34 match self {
35 Self::Statusbar => "statusbar",
36 Self::Background => "background",
37 Self::Border => "border",
38 Self::Font => "font",
39 Self::Sound => "sound",
40 }
41 }
42
43 pub fn accepted_extensions(&self) -> &'static [&'static str] {
45 match self {
46 Self::Statusbar | Self::Background | Self::Border => &[".tga", ".png", ".webp", ".jpg", ".jpeg", ".blp"],
47 Self::Font => &[".ttf", ".otf"],
48 Self::Sound => &[".ogg", ".mp3", ".wav"],
49 }
50 }
51
52 pub fn output_extension(&self) -> &'static str {
54 match self {
55 Self::Statusbar | Self::Background | Self::Border => ".tga",
56 Self::Font => "",
57 Self::Sound => ".ogg",
58 }
59 }
60
61 pub fn supports_locale(&self) -> bool {
63 matches!(self, Self::Font)
64 }
65}
66
67impl std::fmt::Display for MediaType {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 f.write_str(self.lsm_type())
70 }
71}
72
73impl std::str::FromStr for MediaType {
74 type Err = String;
75
76 fn from_str(s: &str) -> Result<Self, Self::Err> {
77 match s.to_lowercase().as_str() {
78 "statusbar" => Ok(Self::Statusbar),
79 "background" => Ok(Self::Background),
80 "border" => Ok(Self::Border),
81 "font" => Ok(Self::Font),
82 "sound" => Ok(Self::Sound),
83 _ => Err(format!("Unknown media type: {s}")),
84 }
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Default)]
90pub struct EntryMetadata {
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub image_width: Option<u32>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub image_height: Option<u32>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub font_family: Option<String>,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub font_style: Option<String>,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub font_is_monospace: Option<bool>,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub font_num_glyphs: Option<u32>,
112 #[serde(default, skip_serializing_if = "Vec::is_empty")]
113 pub locales: Vec<String>,
115
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub audio_duration_secs: Option<f64>,
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub audio_sample_rate: Option<u32>,
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub audio_channels: Option<u32>,
126}
127
128#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
130pub struct MediaEntry {
131 pub id: uuid::Uuid,
133 #[serde(rename = "type")]
134 pub media_type: MediaType,
136 pub key: String,
138 pub file: String,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub original_name: Option<String>,
143 pub imported_at: chrono::DateTime<chrono::Utc>,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub checksum: Option<String>,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub metadata: Option<EntryMetadata>,
151 #[serde(default, skip_serializing_if = "Vec::is_empty")]
152 pub tags: Vec<String>,
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn test_media_type_parse_case_insensitive() {
162 assert_eq!("statusbar".parse::<MediaType>().unwrap(), MediaType::Statusbar);
163 assert_eq!("STATUSBAR".parse::<MediaType>().unwrap(), MediaType::Statusbar);
164 assert_eq!("Font".parse::<MediaType>().unwrap(), MediaType::Font);
165 }
166
167 #[test]
168 fn test_media_type_parse_invalid() {
169 let result = "video".parse::<MediaType>();
170 assert!(result.is_err());
171 assert!(result.unwrap_err().contains("Unknown media type"));
172 }
173
174 #[test]
175 fn test_media_type_extension_contracts() {
176 assert_eq!(MediaType::Statusbar.output_extension(), ".tga");
177 assert_eq!(MediaType::Background.output_extension(), ".tga");
178 assert_eq!(MediaType::Border.output_extension(), ".tga");
179 assert_eq!(MediaType::Font.output_extension(), "");
180 assert_eq!(MediaType::Sound.output_extension(), ".ogg");
181
182 assert!(MediaType::Statusbar.accepted_extensions().contains(&".png"));
183 assert!(MediaType::Statusbar.accepted_extensions().contains(&".blp"));
184 assert!(MediaType::Font.accepted_extensions().contains(&".ttf"));
185 assert!(MediaType::Sound.accepted_extensions().contains(&".wav"));
186 }
187
188 #[test]
189 fn test_media_type_locale_support_contract() {
190 assert!(!MediaType::Statusbar.supports_locale());
191 assert!(!MediaType::Background.supports_locale());
192 assert!(!MediaType::Border.supports_locale());
193 assert!(MediaType::Font.supports_locale());
194 assert!(!MediaType::Sound.supports_locale());
195 }
196}