summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/db.rs42
-rw-r--r--src/files.rs43
-rw-r--r--src/flac.rs9
-rw-r--r--src/main.rs82
4 files changed, 137 insertions, 39 deletions
diff --git a/src/db.rs b/src/db.rs
index 1016894..ae3bf51 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -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(())
}