unbundle 5.2.0

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

use std::path::Path;
use std::time::Duration;

use unbundle::{MediaFile, ThumbnailHandle, ThumbnailOptions};

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

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

    let mut unbundler = MediaFile::open(path).expect("Failed to open");
    let thumb = ThumbnailHandle::at_timestamp(&mut unbundler, Duration::from_secs(1), 320)
        .expect("Failed to generate thumbnail");

    // The longest edge should be <= 320.
    assert!(
        thumb.width() <= 320 && thumb.height() <= 320,
        "Thumbnail should fit within 320px, got {}x{}",
        thumb.width(),
        thumb.height()
    );
    assert!(thumb.width() > 0 && thumb.height() > 0);
}

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

    let mut unbundler = MediaFile::open(path).expect("Failed to open");
    let thumb =
        ThumbnailHandle::at_frame(&mut unbundler, 30, 200).expect("Failed to generate thumbnail");

    assert!(
        thumb.width() <= 200 && thumb.height() <= 200,
        "Thumbnail should fit within 200px"
    );
}

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

    let mut unbundler = MediaFile::open(path).expect("Failed to open");
    let video_meta = unbundler.metadata().video.as_ref().unwrap().clone();
    let original_ratio = video_meta.width as f64 / video_meta.height as f64;

    let thumb =
        ThumbnailHandle::at_frame(&mut unbundler, 0, 640).expect("Failed to generate thumbnail");
    let thumb_ratio = thumb.width() as f64 / thumb.height() as f64;

    assert!(
        (original_ratio - thumb_ratio).abs() < 0.1,
        "Aspect ratio should be preserved: original={original_ratio:.2}, thumb={thumb_ratio:.2}"
    );
}

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

    let mut unbundler = MediaFile::open(path).expect("Failed to open");
    let config = ThumbnailOptions::new(3, 2).with_thumbnail_width(160);
    let grid = ThumbnailHandle::grid(&mut unbundler, &config).expect("Failed to generate grid");

    // Grid should be 3 * 160 = 480 wide.
    assert_eq!(grid.width(), 480, "Grid width should be 3 × 160 = 480");
    assert!(grid.height() > 0, "Grid should have non-zero height");
}

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

    let mut unbundler = MediaFile::open(path).expect("Failed to open");
    let config = ThumbnailOptions::new(2, 2);
    assert_eq!(config.thumbnail_width, 320, "Default width should be 320");

    let grid = ThumbnailHandle::grid(&mut unbundler, &config).expect("Failed to generate grid");

    assert_eq!(grid.width(), 640, "Grid width should be 2 × 320 = 640");
}

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

    let mut unbundler = MediaFile::open(path).expect("Failed to open");
    let thumb = ThumbnailHandle::smart(&mut unbundler, 10, 320)
        .expect("Failed to generate smart thumbnail");

    // Smart thumbnail should pick a frame with content, not pure black.
    let gray = thumb.to_luma8();
    let mean: f64 =
        gray.as_raw().iter().map(|&p| p as f64).sum::<f64>() / gray.as_raw().len() as f64;

    // The testsrc fixture is colourful, so mean should be well above 0.
    assert!(
        mean > 10.0,
        "Smart thumbnail should not be a black frame (mean={mean:.1})"
    );
}

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

    let mut unbundler = MediaFile::open(path).expect("Failed to open");
    let thumb =
        ThumbnailHandle::smart(&mut unbundler, 5, 256).expect("Failed to generate smart thumbnail");

    assert!(
        thumb.width() <= 256 && thumb.height() <= 256,
        "Should fit within 256px, got {}x{}",
        thumb.width(),
        thumb.height()
    );
}

#[test]
fn thumbnail_options_alias_builder() {
    let config = ThumbnailOptions::new(4, 3).thumbnail_width(200);
    assert_eq!(config.columns, 4);
    assert_eq!(config.rows, 3);
    assert_eq!(config.thumbnail_width, 200);
}

#[test]
fn thumbnail_config_builder() {
    let config = ThumbnailOptions::new(4, 3).with_thumbnail_width(200);
    assert_eq!(config.columns, 4);
    assert_eq!(config.rows, 3);
    assert_eq!(config.thumbnail_width, 200);
}