feat: backfill media metadata

This commit is contained in:
kieran 2025-01-27 21:48:57 +00:00
parent 201a3aaa49
commit 9f78c1a54f
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
6 changed files with 135 additions and 5 deletions

View File

@ -0,0 +1,93 @@
use crate::db::{Database, FileUpload};
use crate::filesystem::FileStore;
use crate::processing::probe_file;
use anyhow::Result;
use log::{error, info, warn};
pub struct MediaMetadata {
db: Database,
fs: FileStore,
}
impl MediaMetadata {
pub fn new(db: Database, fs: FileStore) -> Self {
Self { db, fs }
}
pub async fn process(&mut self) -> Result<()> {
let to_migrate = self.db.get_missing_media_metadata().await?;
info!("{} files are missing metadata", to_migrate.len());
for file in to_migrate {
// probe file and update metadata
let path = self.fs.get(&file.id);
if let Ok(data) = probe_file(&path) {
let bv = data.best_video();
let duration = if data.duration < 0.0 {
None
} else {
Some(data.duration)
};
let bitrate = if data.bitrate == 0 {
None
} else {
Some(data.bitrate as u32)
};
info!(
"Updating metadata: id={}, dim={}x{}, dur={}, br={}",
hex::encode(&file.id),
bv.map(|v| v.width).unwrap_or(0),
bv.map(|v| v.height).unwrap_or(0),
duration.unwrap_or(0.0),
bitrate.unwrap_or(0)
);
if let Err(e) = self
.db
.update_metadata(
&file.id,
bv.map(|v| v.width as u32),
bv.map(|v| v.height as u32),
duration,
bitrate,
)
.await
{
error!("Failed to update metadata: {}", e);
}
} else {
warn!("Skipping missing file: {}", hex::encode(&file.id));
}
}
Ok(())
}
}
impl Database {
pub async fn get_missing_media_metadata(&mut self) -> Result<Vec<FileUpload>> {
let results: Vec<FileUpload> = sqlx::query_as("select * from uploads where (width is null or height is null or bitrate is null or duration is null) and (mime_type like 'image/%' or mime_type like 'video/%')")
.fetch_all(&self.pool)
.await?;
Ok(results)
}
pub async fn update_metadata(
&mut self,
id: &Vec<u8>,
width: Option<u32>,
height: Option<u32>,
duration: Option<f32>,
bitrate: Option<u32>,
) -> Result<()> {
sqlx::query("update uploads set width=?, height=?, duration=?, bitrate=? where id=?")
.bind(width)
.bind(height)
.bind(duration)
.bind(bitrate)
.bind(id)
.execute(&self.pool)
.await?;
Ok(())
}
}

24
src/background/mod.rs Normal file
View File

@ -0,0 +1,24 @@
use crate::db::Database;
use crate::filesystem::FileStore;
use anyhow::Result;
use log::info;
use tokio::task::JoinHandle;
#[cfg(feature = "media-compression")]
mod media_metadata;
pub fn start_background_tasks(db: Database, file_store: FileStore) -> Vec<JoinHandle<Result<()>>> {
let mut ret = vec![];
#[cfg(feature = "media-compression")]
{
ret.push(tokio::spawn(async move {
info!("Starting MediaMetadata background task");
let mut m = media_metadata::MediaMetadata::new(db.clone(), file_store.clone());
m.process().await?;
info!("MediaMetadata background task completed");
Ok(())
}));
}
ret
}

View File

@ -12,6 +12,7 @@ use rocket::shield::Shield;
use route96::analytics::plausible::PlausibleAnalytics; use route96::analytics::plausible::PlausibleAnalytics;
#[cfg(feature = "analytics")] #[cfg(feature = "analytics")]
use route96::analytics::AnalyticsFairing; use route96::analytics::AnalyticsFairing;
use route96::background::start_background_tasks;
use route96::cors::CORS; use route96::cors::CORS;
use route96::db::Database; use route96::db::Database;
use route96::filesystem::FileStore; use route96::filesystem::FileStore;
@ -63,8 +64,9 @@ async fn main() -> Result<(), Error> {
.limit("form", upload_limit); .limit("form", upload_limit);
config.ident = Ident::try_new("route96").unwrap(); config.ident = Ident::try_new("route96").unwrap();
let fs = FileStore::new(settings.clone());
let mut rocket = rocket::Rocket::custom(config) let mut rocket = rocket::Rocket::custom(config)
.manage(FileStore::new(settings.clone())) .manage(fs.clone())
.manage(settings.clone()) .manage(settings.clone())
.manage(db.clone()) .manage(db.clone())
.attach(CORS) .attach(CORS)
@ -93,10 +95,19 @@ async fn main() -> Result<(), Error> {
{ {
rocket = rocket.mount("/", routes![routes::get_blob_thumb]); rocket = rocket.mount("/", routes![routes::get_blob_thumb]);
} }
let jh = start_background_tasks(db, fs);
if let Err(e) = rocket.launch().await { if let Err(e) = rocket.launch().await {
error!("Rocker error {}", e); error!("Rocker error {}", e);
for j in jh {
let _ = j.await?;
}
Err(Error::from(e)) Err(Error::from(e))
} else { } else {
for j in jh {
let _ = j.await?;
}
Ok(()) Ok(())
} }
} }

View File

@ -43,6 +43,7 @@ pub struct NewFileResult {
pub labels: Vec<FileLabel>, pub labels: Vec<FileLabel>,
} }
#[derive(Clone)]
pub struct FileStore { pub struct FileStore {
settings: Settings, settings: Settings,
} }

View File

@ -1,6 +1,7 @@
#[cfg(feature = "analytics")] #[cfg(feature = "analytics")]
pub mod analytics; pub mod analytics;
pub mod auth; pub mod auth;
pub mod background;
pub mod cors; pub mod cors;
pub mod db; pub mod db;
pub mod filesystem; pub mod filesystem;

View File

@ -137,7 +137,7 @@ pub fn can_compress(mime_type: &str) -> bool {
} }
pub fn compress_file( pub fn compress_file(
stream: &Path, path: &Path,
mime_type: &str, mime_type: &str,
out_dir: &Path, out_dir: &Path,
) -> Result<NewFileProcessorResult, Error> { ) -> Result<NewFileProcessorResult, Error> {
@ -147,12 +147,12 @@ pub fn compress_file(
if mime_type.starts_with("image/") { if mime_type.starts_with("image/") {
let mut proc = WebpProcessor::new(); let mut proc = WebpProcessor::new();
return proc.compress(stream, mime_type, out_dir); return proc.compress(path, mime_type, out_dir);
} }
bail!("No media processor") bail!("No media processor")
} }
pub fn probe_file(stream: &Path) -> Result<DemuxerInfo> { pub fn probe_file(path: &Path) -> Result<DemuxerInfo> {
let mut demuxer = Demuxer::new(stream.to_str().unwrap())?; let mut demuxer = Demuxer::new(path.to_str().unwrap())?;
unsafe { demuxer.probe_input() } unsafe { demuxer.probe_input() }
} }