From 275392373060afc6a895012e2064a4f4bbf37ea1 Mon Sep 17 00:00:00 2001 From: jakka Date: Sat, 27 Sep 2025 16:49:36 +0300 Subject: major reencoding logic rewrite, code cleanup --- src/db.rs | 73 +++++++++------------ src/files.rs | 35 +++++----- src/flac.rs | 209 ++++++++++++++++++++++++++++++++++------------------------- 3 files changed, 168 insertions(+), 149 deletions(-) (limited to 'src') diff --git a/src/db.rs b/src/db.rs index 7f0ccef..cdce946 100644 --- a/src/db.rs +++ b/src/db.rs @@ -22,21 +22,21 @@ const GET_MODTIME: &str = "SELECT modtime FROM flacs WHERE path = ?1"; pub trait Database { type Conn; - fn new(path: Option>) -> Result; - fn insert_file(&self, filename: impl AsRef) -> Result<()>; - fn update_file(&self, filename: impl AsRef) -> Result<()>; - fn check_file(&self, filename: impl AsRef) -> Result; + fn new(path: Option<&PathBuf>) -> Result; + fn insert_file(&self, filename: &Path) -> Result<()>; + fn update_file(&self, filename: &Path) -> Result<()>; + fn check_file(&self, filename: &Path) -> Result; fn init_clean_files(&self) -> Result, rusqlite::Error>; - fn remove_file(&self, filename: impl AsRef) -> Result<()>; + fn remove_file(&self, filename: &Path) -> Result<()>; fn get_toencode_files(&self) -> Result, rusqlite::Error>; fn get_toencode_number(&self) -> Result; - fn get_modtime(&self, file: impl AsRef) -> Result; + fn get_modtime(&self, file: &Path) -> Result; fn vacuum(&self) -> Result<()>; } impl Database for Connection { type Conn = Connection; - fn new(path: Option>) -> Result { + fn new(path: Option<&PathBuf>) -> Result { let conn = if let Some(file) = path { Connection::open(file)? } else if let Some(base_dir) = BaseDirs::new() { @@ -49,11 +49,10 @@ impl Database for Connection { Ok(conn) } - fn insert_file(&self, filename: impl AsRef) -> Result<()> { + fn insert_file(&self, filename: &Path) -> Result<()> { let toencode = !matches!(get_vendor(&filename)?.as_str(), CURRENT_VENDOR); let modtime = filename - .as_ref() .metadata()? .modified()? .duration_since(UNIX_EPOCH)? @@ -61,15 +60,14 @@ impl Database for Connection { self.execute( ADD_ITEM, - params![filename.as_ref().to_str().unwrap(), toencode, modtime], + params![filename.to_str().unwrap(), toencode, modtime], )?; Ok(()) } - fn update_file(&self, filename: impl AsRef) -> Result<()> { + fn update_file(&self, filename: &Path) -> Result<()> { let modtime = filename - .as_ref() .metadata()? .modified()? .duration_since(UNIX_EPOCH)? @@ -77,21 +75,17 @@ impl Database for Connection { self.execute( UPDATE_ITEM, - params![filename.as_ref().to_str().unwrap(), false, modtime], + params![filename.to_str().unwrap(), false, modtime], )?; Ok(()) } - fn check_file(&self, filename: impl AsRef) -> Result { - if self.query_one( - CHECK_FILE, - params!(filename.as_ref().to_str().unwrap()), - |row| { - let num: bool = row.get(0)?; - Ok(num) - }, - )? { + fn check_file(&self, filename: &Path) -> Result { + if self.query_one(CHECK_FILE, params!(filename.to_str().unwrap()), |row| { + let num: bool = row.get(0)?; + Ok(num) + })? { Ok(true) } else { Ok(false) @@ -110,8 +104,8 @@ impl Database for Connection { Ok(files) } - fn remove_file(&self, filename: impl AsRef) -> Result<()> { - self.execute(REMOVE_FILE, params!(filename.as_ref().to_str().unwrap()))?; + fn remove_file(&self, filename: &Path) -> Result<()> { + self.execute(REMOVE_FILE, params!(filename.to_str().unwrap()))?; Ok(()) } @@ -133,15 +127,13 @@ impl Database for Connection { }) } - fn get_modtime(&self, file: impl AsRef) -> Result { - Ok(self.query_one( - GET_MODTIME, - params![file.as_ref().to_str().unwrap()], - |row| { + fn get_modtime(&self, file: &Path) -> Result { + Ok( + self.query_one(GET_MODTIME, params![file.to_str().unwrap()], |row| { let modtime: u64 = row.get(0)?; Ok(modtime) - }, - )?) + })?, + ) } fn vacuum(&self) -> Result<()> { @@ -157,7 +149,7 @@ mod tests { #[test] fn check_localfiles() { - let dbname = String::from("temp1.db"); + let dbname = PathBuf::from("temp1.db"); let filenames = [ "./samples/16bit.flac", "./samples/24bit.flac", @@ -166,7 +158,8 @@ mod tests { let mut counter = 0; let conn = Connection::new(Some(&dbname)).unwrap(); for file in filenames { - conn.insert_file(&file.to_string()).unwrap(); + let filename = PathBuf::from(file); + conn.insert_file(&filename).unwrap(); } let mut stmt = conn.prepare(TOENCODE_PATHS).unwrap(); let mut returned = stmt.query(()).unwrap(); @@ -180,7 +173,7 @@ mod tests { #[test] fn check_update() { - let dbname = String::from("temp2.db"); + let dbname = PathBuf::from("temp2.db"); let filenames = [ "./samples/16bit.flac", "./samples/24bit.flac", @@ -188,7 +181,7 @@ mod tests { ]; let conn = Connection::new(Some(&dbname)).unwrap(); for file in filenames { - conn.insert_file(Path::new(file).canonicalize().unwrap()) + conn.insert_file(&Path::new(file).canonicalize().unwrap()) .unwrap(); } @@ -206,14 +199,8 @@ mod tests { ) .unwrap(); - conn.update_file( - Path::new("./samples/16bit.flac") - .canonicalize() - .unwrap() - .to_str() - .unwrap(), - ) - .unwrap(); + conn.update_file(&Path::new("./samples/16bit.flac").canonicalize().unwrap()) + .unwrap(); let mut stmt = conn.prepare(TOENCODE_PATHS).unwrap(); let mut returned = stmt.query(()).unwrap(); diff --git a/src/files.rs b/src/files.rs index 0407469..afb42d7 100644 --- a/src/files.rs +++ b/src/files.rs @@ -29,9 +29,9 @@ struct FileError { } impl FileError { - fn new(file: impl AsRef, error: anyhow::Error) -> Self { + fn new(file: &Path, error: anyhow::Error) -> Self { FileError { - file: file.as_ref().to_path_buf(), + file: file.to_path_buf(), error, } } @@ -50,35 +50,34 @@ impl Display for FileError { impl Error for FileError {} -fn handle_file(file: impl AsRef, conn: &Connection) -> Result<()> { - if conn.check_file(&file)? { +fn handle_file(file: &Path, conn: &Connection) -> Result<()> { + if conn.check_file(file)? { let modtime = file - .as_ref() .metadata()? .modified()? .duration_since(UNIX_EPOCH)? .as_secs(); - let db_modtime = conn.get_modtime(&file)?; + let db_modtime = conn.get_modtime(file)?; if modtime != db_modtime { - conn.update_file(&file)?; + conn.update_file(file)?; } return Ok(()); } - conn.insert_file(&file)?; + conn.insert_file(file)?; Ok(()) } pub fn index_files_recursively( - path: impl AsRef, + path: &Path, conn: &Connection, handler: Arc, ) -> Result<()> { - if !path.as_ref().is_dir() { + if !path.is_dir() { return Err(anyhow!("Invalid root directory")); } - let abspath = path.as_ref().canonicalize()?; + let abspath = path.canonicalize()?; #[cfg(not(test))] let bar = ProgressBar::with_draw_target(Some(0), ProgressDrawTarget::stdout_with_hz(60)) @@ -108,7 +107,7 @@ pub fn index_files_recursively( } if path.extension().is_some_and(|x| x == "flac") { if let Err(error) = handle_file(&path, conn) { - eprintln!("{}", FileError::new(path, error)); + eprintln!("{}", FileError::new(&path, error)); } else { #[cfg(not(test))] bar.inc(1); @@ -172,7 +171,7 @@ pub fn reencode_files(conn: Connection, handler: Arc, threads: usize Err(error) => eprintln!("{}", FileError::new(&file, error)), Ok(false) => { if let Err(error) = lock.lock().unwrap().update_file(&file) { - eprintln!("{}", FileError::new(file, error)); + eprintln!("{}", FileError::new(&file, error)); } #[cfg(not(test))] bar.inc(1) @@ -205,6 +204,7 @@ pub fn clean_files(conn: &Connection, handler: Arc) -> Result<()> { spinner.tick(); files.iter().for_each(|file| { + #[allow(clippy::collapsible_if)] if handler.load(Ordering::SeqCst) && !file.exists() { if let Err(error) = conn.remove_file(file) { eprintln!("{}", FileError::new(file, error)) @@ -227,7 +227,7 @@ mod tests { #[test] fn test_index_lots_of_files() { - let dbname = "temp3.db"; + let dbname = PathBuf::from("temp3.db"); let handler = Arc::new(AtomicBool::new(true)); let conn = Connection::new(Some(&dbname)).unwrap(); index_files_recursively(Path::new("./testfiles"), &conn, handler).unwrap(); @@ -236,7 +236,7 @@ mod tests { #[test] fn test_clean_files() { - let dbname = "temp4.db"; + let dbname = PathBuf::from("temp4.db"); let handler = Arc::new(AtomicBool::new(true)); let conn = Connection::new(Some(&dbname)).unwrap(); let filenames = [ @@ -247,7 +247,8 @@ mod tests { ]; std::fs::copy("./samples/32bit.flac", "./samples/nonexisting.flac").unwrap(); for file in filenames { - conn.insert_file(&file).unwrap(); + let filename = PathBuf::from(file); + conn.insert_file(&filename).unwrap(); } std::fs::remove_file("./samples/nonexisting.flac").unwrap(); @@ -260,7 +261,7 @@ mod tests { #[test] fn test_reencode_lots_of_files() { - let dbname = "temp5.db"; + let dbname = PathBuf::from("temp5.db"); let handler = Arc::new(AtomicBool::new(true)); let conn = Connection::new(Some(&dbname)).unwrap(); let temp = handler.clone(); diff --git a/src/flac.rs b/src/flac.rs index 7b542ac..e882cd7 100644 --- a/src/flac.rs +++ b/src/flac.rs @@ -1,7 +1,9 @@ use anyhow::{Result, anyhow}; -use claxon::{FlacReader, FlacReaderOptions}; use flac_bound::FlacEncoder; -use metaflac::{Block, Tag}; +use flac_codec::{ + decode::{Metadata, verify}, + *, +}; use std::{ path::Path, sync::{ @@ -10,52 +12,63 @@ use std::{ }, }; -pub const CURRENT_VENDOR: &str = "reference libFLAC 1.5.0 20250211"; - +pub(crate) const CURRENT_VENDOR: &str = "reference libFLAC 1.5.0 20250211"; const BADTAGS: [&str; 3] = ["encoded_by", "encodedby", "encoder"]; -fn write_tags(filename: impl AsRef) -> Result<()> { - let tags = Tag::read_from_path(&filename)?; - let temp_name = filename.as_ref().with_extension("tmp"); - let mut output = Tag::read_from_path(&temp_name)?; - - for block in tags.blocks() { - match block { - Block::VorbisComment(block) => { - for (key, val) in block.comments.iter() { - if !BADTAGS.contains(&key.to_lowercase().as_str()) { - output.set_vorbis(key, val.to_owned()); - } - } - } - Block::Padding(_) => {} - _ => output.push_block(block.to_owned()), - } - } - - output.write_to_path(temp_name)?; - Ok(()) -} +fn encode_file(filename: &Path, handler: Arc) -> Result { + if verify(filename).is_err() { + return Err(anyhow!("corrupt file")); + }; -fn encode_file(filename: impl AsRef, handler: Arc) -> Result { - let temp_name = filename.as_ref().with_extension("tmp"); + let temp_name = filename.with_extension("tmp"); if temp_name.exists() { std::fs::remove_file(&temp_name)?; } - let mut decoder = FlacReader::open(&filename)?; - let streaminfo = decoder.streaminfo(); - let num_channels: usize = streaminfo.channels.try_into()?; + let mut reader = decode::FlacSampleReader::open(filename)?; + + let blocklist = reader.metadata(); + + let streaminfo = blocklist.streaminfo(); + + let channels = streaminfo.channel_count() as u32; + + let metadata = blocklist + .blocks() + .filter_map(|block| { + use metadata::Block; + use metadata::BlockRef::*; + match block { + SeekTable(table) => Some(Block::SeekTable(table.clone())), + Application(app) => Some(Block::Application(app.clone())), + Cuesheet(sheet) => Some(Block::Cuesheet(sheet.clone())), + Picture(picture) => Some(Block::Picture(picture.clone())), + VorbisComment(comments) => { + let mut cloned = comments.clone(); + for tag in BADTAGS { + cloned.remove(tag); + } + cloned.vendor_string = CURRENT_VENDOR.to_string(); + Some(Block::VorbisComment(cloned)) + } + _ => None, + } + }) + .collect::>(); let mut encoder = if let Some(encoder) = FlacEncoder::new() { - if let Ok(encoder) = encoder - .channels(streaminfo.channels) - .bits_per_sample(streaminfo.bits_per_sample) - .sample_rate(streaminfo.sample_rate) - .compression_level(8) - .verify(false) - .init_file(&temp_name) - { + if let Ok(encoder) = { + let mut encoder = encoder + .channels(streaminfo.channel_count() as u32) + .bits_per_sample(streaminfo.bits_per_sample()) + .sample_rate(streaminfo.sample_rate()) + .compression_level(8) + .verify(false); + if let Some(size) = reader.total_samples() { + encoder = encoder.total_samples_estimate(size) + } + encoder.init_file(&temp_name) + } { encoder } else { return Err(anyhow!("failed to create encoder")); @@ -64,28 +77,26 @@ fn encode_file(filename: impl AsRef, handler: Arc) -> Result { - for ch in 0..block.channels() { - buffer.push(block.channel(ch)); + match reader.fill_buf() { + Ok(buf) => { + if !buf.is_empty() { + let length = buf.len(); + if encoder + .process_interleaved(buf, length as u32 / channels) + .is_err() + { + return Err(anyhow!( + "Error while processing samples:\t{:?}", + encoder.state() + )); + }; + + reader.consume(length); + } else { + break; } - - if encoder.process(&buffer).is_err() { - return Err(anyhow!( - "Error while processing samples:\t{:?}", - encoder.state() - )); - }; - buffer.clear(); - buffer = buffer.into_iter().map(|_| unreachable!()).collect(); - block_buffer = block.into_buffer(); } - Ok(None) => break, Err(error) => return Err(error.into()), } } @@ -99,32 +110,51 @@ fn encode_file(filename: impl AsRef, handler: Arc) -> Result { + let _ = blocklist.insert(b); + } + Picture(b) => { + let _ = blocklist.insert(b); + } + VorbisComment(b) => { + let _ = blocklist.insert(b); + } + Cuesheet(b) => { + let _ = blocklist.insert(b); + } + SeekTable(b) => { + let _ = blocklist.insert(b); + } + _ => {} + } + } + Ok::<(), flac_codec::Error>(()) + })?; + + std::fs::rename(&temp_name, filename)?; + Ok(false) } -pub fn handle_encode(filename: impl AsRef, handler: Arc) -> Result { - match encode_file(&filename, handler) { +pub fn handle_encode(filename: &Path, handler: Arc) -> Result { + match encode_file(filename, handler) { Err(error) => { - let _ = std::fs::remove_file(filename.as_ref().with_extension("tmp")); + let _ = std::fs::remove_file(filename.with_extension("tmp")); Err(error) } Ok(res) => Ok(res), } } -pub fn get_vendor(file: impl AsRef) -> Result { - if let Some(vendor) = FlacReader::open_ext( - file, - FlacReaderOptions { - metadata_only: true, - read_vorbis_comment: true, - }, - )? - .vendor() - { - Ok(vendor.to_string()) +pub fn get_vendor(file: &Path) -> Result { + let blocklist = metadata::BlockList::open(file)?; + if let Some(data) = blocklist.get::() { + Ok(data.vendor_string.to_owned()) } else { Err(anyhow!("Vendor string not found")) } @@ -133,17 +163,18 @@ pub fn get_vendor(file: impl AsRef) -> Result { #[cfg(test)] mod tests { use super::*; + use std::path::PathBuf; #[test] fn bit16() { - let name = "./samples/16bit.flac"; - let tempname = "./samples/16bit.flac.temp"; - std::fs::copy(name, tempname).unwrap(); + let name = PathBuf::from("./samples/16bit.flac"); + let tempname = PathBuf::from("./samples/16bit.flac.temp"); + std::fs::copy(&name, &tempname).unwrap(); let handler = Arc::new(AtomicBool::new(true)); - encode_file(name, handler).unwrap(); + encode_file(&name, handler).unwrap(); let output = std::process::Command::new("flac") .arg("-wts") - .arg(name) + .arg(&name) .status(); std::fs::rename(tempname, name).unwrap(); assert!(output.unwrap().success()); @@ -151,14 +182,14 @@ mod tests { #[test] fn bit24() { - let name = "./samples/24bit.flac"; - let tempname = "./samples/24bit.flac.temp"; - std::fs::copy(name, tempname).unwrap(); + let name = PathBuf::from("./samples/24bit.flac"); + let tempname = PathBuf::from("./samples/24bit.flac.temp"); + std::fs::copy(&name, &tempname).unwrap(); let handler = Arc::new(AtomicBool::new(true)); - encode_file(name, handler).unwrap(); + encode_file(&name, handler).unwrap(); let output = std::process::Command::new("flac") .arg("-wts") - .arg(name) + .arg(&name) .status(); std::fs::rename(tempname, name).unwrap(); assert!(output.unwrap().success()); @@ -166,14 +197,14 @@ mod tests { #[test] fn bit32() { - let name = "./samples/32bit.flac"; - let tempname = "./samples/32bit.flac.temp"; - std::fs::copy(name, tempname).unwrap(); + let name = PathBuf::from("./samples/32bit.flac"); + let tempname = PathBuf::from("./samples/32bit.flac.temp"); + std::fs::copy(&name, &tempname).unwrap(); let handler = Arc::new(AtomicBool::new(true)); - encode_file(name, handler).unwrap(); + encode_file(&name, handler).unwrap(); let output = std::process::Command::new("flac") .arg("-wts") - .arg(name) + .arg(&name) .status(); std::fs::rename(tempname, name).unwrap(); assert!(output.unwrap().success()); -- cgit v1.3.1