diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/db.rs | 42 | ||||
| -rw-r--r-- | src/files.rs | 43 | ||||
| -rw-r--r-- | src/flac.rs | 9 | ||||
| -rw-r--r-- | src/main.rs | 82 |
4 files changed, 137 insertions, 39 deletions
@@ -2,11 +2,9 @@ use anyhow::{Result, anyhow}; use directories::BaseDirs; use futures_util::Stream; use libsql::{Builder, Connection, params}; -use pin_utils::pin_mut; use std::{ ffi::OsStr, - fmt::Display, - path::{Path, absolute}, + path::Path, time::{Duration, UNIX_EPOCH}, }; @@ -19,20 +17,9 @@ const TOENCODE_QUERY: &str = "SELECT path FROM flacs WHERE toencode"; const CHECK_FILE: &str = "SELECT exists(SELECT 1 FROM flacs WHERE path = ?1)"; const FETCH_MODTIME: &str = "SELECT modtime FROM flacs WHERE path = ?1"; const FETCH_FILES: &str = "SELECT path FROM flacs"; -const REMOVE_FILE: &str = "DELETE FROM flac WHERE path = ?1"; - -#[derive(Debug)] -pub enum Errors { - EmptyQuery, -} - -impl Display for Errors { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Errors::EmptyQuery => write!(f, "Empty query"), - } - } -} +const REMOVE_FILE: &str = "DELETE FROM flacs WHERE path = ?1"; +const DEDUPE_DB: &str = + "DELETE FROM flacs WHERE rowid NOT IN (SELECT MAX(rowid) FROM flacs GROUP BY path)"; #[derive(Debug, Clone)] pub struct Database(pub Connection); @@ -46,7 +33,7 @@ impl Database { } pub async fn insert_file(&self, filename: &impl AsRef<OsStr>) -> Result<()> { - let abs_filename = absolute(Path::new(filename))?; + let abs_filename = Path::new(filename).canonicalize()?; let toencode = !matches!(get_vendor(&abs_filename)?.as_str(), CURRENT_VENDOR); let modtime = abs_filename @@ -66,7 +53,7 @@ impl Database { } pub async fn update_file(&self, filename: &impl AsRef<OsStr>) -> Result<()> { - let abs_filename = absolute(Path::new(filename))?; + let abs_filename = Path::new(filename).canonicalize()?; let modtime = abs_filename .metadata()? @@ -85,7 +72,7 @@ impl Database { } pub async fn check_file(&self, filename: &impl AsRef<OsStr>) -> Result<bool> { - let abs_filename = absolute(Path::new(filename))?; + let abs_filename = Path::new(filename).canonicalize()?; if let Some(row) = self .0 @@ -101,7 +88,7 @@ impl Database { } pub async fn get_modtime(&self, filename: &impl AsRef<OsStr>) -> Result<u64> { - let abs_filename = absolute(Path::new(filename))?; + let abs_filename = Path::new(filename).canonicalize()?; if let Some(row) = self .0 @@ -122,8 +109,9 @@ impl Database { pub async fn clean_files(&self) -> Result<()> { let mut tasks = tokio::task::JoinSet::new(); + self.0.execute(DEDUPE_DB, ()).await?; while let Ok(Some(row)) = self.0.query(FETCH_FILES, ()).await?.next().await { - let path = absolute(Path::new(row.get_str(0)?))?; + let path = Path::new(row.get_str(0)?).canonicalize()?; let conn = self.0.clone(); tasks.spawn(async move { if !path.exists() { @@ -175,7 +163,7 @@ mod tests { .await .unwrap() .into_stream(); - pin_mut!(returned); + pin_utils::pin_mut!(returned); let mut counter = 0; @@ -201,7 +189,11 @@ mod tests { .execute( REPLACE_ITEM, params![ - absolute(Path::new("16bit.flac")).unwrap().to_str(), + Path::new("16bit.flac") + .canonicalize() + .unwrap() + .to_str() + .unwrap(), true, "" ], @@ -216,7 +208,7 @@ mod tests { .await .unwrap() .into_stream(); - pin_mut!(returned); + pin_utils::pin_mut!(returned); let mut counter = 0; while let Some(Ok(_)) = returned.next().await { counter += 1 diff --git a/src/files.rs b/src/files.rs index dc96650..c0f4b68 100644 --- a/src/files.rs +++ b/src/files.rs @@ -3,7 +3,7 @@ use futures_util::StreamExt; use pin_utils::pin_mut; use std::{ fmt::Display, - path::{Path, PathBuf, absolute}, + path::{Path, PathBuf}, time::UNIX_EPOCH, }; use tokio::{fs::read_dir, task::JoinSet}; @@ -64,7 +64,7 @@ pub async fn index_files_recursively(path: &Path, conn: &Database) -> Result<()> if !path.is_dir() { return Err(anyhow!("Invalid root directory")); } - let abspath = absolute(path)?; + let abspath = path.canonicalize()?; let mut tasks = JoinSet::new(); let mut dirs = vec![abspath]; @@ -98,7 +98,14 @@ pub async fn index_files_recursively(path: &Path, conn: &Database) -> Result<()> Ok(()) } -pub async fn reencode_files(conn: &Database) -> Result<()> { +pub async fn reencode_files(conn: &Database, folderpath: Option<&PathBuf>) -> Result<()> { + let mut nocheck = true; + let mut path = Path::new(""); + if let Some(real_path) = folderpath { + nocheck = false; + path = real_path; + } + let stream = conn.get_toencode_stream().await?; pin_mut!(stream); @@ -106,8 +113,10 @@ pub async fn reencode_files(conn: &Database) -> Result<()> { while let Some(Ok(row)) = stream.next().await { if let Some(file) = row.get_value(0)?.as_text() { - let filename = PathBuf::from(file.clone()); - tasks.spawn_blocking(move || handle_encode(filename)); + let filename = Path::new(file).canonicalize()?; + if nocheck || filename.starts_with(path) { + tasks.spawn_blocking(move || handle_encode(filename)); + } } } @@ -122,6 +131,30 @@ pub async fn reencode_files(conn: &Database) -> Result<()> { Ok(()) } +pub async fn count_reencode_files(conn: &Database, folderpath: Option<&PathBuf>) -> Result<u64> { + let mut nocheck = true; + let mut path = Path::new(""); + if let Some(real_path) = folderpath { + nocheck = false; + path = real_path; + } + + let mut counter: u64 = 0; + let stream = conn.get_toencode_stream().await?; + pin_mut!(stream); + + while let Some(Ok(row)) = stream.next().await { + if let Some(file) = row.get_value(0)?.as_text() { + let filename = Path::new(file).canonicalize()?; + if nocheck || filename.starts_with(path) { + counter += 1; + } + } + } + + Ok(counter) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/flac.rs b/src/flac.rs index 940e816..64ac6a9 100644 --- a/src/flac.rs +++ b/src/flac.rs @@ -20,6 +20,9 @@ use crate::files; pub const CURRENT_VENDOR: &str = "reference libFLAC 1.5.0 20250211"; +type BoxedFormatReader = Box<dyn FormatReader>; +type BoxedAudioDecoder = Box<dyn AudioDecoder + 'static>; + struct StreamConfig { channels: u32, bits_per_sample: Bps, @@ -187,11 +190,7 @@ fn encode_cycle_32( fn init_decoder( filename: impl AsRef<Path>, -) -> Result<( - Box<dyn FormatReader>, - Box<dyn AudioDecoder + 'static>, - StreamConfig, -)> { +) -> Result<(BoxedFormatReader, BoxedAudioDecoder, StreamConfig)> { let src = std::fs::File::open(filename)?; let mss = MediaSourceStream::new(Box::new(src), Default::default()); let mut hint = Hint::new(); diff --git a/src/main.rs b/src/main.rs index 00a00c7..09bb966 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,83 @@ mod db; mod files; mod flac; -use anyhow::Result; +use anyhow::Error; +use clap::{Arg, ArgAction, command, value_parser}; +use std::path::PathBuf; -#[tokio::main] -async fn main() -> Result<()> { - todo!() +fn main() -> Result<(), Error> { + let matches = command!() + .help_expected(true) + .arg( + Arg::new("path") + .short('p') + .long("path") + .value_parser(value_parser!(PathBuf)) + .help("Path for indexing/reencoding"), + ) + .arg( + Arg::new("index") + .short('i') + .long("index") + .action(ArgAction::SetTrue) + .requires("path") + .help("Only index files"), + ) + .arg( + Arg::new("doit") + .long("doit") + .action(ArgAction::SetTrue) + .conflicts_with("index") + .help("Actually reencode"), + ) + .arg( + Arg::new("clean") + .short('c') + .long("clean") + .action(ArgAction::SetTrue) + .help("Clean and dedupe database"), + ) + .arg( + Arg::new("threads") + .short('t') + .long("threads") + .value_parser(value_parser!(usize)) + .default_value("4") + .help("Set number of reencoding threads (default: 4)"), + ) + .arg( + Arg::new("db") + .long("db") + .value_parser(value_parser!(PathBuf)) + .help("Path to database file"), + ) + .get_matches(); + + let threads = *matches.get_one::<usize>("threads").unwrap(); + let runtime = tokio::runtime::Builder::new_multi_thread() + .max_blocking_threads(threads) + .enable_all() + .build()?; + runtime.block_on(async move { + let conn = if let Some(path) = matches.get_one::<PathBuf>("db") { + db::Database::new(path).await? + } else { + db::open_default_db().await? + }; + let path = matches.get_one::<PathBuf>("path"); + if matches.get_flag("index") { + let folderpath = path.unwrap(); + files::index_files_recursively(folderpath, &conn).await + } else if matches.get_flag("doit") { + files::reencode_files(&conn, path).await + } else if matches.get_flag("clean") { + conn.clean_files().await + } else { + let count = files::count_reencode_files(&conn, path).await.unwrap(); + println!("Files to reencode:\t{count}"); + Ok(()) + } + })?; + + Ok(()) } |
