unbundle 5.2.0

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

use std::{path::Path, time::Duration};

use unbundle::{MediaFile, SubtitleFormat, UnbundleError};

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

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

// ── happy‐path tests ───────────────────────────────────────────────

#[test]
fn extract_subtitle_entries() {
    let path = sample_with_subtitles();
    if !Path::new(path).exists() {
        return;
    }

    let mut unbundler = MediaFile::open(path).expect("Failed to open fixture");
    let entries = unbundler
        .subtitle()
        .extract()
        .expect("Failed to extract subtitles");

    assert!(!entries.is_empty(), "Expected at least one subtitle entry");
    let first = &entries[0];
    assert!(!first.text.is_empty(), "Subtitle text should not be empty");
    assert!(first.end_time >= first.start_time, "end_time >= start_time");
}

#[test]
fn save_subtitles_srt() {
    let path = sample_with_subtitles();
    if !Path::new(path).exists() {
        return;
    }

    let tmp = tempfile::NamedTempFile::new().expect("Failed to create temp file");
    let output_path = tmp.path().with_extension("srt");

    let mut unbundler = MediaFile::open(path).expect("Failed to open fixture");
    unbundler
        .subtitle()
        .save(&output_path, SubtitleFormat::Srt)
        .expect("Failed to save SRT");

    let content = std::fs::read_to_string(&output_path).expect("Failed to read SRT");
    assert!(!content.is_empty(), "SRT file should not be empty");
    // SRT files have numeric cue indices.
    assert!(
        content.contains("1\n") || content.contains("1\r\n"),
        "SRT should start with cue 1"
    );
    assert!(
        content.contains("-->"),
        "SRT should contain --> timestamp separator"
    );

    let _ = std::fs::remove_file(&output_path);
}

#[test]
fn save_subtitles_webvtt() {
    let path = sample_with_subtitles();
    if !Path::new(path).exists() {
        return;
    }

    let tmp = tempfile::NamedTempFile::new().expect("Failed to create temp file");
    let output_path = tmp.path().with_extension("vtt");

    let mut unbundler = MediaFile::open(path).expect("Failed to open fixture");
    unbundler
        .subtitle()
        .save(&output_path, SubtitleFormat::WebVtt)
        .expect("Failed to save WebVTT");

    let content = std::fs::read_to_string(&output_path).expect("Failed to read WebVTT");
    assert!(
        content.starts_with("WEBVTT"),
        "WebVTT must have WEBVTT header"
    );
    assert!(
        content.contains("-->"),
        "WebVTT should contain --> separator"
    );

    let _ = std::fs::remove_file(&output_path);
}

#[test]
fn extract_subtitle_text() {
    let path = sample_with_subtitles();
    if !Path::new(path).exists() {
        return;
    }

    let mut unbundler = MediaFile::open(path).expect("Failed to open fixture");
    let text = unbundler
        .subtitle()
        .extract_text(SubtitleFormat::Raw)
        .expect("Failed to extract subtitle text");

    assert!(!text.is_empty(), "Extracted text should not be empty");
}

#[test]
fn subtitle_format_display() {
    assert_eq!(format!("{}", SubtitleFormat::Srt), "SRT");
    assert_eq!(format!("{}", SubtitleFormat::WebVtt), "WebVTT");
    assert_eq!(format!("{}", SubtitleFormat::Raw), "Raw");
}

// ── error tests ────────────────────────────────────────────────────

#[test]
fn no_subtitle_stream_error() {
    let path = sample_video_path();
    if !Path::new(path).exists() {
        return;
    }

    let mut unbundler = MediaFile::open(path).expect("Failed to open fixture");
    let result = unbundler.subtitle().extract();
    assert!(result.is_err());

    match result.unwrap_err() {
        UnbundleError::NoSubtitleStream => {}
        other => panic!("Expected NoSubtitleStream, got: {other}"),
    }
}

// ── metadata tests ─────────────────────────────────────────────────

#[test]
fn subtitle_metadata_present() {
    let path = sample_with_subtitles();
    if !Path::new(path).exists() {
        return;
    }

    let unbundler = MediaFile::open(path).expect("Failed to open fixture");
    let metadata = unbundler.metadata();
    assert!(
        metadata.subtitle.is_some(),
        "Expected subtitle metadata for fixture with subtitles",
    );

    let sub = metadata.subtitle.as_ref().unwrap();
    assert!(!sub.codec.is_empty(), "Subtitle codec should be non-empty");
}

#[test]
fn subtitle_tracks_listed() {
    let path = sample_with_subtitles();
    if !Path::new(path).exists() {
        return;
    }

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

    if let Some(tracks) = &metadata.subtitle_tracks {
        assert!(!tracks.is_empty(), "Expected at least one subtitle track");
    }
}

#[test]
fn render_at_returns_none_when_no_bitmap_event_at_time() {
    let path = sample_with_subtitles();
    if !Path::new(path).exists() {
        return;
    }

    let mut unbundler = MediaFile::open(path).expect("Failed to open fixture");
    let rendered = unbundler
        .subtitle()
        .render_at(Duration::from_secs(0))
        .expect("render_at failed");

    if let Some(image) = rendered {
        assert!(image.width() > 0);
        assert!(image.height() > 0);
    }
}

#[test]
fn bitmap_event_as_image_accessor_compiles_and_returns_image() {
    let path = sample_with_subtitles();
    if !Path::new(path).exists() {
        return;
    }

    let mut unbundler = MediaFile::open(path).expect("Failed to open fixture");
    let bitmaps = unbundler
        .subtitle()
        .extract_bitmaps()
        .expect("extract_bitmaps failed");

    if let Some(first) = bitmaps.first() {
        let image = first.as_image();
        assert!(image.width() > 0);
        assert!(image.height() > 0);
    }
}