Skip to main content

ff_format/
media.rs

1//! Media container information.
2//!
3//! This module provides the [`MediaInfo`] struct for representing metadata about
4//! a media file as a whole, including all its video and audio streams.
5//!
6//! # Examples
7//!
8//! ```
9//! use ff_format::media::{MediaInfo, MediaInfoBuilder};
10//! use ff_format::stream::{VideoStreamInfo, AudioStreamInfo};
11//! use ff_format::{PixelFormat, SampleFormat, Rational};
12//! use ff_format::codec::{VideoCodec, AudioCodec};
13//! use std::time::Duration;
14//! use std::path::PathBuf;
15//!
16//! // Create video stream info
17//! let video = VideoStreamInfo::builder()
18//!     .index(0)
19//!     .codec(VideoCodec::H264)
20//!     .width(1920)
21//!     .height(1080)
22//!     .frame_rate(Rational::new(30, 1))
23//!     .pixel_format(PixelFormat::Yuv420p)
24//!     .build();
25//!
26//! // Create audio stream info
27//! let audio = AudioStreamInfo::builder()
28//!     .index(1)
29//!     .codec(AudioCodec::Aac)
30//!     .sample_rate(48000)
31//!     .channels(2)
32//!     .sample_format(SampleFormat::F32)
33//!     .build();
34//!
35//! // Create media info
36//! let media = MediaInfo::builder()
37//!     .path("/path/to/video.mp4")
38//!     .format("mp4")
39//!     .format_long_name("QuickTime / MOV")
40//!     .duration(Duration::from_secs(120))
41//!     .file_size(1_000_000)
42//!     .bitrate(8_000_000)
43//!     .video_stream(video)
44//!     .audio_stream(audio)
45//!     .metadata("title", "Sample Video")
46//!     .metadata("artist", "Test Artist")
47//!     .build();
48//!
49//! assert!(media.has_video());
50//! assert!(media.has_audio());
51//! assert_eq!(media.resolution(), Some((1920, 1080)));
52//! ```
53
54use std::collections::HashMap;
55use std::path::{Path, PathBuf};
56use std::time::Duration;
57
58use crate::stream::{AudioStreamInfo, VideoStreamInfo};
59
60/// Information about a media file.
61///
62/// This struct contains all metadata about a media container, including
63/// format information, duration, file size, and all contained streams.
64///
65/// # Construction
66///
67/// Use [`MediaInfo::builder()`] for fluent construction:
68///
69/// ```
70/// use ff_format::media::MediaInfo;
71/// use std::time::Duration;
72///
73/// let info = MediaInfo::builder()
74///     .path("/path/to/video.mp4")
75///     .format("mp4")
76///     .duration(Duration::from_secs(120))
77///     .file_size(1_000_000)
78///     .build();
79/// ```
80#[derive(Debug, Clone)]
81pub struct MediaInfo {
82    /// File path
83    path: PathBuf,
84    /// Container format name (e.g., "mp4", "mkv", "avi")
85    format: String,
86    /// Long format name from the container format description
87    format_long_name: Option<String>,
88    /// Total duration
89    duration: Duration,
90    /// File size in bytes
91    file_size: u64,
92    /// Overall bitrate in bits per second
93    bitrate: Option<u64>,
94    /// Video streams in the file
95    video_streams: Vec<VideoStreamInfo>,
96    /// Audio streams in the file
97    audio_streams: Vec<AudioStreamInfo>,
98    /// File metadata (title, artist, etc.)
99    metadata: HashMap<String, String>,
100}
101
102impl MediaInfo {
103    /// Creates a new builder for constructing `MediaInfo`.
104    ///
105    /// # Examples
106    ///
107    /// ```
108    /// use ff_format::media::MediaInfo;
109    /// use std::time::Duration;
110    ///
111    /// let info = MediaInfo::builder()
112    ///     .path("/path/to/video.mp4")
113    ///     .format("mp4")
114    ///     .duration(Duration::from_secs(120))
115    ///     .file_size(1_000_000)
116    ///     .build();
117    /// ```
118    #[must_use]
119    pub fn builder() -> MediaInfoBuilder {
120        MediaInfoBuilder::default()
121    }
122
123    /// Returns the file path.
124    #[must_use]
125    #[inline]
126    pub fn path(&self) -> &Path {
127        &self.path
128    }
129
130    /// Returns the container format name.
131    #[must_use]
132    #[inline]
133    pub fn format(&self) -> &str {
134        &self.format
135    }
136
137    /// Returns the long format name, if available.
138    #[must_use]
139    #[inline]
140    pub fn format_long_name(&self) -> Option<&str> {
141        self.format_long_name.as_deref()
142    }
143
144    /// Returns the total duration.
145    #[must_use]
146    #[inline]
147    pub const fn duration(&self) -> Duration {
148        self.duration
149    }
150
151    /// Returns the file size in bytes.
152    #[must_use]
153    #[inline]
154    pub const fn file_size(&self) -> u64 {
155        self.file_size
156    }
157
158    /// Returns the overall bitrate in bits per second, if known.
159    #[must_use]
160    #[inline]
161    pub const fn bitrate(&self) -> Option<u64> {
162        self.bitrate
163    }
164
165    /// Returns all video streams in the file.
166    #[must_use]
167    #[inline]
168    pub fn video_streams(&self) -> &[VideoStreamInfo] {
169        &self.video_streams
170    }
171
172    /// Returns all audio streams in the file.
173    #[must_use]
174    #[inline]
175    pub fn audio_streams(&self) -> &[AudioStreamInfo] {
176        &self.audio_streams
177    }
178
179    /// Returns the file metadata.
180    #[must_use]
181    #[inline]
182    pub fn metadata(&self) -> &HashMap<String, String> {
183        &self.metadata
184    }
185
186    /// Returns a specific metadata value by key.
187    #[must_use]
188    #[inline]
189    pub fn metadata_value(&self, key: &str) -> Option<&str> {
190        self.metadata.get(key).map(String::as_str)
191    }
192
193    // === Stream Query Methods ===
194
195    /// Returns `true` if the file contains at least one video stream.
196    #[must_use]
197    #[inline]
198    pub fn has_video(&self) -> bool {
199        !self.video_streams.is_empty()
200    }
201
202    /// Returns `true` if the file contains at least one audio stream.
203    #[must_use]
204    #[inline]
205    pub fn has_audio(&self) -> bool {
206        !self.audio_streams.is_empty()
207    }
208
209    /// Returns the number of video streams.
210    #[must_use]
211    #[inline]
212    pub fn video_stream_count(&self) -> usize {
213        self.video_streams.len()
214    }
215
216    /// Returns the number of audio streams.
217    #[must_use]
218    #[inline]
219    pub fn audio_stream_count(&self) -> usize {
220        self.audio_streams.len()
221    }
222
223    /// Returns the total number of streams (video + audio).
224    #[must_use]
225    #[inline]
226    pub fn stream_count(&self) -> usize {
227        self.video_streams.len() + self.audio_streams.len()
228    }
229
230    // === Primary Stream Selection ===
231
232    /// Returns the primary video stream.
233    ///
234    /// The primary video stream is the first video stream in the file.
235    /// Returns `None` if there are no video streams.
236    #[must_use]
237    #[inline]
238    pub fn primary_video(&self) -> Option<&VideoStreamInfo> {
239        self.video_streams.first()
240    }
241
242    /// Returns the primary audio stream.
243    ///
244    /// The primary audio stream is the first audio stream in the file.
245    /// Returns `None` if there are no audio streams.
246    #[must_use]
247    #[inline]
248    pub fn primary_audio(&self) -> Option<&AudioStreamInfo> {
249        self.audio_streams.first()
250    }
251
252    /// Returns a video stream by index within the video streams list.
253    #[must_use]
254    #[inline]
255    pub fn video_stream(&self, index: usize) -> Option<&VideoStreamInfo> {
256        self.video_streams.get(index)
257    }
258
259    /// Returns an audio stream by index within the audio streams list.
260    #[must_use]
261    #[inline]
262    pub fn audio_stream(&self, index: usize) -> Option<&AudioStreamInfo> {
263        self.audio_streams.get(index)
264    }
265
266    // === Convenience Methods ===
267
268    /// Returns the resolution of the primary video stream.
269    ///
270    /// Returns `None` if there are no video streams.
271    #[must_use]
272    #[inline]
273    pub fn resolution(&self) -> Option<(u32, u32)> {
274        self.primary_video().map(|v| (v.width(), v.height()))
275    }
276
277    /// Returns the frame rate of the primary video stream.
278    ///
279    /// Returns `None` if there are no video streams.
280    #[must_use]
281    #[inline]
282    pub fn frame_rate(&self) -> Option<f64> {
283        self.primary_video().map(VideoStreamInfo::fps)
284    }
285
286    /// Returns the sample rate of the primary audio stream.
287    ///
288    /// Returns `None` if there are no audio streams.
289    #[must_use]
290    #[inline]
291    pub fn sample_rate(&self) -> Option<u32> {
292        self.primary_audio().map(AudioStreamInfo::sample_rate)
293    }
294
295    /// Returns the channel count of the primary audio stream.
296    ///
297    /// Returns `None` if there are no audio streams.
298    #[must_use]
299    #[inline]
300    pub fn channels(&self) -> Option<u32> {
301        self.primary_audio().map(AudioStreamInfo::channels)
302    }
303
304    /// Returns `true` if this is a video-only file (no audio streams).
305    #[must_use]
306    #[inline]
307    pub fn is_video_only(&self) -> bool {
308        self.has_video() && !self.has_audio()
309    }
310
311    /// Returns `true` if this is an audio-only file (no video streams).
312    #[must_use]
313    #[inline]
314    pub fn is_audio_only(&self) -> bool {
315        self.has_audio() && !self.has_video()
316    }
317
318    /// Returns the file name (without directory path).
319    #[must_use]
320    #[inline]
321    pub fn file_name(&self) -> Option<&str> {
322        self.path.file_name().and_then(|n| n.to_str())
323    }
324
325    /// Returns the file extension.
326    #[must_use]
327    #[inline]
328    pub fn extension(&self) -> Option<&str> {
329        self.path.extension().and_then(|e| e.to_str())
330    }
331
332    // === Common Metadata Accessors ===
333
334    /// Returns the title from metadata.
335    ///
336    /// This is a convenience method for accessing the "title" metadata key,
337    /// which is commonly used by media containers to store the title of the content.
338    #[must_use]
339    #[inline]
340    pub fn title(&self) -> Option<&str> {
341        self.metadata_value("title")
342    }
343
344    /// Returns the artist from metadata.
345    ///
346    /// This is a convenience method for accessing the "artist" metadata key.
347    #[must_use]
348    #[inline]
349    pub fn artist(&self) -> Option<&str> {
350        self.metadata_value("artist")
351    }
352
353    /// Returns the album from metadata.
354    ///
355    /// This is a convenience method for accessing the "album" metadata key.
356    #[must_use]
357    #[inline]
358    pub fn album(&self) -> Option<&str> {
359        self.metadata_value("album")
360    }
361
362    /// Returns the creation time from metadata.
363    ///
364    /// This is a convenience method for accessing the `creation_time` metadata key,
365    /// which is commonly used by media containers to store when the file was created.
366    /// The format is typically ISO 8601 (e.g., `2024-01-15T10:30:00.000000Z`).
367    #[must_use]
368    #[inline]
369    pub fn creation_time(&self) -> Option<&str> {
370        self.metadata_value("creation_time")
371    }
372
373    /// Returns the date from metadata.
374    ///
375    /// This is a convenience method for accessing the "date" metadata key.
376    #[must_use]
377    #[inline]
378    pub fn date(&self) -> Option<&str> {
379        self.metadata_value("date")
380    }
381
382    /// Returns the comment from metadata.
383    ///
384    /// This is a convenience method for accessing the "comment" metadata key.
385    #[must_use]
386    #[inline]
387    pub fn comment(&self) -> Option<&str> {
388        self.metadata_value("comment")
389    }
390
391    /// Returns the encoder from metadata.
392    ///
393    /// This is a convenience method for accessing the "encoder" metadata key,
394    /// which stores information about the software used to create the file.
395    #[must_use]
396    #[inline]
397    pub fn encoder(&self) -> Option<&str> {
398        self.metadata_value("encoder")
399    }
400}
401
402impl Default for MediaInfo {
403    fn default() -> Self {
404        Self {
405            path: PathBuf::new(),
406            format: String::new(),
407            format_long_name: None,
408            duration: Duration::ZERO,
409            file_size: 0,
410            bitrate: None,
411            video_streams: Vec::new(),
412            audio_streams: Vec::new(),
413            metadata: HashMap::new(),
414        }
415    }
416}
417
418/// Builder for constructing [`MediaInfo`].
419///
420/// # Examples
421///
422/// ```
423/// use ff_format::media::MediaInfo;
424/// use std::time::Duration;
425///
426/// let info = MediaInfo::builder()
427///     .path("/path/to/video.mp4")
428///     .format("mp4")
429///     .format_long_name("QuickTime / MOV")
430///     .duration(Duration::from_secs(120))
431///     .file_size(1_000_000)
432///     .bitrate(8_000_000)
433///     .metadata("title", "Sample Video")
434///     .build();
435/// ```
436#[derive(Debug, Clone, Default)]
437pub struct MediaInfoBuilder {
438    path: PathBuf,
439    format: String,
440    format_long_name: Option<String>,
441    duration: Duration,
442    file_size: u64,
443    bitrate: Option<u64>,
444    video_streams: Vec<VideoStreamInfo>,
445    audio_streams: Vec<AudioStreamInfo>,
446    metadata: HashMap<String, String>,
447}
448
449impl MediaInfoBuilder {
450    /// Sets the file path.
451    #[must_use]
452    pub fn path(mut self, path: impl Into<PathBuf>) -> Self {
453        self.path = path.into();
454        self
455    }
456
457    /// Sets the container format name.
458    #[must_use]
459    pub fn format(mut self, format: impl Into<String>) -> Self {
460        self.format = format.into();
461        self
462    }
463
464    /// Sets the long format name.
465    #[must_use]
466    pub fn format_long_name(mut self, name: impl Into<String>) -> Self {
467        self.format_long_name = Some(name.into());
468        self
469    }
470
471    /// Sets the total duration.
472    #[must_use]
473    pub fn duration(mut self, duration: Duration) -> Self {
474        self.duration = duration;
475        self
476    }
477
478    /// Sets the file size in bytes.
479    #[must_use]
480    pub fn file_size(mut self, size: u64) -> Self {
481        self.file_size = size;
482        self
483    }
484
485    /// Sets the overall bitrate in bits per second.
486    #[must_use]
487    pub fn bitrate(mut self, bitrate: u64) -> Self {
488        self.bitrate = Some(bitrate);
489        self
490    }
491
492    /// Adds a video stream.
493    #[must_use]
494    pub fn video_stream(mut self, stream: VideoStreamInfo) -> Self {
495        self.video_streams.push(stream);
496        self
497    }
498
499    /// Sets all video streams at once, replacing any existing streams.
500    #[must_use]
501    pub fn video_streams(mut self, streams: Vec<VideoStreamInfo>) -> Self {
502        self.video_streams = streams;
503        self
504    }
505
506    /// Adds an audio stream.
507    #[must_use]
508    pub fn audio_stream(mut self, stream: AudioStreamInfo) -> Self {
509        self.audio_streams.push(stream);
510        self
511    }
512
513    /// Sets all audio streams at once, replacing any existing streams.
514    #[must_use]
515    pub fn audio_streams(mut self, streams: Vec<AudioStreamInfo>) -> Self {
516        self.audio_streams = streams;
517        self
518    }
519
520    /// Adds a metadata key-value pair.
521    #[must_use]
522    pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
523        self.metadata.insert(key.into(), value.into());
524        self
525    }
526
527    /// Sets all metadata at once, replacing any existing metadata.
528    #[must_use]
529    pub fn metadata_map(mut self, metadata: HashMap<String, String>) -> Self {
530        self.metadata = metadata;
531        self
532    }
533
534    /// Builds the [`MediaInfo`].
535    #[must_use]
536    pub fn build(self) -> MediaInfo {
537        MediaInfo {
538            path: self.path,
539            format: self.format,
540            format_long_name: self.format_long_name,
541            duration: self.duration,
542            file_size: self.file_size,
543            bitrate: self.bitrate,
544            video_streams: self.video_streams,
545            audio_streams: self.audio_streams,
546            metadata: self.metadata,
547        }
548    }
549}
550
551#[cfg(test)]
552mod tests {
553    use super::*;
554    use crate::codec::{AudioCodec, VideoCodec};
555    use crate::time::Rational;
556    use crate::{PixelFormat, SampleFormat};
557
558    fn sample_video_stream() -> VideoStreamInfo {
559        VideoStreamInfo::builder()
560            .index(0)
561            .codec(VideoCodec::H264)
562            .codec_name("h264")
563            .width(1920)
564            .height(1080)
565            .frame_rate(Rational::new(30, 1))
566            .pixel_format(PixelFormat::Yuv420p)
567            .duration(Duration::from_secs(120))
568            .build()
569    }
570
571    fn sample_audio_stream() -> AudioStreamInfo {
572        AudioStreamInfo::builder()
573            .index(1)
574            .codec(AudioCodec::Aac)
575            .codec_name("aac")
576            .sample_rate(48000)
577            .channels(2)
578            .sample_format(SampleFormat::F32)
579            .duration(Duration::from_secs(120))
580            .build()
581    }
582
583    mod media_info_tests {
584        use super::*;
585
586        #[test]
587        fn test_builder_basic() {
588            let info = MediaInfo::builder()
589                .path("/path/to/video.mp4")
590                .format("mp4")
591                .duration(Duration::from_secs(120))
592                .file_size(1_000_000)
593                .build();
594
595            assert_eq!(info.path(), Path::new("/path/to/video.mp4"));
596            assert_eq!(info.format(), "mp4");
597            assert_eq!(info.duration(), Duration::from_secs(120));
598            assert_eq!(info.file_size(), 1_000_000);
599            assert!(info.format_long_name().is_none());
600            assert!(info.bitrate().is_none());
601        }
602
603        #[test]
604        fn test_builder_full() {
605            let video = sample_video_stream();
606            let audio = sample_audio_stream();
607
608            let info = MediaInfo::builder()
609                .path("/path/to/video.mp4")
610                .format("mp4")
611                .format_long_name("QuickTime / MOV")
612                .duration(Duration::from_secs(120))
613                .file_size(150_000_000)
614                .bitrate(10_000_000)
615                .video_stream(video)
616                .audio_stream(audio)
617                .metadata("title", "Test Video")
618                .metadata("artist", "Test Artist")
619                .build();
620
621            assert_eq!(info.format_long_name(), Some("QuickTime / MOV"));
622            assert_eq!(info.bitrate(), Some(10_000_000));
623            assert_eq!(info.video_stream_count(), 1);
624            assert_eq!(info.audio_stream_count(), 1);
625            assert_eq!(info.metadata_value("title"), Some("Test Video"));
626            assert_eq!(info.metadata_value("artist"), Some("Test Artist"));
627            assert!(info.metadata_value("nonexistent").is_none());
628        }
629
630        #[test]
631        fn test_default() {
632            let info = MediaInfo::default();
633            assert_eq!(info.path(), Path::new(""));
634            assert_eq!(info.format(), "");
635            assert_eq!(info.duration(), Duration::ZERO);
636            assert_eq!(info.file_size(), 0);
637            assert!(!info.has_video());
638            assert!(!info.has_audio());
639        }
640
641        #[test]
642        fn test_has_streams() {
643            // No streams
644            let empty = MediaInfo::default();
645            assert!(!empty.has_video());
646            assert!(!empty.has_audio());
647
648            // Video only
649            let video_only = MediaInfo::builder()
650                .video_stream(sample_video_stream())
651                .build();
652            assert!(video_only.has_video());
653            assert!(!video_only.has_audio());
654            assert!(video_only.is_video_only());
655            assert!(!video_only.is_audio_only());
656
657            // Audio only
658            let audio_only = MediaInfo::builder()
659                .audio_stream(sample_audio_stream())
660                .build();
661            assert!(!audio_only.has_video());
662            assert!(audio_only.has_audio());
663            assert!(!audio_only.is_video_only());
664            assert!(audio_only.is_audio_only());
665
666            // Both
667            let both = MediaInfo::builder()
668                .video_stream(sample_video_stream())
669                .audio_stream(sample_audio_stream())
670                .build();
671            assert!(both.has_video());
672            assert!(both.has_audio());
673            assert!(!both.is_video_only());
674            assert!(!both.is_audio_only());
675        }
676
677        #[test]
678        fn test_primary_streams() {
679            let video1 = VideoStreamInfo::builder()
680                .index(0)
681                .width(1920)
682                .height(1080)
683                .build();
684            let video2 = VideoStreamInfo::builder()
685                .index(2)
686                .width(1280)
687                .height(720)
688                .build();
689            let audio1 = AudioStreamInfo::builder()
690                .index(1)
691                .sample_rate(48000)
692                .build();
693            let audio2 = AudioStreamInfo::builder()
694                .index(3)
695                .sample_rate(44100)
696                .build();
697
698            let info = MediaInfo::builder()
699                .video_stream(video1)
700                .video_stream(video2)
701                .audio_stream(audio1)
702                .audio_stream(audio2)
703                .build();
704
705            // Primary should be first
706            let primary_video = info.primary_video().unwrap();
707            assert_eq!(primary_video.width(), 1920);
708            assert_eq!(primary_video.index(), 0);
709
710            let primary_audio = info.primary_audio().unwrap();
711            assert_eq!(primary_audio.sample_rate(), 48000);
712            assert_eq!(primary_audio.index(), 1);
713        }
714
715        #[test]
716        fn test_stream_access_by_index() {
717            let video1 = VideoStreamInfo::builder().width(1920).build();
718            let video2 = VideoStreamInfo::builder().width(1280).build();
719            let audio1 = AudioStreamInfo::builder().sample_rate(48000).build();
720
721            let info = MediaInfo::builder()
722                .video_stream(video1)
723                .video_stream(video2)
724                .audio_stream(audio1)
725                .build();
726
727            assert_eq!(info.video_stream(0).unwrap().width(), 1920);
728            assert_eq!(info.video_stream(1).unwrap().width(), 1280);
729            assert!(info.video_stream(2).is_none());
730
731            assert_eq!(info.audio_stream(0).unwrap().sample_rate(), 48000);
732            assert!(info.audio_stream(1).is_none());
733        }
734
735        #[test]
736        fn test_resolution_and_frame_rate() {
737            let info = MediaInfo::builder()
738                .video_stream(sample_video_stream())
739                .build();
740
741            assert_eq!(info.resolution(), Some((1920, 1080)));
742            assert!((info.frame_rate().unwrap() - 30.0).abs() < 0.001);
743
744            // No video
745            let no_video = MediaInfo::default();
746            assert!(no_video.resolution().is_none());
747            assert!(no_video.frame_rate().is_none());
748        }
749
750        #[test]
751        fn test_sample_rate_and_channels() {
752            let info = MediaInfo::builder()
753                .audio_stream(sample_audio_stream())
754                .build();
755
756            assert_eq!(info.sample_rate(), Some(48000));
757            assert_eq!(info.channels(), Some(2));
758
759            // No audio
760            let no_audio = MediaInfo::default();
761            assert!(no_audio.sample_rate().is_none());
762            assert!(no_audio.channels().is_none());
763        }
764
765        #[test]
766        fn test_stream_counts() {
767            let info = MediaInfo::builder()
768                .video_stream(sample_video_stream())
769                .video_stream(sample_video_stream())
770                .audio_stream(sample_audio_stream())
771                .audio_stream(sample_audio_stream())
772                .audio_stream(sample_audio_stream())
773                .build();
774
775            assert_eq!(info.video_stream_count(), 2);
776            assert_eq!(info.audio_stream_count(), 3);
777            assert_eq!(info.stream_count(), 5);
778        }
779
780        #[test]
781        fn test_file_name_and_extension() {
782            let info = MediaInfo::builder().path("/path/to/my_video.mp4").build();
783
784            assert_eq!(info.file_name(), Some("my_video.mp4"));
785            assert_eq!(info.extension(), Some("mp4"));
786
787            // Empty path
788            let empty = MediaInfo::default();
789            assert!(empty.file_name().is_none());
790            assert!(empty.extension().is_none());
791        }
792
793        #[test]
794        fn test_metadata_operations() {
795            let mut map = HashMap::new();
796            map.insert("key1".to_string(), "value1".to_string());
797            map.insert("key2".to_string(), "value2".to_string());
798
799            let info = MediaInfo::builder()
800                .metadata_map(map)
801                .metadata("key3", "value3")
802                .build();
803
804            assert_eq!(info.metadata().len(), 3);
805            assert_eq!(info.metadata_value("key1"), Some("value1"));
806            assert_eq!(info.metadata_value("key2"), Some("value2"));
807            assert_eq!(info.metadata_value("key3"), Some("value3"));
808        }
809
810        #[test]
811        fn test_clone() {
812            let info = MediaInfo::builder()
813                .path("/path/to/video.mp4")
814                .format("mp4")
815                .format_long_name("QuickTime / MOV")
816                .duration(Duration::from_secs(120))
817                .file_size(1_000_000)
818                .video_stream(sample_video_stream())
819                .audio_stream(sample_audio_stream())
820                .metadata("title", "Test")
821                .build();
822
823            let cloned = info.clone();
824            assert_eq!(info.path(), cloned.path());
825            assert_eq!(info.format(), cloned.format());
826            assert_eq!(info.format_long_name(), cloned.format_long_name());
827            assert_eq!(info.duration(), cloned.duration());
828            assert_eq!(info.file_size(), cloned.file_size());
829            assert_eq!(info.video_stream_count(), cloned.video_stream_count());
830            assert_eq!(info.audio_stream_count(), cloned.audio_stream_count());
831            assert_eq!(info.metadata_value("title"), cloned.metadata_value("title"));
832        }
833
834        #[test]
835        fn test_debug() {
836            let info = MediaInfo::builder()
837                .path("/path/to/video.mp4")
838                .format("mp4")
839                .duration(Duration::from_secs(120))
840                .file_size(1_000_000)
841                .build();
842
843            let debug = format!("{info:?}");
844            assert!(debug.contains("MediaInfo"));
845            assert!(debug.contains("mp4"));
846        }
847
848        #[test]
849        fn test_video_streams_setter() {
850            let streams = vec![sample_video_stream(), sample_video_stream()];
851
852            let info = MediaInfo::builder().video_streams(streams).build();
853
854            assert_eq!(info.video_stream_count(), 2);
855        }
856
857        #[test]
858        fn test_audio_streams_setter() {
859            let streams = vec![
860                sample_audio_stream(),
861                sample_audio_stream(),
862                sample_audio_stream(),
863            ];
864
865            let info = MediaInfo::builder().audio_streams(streams).build();
866
867            assert_eq!(info.audio_stream_count(), 3);
868        }
869    }
870
871    mod media_info_builder_tests {
872        use super::*;
873
874        #[test]
875        fn test_builder_default() {
876            let builder = MediaInfoBuilder::default();
877            let info = builder.build();
878            assert_eq!(info.path(), Path::new(""));
879            assert_eq!(info.format(), "");
880            assert_eq!(info.duration(), Duration::ZERO);
881        }
882
883        #[test]
884        fn test_builder_clone() {
885            let builder = MediaInfo::builder()
886                .path("/path/to/video.mp4")
887                .format("mp4")
888                .duration(Duration::from_secs(120));
889
890            let cloned = builder.clone();
891            let info1 = builder.build();
892            let info2 = cloned.build();
893
894            assert_eq!(info1.path(), info2.path());
895            assert_eq!(info1.format(), info2.format());
896            assert_eq!(info1.duration(), info2.duration());
897        }
898
899        #[test]
900        fn test_builder_debug() {
901            let builder = MediaInfo::builder()
902                .path("/path/to/video.mp4")
903                .format("mp4");
904
905            let debug = format!("{builder:?}");
906            assert!(debug.contains("MediaInfoBuilder"));
907        }
908    }
909
910    mod metadata_convenience_tests {
911        use super::*;
912
913        #[test]
914        fn test_title() {
915            let info = MediaInfo::builder()
916                .metadata("title", "Sample Video Title")
917                .build();
918
919            assert_eq!(info.title(), Some("Sample Video Title"));
920        }
921
922        #[test]
923        fn test_title_missing() {
924            let info = MediaInfo::default();
925            assert!(info.title().is_none());
926        }
927
928        #[test]
929        fn test_artist() {
930            let info = MediaInfo::builder()
931                .metadata("artist", "Test Artist")
932                .build();
933
934            assert_eq!(info.artist(), Some("Test Artist"));
935        }
936
937        #[test]
938        fn test_album() {
939            let info = MediaInfo::builder().metadata("album", "Test Album").build();
940
941            assert_eq!(info.album(), Some("Test Album"));
942        }
943
944        #[test]
945        fn test_creation_time() {
946            let info = MediaInfo::builder()
947                .metadata("creation_time", "2024-01-15T10:30:00.000000Z")
948                .build();
949
950            assert_eq!(info.creation_time(), Some("2024-01-15T10:30:00.000000Z"));
951        }
952
953        #[test]
954        fn test_date() {
955            let info = MediaInfo::builder().metadata("date", "2024-01-15").build();
956
957            assert_eq!(info.date(), Some("2024-01-15"));
958        }
959
960        #[test]
961        fn test_comment() {
962            let info = MediaInfo::builder()
963                .metadata("comment", "This is a test comment")
964                .build();
965
966            assert_eq!(info.comment(), Some("This is a test comment"));
967        }
968
969        #[test]
970        fn test_encoder() {
971            let info = MediaInfo::builder()
972                .metadata("encoder", "Lavf58.76.100")
973                .build();
974
975            assert_eq!(info.encoder(), Some("Lavf58.76.100"));
976        }
977
978        #[test]
979        fn test_multiple_metadata_fields() {
980            let info = MediaInfo::builder()
981                .metadata("title", "My Video")
982                .metadata("artist", "John Doe")
983                .metadata("album", "My Collection")
984                .metadata("date", "2024")
985                .metadata("comment", "A great video")
986                .metadata("encoder", "FFmpeg")
987                .metadata("custom_field", "custom_value")
988                .build();
989
990            assert_eq!(info.title(), Some("My Video"));
991            assert_eq!(info.artist(), Some("John Doe"));
992            assert_eq!(info.album(), Some("My Collection"));
993            assert_eq!(info.date(), Some("2024"));
994            assert_eq!(info.comment(), Some("A great video"));
995            assert_eq!(info.encoder(), Some("FFmpeg"));
996            assert_eq!(info.metadata_value("custom_field"), Some("custom_value"));
997            assert_eq!(info.metadata().len(), 7);
998        }
999    }
1000}