Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/znippy

plugins/native/znippy-plugin-media/src/native.rs

//! The `ArchiveTypePlugin` implementation registered in the znippy CLI.
//!
//! One Arrow schema spans image/audio/video; a per-file [`MediaMeta`] fills only
//! the columns relevant to its kind and the rest serialize to null cells. The
//! writer maps `ExtensionValue::U32` onto `UInt32` columns and everything else
//! onto `Utf8`, so numeric facets (width / duration / sample rate) stay typed and
//! queryable.

use std::collections::HashMap;

use znippy_common::plugin::{
    default_command, ArchiveTypePlugin, Col, ExtensionRow, ExtensionValue, Claim, HandlerSpec,
};

use crate::{MEDIA_EXTENSIONS, MEDIA_TYPE_ID, MediaMeta, extract_media_metadata};

/// Native media handler: images (dims/color/EXIF), audio (duration/rate/tags),
/// video (dims/codecs/framerate). Stores the blob verbatim, extracts metadata.
pub struct NativeMediaPlugin;

impl NativeMediaPlugin {
    pub fn new() -> Self {
        NativeMediaPlugin
    }
}

impl Default for NativeMediaPlugin {
    fn default() -> Self {
        Self::new()
    }
}

/// The media handler, stated once.
pub static MEDIA_SPEC: HandlerSpec = HandlerSpec {
    name: "media",
    aliases: &["image", "audio", "video"],
    type_id: MEDIA_TYPE_ID,
    ecosystem: "Media files (images / audio / video)",
    extensions: MEDIA_EXTENSIONS,
    // Case-insensitive (`IMG.JPG` is a photo), as `is_media_path`.
    claim: Claim::ExtensionsAnyCase,
    description: "Image / audio / video metadata (dimensions, EXIF, duration, codecs, \
                  tags) — pure-Rust, blob stored verbatim",
    fields: &[
        ("media_kind", Col::Utf8),
        ("format", Col::Utf8),
        ("width", Col::U32),
        ("height", Col::U32),
        ("color", Col::Utf8),
        ("duration_ms", Col::U32),
        ("sample_rate", Col::U32),
        ("channels", Col::U32),
        ("bitrate", Col::U32),
        ("audio_codec", Col::Utf8),
        ("video_codec", Col::Utf8),
        ("framerate", Col::Utf8),
        ("title", Col::Utf8),
        ("artist", Col::Utf8),
        ("album", Col::Utf8),
        ("camera", Col::Utf8),
        ("datetime", Col::Utf8),
        ("orientation", Col::U32),
        ("gps", Col::Utf8),
    ],
    commands: &[("probe", "Print parsed media metadata for a file (kind/format/dims/duration/codecs)")],
};

impl ArchiveTypePlugin for NativeMediaPlugin {
    fn spec(&self) -> &HandlerSpec {
        &MEDIA_SPEC
    }

    fn run_command(&self, cmd: &str, args: &[String]) -> anyhow::Result<()> {
        if cmd != "probe" {
            return default_command(self, cmd, args);
        }
        let path = args.first().ok_or_else(|| anyhow::anyhow!("usage: media probe <file>"))?;
        let data = std::fs::read(path)?;
        match extract_media_metadata(path, &data) {
            Some(m) => {
                println!("{:#?}", m);
                Ok(())
            }
            None => anyhow::bail!("media: could not parse '{}'", path),
        }
    }

    fn extract_metadata(&self, path: &str, data: &[u8]) -> Option<ExtensionRow> {
        let m = extract_media_metadata(path, data)?;
        Some(media_meta_to_row(&m))
    }
}

/// Map a [`MediaMeta`] onto the Arrow row. Only present fields are inserted;
/// absent keys yield null cells (the writer appends null when a key is missing).
fn media_meta_to_row(m: &MediaMeta) -> ExtensionRow {
    let mut f: HashMap<String, ExtensionValue> = HashMap::new();

    // String columns
    if let Some(kind) = m.kind {
        f.insert("media_kind".to_string(), ExtensionValue::Str(kind.as_str().to_string()));
    }
    let mut put_str = |k: &str, v: &Option<String>| {
        if let Some(s) = v {
            f.insert(k.to_string(), ExtensionValue::Str(s.clone()));
        }
    };
    put_str("format", &m.format);
    put_str("color", &m.color);
    put_str("audio_codec", &m.audio_codec);
    put_str("video_codec", &m.video_codec);
    put_str("framerate", &m.framerate);
    put_str("title", &m.title);
    put_str("artist", &m.artist);
    put_str("album", &m.album);
    put_str("camera", &m.camera);
    put_str("datetime", &m.datetime);
    put_str("gps", &m.gps);

    // Numeric (UInt32) columns
    let mut put_u32 = |k: &str, v: Option<u32>| {
        if let Some(n) = v {
            f.insert(k.to_string(), ExtensionValue::U32(n));
        }
    };
    put_u32("width", m.width);
    put_u32("height", m.height);
    put_u32("duration_ms", m.duration_ms);
    put_u32("sample_rate", m.sample_rate);
    put_u32("channels", m.channels);
    put_u32("bitrate", m.bitrate);
    put_u32("orientation", m.orientation);

    ExtensionRow { fields: f }
}