diff --git a/Cargo.toml b/Cargo.toml index 185ec5d..02147fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,6 +93,7 @@ default = [ "extractors-web", "extractors-audio", "extractors-photo", + "extractors-media", ] desktop = ["webbrowser", "opener", "is_executable"] previews = [] @@ -100,3 +101,4 @@ previews-image = ["image", "webp", "kamadak-exif"] extractors-web = ["webpage"] extractors-audio = ["id3"] extractors-photo = ["kamadak-exif"] +extractors-media = [] diff --git a/src/extractors/media.rs b/src/extractors/media.rs new file mode 100644 index 0000000..712b4d2 --- /dev/null +++ b/src/extractors/media.rs @@ -0,0 +1,104 @@ +use std::{process::Command, sync::Arc}; + +use super::Extractor; +use crate::{ + addressing::Address, + database::{ + entry::{Entry, EntryValue}, + stores::{fs::FILE_MIME_KEY, UpStore}, + UpEndConnection, + }, + util::jobs::{JobContainer, JobState}, +}; +use anyhow::{anyhow, Result}; + +const DURATION_KEY: &str = "MEDIA_DURATION"; + +pub struct MediaExtractor; + +impl Extractor for MediaExtractor { + fn get( + &self, + address: &Address, + _connection: &UpEndConnection, + store: Arc>, + mut job_container: JobContainer, + ) -> Result> { + if let Address::Hash(hash) = address { + let files = store.retrieve(hash)?; + + if let Some(file) = files.get(0) { + let file_path = file.get_file_path(); + let mut job_handle = job_container.add_job( + None, + &format!( + r#"Getting media info from "{:}""#, + file_path + .components() + .last() + .unwrap() + .as_os_str() + .to_string_lossy() + ), + )?; + + // https://superuser.com/a/945604/409504 + let ffprobe_cmd = Command::new("ffprobe") + .args(["-v", "error"]) + .args(["-show_entries", "format=duration"]) + .args(["-of", "default=noprint_wrappers=1:nokey=1"]) + .arg(file_path) + .output()?; + + if !ffprobe_cmd.status.success() { + return Err(anyhow!( + "Failed to retrieve file duration: {:?}", + String::from_utf8_lossy(&ffprobe_cmd.stderr) + )); + } + + let duration = String::from_utf8(ffprobe_cmd.stdout)? + .trim() + .parse::()?; + + let result = vec![Entry { + entity: address.clone(), + attribute: DURATION_KEY.to_string(), + value: EntryValue::Number(duration), + }]; + + let _ = job_handle.update_state(JobState::Done); + Ok(result) + } else { + Err(anyhow!("Couldn't find file for {hash:?}!")) + } + } else { + Ok(vec![]) + } + } + + fn is_needed(&self, address: &Address, connection: &UpEndConnection) -> Result { + let is_media = connection.retrieve_object(address)?.iter().any(|e| { + if e.attribute == FILE_MIME_KEY { + if let EntryValue::String(mime) = &e.value { + return mime.starts_with("audio") || mime.starts_with("video"); + } + } + false + }); + + if !is_media { + return Ok(false); + } + + let is_extracted = !connection + .query(format!("(matches @{} (contains \"{}\") ?)", address, DURATION_KEY).parse()?)? + .is_empty(); + + if is_extracted { + return Ok(false); + } + + Ok(true) + } +} diff --git a/src/extractors/mod.rs b/src/extractors/mod.rs index a2b938b..3034107 100644 --- a/src/extractors/mod.rs +++ b/src/extractors/mod.rs @@ -20,6 +20,9 @@ pub mod audio; #[cfg(feature = "extractors-photo")] pub mod photo; +#[cfg(feature = "extractors-media")] +pub mod media; + pub trait Extractor { fn get( &self, @@ -123,7 +126,13 @@ pub fn extract( #[cfg(feature = "extractors-photo")] { entry_count += - photo::ExifExtractor.insert_info(address, connection, store.clone(), job_container)?; + photo::ExifExtractor.insert_info(address, connection, store.clone(), job_container.clone())?; + } + + #[cfg(feature = "extractors-media")] + { + entry_count += + media::MediaExtractor.insert_info(address, connection, store.clone(), job_container)?; } trace!("Extracting metadata for {address:?} - got {entry_count} entries.");