1use crate::types::{Enclosure, Entry, FeedMeta, MediaRating, Tag};
29
30pub const MEDIA_NAMESPACE: &str = "http://search.yahoo.com/mrss/";
32
33#[derive(Debug, Clone, Default, PartialEq)]
60#[allow(clippy::derive_partial_eq_without_eq)]
61pub struct MediaContent {
62 pub url: String,
69 pub type_: Option<String>,
71 pub medium: Option<String>,
73 pub file_size: Option<u64>,
75 pub bitrate: Option<u32>,
77 pub framerate: Option<f32>,
79 pub width: Option<u32>,
81 pub height: Option<u32>,
83 pub duration: Option<u32>,
85 pub expression: Option<String>,
91 pub is_default: Option<bool>,
93}
94
95#[derive(Debug, Clone, Default, PartialEq, Eq)]
119pub struct MediaThumbnail {
120 pub url: String,
127 pub width: Option<u32>,
129 pub height: Option<u32>,
131 pub time: Option<String>,
135}
136
137fn parse_rating(scheme: Option<&str>, text: &str) -> Option<MediaRating> {
139 let content = text.trim().to_string();
140 if content.is_empty() {
141 return None;
142 }
143 Some(MediaRating {
144 scheme: scheme.map(str::to_owned),
145 content,
146 })
147}
148
149pub fn handle_feed_element(element: &str, scheme: Option<&str>, text: &str, feed: &mut FeedMeta) {
158 match element {
159 "rating" if feed.media_rating.is_none() => {
160 feed.media_rating = parse_rating(scheme, text);
161 }
162 "keywords" if feed.media_keywords.is_none() && !text.trim().is_empty() => {
163 feed.media_keywords = Some(text.trim().to_string());
164 }
165 _ => {}
166 }
167}
168
169pub fn handle_entry_element(element: &str, text: &str, entry: &mut Entry) {
180 match element {
181 "title" if entry.title.is_none() => {
182 entry.title = Some(text.to_string());
183 }
184 "description" if entry.summary.is_none() => {
185 entry.summary = Some(text.to_string());
186 }
187 "keywords" => {
188 if entry.media_keywords.is_none() && !text.is_empty() {
190 entry.media_keywords = Some(text.to_string());
191 }
192 for keyword in text.split(',') {
194 let keyword = keyword.trim();
195 if !keyword.is_empty() {
196 entry.tags.push(Tag::new(keyword));
197 }
198 }
199 }
200 "category" if !text.is_empty() => {
201 entry.tags.push(Tag::new(text));
202 }
203 _ => {
204 }
208 }
209}
210
211pub fn handle_entry_rating(scheme: Option<&str>, text: &str, entry: &mut Entry) {
215 if entry.media_rating.is_none() {
216 entry.media_rating = parse_rating(scheme, text);
217 }
218}
219
220pub fn media_content_to_enclosure(content: &MediaContent) -> Enclosure {
243 Enclosure {
244 url: content.url.clone().into(),
245 enclosure_type: content.type_.as_ref().map(|t| t.clone().into()),
246 length: content.file_size.map(|v| v.to_string()),
247 title: None,
248 duration: None,
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_media_title() {
258 let mut entry = Entry::default();
259 handle_entry_element("title", "Video Title", &mut entry);
260
261 assert_eq!(entry.title.as_deref(), Some("Video Title"));
262 }
263
264 #[test]
265 fn test_media_description() {
266 let mut entry = Entry::default();
267 handle_entry_element("description", "Video description", &mut entry);
268
269 assert_eq!(entry.summary.as_deref(), Some("Video description"));
270 }
271
272 #[test]
273 fn test_media_keywords() {
274 let mut entry = Entry::default();
275 handle_entry_element("keywords", "tech, programming, rust", &mut entry);
276
277 assert_eq!(entry.tags.len(), 3);
278 assert_eq!(entry.tags[0].term, "tech");
279 assert_eq!(entry.tags[1].term, "programming");
280 assert_eq!(entry.tags[2].term, "rust");
281 }
282
283 #[test]
284 fn test_media_keywords_with_spaces() {
285 let mut entry = Entry::default();
286 handle_entry_element("keywords", " tech , programming ", &mut entry);
287
288 assert_eq!(entry.tags.len(), 2);
289 assert_eq!(entry.tags[0].term, "tech");
290 assert_eq!(entry.tags[1].term, "programming");
291 }
292
293 #[test]
294 fn test_media_category() {
295 let mut entry = Entry::default();
296 handle_entry_element("category", "Technology", &mut entry);
297
298 assert_eq!(entry.tags.len(), 1);
299 assert_eq!(entry.tags[0].term, "Technology");
300 }
301
302 #[test]
303 fn test_media_content_default() {
304 let content = MediaContent::default();
305 assert!(content.url.is_empty());
306 assert!(content.type_.is_none());
307 assert!(content.medium.is_none());
308 assert!(content.file_size.is_none());
309 assert!(content.bitrate.is_none());
310 assert!(content.framerate.is_none());
311 assert!(content.width.is_none());
312 assert!(content.height.is_none());
313 assert!(content.duration.is_none());
314 assert!(content.expression.is_none());
315 assert!(content.is_default.is_none());
316 }
317
318 #[test]
319 fn test_media_content_full_attributes() {
320 let content = MediaContent {
321 url: "https://example.com/video.mp4".to_string(),
322 type_: Some("video/mp4".to_string()),
323 medium: Some("video".to_string()),
324 file_size: Some(10_485_760), bitrate: Some(1500), framerate: Some(30.0),
327 width: Some(1920),
328 height: Some(1080),
329 duration: Some(600), expression: Some("full".to_string()),
331 is_default: Some(true),
332 };
333
334 assert_eq!(content.url, "https://example.com/video.mp4");
335 assert_eq!(content.type_.as_deref(), Some("video/mp4"));
336 assert_eq!(content.medium.as_deref(), Some("video"));
337 assert_eq!(content.file_size, Some(10_485_760));
338 assert_eq!(content.bitrate, Some(1500));
339 assert_eq!(content.framerate, Some(30.0));
340 assert_eq!(content.width, Some(1920));
341 assert_eq!(content.height, Some(1080));
342 assert_eq!(content.duration, Some(600));
343 assert_eq!(content.expression.as_deref(), Some("full"));
344 assert_eq!(content.is_default, Some(true));
345 }
346
347 #[test]
348 fn test_media_content_audio() {
349 let content = MediaContent {
350 url: "https://example.com/audio.mp3".to_string(),
351 type_: Some("audio/mpeg".to_string()),
352 medium: Some("audio".to_string()),
353 file_size: Some(5_242_880), bitrate: Some(128), duration: Some(180), ..Default::default()
357 };
358
359 assert_eq!(content.medium.as_deref(), Some("audio"));
360 assert_eq!(content.bitrate, Some(128));
361 assert!(content.width.is_none());
362 assert!(content.height.is_none());
363 assert!(content.framerate.is_none());
364 }
365
366 #[test]
367 fn test_media_content_image() {
368 let content = MediaContent {
369 url: "https://example.com/image.jpg".to_string(),
370 type_: Some("image/jpeg".to_string()),
371 medium: Some("image".to_string()),
372 width: Some(800),
373 height: Some(600),
374 ..Default::default()
375 };
376
377 assert_eq!(content.medium.as_deref(), Some("image"));
378 assert_eq!(content.width, Some(800));
379 assert_eq!(content.height, Some(600));
380 assert!(content.duration.is_none());
381 }
382
383 #[test]
384 fn test_media_content_expression_variants() {
385 let full = MediaContent {
386 expression: Some("full".to_string()),
387 ..Default::default()
388 };
389 let sample = MediaContent {
390 expression: Some("sample".to_string()),
391 ..Default::default()
392 };
393 let nonstop = MediaContent {
394 expression: Some("nonstop".to_string()),
395 ..Default::default()
396 };
397
398 assert_eq!(full.expression.as_deref(), Some("full"));
399 assert_eq!(sample.expression.as_deref(), Some("sample"));
400 assert_eq!(nonstop.expression.as_deref(), Some("nonstop"));
401 }
402
403 #[test]
404 fn test_media_thumbnail_default() {
405 let thumbnail = MediaThumbnail::default();
406 assert!(thumbnail.url.is_empty());
407 assert!(thumbnail.width.is_none());
408 assert!(thumbnail.height.is_none());
409 assert!(thumbnail.time.is_none());
410 }
411
412 #[test]
413 fn test_media_thumbnail_full_attributes() {
414 let thumbnail = MediaThumbnail {
415 url: "https://example.com/thumb.jpg".to_string(),
416 width: Some(640),
417 height: Some(480),
418 time: Some("12:05:01.123".to_string()),
419 };
420
421 assert_eq!(thumbnail.url, "https://example.com/thumb.jpg");
422 assert_eq!(thumbnail.width, Some(640));
423 assert_eq!(thumbnail.height, Some(480));
424 assert_eq!(thumbnail.time.as_deref(), Some("12:05:01.123"));
425 }
426
427 #[test]
428 fn test_media_thumbnail_without_time() {
429 let thumbnail = MediaThumbnail {
430 url: "https://example.com/poster.jpg".to_string(),
431 width: Some(1920),
432 height: Some(1080),
433 time: None,
434 };
435
436 assert_eq!(thumbnail.width, Some(1920));
437 assert_eq!(thumbnail.height, Some(1080));
438 assert!(thumbnail.time.is_none());
439 }
440
441 #[test]
442 fn test_media_content_to_enclosure() {
443 let content = MediaContent {
444 url: "https://example.com/video.mp4".to_string(),
445 type_: Some("video/mp4".to_string()),
446 file_size: Some(1_024_000),
447 width: Some(1920), height: Some(1080),
449 ..Default::default()
450 };
451
452 let enclosure = media_content_to_enclosure(&content);
453
454 assert_eq!(enclosure.url, "https://example.com/video.mp4");
455 assert_eq!(enclosure.enclosure_type.as_deref(), Some("video/mp4"));
456 assert_eq!(enclosure.length.as_deref(), Some("1024000"));
457 }
458
459 #[test]
460 fn test_media_content_to_enclosure_minimal() {
461 let content = MediaContent {
462 url: "https://example.com/file.bin".to_string(),
463 ..Default::default()
464 };
465
466 let enclosure = media_content_to_enclosure(&content);
467
468 assert_eq!(enclosure.url, "https://example.com/file.bin");
469 assert!(enclosure.enclosure_type.is_none());
470 assert!(enclosure.length.is_none());
471 }
472
473 #[test]
474 fn test_empty_keywords() {
475 let mut entry = Entry::default();
476 handle_entry_element("keywords", "", &mut entry);
477
478 assert!(entry.tags.is_empty());
479 }
480
481 #[test]
482 fn test_keywords_with_empty_values() {
483 let mut entry = Entry::default();
484 handle_entry_element("keywords", "tech, , programming", &mut entry);
485
486 assert_eq!(entry.tags.len(), 2);
487 assert_eq!(entry.tags[0].term, "tech");
488 assert_eq!(entry.tags[1].term, "programming");
489 }
490}