use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MediaInfo {
pub format: String,
pub mime_type: String,
pub size: u64,
pub duration_secs: Option<f64>,
pub width: Option<u32>,
pub height: Option<u32>,
pub sample_rate: Option<u32>,
pub channels: Option<u8>,
pub bit_depth: Option<u8>,
pub frame_rate: Option<f64>,
pub codec: Option<String>,
pub raw_data: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MediaFormat {
Png,
Jpeg,
WebP,
Gif,
Bmp,
Tiff,
Mp4,
WebM,
Avi,
Mkv,
Mov,
Mp3,
Flac,
Wav,
Ogg,
Aac,
}
impl MediaFormat {
pub fn from_extension(ext: &str) -> Option<Self> {
match ext.to_lowercase().as_str() {
"png" => Some(Self::Png),
"jpg" | "jpeg" => Some(Self::Jpeg),
"webp" => Some(Self::WebP),
"gif" => Some(Self::Gif),
"bmp" => Some(Self::Bmp),
"tiff" | "tif" => Some(Self::Tiff),
"mp4" | "m4v" => Some(Self::Mp4),
"webm" => Some(Self::WebM),
"avi" => Some(Self::Avi),
"mkv" => Some(Self::Mkv),
"mov" => Some(Self::Mov),
"mp3" => Some(Self::Mp3),
"flac" => Some(Self::Flac),
"wav" => Some(Self::Wav),
"ogg" | "oga" => Some(Self::Ogg),
"aac" | "m4a" => Some(Self::Aac),
_ => None,
}
}
pub fn is_image(&self) -> bool {
matches!(
self,
Self::Png | Self::Jpeg | Self::WebP | Self::Gif | Self::Bmp | Self::Tiff
)
}
pub fn is_video(&self) -> bool {
matches!(
self,
Self::Mp4 | Self::WebM | Self::Avi | Self::Mkv | Self::Mov
)
}
pub fn is_audio(&self) -> bool {
matches!(
self,
Self::Mp3 | Self::Flac | Self::Wav | Self::Ogg | Self::Aac
)
}
pub fn mime_type(&self) -> &'static str {
match self {
Self::Png => "image/png",
Self::Jpeg => "image/jpeg",
Self::WebP => "image/webp",
Self::Gif => "image/gif",
Self::Bmp => "image/bmp",
Self::Tiff => "image/tiff",
Self::Mp4 => "video/mp4",
Self::WebM => "video/webm",
Self::Avi => "video/x-msvideo",
Self::Mkv => "video/x-matroska",
Self::Mov => "video/quicktime",
Self::Mp3 => "audio/mpeg",
Self::Flac => "audio/flac",
Self::Wav => "audio/wav",
Self::Ogg => "audio/ogg",
Self::Aac => "audio/aac",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DocumentFormat {
Pdf,
Html,
Xml,
Markdown,
Rst,
Latex,
Docx,
Odt,
}
impl DocumentFormat {
pub fn from_extension(ext: &str) -> Option<Self> {
match ext.to_lowercase().as_str() {
"pdf" => Some(Self::Pdf),
"html" | "htm" => Some(Self::Html),
"xml" => Some(Self::Xml),
"md" | "markdown" => Some(Self::Markdown),
"rst" => Some(Self::Rst),
"tex" | "latex" => Some(Self::Latex),
"docx" => Some(Self::Docx),
"odt" => Some(Self::Odt),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CodeFormat {
Rust,
Python,
JavaScript,
TypeScript,
C,
Cpp,
Java,
Go,
Ruby,
Php,
Swift,
Kotlin,
Scala,
Haskell,
Shell,
Sql,
}
impl CodeFormat {
pub fn from_extension(ext: &str) -> Option<Self> {
match ext.to_lowercase().as_str() {
"rs" => Some(Self::Rust),
"py" | "pyw" => Some(Self::Python),
"js" | "mjs" | "cjs" => Some(Self::JavaScript),
"ts" | "tsx" => Some(Self::TypeScript),
"c" | "h" => Some(Self::C),
"cpp" | "cc" | "cxx" | "hpp" | "hxx" => Some(Self::Cpp),
"java" => Some(Self::Java),
"go" => Some(Self::Go),
"rb" => Some(Self::Ruby),
"php" => Some(Self::Php),
"swift" => Some(Self::Swift),
"kt" | "kts" => Some(Self::Kotlin),
"scala" | "sc" => Some(Self::Scala),
"hs" => Some(Self::Haskell),
"sh" | "bash" | "zsh" => Some(Self::Shell),
"sql" => Some(Self::Sql),
_ => None,
}
}
}
pub struct FormatHandler;
impl FormatHandler {
#[cfg(feature = "media-formats")]
pub fn extract_media_info(path: &Path) -> anyhow::Result<MediaInfo> {
use std::fs;
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
let format = MediaFormat::from_extension(ext)
.ok_or_else(|| anyhow::anyhow!("Unsupported media format: {}", ext))?;
let metadata = fs::metadata(path)?;
let size = metadata.len();
if format.is_image() {
Self::extract_image_info(path, format, size)
} else if format.is_video() {
Self::extract_video_info(path, format, size)
} else if format.is_audio() {
Self::extract_audio_info(path, format, size)
} else {
Err(anyhow::anyhow!("Unknown media format"))
}
}
#[cfg(feature = "media-formats")]
fn extract_image_info(
path: &Path,
format: MediaFormat,
size: u64,
) -> anyhow::Result<MediaInfo> {
use image::GenericImageView;
let img = image::open(path)?;
let (width, height) = img.dimensions();
let raw_data = img.to_rgb8().into_raw();
Ok(MediaInfo {
format: format!("{:?}", format).to_lowercase(),
mime_type: format.mime_type().into(),
size,
duration_secs: None,
width: Some(width),
height: Some(height),
sample_rate: None,
channels: None,
bit_depth: None,
frame_rate: None,
codec: None,
raw_data,
})
}
#[cfg(feature = "media-formats")]
fn extract_video_info(
_path: &Path,
format: MediaFormat,
size: u64,
) -> anyhow::Result<MediaInfo> {
Ok(MediaInfo {
format: format!("{:?}", format).to_lowercase(),
mime_type: format.mime_type().into(),
size,
duration_secs: None,
width: None,
height: None,
sample_rate: None,
channels: None,
bit_depth: None,
frame_rate: None,
codec: None,
raw_data: Vec::new(),
})
}
#[cfg(feature = "media-formats")]
fn extract_audio_info(
path: &Path,
format: MediaFormat,
size: u64,
) -> anyhow::Result<MediaInfo> {
use symphonia::core::formats::FormatOptions;
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;
let file = std::fs::File::open(path)?;
let mss = MediaSourceStream::new(Box::new(file), Default::default());
let mut hint = Hint::new();
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
hint.with_extension(ext);
}
let format_opts = FormatOptions::default();
let metadata_opts = MetadataOptions::default();
let probed =
symphonia::default::get_probe().format(&hint, mss, &format_opts, &metadata_opts)?;
let reader = probed.format;
let track = reader
.default_track()
.ok_or_else(|| anyhow::anyhow!("No audio track found"))?;
let codec_params = track.codec_params.clone();
let duration = codec_params.n_frames.and_then(|frames| {
codec_params
.sample_rate
.map(|rate| frames as f64 / rate as f64)
});
let raw_samples: Vec<u8> = Vec::new();
Ok(MediaInfo {
format: format!("{:?}", format).to_lowercase(),
mime_type: format.mime_type().into(),
size,
duration_secs: duration,
width: None,
height: None,
sample_rate: codec_params.sample_rate,
channels: codec_params.channels.map(|c| c.count() as u8),
bit_depth: codec_params.bits_per_sample.map(|b| b as u8),
frame_rate: None,
codec: Some(format!("{:?}", codec_params.codec)),
raw_data: raw_samples,
})
}
#[cfg(not(feature = "media-formats"))]
pub fn extract_media_info(_path: &Path) -> anyhow::Result<MediaInfo> {
Err(anyhow::anyhow!(
"Media format support not enabled. Enable the 'media-formats' feature."
))
}
pub fn detect_format(path: &Path) -> FormatType {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if MediaFormat::from_extension(ext).is_some() {
FormatType::Media
} else if DocumentFormat::from_extension(ext).is_some() {
FormatType::Document
} else if CodeFormat::from_extension(ext).is_some() {
FormatType::Code
} else {
match ext.to_lowercase().as_str() {
"json" | "jsonl" | "ndjson" | "csv" | "tsv" | "yaml" | "yml" | "toml" => {
FormatType::Structured
}
"txt" | "text" | "log" => FormatType::PlainText,
_ => FormatType::Binary,
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FormatType {
PlainText,
Structured,
Code,
Document,
Media,
Binary,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_media_format_detection() {
assert!(MediaFormat::from_extension("png").unwrap().is_image());
assert!(MediaFormat::from_extension("mp4").unwrap().is_video());
assert!(MediaFormat::from_extension("mp3").unwrap().is_audio());
assert!(MediaFormat::from_extension("txt").is_none());
}
#[test]
fn test_code_format_detection() {
assert_eq!(CodeFormat::from_extension("rs"), Some(CodeFormat::Rust));
assert_eq!(CodeFormat::from_extension("py"), Some(CodeFormat::Python));
assert_eq!(CodeFormat::from_extension("unknown"), None);
}
#[test]
fn test_format_type_detection() {
use std::path::PathBuf;
assert_eq!(
FormatHandler::detect_format(&PathBuf::from("test.png")),
FormatType::Media
);
assert_eq!(
FormatHandler::detect_format(&PathBuf::from("test.rs")),
FormatType::Code
);
assert_eq!(
FormatHandler::detect_format(&PathBuf::from("test.json")),
FormatType::Structured
);
}
}