feat: add media (duration) extractor
parent
565dea3166
commit
ea3fc015f5
|
@ -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 = []
|
||||
|
|
|
@ -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<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),
|
||||
}];
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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.");
|
||||
|
|
Loading…
Reference in New Issue