use crate::error::{EncodingErrorMode, MediaInfoError, Result};
use crate::ffi::{MediaInfoHandle, MediaInfoLib};
use crate::platform;
use crate::track::Track;
use crate::xml;
use log::{debug, warn};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::{Read, Seek, SeekFrom, Write};
use std::net::TcpStream;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct LibVersion {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl LibVersion {
pub fn new(major: u32, minor: u32, patch: u32) -> Self {
LibVersion {
major,
minor,
patch,
}
}
pub fn parse(version_str: &str) -> Option<Self> {
let parts: Vec<&str> = version_str.split('.').collect();
if parts.is_empty() {
return None;
}
let major = parts.first()?.parse().ok()?;
let minor = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
Some(LibVersion {
major,
minor,
patch,
})
}
pub fn supports_cover_data(&self) -> bool {
*self >= LibVersion::new(18, 3, 0)
}
pub fn supports_reset(&self) -> bool {
*self >= LibVersion::new(19, 9, 0)
}
pub fn xml_option_name(&self) -> &'static str {
if *self >= LibVersion::new(17, 10, 0) {
"OLDXML"
} else {
"XML"
}
}
#[allow(dead_code)]
pub fn is_thread_safe(&self) -> bool {
*self >= LibVersion::new(20, 3, 0)
}
}
#[derive(Debug, Clone)]
pub struct ParseOptions {
pub library_file: Option<PathBuf>,
pub library_search_dir: Option<PathBuf>,
pub cover_data: bool,
pub parse_speed: f32,
pub full: bool,
pub legacy_stream_display: bool,
pub mediainfo_options: Option<HashMap<String, String>>,
pub output: Option<String>,
pub buffer_size: Option<usize>,
pub encoding_errors: EncodingErrorMode,
}
pub trait ReadSeek: Read + Seek {}
impl<T: Read + Seek + ?Sized> ReadSeek for T {}
pub enum MediaInfoInput<'a> {
Path(&'a Path),
Url(&'a str),
Reader(&'a mut dyn ReadSeek),
}
pub trait MediaInfoSource<'a> {
fn into_input(self) -> MediaInfoInput<'a>;
}
impl<'a> MediaInfoSource<'a> for MediaInfoInput<'a> {
fn into_input(self) -> MediaInfoInput<'a> {
self
}
}
impl<'a> MediaInfoSource<'a> for &'a Path {
fn into_input(self) -> MediaInfoInput<'a> {
MediaInfoInput::Path(self)
}
}
impl<'a> MediaInfoSource<'a> for &'a PathBuf {
fn into_input(self) -> MediaInfoInput<'a> {
MediaInfoInput::Path(self.as_path())
}
}
impl<'a> MediaInfoSource<'a> for &'a str {
fn into_input(self) -> MediaInfoInput<'a> {
if self.contains("://") {
MediaInfoInput::Url(self)
} else {
MediaInfoInput::Path(Path::new(self))
}
}
}
impl<'a> MediaInfoSource<'a> for &'a String {
fn into_input(self) -> MediaInfoInput<'a> {
self.as_str().into_input()
}
}
impl<'a, R: ReadSeek> MediaInfoSource<'a> for &'a mut R {
fn into_input(self) -> MediaInfoInput<'a> {
MediaInfoInput::Reader(self)
}
}
impl<'a> MediaInfoSource<'a> for &'a mut dyn ReadSeek {
fn into_input(self) -> MediaInfoInput<'a> {
MediaInfoInput::Reader(self)
}
}
impl Default for ParseOptions {
fn default() -> Self {
ParseOptions {
library_file: None,
library_search_dir: None,
cover_data: false,
parse_speed: 0.5,
full: true,
legacy_stream_display: false,
mediainfo_options: None,
output: None,
buffer_size: Some(64 * 1024),
encoding_errors: EncodingErrorMode::default(),
}
}
}
impl ParseOptions {
pub fn new() -> Self {
Self::default()
}
pub fn library_file<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.library_file = Some(path.into());
self
}
pub fn library_search_dir<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.library_search_dir = Some(path.into());
self
}
pub fn cover_data(mut self, cover_data: bool) -> Self {
self.cover_data = cover_data;
self
}
pub fn parse_speed(mut self, speed: f32) -> Self {
self.parse_speed = speed;
self
}
pub fn full(mut self, full: bool) -> Self {
self.full = full;
self
}
pub fn legacy_stream_display(mut self, legacy: bool) -> Self {
self.legacy_stream_display = legacy;
self
}
pub fn mediainfo_options(mut self, options: HashMap<String, String>) -> Self {
self.mediainfo_options = Some(options);
self
}
pub fn output<S: Into<String>>(mut self, output: S) -> Self {
self.output = Some(output.into());
self
}
pub fn buffer_size(mut self, size: Option<usize>) -> Self {
self.buffer_size = size;
self
}
pub fn encoding_errors(mut self, mode: EncodingErrorMode) -> Self {
self.encoding_errors = mode;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MediaInfo {
tracks: Vec<Track>,
}
fn parse_lock() -> std::sync::MutexGuard<'static, ()> {
static PARSE_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
PARSE_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("parse lock poisoned")
}
#[derive(Debug, Clone, PartialEq)]
pub enum ParseOutput {
MediaInfo(MediaInfo),
Output(String),
}
impl ParseOutput {
pub fn as_media_info(&self) -> Option<&MediaInfo> {
match self {
ParseOutput::MediaInfo(mi) => Some(mi),
ParseOutput::Output(_) => None,
}
}
pub fn as_output(&self) -> Option<&str> {
match self {
ParseOutput::MediaInfo(_) => None,
ParseOutput::Output(text) => Some(text.as_str()),
}
}
pub fn into_media_info(self) -> Option<MediaInfo> {
match self {
ParseOutput::MediaInfo(mi) => Some(mi),
ParseOutput::Output(_) => None,
}
}
pub fn into_output(self) -> Option<String> {
match self {
ParseOutput::MediaInfo(_) => None,
ParseOutput::Output(text) => Some(text),
}
}
}
impl MediaInfo {
pub fn new(tracks: Vec<Track>) -> Self {
MediaInfo { tracks }
}
pub fn from_xml(xml: &str) -> Result<Self> {
Self::from_xml_with_encoding(xml, EncodingErrorMode::Strict)
}
pub fn from_xml_with_encoding(xml: &str, encoding_mode: EncodingErrorMode) -> Result<Self> {
let tracks = xml::parse_xml_with_encoding(xml, encoding_mode)?;
Ok(MediaInfo { tracks })
}
pub fn from_xml_bytes_with_encoding(
xml_bytes: &[u8],
encoding_mode: EncodingErrorMode,
) -> Result<Self> {
let tracks = xml::parse_xml_bytes_with_encoding(xml_bytes, encoding_mode)?;
Ok(MediaInfo { tracks })
}
pub fn from_xml_bytes(xml_bytes: &[u8]) -> Result<Self> {
Self::from_xml_bytes_with_encoding(xml_bytes, EncodingErrorMode::Strict)
}
pub fn can_parse(library_file: Option<&Path>) -> bool {
Self::load_library(library_file, None).is_ok()
}
pub fn library_version(library_file: Option<&Path>) -> Result<(String, LibVersion)> {
let (_handle, version_number, version) = Self::load_library(library_file, None)?;
Ok((version_number, version))
}
pub fn parse<'a, S>(source: S) -> Result<ParseOutput>
where
S: MediaInfoSource<'a>,
{
Self::parse_with_options(source, &ParseOptions::default())
}
pub fn parse_path<P: AsRef<Path>>(path: P) -> Result<ParseOutput> {
Self::parse(path.as_ref())
}
pub fn parse_media_info<'a, S>(source: S) -> Result<Self>
where
S: MediaInfoSource<'a>,
{
Self::parse_media_info_with_options(source, &ParseOptions::default())
}
pub fn parse_media_info_path<P: AsRef<Path>>(path: P) -> Result<Self> {
Self::parse_media_info(path.as_ref())
}
pub fn parse_media_info_with_options<'a, S>(source: S, options: &ParseOptions) -> Result<Self>
where
S: MediaInfoSource<'a>,
{
if options.output.is_some() {
return Err(MediaInfoError::invalid_input(
"output is only supported by parse or parse_with_options",
));
}
match Self::parse_with_options(source, options)? {
ParseOutput::MediaInfo(mi) => Ok(mi),
ParseOutput::Output(_) => Err(MediaInfoError::invalid_input(
"output is only supported by parse or parse_with_options",
)),
}
}
pub fn parse_media_info_path_with_options<P: AsRef<Path>>(
path: P,
options: &ParseOptions,
) -> Result<Self> {
Self::parse_media_info_with_options(path.as_ref(), options)
}
pub fn parse_with_options<'a, S>(source: S, options: &ParseOptions) -> Result<ParseOutput>
where
S: MediaInfoSource<'a>,
{
let input = source.into_input();
Self::parse_input_with_options(input, options)
}
pub fn parse_path_with_options<P: AsRef<Path>>(
path: P,
options: &ParseOptions,
) -> Result<ParseOutput> {
Self::parse_with_options(path.as_ref(), options)
}
pub fn parse_source<'a, S>(source: S) -> Result<ParseOutput>
where
S: MediaInfoSource<'a>,
{
Self::parse(source)
}
pub fn parse_source_with_options<'a, S>(
source: S,
options: &ParseOptions,
) -> Result<ParseOutput>
where
S: MediaInfoSource<'a>,
{
Self::parse_with_options(source, options)
}
pub fn parse_to_string<P: AsRef<Path>>(filename: P, output_format: &str) -> Result<String> {
let options = ParseOptions::new().output(output_format.to_string());
Self::parse_to_string_internal(filename, &options)
}
pub fn parse_to_string_with_options<P: AsRef<Path>>(
filename: P,
options: &ParseOptions,
) -> Result<String> {
Self::parse_to_string_internal(filename, options)
}
fn parse_to_string_internal<P: AsRef<Path>>(
filename: P,
options: &ParseOptions,
) -> Result<String> {
let _parse_guard = parse_lock();
Self::parse_to_string_internal_unlocked(filename, options)
}
fn parse_to_string_internal_unlocked<P: AsRef<Path>>(
filename: P,
options: &ParseOptions,
) -> Result<String> {
let path = filename.as_ref();
let path_str = path.to_string_lossy().to_string();
if path_str.contains("://") {
return Self::parse_to_string_from_url_unlocked(&path_str, options);
}
let (handle, version_number, version) = Self::load_library(
options.library_file.as_deref(),
options.library_search_dir.as_deref(),
)?;
Self::configure_parse_options(&handle, &version, &version_number, options);
Self::configure_output_options(&handle, &version, options);
if !path.exists() {
return Err(MediaInfoError::file_not_found(path));
}
if handle.open(&path_str) == 0 {
return Err(MediaInfoError::parse_error(&path_str));
}
let output = handle.inform();
if options.mediainfo_options.is_some() && version.supports_reset() {
handle.option("Reset", "");
}
Ok(output)
}
fn parse_to_string_from_url(url: &str, options: &ParseOptions) -> Result<String> {
let _parse_guard = parse_lock();
Self::parse_to_string_from_url_unlocked(url, options)
}
fn parse_to_string_from_url_unlocked(url: &str, options: &ParseOptions) -> Result<String> {
let (handle, version_number, version) = Self::load_library(
options.library_file.as_deref(),
options.library_search_dir.as_deref(),
)?;
Self::configure_parse_options(&handle, &version, &version_number, options);
Self::configure_output_options(&handle, &version, options);
if handle.open(url) == 0 {
if options.mediainfo_options.is_some() && version.supports_reset() {
handle.option("Reset", "");
}
if url.starts_with("http://") {
debug!(
"native URL opener could not handle {}; falling back to built-in HTTP fetch",
url
);
return Self::parse_url_via_http(url, options);
}
return Err(MediaInfoError::parse_error(url));
}
let output = handle.inform();
if options.mediainfo_options.is_some() && version.supports_reset() {
handle.option("Reset", "");
}
Ok(output)
}
pub fn parse_from_reader<R: ReadSeek>(reader: &mut R) -> Result<ParseOutput> {
Self::parse_from_reader_with_options(reader, &ParseOptions::default())
}
pub fn parse_from_reader_with_options<R: ReadSeek>(
reader: &mut R,
options: &ParseOptions,
) -> Result<ParseOutput> {
if options.output.is_some() {
let output = Self::parse_reader_to_string_internal(reader, options)?;
return Ok(ParseOutput::Output(output));
}
let output = Self::parse_reader_to_string_internal(reader, options)?;
let mi =
MediaInfo::from_xml_bytes_with_encoding(output.as_bytes(), options.encoding_errors)?;
Ok(ParseOutput::MediaInfo(mi))
}
pub fn parse_input(input: MediaInfoInput<'_>) -> Result<ParseOutput> {
Self::parse_input_with_options(input, &ParseOptions::default())
}
pub fn parse_input_with_options(
input: MediaInfoInput<'_>,
options: &ParseOptions,
) -> Result<ParseOutput> {
if options.output.is_some() {
let output = Self::parse_input_to_string_with_options(input, options)?;
return Ok(ParseOutput::Output(output));
}
let output = Self::parse_input_to_string_with_options(input, options)?;
let mi =
MediaInfo::from_xml_bytes_with_encoding(output.as_bytes(), options.encoding_errors)?;
Ok(ParseOutput::MediaInfo(mi))
}
pub fn parse_input_to_string(input: MediaInfoInput<'_>, output_format: &str) -> Result<String> {
let options = ParseOptions::new().output(output_format.to_string());
Self::parse_input_to_string_with_options(input, &options)
}
pub fn parse_input_to_string_with_options(
input: MediaInfoInput<'_>,
options: &ParseOptions,
) -> Result<String> {
match input {
MediaInfoInput::Path(path) => Self::parse_to_string_with_options(path, options),
MediaInfoInput::Url(url) => Self::parse_to_string_from_url(url, options),
MediaInfoInput::Reader(reader) => {
Self::parse_reader_to_string_with_options(reader, options)
}
}
}
pub fn parse_reader_to_string<R: ReadSeek + ?Sized>(
reader: &mut R,
output_format: &str,
) -> Result<String> {
let options = ParseOptions::new().output(output_format.to_string());
Self::parse_reader_to_string_internal(reader, &options)
}
pub fn parse_reader_to_string_with_options<R: ReadSeek + ?Sized>(
reader: &mut R,
options: &ParseOptions,
) -> Result<String> {
Self::parse_reader_to_string_internal(reader, options)
}
fn parse_reader_to_string_internal<R: ReadSeek + ?Sized>(
reader: &mut R,
options: &ParseOptions,
) -> Result<String> {
let _parse_guard = parse_lock();
Self::parse_reader_to_string_internal_unlocked(reader, options)
}
fn parse_reader_to_string_internal_unlocked<R: ReadSeek + ?Sized>(
reader: &mut R,
options: &ParseOptions,
) -> Result<String> {
let file_size = reader.seek(SeekFrom::End(0))?;
reader.seek(SeekFrom::Start(0))?;
let (handle, version_number, version) = Self::load_library(
options.library_file.as_deref(),
options.library_search_dir.as_deref(),
)?;
Self::configure_parse_options(&handle, &version, &version_number, options);
Self::configure_output_options(&handle, &version, options);
handle.open_buffer_init(file_size, 0);
if let Some(buffer_size) = options.buffer_size {
let mut buffer = vec![0u8; buffer_size];
loop {
let bytes_read = reader.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
let result = handle.open_buffer_continue(&buffer[..bytes_read]);
if result & 0x08 != 0 {
break;
}
let seek_pos = handle.open_buffer_continue_goto_get();
if seek_pos != u64::MAX {
reader.seek(SeekFrom::Start(seek_pos))?;
let current_pos = reader.stream_position()?;
handle.open_buffer_init(file_size, current_pos);
}
}
} else {
let mut buffer = Vec::new();
loop {
buffer.clear();
reader.read_to_end(&mut buffer)?;
if buffer.is_empty() {
break;
}
let result = handle.open_buffer_continue(&buffer);
if result & 0x08 != 0 {
break;
}
let seek_pos = handle.open_buffer_continue_goto_get();
if seek_pos != u64::MAX {
reader.seek(SeekFrom::Start(seek_pos))?;
let current_pos = reader.stream_position()?;
handle.open_buffer_init(file_size, current_pos);
} else {
break;
}
}
}
handle.open_buffer_finalize();
let output = handle.inform();
if options.mediainfo_options.is_some() && version.supports_reset() {
handle.option("Reset", "");
}
Ok(output)
}
fn parse_url_via_http(url: &str, options: &ParseOptions) -> Result<String> {
let bytes = Self::fetch_http_bytes(url).map_err(|_| MediaInfoError::parse_error(url))?;
let mut cursor = std::io::Cursor::new(bytes);
Self::parse_reader_to_string_internal_unlocked(&mut cursor, options)
}
fn fetch_http_bytes(url: &str) -> Result<Vec<u8>> {
let (host, host_header, path, port) =
Self::split_http_url(url).ok_or_else(|| MediaInfoError::parse_error(url))?;
let mut stream = TcpStream::connect((host.as_str(), port))?;
let request = format!(
"GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
path, host_header
);
stream.write_all(request.as_bytes())?;
let mut response = Vec::new();
stream.read_to_end(&mut response)?;
let header_end = Self::find_http_header_end(&response)
.ok_or_else(|| MediaInfoError::parse_error(url))?;
let status_line = response
.get(..header_end)
.and_then(|header| header.split(|b| *b == b'\n').next())
.and_then(|line| line.strip_suffix(b"\r"))
.ok_or_else(|| MediaInfoError::parse_error(url))?;
let status_str =
std::str::from_utf8(status_line).map_err(|_| MediaInfoError::parse_error(url))?;
let status_code = status_str
.split_whitespace()
.nth(1)
.and_then(|code| code.parse::<u16>().ok())
.ok_or_else(|| MediaInfoError::parse_error(url))?;
if status_code != 200 && status_code != 206 {
return Err(MediaInfoError::parse_error(url));
}
Ok(response[(header_end + 4)..].to_vec())
}
fn split_http_url(url: &str) -> Option<(String, String, String, u16)> {
let without_scheme = url.strip_prefix("http://")?;
let mut parts = without_scheme.splitn(2, '/');
let host_port = parts.next().unwrap_or("");
if host_port.is_empty() {
return None;
}
let path = format!("/{}", parts.next().unwrap_or(""));
let mut host = host_port;
let mut port = 80u16;
if let Some((left, right)) = host_port.rsplit_once(':')
&& right.chars().all(|c| c.is_ascii_digit())
{
host = left;
port = right.parse().ok()?;
}
let host_header = if host_port.contains(':') {
host_port
} else {
host
};
Some((host.to_string(), host_header.to_string(), path, port))
}
fn find_http_header_end(response: &[u8]) -> Option<usize> {
response.windows(4).position(|window| window == b"\r\n\r\n")
}
pub fn tracks(&self) -> &[Track] {
&self.tracks
}
pub fn tracks_mut(&mut self) -> &mut Vec<Track> {
&mut self.tracks
}
pub fn into_tracks(self) -> Vec<Track> {
self.tracks
}
pub fn general_tracks(&self) -> Vec<&Track> {
self.tracks
.iter()
.filter(|t| t.track_type() == "General")
.collect()
}
pub fn video_tracks(&self) -> Vec<&Track> {
self.tracks
.iter()
.filter(|t| t.track_type() == "Video")
.collect()
}
pub fn audio_tracks(&self) -> Vec<&Track> {
self.tracks
.iter()
.filter(|t| t.track_type() == "Audio")
.collect()
}
pub fn text_tracks(&self) -> Vec<&Track> {
self.tracks
.iter()
.filter(|t| t.track_type() == "Text")
.collect()
}
pub fn other_tracks(&self) -> Vec<&Track> {
self.tracks
.iter()
.filter(|t| t.track_type() == "Other")
.collect()
}
pub fn image_tracks(&self) -> Vec<&Track> {
self.tracks
.iter()
.filter(|t| t.track_type() == "Image")
.collect()
}
pub fn menu_tracks(&self) -> Vec<&Track> {
self.tracks
.iter()
.filter(|t| t.track_type() == "Menu")
.collect()
}
pub fn to_data(&self) -> serde_json::Map<String, serde_json::Value> {
let tracks_data: Vec<serde_json::Value> = self
.tracks
.iter()
.map(|t| serde_json::Value::Object(t.to_data()))
.collect();
let mut data = serde_json::Map::new();
data.insert("tracks".to_string(), serde_json::Value::Array(tracks_data));
data
}
pub fn to_json(&self) -> Result<String> {
serde_json::to_string(&self.to_data())
.map_err(|e| MediaInfoError::invalid_input(e.to_string()))
}
fn load_library(
library_file: Option<&Path>,
library_search_dir: Option<&Path>,
) -> Result<(MediaInfoHandle, String, LibVersion)> {
let search_dir = library_search_dir
.map(PathBuf::from)
.or_else(Self::default_library_search_dir);
let paths = if let Some(path) = library_file {
vec![path.to_path_buf()]
} else {
platform::get_library_paths(search_dir.as_deref())
};
let lib = Arc::new(MediaInfoLib::load_from_paths(&paths)?);
let handle = MediaInfoHandle::new(lib);
let version_str = handle.option("Info_Version", "");
let version_number = Self::extract_version_number(&version_str)?;
let version =
LibVersion::parse(&version_number).ok_or(MediaInfoError::VersionDetectionFailed)?;
Ok((handle, version_number, version))
}
fn default_library_search_dir() -> Option<PathBuf> {
if let Ok(dir) = std::env::var("RS_MEDIAINFO_LIBRARY_DIR")
&& !dir.trim().is_empty()
{
return Some(PathBuf::from(dir));
}
if let Some(dir) = option_env!("RS_MEDIAINFO_BUNDLED_DIR") {
return Some(PathBuf::from(dir));
}
if let Ok(exe) = std::env::current_exe()
&& let Some(parent) = exe.parent()
{
return Some(parent.to_path_buf());
}
std::env::current_dir().ok()
}
fn extract_version_number(version_str: &str) -> Result<String> {
let version_part = version_str
.strip_prefix("MediaInfoLib - v")
.or_else(|| version_str.strip_prefix("MediaInfoLib - V"))
.ok_or(MediaInfoError::VersionDetectionFailed)?;
let version_num = version_part
.split_whitespace()
.next()
.ok_or(MediaInfoError::VersionDetectionFailed)?;
Ok(version_num.to_string())
}
fn configure_parse_options(
handle: &MediaInfoHandle,
version: &LibVersion,
version_number: &str,
options: &ParseOptions,
) {
if version.supports_cover_data() {
handle.option("Cover_Data", if options.cover_data { "base64" } else { "" });
}
handle.option("ParseSpeed", &options.parse_speed.to_string());
if let Some(ref custom_options) = options.mediainfo_options {
if !version.supports_reset() {
let message = format!(
"This version of MediaInfo (v{}) does not support resetting all options to their default values, passing it custom options is not recommended and may result in unpredictable behavior, see https://github.com/MediaArea/MediaInfoLib/issues/1128",
version_number
);
Self::emit_warning(&message);
}
for (key, value) in custom_options {
handle.option(key, value);
}
}
}
fn configure_output_options(
handle: &MediaInfoHandle,
version: &LibVersion,
options: &ParseOptions,
) {
handle.option("CharSet", "UTF-8");
handle.option("Output", "");
let inform_value = if let Some(output) = options.output.as_deref() {
Self::normalize_output_option(output)
} else {
version.xml_option_name()
};
handle.option("Inform", inform_value);
handle.option("Complete", if options.full { "1" } else { "" });
handle.option(
"LegacyStreamDisplay",
if options.legacy_stream_display {
"1"
} else {
""
},
);
}
fn normalize_output_option(value: &str) -> &str {
if value.eq_ignore_ascii_case("text") {
""
} else {
value
}
}
fn emit_warning(message: &str) {
warn!("{}", message);
}
}
impl std::str::FromStr for MediaInfo {
type Err = MediaInfoError;
fn from_str(s: &str) -> Result<Self> {
MediaInfo::from_xml(s)
}
}
impl TryFrom<&str> for MediaInfo {
type Error = MediaInfoError;
fn try_from(value: &str) -> Result<Self> {
MediaInfo::from_xml(value)
}
}
impl std::fmt::Display for MediaInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<MediaInfo {} tracks>", self.tracks.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_library_search_dir_env_override() {
let key = "RS_MEDIAINFO_LIBRARY_DIR";
let original = std::env::var(key).ok();
unsafe {
std::env::set_var(key, "/tmp/rsmediainfo-test");
}
let resolved = MediaInfo::default_library_search_dir();
assert_eq!(resolved, Some(PathBuf::from("/tmp/rsmediainfo-test")));
unsafe {
if let Some(value) = original {
std::env::set_var(key, value);
} else {
std::env::remove_var(key);
}
}
}
#[test]
fn test_normalize_output_option_text_alias() {
assert_eq!(MediaInfo::normalize_output_option("text"), "");
assert_eq!(MediaInfo::normalize_output_option("TEXT"), "");
assert_eq!(MediaInfo::normalize_output_option("JSON"), "JSON");
}
}