unbundle 5.2.0

Unbundle media files - extract still frames, audio tracks, and subtitles from video files
Documentation
//! Chapter metadata integration tests.
//!
//! These tests require the fixture files generated by
//! `tests/fixtures/generate_fixtures.sh` (or `.bat` on Windows).

use std::path::Path;

use unbundle::MediaFile;

fn sample_with_chapters_path() -> &'static str {
    "tests/fixtures/sample_with_chapters.mkv"
}

fn sample_video_path() -> &'static str {
    "tests/fixtures/sample_video.mp4"
}

#[test]
fn chapters_present_in_file_with_chapters() {
    let path = sample_with_chapters_path();
    if !Path::new(path).exists() {
        eprintln!("Skipping: fixture '{path}' not found.");
        return;
    }

    let unbundler = MediaFile::open(path).expect("Failed to open");
    let metadata = unbundler.metadata();
    let chapters = metadata
        .chapters
        .as_ref()
        .expect("Expected chapters in fixture");

    assert_eq!(chapters.len(), 3, "Expected 3 chapters");
}

#[test]
fn chapter_titles_match() {
    let path = sample_with_chapters_path();
    if !Path::new(path).exists() {
        eprintln!("Skipping: fixture '{path}' not found.");
        return;
    }

    let unbundler = MediaFile::open(path).expect("Failed to open");
    let chapters = unbundler
        .metadata()
        .chapters
        .as_ref()
        .expect("Expected chapters")
        .clone();

    assert_eq!(chapters[0].title.as_deref(), Some("Introduction"));
    assert_eq!(chapters[1].title.as_deref(), Some("Main Content"));
    assert_eq!(chapters[2].title.as_deref(), Some("Conclusion"));
}

#[test]
fn chapter_timestamps_are_ordered() {
    let path = sample_with_chapters_path();
    if !Path::new(path).exists() {
        eprintln!("Skipping: fixture '{path}' not found.");
        return;
    }

    let unbundler = MediaFile::open(path).expect("Failed to open");
    let chapters = unbundler
        .metadata()
        .chapters
        .as_ref()
        .expect("Expected chapters")
        .clone();

    for window in chapters.windows(2) {
        assert!(
            window[0].end <= window[1].start || window[0].end == window[1].start,
            "Chapters should be ordered: {:?} should end before {:?} starts",
            window[0],
            window[1]
        );
    }
}

#[test]
fn chapter_indices_are_sequential() {
    let path = sample_with_chapters_path();
    if !Path::new(path).exists() {
        eprintln!("Skipping: fixture '{path}' not found.");
        return;
    }

    let unbundler = MediaFile::open(path).expect("Failed to open");
    let chapters = unbundler
        .metadata()
        .chapters
        .as_ref()
        .expect("Expected chapters")
        .clone();

    for (i, chapter) in chapters.iter().enumerate() {
        assert_eq!(chapter.index, i, "Chapter index should be sequential");
    }
}

#[test]
fn no_chapters_in_file_without() {
    let path = sample_video_path();
    if !Path::new(path).exists() {
        eprintln!("Skipping: fixture '{path}' not found.");
        return;
    }

    let unbundler = MediaFile::open(path).expect("Failed to open");
    assert!(
        unbundler.metadata().chapters.is_none(),
        "sample_video.mp4 should have no chapters"
    );
}

#[test]
fn chapter_start_end_durations() {
    let path = sample_with_chapters_path();
    if !Path::new(path).exists() {
        eprintln!("Skipping: fixture '{path}' not found.");
        return;
    }

    let unbundler = MediaFile::open(path).expect("Failed to open");
    let chapters = unbundler
        .metadata()
        .chapters
        .as_ref()
        .expect("Expected chapters")
        .clone();

    // First chapter: 0s–2s
    let first = &chapters[0];
    assert!(
        first.start.as_secs_f64() < 0.1,
        "First chapter should start near 0"
    );
    assert!(
        (first.end.as_secs_f64() - 2.0).abs() < 0.1,
        "First chapter should end near 2s, got {:?}",
        first.end,
    );
}