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:
@@ -1,6 +1,9 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::{Parser, Subcommand};
|
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_cas::{CasConfig, CasStore, ContentFetcher, FileReader};
|
||||||
use musicfs_core::{FileId, FileMeta, LoggingConfig, OriginId, RealPath, VirtualPath};
|
use musicfs_core::{FileId, FileMeta, LoggingConfig, OriginId, RealPath, VirtualPath};
|
||||||
use musicfs_fuse::MusicFs;
|
use musicfs_fuse::MusicFs;
|
||||||
@@ -9,7 +12,7 @@ use musicfs_origins::{LocalOrigin, Origin};
|
|||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::{Read as _, Write};
|
||||||
use std::os::unix::io::AsRawFd;
|
use std::os::unix::io::AsRawFd;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
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 fetcher = Arc::new(ContentFetcher::new(store.clone()));
|
||||||
let mut files = Vec::new();
|
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 {
|
for origin_cfg in &config.origins {
|
||||||
if !origin_cfg.enabled {
|
if !origin_cfg.enabled {
|
||||||
continue;
|
continue;
|
||||||
@@ -302,7 +311,9 @@ fn run_mount(config: musicfs_core::Config) -> Result<()> {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let origin_path = PathBuf::from(path_str);
|
let origin_path = PathBuf::from(path_str);
|
||||||
info!("Scanning music files for origin {}...", origin_cfg.id);
|
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!(
|
info!(
|
||||||
"Found {} music files for origin {}",
|
"Found {} music files for origin {}",
|
||||||
origin_files.len(),
|
origin_files.len(),
|
||||||
@@ -789,6 +800,7 @@ async fn scan_music_files(
|
|||||||
dir: &Path,
|
dir: &Path,
|
||||||
origin_id: &OriginId,
|
origin_id: &OriginId,
|
||||||
db: &Database,
|
db: &Database,
|
||||||
|
format_registry: &Arc<FormatHandlerRegistry>,
|
||||||
) -> Result<Vec<FileMeta>> {
|
) -> Result<Vec<FileMeta>> {
|
||||||
let parser = MetadataParser::new();
|
let parser = MetadataParser::new();
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
@@ -800,6 +812,7 @@ async fn scan_music_files(
|
|||||||
origin_id,
|
origin_id,
|
||||||
&parser,
|
&parser,
|
||||||
db,
|
db,
|
||||||
|
format_registry,
|
||||||
&mut files,
|
&mut files,
|
||||||
&mut file_id_counter,
|
&mut file_id_counter,
|
||||||
)
|
)
|
||||||
@@ -814,6 +827,7 @@ async fn scan_dir_recursive(
|
|||||||
origin_id: &OriginId,
|
origin_id: &OriginId,
|
||||||
parser: &MetadataParser,
|
parser: &MetadataParser,
|
||||||
db: &Database,
|
db: &Database,
|
||||||
|
format_registry: &Arc<FormatHandlerRegistry>,
|
||||||
files: &mut Vec<FileMeta>,
|
files: &mut Vec<FileMeta>,
|
||||||
id_counter: &mut i64,
|
id_counter: &mut i64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@@ -825,7 +839,14 @@ async fn scan_dir_recursive(
|
|||||||
|
|
||||||
if metadata.is_dir() {
|
if metadata.is_dir() {
|
||||||
Box::pin(scan_dir_recursive(
|
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?;
|
.await?;
|
||||||
} else if is_audio_file(&path) {
|
} else if is_audio_file(&path) {
|
||||||
@@ -853,14 +874,18 @@ async fn scan_dir_recursive(
|
|||||||
path: real_path_for_db.clone(),
|
path: real_path_for_db.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let format_layout = analyze_format_layout(&path, metadata.len(), format_registry);
|
||||||
|
|
||||||
let file_id = db
|
let file_id = db
|
||||||
.upsert_file(
|
.upsert_file_with_layout(
|
||||||
origin_id,
|
origin_id,
|
||||||
&real_path.path,
|
&real_path.path,
|
||||||
&virtual_path,
|
&virtual_path,
|
||||||
audio_meta.as_ref().unwrap_or(&Default::default()),
|
audio_meta.as_ref().unwrap_or(&Default::default()),
|
||||||
metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH),
|
metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||||
metadata.len(),
|
metadata.len(),
|
||||||
|
format_layout.as_ref(),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.unwrap_or_else(|e| {
|
.unwrap_or_else(|e| {
|
||||||
debug!("Failed to upsert file to DB: {}", 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 {
|
fn build_virtual_path(path: &Path, audio: Option<&musicfs_core::AudioMeta>) -> VirtualPath {
|
||||||
if let Some(meta) = audio {
|
if let Some(meta) = audio {
|
||||||
let artist = meta.artist.as_deref().unwrap_or("Unknown Artist");
|
let artist = meta.artist.as_deref().unwrap_or("Unknown Artist");
|
||||||
|
|||||||
Reference in New Issue
Block a user