feat(sync): populate format_layout during origin scan

- Create FormatHandlerRegistry in CLI initialization
- Register Id3v2Handler and FlacHandler
- Add analyze_format_layout() helper to read file headers
- Update scan functions to call handler.analyze()
- Use upsert_file_with_layout() when format_layout available
- Graceful degradation for unsupported formats
- Full workspace compiles successfully
This commit is contained in:
Alexander
2026-05-17 17:31:34 +02:00
parent 4f4a4169f8
commit ebf4044a01
+72 -5
View File
@@ -1,6 +1,9 @@
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use musicfs_cache::{Database, RenameError, TrashedFilter, TreeBuilder, VirtualTree};
use musicfs_cache::{
Database, FlacHandler, FormatHandlerRegistry, FormatLayout, Id3v2Handler, RenameError,
TrashedFilter, TreeBuilder, VirtualTree,
};
use musicfs_cas::{CasConfig, CasStore, ContentFetcher, FileReader};
use musicfs_core::{FileId, FileMeta, LoggingConfig, OriginId, RealPath, VirtualPath};
use musicfs_fuse::MusicFs;
@@ -9,7 +12,7 @@ use musicfs_origins::{LocalOrigin, Origin};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::io::{Read as _, Write};
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@@ -267,6 +270,12 @@ fn run_mount(config: musicfs_core::Config) -> Result<()> {
let fetcher = Arc::new(ContentFetcher::new(store.clone()));
let mut files = Vec::new();
let mut format_registry = FormatHandlerRegistry::new();
format_registry.register(Arc::new(Id3v2Handler::new()));
format_registry.register(Arc::new(FlacHandler::new()));
let format_registry = Arc::new(format_registry);
info!("Format handler registry initialized (MP3, FLAC)");
for origin_cfg in &config.origins {
if !origin_cfg.enabled {
continue;
@@ -302,7 +311,9 @@ fn run_mount(config: musicfs_core::Config) -> Result<()> {
.unwrap();
let origin_path = PathBuf::from(path_str);
info!("Scanning music files for origin {}...", origin_cfg.id);
let origin_files = scan_music_files(&origin_path, &origin_id, db.as_ref()).await?;
let origin_files =
scan_music_files(&origin_path, &origin_id, db.as_ref(), &format_registry)
.await?;
info!(
"Found {} music files for origin {}",
origin_files.len(),
@@ -789,6 +800,7 @@ async fn scan_music_files(
dir: &Path,
origin_id: &OriginId,
db: &Database,
format_registry: &Arc<FormatHandlerRegistry>,
) -> Result<Vec<FileMeta>> {
let parser = MetadataParser::new();
let mut files = Vec::new();
@@ -800,6 +812,7 @@ async fn scan_music_files(
origin_id,
&parser,
db,
format_registry,
&mut files,
&mut file_id_counter,
)
@@ -814,6 +827,7 @@ async fn scan_dir_recursive(
origin_id: &OriginId,
parser: &MetadataParser,
db: &Database,
format_registry: &Arc<FormatHandlerRegistry>,
files: &mut Vec<FileMeta>,
id_counter: &mut i64,
) -> Result<()> {
@@ -825,7 +839,14 @@ async fn scan_dir_recursive(
if metadata.is_dir() {
Box::pin(scan_dir_recursive(
base, &path, origin_id, parser, db, files, id_counter,
base,
&path,
origin_id,
parser,
db,
format_registry,
files,
id_counter,
))
.await?;
} else if is_audio_file(&path) {
@@ -853,14 +874,18 @@ async fn scan_dir_recursive(
path: real_path_for_db.clone(),
};
let format_layout = analyze_format_layout(&path, metadata.len(), format_registry);
let file_id = db
.upsert_file(
.upsert_file_with_layout(
origin_id,
&real_path.path,
&virtual_path,
audio_meta.as_ref().unwrap_or(&Default::default()),
metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH),
metadata.len(),
format_layout.as_ref(),
None,
)
.unwrap_or_else(|e| {
debug!("Failed to upsert file to DB: {}", e);
@@ -899,6 +924,48 @@ fn is_audio_file(path: &Path) -> bool {
)
}
const HEADER_READ_SIZE: usize = 65536;
fn analyze_format_layout(
path: &Path,
file_size: u64,
registry: &FormatHandlerRegistry,
) -> Option<FormatLayout> {
let ext = path.extension().and_then(|e| e.to_str())?;
let handler = registry.get_by_extension(&ext.to_lowercase())?;
let mut file = match std::fs::File::open(path) {
Ok(f) => f,
Err(e) => {
warn!("Failed to open file for format analysis {:?}: {}", path, e);
return None;
}
};
let mut buffer = vec![0u8; HEADER_READ_SIZE.min(file_size as usize)];
if let Err(e) = file.read_exact(&mut buffer) {
warn!(
"Failed to read header for format analysis {:?}: {}",
path, e
);
return None;
}
match handler.analyze(&buffer, file_size) {
Ok(layout) => {
debug!(
"Format layout analyzed for {:?}: audio_start={}, audio_end={}",
path, layout.audio_start, layout.audio_end
);
Some(layout)
}
Err(e) => {
debug!("Format analysis failed for {:?}: {}", path, e);
None
}
}
}
fn build_virtual_path(path: &Path, audio: Option<&musicfs_core::AudioMeta>) -> VirtualPath {
if let Some(meta) = audio {
let artist = meta.artist.as_deref().unwrap_or("Unknown Artist");