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