Skip to main content

wow_sharedmedia/
entry.rs

1/// Media type enumeration for LibSharedMedia categories.
2///
3/// Each variant maps to an LSM registration type and has
4/// associated file handling rules.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
6#[serde(rename_all = "lowercase")]
7pub enum MediaType {
8	/// A LibSharedMedia statusbar texture.
9	Statusbar,
10	/// A LibSharedMedia background texture.
11	Background,
12	/// A LibSharedMedia border texture.
13	Border,
14	/// A LibSharedMedia font face.
15	Font,
16	/// A LibSharedMedia sound asset.
17	Sound,
18}
19
20impl MediaType {
21	/// Get the folder name for this media type.
22	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	/// Get the LSM registration type string.
33	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	/// Get accepted input file extensions.
44	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	/// Get the output file extension for WoW storage.
53	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	/// Whether this type supports locale masks.
62	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/// Type-specific metadata extracted at import time.
89#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Default)]
90pub struct EntryMetadata {
91	// Image fields (statusbar, background, border)
92	#[serde(skip_serializing_if = "Option::is_none")]
93	/// Width of an imported image after conversion.
94	pub image_width: Option<u32>,
95	#[serde(skip_serializing_if = "Option::is_none")]
96	/// Height of an imported image after conversion.
97	pub image_height: Option<u32>,
98
99	// Font fields
100	#[serde(skip_serializing_if = "Option::is_none")]
101	/// Font family name extracted from the font metadata.
102	pub font_family: Option<String>,
103	#[serde(skip_serializing_if = "Option::is_none")]
104	/// Font style name extracted from the font metadata.
105	pub font_style: Option<String>,
106	#[serde(skip_serializing_if = "Option::is_none")]
107	/// Whether the font is monospaced.
108	pub font_is_monospace: Option<bool>,
109	#[serde(skip_serializing_if = "Option::is_none")]
110	/// Total glyph count reported by the font.
111	pub font_num_glyphs: Option<u32>,
112	#[serde(default, skip_serializing_if = "Vec::is_empty")]
113	/// Locale masks used when registering a font with LibSharedMedia.
114	pub locales: Vec<String>,
115
116	// Audio fields
117	#[serde(skip_serializing_if = "Option::is_none")]
118	/// Audio duration in seconds.
119	pub audio_duration_secs: Option<f64>,
120	#[serde(skip_serializing_if = "Option::is_none")]
121	/// Audio sample rate in Hz.
122	pub audio_sample_rate: Option<u32>,
123	#[serde(skip_serializing_if = "Option::is_none")]
124	/// Number of audio channels.
125	pub audio_channels: Option<u32>,
126}
127
128/// A single media entry in the addon registry.
129#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
130pub struct MediaEntry {
131	/// Stable UUID for the entry.
132	pub id: uuid::Uuid,
133	#[serde(rename = "type")]
134	/// LibSharedMedia asset type.
135	pub media_type: MediaType,
136	/// Display key used for registration in LibSharedMedia.
137	pub key: String,
138	/// Relative file path inside the addon directory.
139	pub file: String,
140	#[serde(skip_serializing_if = "Option::is_none")]
141	/// Original file name provided by the user, if retained.
142	pub original_name: Option<String>,
143	/// Import timestamp in UTC.
144	pub imported_at: chrono::DateTime<chrono::Utc>,
145	#[serde(skip_serializing_if = "Option::is_none")]
146	/// Optional content checksum for duplicate detection and auditing.
147	pub checksum: Option<String>,
148	#[serde(skip_serializing_if = "Option::is_none")]
149	/// Optional type-specific metadata extracted during import.
150	pub metadata: Option<EntryMetadata>,
151	#[serde(default, skip_serializing_if = "Vec::is_empty")]
152	/// User-defined tags associated with the entry.
153	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}