115 lines
3.7 KiB
Rust
115 lines
3.7 KiB
Rust
use std::{process::Command, sync::Arc};
|
|
|
|
use super::Extractor;
|
|
use anyhow::{anyhow, Result};
|
|
use upend_base::{
|
|
addressing::Address,
|
|
constants::ATTR_LABEL,
|
|
entry::{Entry, EntryValue},
|
|
};
|
|
use upend_db::{
|
|
jobs::{JobContainer, JobState},
|
|
stores::{fs::FILE_MIME_KEY, UpStore},
|
|
UpEndConnection,
|
|
};
|
|
|
|
const DURATION_KEY: &str = "MEDIA_DURATION";
|
|
|
|
pub struct MediaExtractor;
|
|
|
|
impl Extractor for MediaExtractor {
|
|
fn get(
|
|
&self,
|
|
address: &Address,
|
|
_connection: &UpEndConnection,
|
|
store: Arc<Box<dyn UpStore + Send + Sync>>,
|
|
mut job_container: JobContainer,
|
|
) -> Result<Vec<Entry>> {
|
|
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::<f64>()?;
|
|
|
|
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<bool> {
|
|
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 == ATTR_LABEL {
|
|
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)
|
|
}
|
|
}
|