summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjakka <jakkadoujin@gmail.com>2025-06-19 00:33:52 +0300
committerjakka <jakkadoujin@gmail.com>2025-06-19 00:33:52 +0300
commiteb838afe24b3f33d8dff88d570574aeea8060f15 (patch)
tree144e2d61e16eb02b3b314fdd376c6afabfb467b7
parent6d9e1300a2769fe4c0f4b290122143f52fc7a3ef (diff)
rewrote flac logic. added (non-working) batch reencoding test. added reencode status update on reencoding
-rw-r--r--Cargo.lock9
-rw-r--r--Cargo.toml2
-rw-r--r--src/db.rs19
-rw-r--r--src/files.rs97
-rw-r--r--src/flac.rs238
-rw-r--r--src/main.rs22
6 files changed, 208 insertions, 179 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 4b25df9..382b920 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1512,7 +1512,7 @@ dependencies = [
[[package]]
name = "reencoder"
-version = "0.0.1"
+version = "0.1.0"
dependencies = [
"anyhow",
"clap",
@@ -1765,12 +1765,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "slab"
-version = "0.4.9"
+version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
-dependencies = [
- "autocfg",
-]
+checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
[[package]]
name = "smallvec"
diff --git a/Cargo.toml b/Cargo.toml
index 8426d2e..3e0de73 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "reencoder"
-version = "0.0.1"
+version = "0.1.0"
edition = "2024"
repository = "https://github.com/justjakka/reencoder/"
license = "BSD-3-Clause"
diff --git a/src/db.rs b/src/db.rs
index 93d6d7e..742c003 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -3,7 +3,6 @@ use directories::BaseDirs;
use futures_util::Stream;
use libsql::{Builder, Connection, params};
use std::{
- ffi::OsStr,
path::Path,
time::{Duration, UNIX_EPOCH},
};
@@ -32,8 +31,8 @@ impl Database {
Ok(Database(conn))
}
- pub async fn insert_file(&self, filename: &impl AsRef<OsStr>) -> Result<()> {
- let abs_filename = Path::new(filename).canonicalize()?;
+ pub async fn insert_file(&self, filename: impl AsRef<Path>) -> Result<()> {
+ let abs_filename = filename.as_ref().canonicalize()?;
let toencode = !matches!(get_vendor(&abs_filename)?.as_str(), CURRENT_VENDOR);
let modtime = abs_filename
@@ -52,8 +51,8 @@ impl Database {
Ok(())
}
- pub async fn update_file(&self, filename: &impl AsRef<OsStr>) -> Result<()> {
- let abs_filename = Path::new(filename).canonicalize()?;
+ pub async fn update_file(&self, filename: impl AsRef<Path>) -> Result<()> {
+ let abs_filename = filename.as_ref().canonicalize()?;
let modtime = abs_filename
.metadata()?
@@ -71,8 +70,8 @@ impl Database {
Ok(())
}
- pub async fn check_file(&self, filename: &impl AsRef<OsStr>) -> Result<bool> {
- let abs_filename = Path::new(filename).canonicalize()?;
+ pub async fn check_file(&self, filename: impl AsRef<Path>) -> Result<bool> {
+ let abs_filename = filename.as_ref().canonicalize()?;
if let Some(row) = self
.0
@@ -87,8 +86,8 @@ impl Database {
}
}
- pub async fn get_modtime(&self, filename: &impl AsRef<OsStr>) -> Result<u64> {
- let abs_filename = Path::new(filename).canonicalize()?;
+ pub async fn get_modtime(&self, filename: impl AsRef<Path>) -> Result<u64> {
+ let abs_filename = filename.as_ref().canonicalize()?;
if let Some(row) = self
.0
@@ -125,6 +124,8 @@ impl Database {
tasks.join_all().await;
+ self.0.execute("VACUUM;", ()).await?;
+
Ok(())
}
diff --git a/src/files.rs b/src/files.rs
index c0f4b68..743e489 100644
--- a/src/files.rs
+++ b/src/files.rs
@@ -17,8 +17,11 @@ pub struct FileError {
}
impl FileError {
- pub fn new(file: PathBuf, error: anyhow::Error) -> Self {
- FileError { file, error }
+ pub fn new(file: impl AsRef<Path>, error: anyhow::Error) -> Self {
+ FileError {
+ file: file.as_ref().to_path_buf(),
+ error,
+ }
}
}
@@ -60,15 +63,17 @@ async fn handle_file(file: PathBuf, conn: Database) -> Result<()> {
Ok(())
}
-pub async fn index_files_recursively(path: &Path, conn: &Database) -> Result<()> {
- if !path.is_dir() {
+pub async fn index_files_recursively(path: impl AsRef<Path>, conn: &Database) -> Result<()> {
+ if !path.as_ref().is_dir() {
return Err(anyhow!("Invalid root directory"));
}
- let abspath = path.canonicalize()?;
+ let abspath = path.as_ref().canonicalize()?;
let mut tasks = JoinSet::new();
let mut dirs = vec![abspath];
+ let mut counter: u64 = 0;
+
while let Some(dir) = dirs.pop() {
let mut read_dir = read_dir(dir).await?;
@@ -91,37 +96,59 @@ pub async fn index_files_recursively(path: &Path, conn: &Database) -> Result<()>
match task {
Ok(Err(error)) => eprintln!("{error}"),
Err(error) => eprintln!("Error encountered:\t{}", error),
- _ => {}
+ _ => {
+ counter += 1;
+ print!("\rParsed files:\t{counter}");
+ }
}
}
-
Ok(())
}
-pub async fn reencode_files(conn: &Database, folderpath: Option<&PathBuf>) -> Result<()> {
- let mut nocheck = true;
- let mut path = Path::new("");
+fn check_path(folderpath: Option<&PathBuf>) -> (PathBuf, bool) {
if let Some(real_path) = folderpath {
- nocheck = false;
- path = real_path;
+ (real_path.to_owned(), false)
+ } else {
+ (PathBuf::new(), true)
}
+}
+
+pub async fn reencode_files(folderpath: Option<&PathBuf>, conn: &Database) -> Result<()> {
+ let (path, nocheck) = check_path(folderpath);
let stream = conn.get_toencode_stream().await?;
pin_mut!(stream);
let mut tasks = JoinSet::new();
+ let mut counter: u64 = 0;
+
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) {
+ if nocheck || filename.starts_with(&path) {
tasks.spawn_blocking(move || handle_encode(filename));
}
}
}
+ let mut update_tasks = JoinSet::new();
+
while let Some(task) = tasks.join_next().await {
match task {
+ Ok(Ok(path)) => {
+ let newconn = conn.clone();
+ update_tasks.spawn(async move { newconn.update_file(path).await });
+ counter += 1;
+ print!("\rReencoded files:\t{counter}")
+ }
+ Ok(Err(error)) => eprintln!("{error}"),
+ Err(error) => eprintln!("Error encountered:\t{}", error),
+ }
+ }
+
+ while let Some(task) = update_tasks.join_next().await {
+ match task {
Ok(Err(error)) => eprintln!("{error}"),
Err(error) => eprintln!("Error encountered:\t{}", error),
_ => {}
@@ -131,13 +158,8 @@ pub async fn reencode_files(conn: &Database, folderpath: Option<&PathBuf>) -> Re
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;
- }
+pub async fn count_reencode_files(folderpath: Option<&PathBuf>, conn: &Database) -> Result<u64> {
+ let (path, nocheck) = check_path(folderpath);
let mut counter: u64 = 0;
let stream = conn.get_toencode_stream().await?;
@@ -146,7 +168,7 @@ pub async fn count_reencode_files(conn: &Database, folderpath: Option<&PathBuf>)
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) {
+ if nocheck || filename.starts_with(&path) {
counter += 1;
}
}
@@ -160,25 +182,24 @@ mod tests {
use super::*;
#[tokio::test]
- async fn test_lots_of_files() {
+ async fn test_index_lots_of_files() {
let conn = Database::new("temp3.db").await.unwrap();
- index_files_recursively(Path::new("/mnt/Music"), &conn)
+ index_files_recursively(Path::new("./testfiles"), &conn)
+ .await
+ .unwrap();
+
+ std::fs::remove_file("temp3.db").unwrap();
+ }
+
+ #[tokio::test]
+ async fn test_reencode_lots_of_files() {
+ let conn = Database::new("temp4.db").await.unwrap();
+ let path = PathBuf::from("./testfiles");
+ index_files_recursively(Path::new("./testfiles"), &conn)
.await
.unwrap();
- println!(
- "\n{}",
- conn.0
- .query("SELECT COUNT(DISTINCT path) FROM flacs", ())
- .await
- .unwrap()
- .next()
- .await
- .unwrap()
- .unwrap()
- .get_value(0)
- .unwrap()
- .as_integer()
- .unwrap()
- );
+ println!("\n{}", count_reencode_files(None, &conn).await.unwrap());
+ reencode_files(Some(&path), &conn).await.unwrap();
+ std::fs::remove_file("temp4.db").unwrap();
}
}
diff --git a/src/flac.rs b/src/flac.rs
index 64ac6a9..42f9d21 100644
--- a/src/flac.rs
+++ b/src/flac.rs
@@ -1,10 +1,9 @@
use anyhow::{Result, anyhow};
use flac_bound::{FlacEncoder, WriteWrapper};
use i24::i24;
-use md5::{Digest, Md5, Md5Core, digest::core_api::CoreWrapper};
+use md5::{Digest, Md5};
use metaflac::{Block, Tag};
use std::{
- ffi::OsStr,
fs::File,
path::{Path, PathBuf},
};
@@ -54,13 +53,107 @@ impl Bps {
}
}
+struct FileEncoder {
+ filename: PathBuf,
+ streamdata: StreamConfig,
+ format: BoxedFormatReader,
+ decoder: BoxedAudioDecoder,
+}
+
+impl FileEncoder {
+ fn new(file: impl AsRef<Path>) -> Result<Self> {
+ let (format, decoder, config) = init_decoder(&file)?;
+ Ok(FileEncoder {
+ filename: file.as_ref().to_path_buf(),
+ streamdata: config,
+ format,
+ decoder,
+ })
+ }
+
+ fn temp_name(&self) -> PathBuf {
+ self.filename.clone().with_extension("tmp")
+ }
+
+ fn encode(self, encoder: FlacEncoder) -> Result<()> {
+ let filename = self.filename.clone();
+ let tempname = self.temp_name();
+
+ let hash = match self.streamdata.bits_per_sample {
+ Bps::_16 => encode_cycle_16(self.format, self.decoder, encoder)?,
+ Bps::_24 => encode_cycle_24(self.format, self.decoder, encoder)?,
+ Bps::_32 => encode_cycle_32(self.format, self.decoder, encoder)?,
+ };
+
+ let tags = Tag::read_from_path(&filename)?;
+ let mut output = Tag::read_from_path(&tempname)?;
+ let mut streaminfo = tags.get_streaminfo().unwrap().clone();
+
+ streaminfo.md5 = hash;
+ output.set_streaminfo(streaminfo);
+
+ for block in tags.blocks() {
+ match block {
+ Block::VorbisComment(comment) => {
+ for (key, val) in comment.comments.clone() {
+ output.set_vorbis(key, val);
+ }
+ }
+ Block::StreamInfo(_) => {}
+ _ => output.push_block(block.clone()),
+ }
+ }
+
+ output.write_to_path(&tempname)?;
+
+ std::fs::rename(tempname, filename)?;
+
+ Ok(())
+ }
+}
+
+fn init_decoder(
+ filename: impl AsRef<Path>,
+) -> 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();
+ hint.with_extension("flac");
+
+ let format_opts: FormatOptions = Default::default();
+ let metadata_opts: MetadataOptions = Default::default();
+
+ let format = symphonia::default::get_probe()
+ .probe(&hint, mss, format_opts, metadata_opts)
+ .unwrap();
+
+ let track = format.default_track(TrackType::Audio).unwrap();
+
+ let decoder = symphonia::default::get_codecs()
+ .make_audio_decoder(
+ track.codec_params.as_ref().unwrap().audio().unwrap(),
+ &Default::default(),
+ )
+ .unwrap();
+
+ let params = track.codec_params.as_ref().unwrap().audio().unwrap();
+
+ let config = StreamConfig {
+ channels: u32::try_from(params.channels.as_ref().unwrap().count()).unwrap(),
+ bits_per_sample: Bps::new(params.bits_per_sample.unwrap())?,
+ sample_rate: params.sample_rate.unwrap(),
+ };
+
+ Ok((format, decoder, config))
+}
+
fn encode_cycle_16(
- mut format: Box<dyn FormatReader>,
- mut decoder: Box<dyn AudioDecoder + 'static>,
+ mut format: BoxedFormatReader,
+ mut decoder: BoxedAudioDecoder,
mut encoder: FlacEncoder,
- hasher: &mut CoreWrapper<Md5Core>,
-) -> Result<()> {
+) -> Result<Vec<u8>> {
let mut buffer: Vec<i32> = Vec::new();
+ let mut hasher = Md5::new();
let track_id = format.default_track(TrackType::Audio).unwrap().id;
loop {
@@ -96,16 +189,16 @@ fn encode_cycle_16(
return Err(anyhow!("Encoding failed:\t{:?}", enc.state()));
}
- Ok(())
+ Ok(hasher.finalize().to_vec())
}
fn encode_cycle_24(
- mut format: Box<dyn FormatReader>,
- mut decoder: Box<dyn AudioDecoder + 'static>,
+ mut format: BoxedFormatReader,
+ mut decoder: BoxedAudioDecoder,
mut encoder: FlacEncoder,
- hasher: &mut CoreWrapper<Md5Core>,
-) -> Result<()> {
+) -> Result<Vec<u8>> {
let mut buffer: Vec<i32> = Vec::new();
+ let mut hasher = Md5::new();
let track_id = format.default_track(TrackType::Audio).unwrap().id;
loop {
@@ -141,16 +234,16 @@ fn encode_cycle_24(
return Err(anyhow!("Encoding failed:\t{:?}", enc.state()));
}
- Ok(())
+ Ok(hasher.finalize().to_vec())
}
fn encode_cycle_32(
- mut format: Box<dyn FormatReader>,
- mut decoder: Box<dyn AudioDecoder + 'static>,
+ mut format: BoxedFormatReader,
+ mut decoder: BoxedAudioDecoder,
mut encoder: FlacEncoder,
- hasher: &mut CoreWrapper<Md5Core>,
-) -> Result<()> {
+) -> Result<Vec<u8>> {
let mut buffer: Vec<i32> = Vec::new();
+ let mut hasher = Md5::new();
let track_id = format.default_track(TrackType::Audio).unwrap().id;
loop {
@@ -185,116 +278,41 @@ fn encode_cycle_32(
return Err(anyhow!("Encoding failed:\t{:?}", enc.state()));
}
- Ok(())
-}
-
-fn init_decoder(
- filename: impl AsRef<Path>,
-) -> 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();
- hint.with_extension("flac");
-
- let format_opts: FormatOptions = Default::default();
- let metadata_opts: MetadataOptions = Default::default();
-
- let format = symphonia::default::get_probe()
- .probe(&hint, mss, format_opts, metadata_opts)
- .unwrap();
-
- let track = format.default_track(TrackType::Audio).unwrap();
-
- let decoder = symphonia::default::get_codecs()
- .make_audio_decoder(
- track.codec_params.as_ref().unwrap().audio().unwrap(),
- &Default::default(),
- )
- .unwrap();
-
- let params = track.codec_params.as_ref().unwrap().audio().unwrap();
-
- let config = StreamConfig {
- channels: u32::try_from(params.channels.as_ref().unwrap().count()).unwrap(),
- bits_per_sample: Bps::new(params.bits_per_sample.unwrap())?,
- sample_rate: params.sample_rate.unwrap(),
- };
-
- Ok((format, decoder, config))
-}
-
-fn write_tags(
- file: impl AsRef<Path>,
- tempname: impl AsRef<Path>,
- hasher: impl Digest,
-) -> Result<()> {
- let tags = Tag::read_from_path(file)?;
- let mut output = Tag::read_from_path(&tempname)?;
- let mut streaminfo = tags.get_streaminfo().unwrap().clone();
-
- streaminfo.md5 = hasher.finalize()[..].to_vec();
- output.set_streaminfo(streaminfo);
-
- for block in tags.blocks() {
- match block {
- Block::VorbisComment(comment) => {
- for (key, val) in comment.comments.clone() {
- output.set_vorbis(key, val);
- }
- }
- Block::StreamInfo(_) => {}
- _ => output.push_block(block.clone()),
- }
- }
-
- output.write_to_path(&tempname)?;
- Ok(())
+ Ok(hasher.finalize().to_vec())
}
-fn encode_file(filename: impl AsRef<OsStr>) -> Result<()> {
- let file = Path::new(&filename);
- let tempname = &format!("{}.tmp", file.to_str().unwrap());
-
- let (format, decoder, config) = init_decoder(file)?;
+fn encode_file(filename: impl AsRef<Path>) -> Result<()> {
+ let filencoder = FileEncoder::new(filename)?;
- let mut outf = File::create(tempname)?;
+ let mut outf = File::create(filencoder.temp_name())?;
let mut outw = WriteWrapper(&mut outf);
let enc = FlacEncoder::new()
.unwrap()
- .channels(config.channels)
- .bits_per_sample(config.bits_per_sample.value())
- .sample_rate(config.sample_rate)
+ .channels(filencoder.streamdata.channels)
+ .bits_per_sample(filencoder.streamdata.bits_per_sample.value())
+ .sample_rate(filencoder.streamdata.sample_rate)
.compression_level(8)
.verify(false)
.init_write(&mut outw)
.unwrap();
- let mut hasher = Md5::new();
-
- match config.bits_per_sample {
- Bps::_16 => encode_cycle_16(format, decoder, enc, &mut hasher),
- Bps::_24 => encode_cycle_24(format, decoder, enc, &mut hasher),
- Bps::_32 => encode_cycle_32(format, decoder, enc, &mut hasher),
- }?;
-
- write_tags(file, tempname, hasher)?;
-
- std::fs::rename(tempname, file)?;
-
- Ok(())
+ filencoder.encode(enc)
}
-pub fn handle_encode(file: PathBuf) -> Result<()> {
+pub fn handle_encode(file: impl AsRef<Path>) -> Result<impl AsRef<Path>> {
if let Err(error) = encode_file(&file) {
Err(anyhow!(files::FileError::new(file, error)))
} else {
- Ok(())
+ Ok(file)
}
}
-pub fn get_vendor(file: &Path) -> Result<String> {
- let tag = Tag::read_from_path(file)?;
- Ok(tag.vorbis_comments().unwrap().vendor_string.clone())
+pub fn get_vendor(file: impl AsRef<Path>) -> Result<String> {
+ if let Some(vorbis) = Tag::read_from_path(file)?.vorbis_comments() {
+ Ok(vorbis.vendor_string.to_owned())
+ } else {
+ Err(anyhow!("Vendor string not found"))
+ }
}
#[cfg(test)]
@@ -307,7 +325,7 @@ mod tests {
let name = "16bit.flac";
let tempname = "16bit.flac.temp";
std::fs::copy(name, tempname).unwrap();
- encode_file(std::path::Path::new(name)).unwrap();
+ encode_file(name).unwrap();
let target_md5 = Tag::read_from_path(tempname)
.unwrap()
.get_streaminfo()
@@ -329,7 +347,7 @@ mod tests {
let name = "24bit.flac";
let tempname = "24bit.flac.temp";
std::fs::copy(name, tempname).unwrap();
- encode_file(std::path::Path::new(name)).unwrap();
+ encode_file(name).unwrap();
let target_md5 = Tag::read_from_path(tempname)
.unwrap()
.get_streaminfo()
@@ -351,7 +369,7 @@ mod tests {
let name = "32bit.flac";
let tempname = "32bit.flac.temp";
std::fs::copy(name, tempname).unwrap();
- encode_file(std::path::Path::new(name)).unwrap();
+ encode_file(name).unwrap();
let target_md5 = Tag::read_from_path(tempname)
.unwrap()
.get_streaminfo()
diff --git a/src/main.rs b/src/main.rs
index d660f0a..5d3835f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -16,14 +16,6 @@ fn build_cli() -> Command {
.value_parser(value_parser!(PathBuf)),
)
.arg(
- Arg::new("index")
- .short('i')
- .long("index")
- .help("Only index files")
- .requires("path")
- .action(ArgAction::SetTrue),
- )
- .arg(
Arg::new("doit")
.long("doit")
.help("Actually reencode files")
@@ -97,13 +89,13 @@ fn main() -> Result<()> {
};
let path = args.get_one::<PathBuf>("path");
- if !args.get_flag("index") && !args.get_flag("clean") && !args.get_flag("doit") {
- let count = files::count_reencode_files(&conn, path).await.unwrap();
+ if path.is_none() && !args.get_flag("clean") && !args.get_flag("doit") {
+ let count = files::count_reencode_files(path, &conn).await.unwrap();
println!("Files to reencode:\t{count}");
- }
-
- if args.get_flag("index") {
- files::index_files_recursively(path.unwrap(), &conn).await?;
+ } else if let Some(realpath) = path {
+ if !args.get_flag("doit") {
+ files::index_files_recursively(realpath, &conn).await?;
+ }
}
if args.get_flag("clean") {
@@ -111,7 +103,7 @@ fn main() -> Result<()> {
}
if args.get_flag("doit") {
- files::reencode_files(&conn, path).await?;
+ files::reencode_files(path, &conn).await?;
}
Ok::<(), anyhow::Error>(())
})?;