Initial commit
This commit is contained in:
154
src/media.rs
Normal file
154
src/media.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::config::QualityTags;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MediaInfo {
|
||||
pub duration_seconds: Option<f64>,
|
||||
pub height: Option<u32>,
|
||||
pub codec: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct FfprobeOutput {
|
||||
format: Option<FfprobeFormat>,
|
||||
streams: Option<Vec<FfprobeStream>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct FfprobeFormat {
|
||||
duration: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct FfprobeStream {
|
||||
codec_type: Option<String>,
|
||||
codec_name: Option<String>,
|
||||
height: Option<u32>,
|
||||
}
|
||||
|
||||
pub fn probe(path: &Path) -> Result<MediaInfo> {
|
||||
let output = Command::new("ffprobe")
|
||||
.arg("-v")
|
||||
.arg("error")
|
||||
.arg("-print_format")
|
||||
.arg("json")
|
||||
.arg("-show_format")
|
||||
.arg("-show_streams")
|
||||
.arg(path)
|
||||
.output()
|
||||
.with_context(|| format!("failed to run ffprobe on {}", path.display()))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"ffprobe failed for {}: {}",
|
||||
path.display(),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
let parsed: FfprobeOutput = serde_json::from_slice(&output.stdout)
|
||||
.with_context(|| "failed to parse ffprobe JSON")?;
|
||||
|
||||
let duration_seconds = parsed
|
||||
.format
|
||||
.and_then(|fmt| fmt.duration)
|
||||
.and_then(|dur| dur.parse::<f64>().ok());
|
||||
|
||||
let video_stream = parsed
|
||||
.streams
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.find(|stream| stream.codec_type.as_deref() == Some("video"));
|
||||
|
||||
let (height, codec) = if let Some(stream) = video_stream {
|
||||
(stream.height, stream.codec_name)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
Ok(MediaInfo {
|
||||
duration_seconds,
|
||||
height,
|
||||
codec,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn quality_tag(info: &MediaInfo, tags: &QualityTags) -> Option<String> {
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
if tags.resolution {
|
||||
if let Some(res) = resolution_tag(info.height) {
|
||||
parts.push(res);
|
||||
}
|
||||
}
|
||||
if tags.codec {
|
||||
if let Some(codec) = codec_tag(info.codec.as_deref()) {
|
||||
parts.push(codec);
|
||||
}
|
||||
}
|
||||
if tags.source {
|
||||
// Source tagging not implemented yet; placeholder for future expansion.
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(parts.join(" "))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolution_tag(height: Option<u32>) -> Option<String> {
|
||||
let height = height?;
|
||||
let tag = if height >= 2160 {
|
||||
"2160p"
|
||||
} else if height >= 1080 {
|
||||
"1080p"
|
||||
} else if height >= 720 {
|
||||
"720p"
|
||||
} else if height >= 480 {
|
||||
"480p"
|
||||
} else {
|
||||
"360p"
|
||||
};
|
||||
Some(tag.to_string())
|
||||
}
|
||||
|
||||
pub fn codec_tag(codec: Option<&str>) -> Option<String> {
|
||||
let codec = codec?.to_ascii_lowercase();
|
||||
let tag = if codec.contains("hevc") || codec.contains("h265") || codec.contains("x265") {
|
||||
"x265"
|
||||
} else if codec.contains("h264") || codec.contains("x264") {
|
||||
"x264"
|
||||
} else if codec.contains("av1") {
|
||||
"av1"
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
Some(tag.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{codec_tag, resolution_tag};
|
||||
|
||||
#[test]
|
||||
fn resolution_tags() {
|
||||
assert_eq!(resolution_tag(Some(2160)).as_deref(), Some("2160p"));
|
||||
assert_eq!(resolution_tag(Some(1080)).as_deref(), Some("1080p"));
|
||||
assert_eq!(resolution_tag(Some(720)).as_deref(), Some("720p"));
|
||||
assert_eq!(resolution_tag(Some(480)).as_deref(), Some("480p"));
|
||||
assert_eq!(resolution_tag(Some(360)).as_deref(), Some("360p"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_tags() {
|
||||
assert_eq!(codec_tag(Some("h264")).as_deref(), Some("x264"));
|
||||
assert_eq!(codec_tag(Some("hevc")).as_deref(), Some("x265"));
|
||||
assert_eq!(codec_tag(Some("av1")).as_deref(), Some("av1"));
|
||||
assert_eq!(codec_tag(Some("vp9")), None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user