use std::{process::Command, sync::Arc}; use super::Extractor; use crate::{ addressing::Address, database::{ constants::LABEL_ATTR, 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), provenance: "SYSTEM EXTRACTOR".to_string(), timestamp: chrono::Utc::now().naive_utc(), }]; 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"); } } if e.attribute == LABEL_ATTR { if let EntryValue::String(label) = &e.value { let label = label.to_lowercase(); return label.ends_with(".ogg") || label.ends_with(".mp3") || label.ends_with(".wav"); } } 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) } }