Add option to preserve file modification time when writing tags.

Issue #21
This commit is contained in:
CDrummond 2025-03-24 07:17:51 +00:00
parent 342440f04b
commit 417ac5f652
6 changed files with 42 additions and 13 deletions

View File

@ -1,6 +1,7 @@
0.4.0 0.4.0
----- -----
1. Add action to export results from database to files. 1. Add action to export results from database to files.
2. Add option to preserve file modification time when writing tags.
0.3.0 0.3.0
----- -----

View File

@ -150,6 +150,8 @@ in mixes. See the `Ignore` section later on for more details.
* `tags` specifies whether analysis results should be written to, and re-read from, * `tags` specifies whether analysis results should be written to, and re-read from,
files. Set to `true` or `false`. If enabled, then results are stored in a `COMMENT` files. Set to `true` or `false`. If enabled, then results are stored in a `COMMENT`
tag that starts with `BLISS_ANALYSIS` tag that starts with `BLISS_ANALYSIS`
* `preserve` specifies whether file modification time should be preserved when
writing tags. Set to `true` or `false`.
Command-line parameters Command-line parameters
@ -173,6 +175,7 @@ tracks are to be analysed and how many old tracks are left in the database.
* `-J` / `--json` JSONRPC port number of your LMS server. * `-J` / `--json` JSONRPC port number of your LMS server.
* `-n` / `--numtracks` Specify maximum number of tracks to analyse. * `-n` / `--numtracks` Specify maximum number of tracks to analyse.
* `-T` / `--tags` Write analysis results to file tags, and read from file tags. * `-T` / `--tags` Write analysis results to file tags, and read from file tags.
* `-p' / '--preserve` Attempt to preserve file modification time when writing tags.
Equivalent items specified in the INI config file (detailed above) will override Equivalent items specified in the INI config file (detailed above) will override
any specified on the commandline. any specified on the commandline.

View File

@ -161,7 +161,7 @@ fn show_errors(failed: &mut Vec<String>, tag_error: &mut Vec<String>) {
} }
#[cfg(not(feature = "ffmpeg"))] #[cfg(not(feature = "ffmpeg"))]
fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, max_threads: usize, use_tags: bool) -> Result<()> { fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, max_threads: usize, use_tags: bool, preserve_mod_times: bool) -> Result<()> {
let total = track_paths.len(); let total = track_paths.len();
let progress = ProgressBar::new(total.try_into().unwrap()).with_style( let progress = ProgressBar::new(total.try_into().unwrap()).with_style(
ProgressStyle::default_bar() ProgressStyle::default_bar()
@ -237,7 +237,7 @@ fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, max
tag_error.push(sname.clone()); tag_error.push(sname.clone());
} }
if use_tags { if use_tags {
tags::write_analysis(&cpath, &track.analysis); tags::write_analysis(&cpath, &track.analysis, preserve_mod_times);
} }
db.add_track(&sname, &meta, &track.analysis); db.add_track(&sname, &meta, &track.analysis);
} }
@ -259,7 +259,7 @@ fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, max
} }
#[cfg(feature = "ffmpeg")] #[cfg(feature = "ffmpeg")]
fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, max_threads: usize, use_tags: bool) -> Result<()> { fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, max_threads: usize, use_tags: bool, preserve_mod_times: bool) -> Result<()> {
let total = track_paths.len(); let total = track_paths.len();
let progress = ProgressBar::new(total.try_into().unwrap()).with_style( let progress = ProgressBar::new(total.try_into().unwrap()).with_style(
ProgressStyle::default_bar() ProgressStyle::default_bar()
@ -292,7 +292,7 @@ fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, max
tag_error.push(sname.clone()); tag_error.push(sname.clone());
} }
if use_tags { if use_tags {
tags::write_analysis(&cpath, &track.analysis); tags::write_analysis(&cpath, &track.analysis, preserve_mod_times);
} }
db.add_track(&sname, &meta, &track.analysis); db.add_track(&sname, &meta, &track.analysis);
analysed += 1; analysed += 1;
@ -405,7 +405,7 @@ fn analyse_new_cue_tracks(db:&db::Db, mpath: &PathBuf, cue_tracks:Vec<cue::CueTr
Ok(()) Ok(())
} }
pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_old: bool, max_num_files: usize, max_threads: usize, ignore_path: &PathBuf, use_tags: bool) { pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_old: bool, max_num_files: usize, max_threads: usize, ignore_path: &PathBuf, use_tags: bool, preserve_mod_times: bool) {
let mut db = db::Db::new(&String::from(db_path)); let mut db = db::Db::new(&String::from(db_path));
db.init(); db.init();
@ -450,7 +450,7 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_o
} }
} else { } else {
if !track_paths.is_empty() { if !track_paths.is_empty() {
match analyse_new_files(&db, &mpath, track_paths, max_threads, use_tags) { match analyse_new_files(&db, &mpath, track_paths, max_threads, use_tags, preserve_mod_times) {
Ok(_) => { changes_made = true; } Ok(_) => { changes_made = true; }
Err(e) => { log::error!("Analysis returned error: {}", e); } Err(e) => { log::error!("Analysis returned error: {}", e); }
} }
@ -482,10 +482,10 @@ pub fn read_tags(db_path: &str, mpaths: &Vec<PathBuf>) {
db.close(); db.close();
} }
pub fn export(db_path: &str, mpaths: &Vec<PathBuf>) { pub fn export(db_path: &str, mpaths: &Vec<PathBuf>, preserve_mod_times: bool) {
let db = db::Db::new(&String::from(db_path)); let db = db::Db::new(&String::from(db_path));
db.init(); db.init();
db.export(&mpaths); db.export(&mpaths, preserve_mod_times);
db.close(); db.close();
} }

View File

@ -344,7 +344,7 @@ impl Db {
} }
} }
pub fn export(&self, mpaths: &Vec<PathBuf>) { pub fn export(&self, mpaths: &Vec<PathBuf>, preserve_mod_times: bool) {
let total = self.get_track_count(); let total = self.get_track_count();
if total > 0 { if total > 0 {
let progress = ProgressBar::new(total.try_into().unwrap()).with_style( let progress = ProgressBar::new(total.try_into().unwrap()).with_style(
@ -377,7 +377,7 @@ impl Db {
let spath = String::from(track_path.to_string_lossy()); let spath = String::from(track_path.to_string_lossy());
let meta = tags::read(&spath, true); let meta = tags::read(&spath, true);
if meta.is_empty() || meta.analysis.is_none() || meta.analysis.unwrap()!=dbtags.analysis { if meta.is_empty() || meta.analysis.is_none() || meta.analysis.unwrap()!=dbtags.analysis {
tags::write_analysis(&spath, &dbtags.analysis); tags::write_analysis(&spath, &dbtags.analysis, preserve_mod_times);
updated+=1; updated+=1;
} }
break; break;

View File

@ -42,6 +42,7 @@ fn main() {
let mut music_paths: Vec<PathBuf> = Vec::new(); let mut music_paths: Vec<PathBuf> = Vec::new();
let mut max_threads: usize = 0; let mut max_threads: usize = 0;
let mut use_tags = false; let mut use_tags = false;
let mut preserve_mod_times = false;
match dirs::home_dir() { match dirs::home_dir() {
Some(path) => { Some(path) => {
@ -76,6 +77,7 @@ fn main() {
arg_parse.refer(&mut max_num_files).add_option(&["-n", "--numfiles"], Store, "Maximum number of files to analyse"); arg_parse.refer(&mut max_num_files).add_option(&["-n", "--numfiles"], Store, "Maximum number of files to analyse");
arg_parse.refer(&mut max_threads).add_option(&["-t", "--threads"], Store, "Maximum number of threads to use for analysis"); arg_parse.refer(&mut max_threads).add_option(&["-t", "--threads"], Store, "Maximum number of threads to use for analysis");
arg_parse.refer(&mut use_tags).add_option(&["-T", "--tags"], StoreTrue, "Read/write analysis results from/to source files"); arg_parse.refer(&mut use_tags).add_option(&["-T", "--tags"], StoreTrue, "Read/write analysis results from/to source files");
arg_parse.refer(&mut preserve_mod_times).add_option(&["-p", "--preserve"], StoreTrue, "Preserve modification time when writing tags to files");
arg_parse.refer(&mut task).add_argument("task", Store, "Task to perform; analyse, tags, ignore, upload, export, stopmixer."); arg_parse.refer(&mut task).add_argument("task", Store, "Task to perform; analyse, tags, ignore, upload, export, stopmixer.");
arg_parse.parse_args_or_exit(); arg_parse.parse_args_or_exit();
} }
@ -147,6 +149,10 @@ fn main() {
Some(val) => { use_tags = val.eq("true"); } Some(val) => { use_tags = val.eq("true"); }
None => { } None => { }
} }
match config.get(TOP_LEVEL_INI_TAG, "preserve") {
Some(val) => { preserve_mod_times = val.eq("true"); }
None => { }
}
} }
Err(e) => { Err(e) => {
log::error!("Failed to load config file. {}", e); log::error!("Failed to load config file. {}", e);
@ -207,10 +213,10 @@ fn main() {
} }
analyse::update_ignore(&db_path, &ignore_path); analyse::update_ignore(&db_path, &ignore_path);
} else if task.eq_ignore_ascii_case("export") { } else if task.eq_ignore_ascii_case("export") {
analyse::export(&db_path, &music_paths); analyse::export(&db_path, &music_paths, preserve_mod_times);
} else { } else {
let ignore_path = PathBuf::from(&ignore_file); let ignore_path = PathBuf::from(&ignore_file);
analyse::analyse_files(&db_path, &music_paths, dry_run, keep_old, max_num_files, max_threads, &ignore_path, use_tags); analyse::analyse_files(&db_path, &music_paths, dry_run, keep_old, max_num_files, max_threads, &ignore_path, use_tags, preserve_mod_times);
} }
} }
} }

View File

@ -12,8 +12,11 @@ use lofty::file::FileType;
use lofty::prelude::{Accessor, AudioFile, ItemKey, TagExt, TaggedFileExt}; use lofty::prelude::{Accessor, AudioFile, ItemKey, TagExt, TaggedFileExt};
use lofty::tag::{ItemValue, Tag, TagItem}; use lofty::tag::{ItemValue, Tag, TagItem};
use regex::Regex; use regex::Regex;
use std::fs::File;
use std::fs;
use std::path::Path; use std::path::Path;
use substring::Substring; use substring::Substring;
use std::time::SystemTime;
use bliss_audio::{Analysis, AnalysisIndex}; use bliss_audio::{Analysis, AnalysisIndex};
const MAX_GENRE_VAL: usize = 192; const MAX_GENRE_VAL: usize = 192;
@ -22,7 +25,7 @@ const ANALYSIS_TAG:ItemKey = ItemKey::Comment;
const ANALYSIS_TAG_START: &str = "BLISS_ANALYSIS"; const ANALYSIS_TAG_START: &str = "BLISS_ANALYSIS";
const ANALYSIS_TAG_VER: u16 = 1; const ANALYSIS_TAG_VER: u16 = 1;
pub fn write_analysis(track: &String, analysis: &Analysis) { pub fn write_analysis(track: &String, analysis: &Analysis, preserve_mod_times: bool) {
let value = format!("{},{},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24}", ANALYSIS_TAG_START, ANALYSIS_TAG_VER, let value = format!("{},{},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24}", ANALYSIS_TAG_START, ANALYSIS_TAG_VER,
analysis[AnalysisIndex::Tempo], analysis[AnalysisIndex::Zcr], analysis[AnalysisIndex::MeanSpectralCentroid], analysis[AnalysisIndex::StdDeviationSpectralCentroid], analysis[AnalysisIndex::MeanSpectralRolloff], analysis[AnalysisIndex::Tempo], analysis[AnalysisIndex::Zcr], analysis[AnalysisIndex::MeanSpectralCentroid], analysis[AnalysisIndex::StdDeviationSpectralCentroid], analysis[AnalysisIndex::MeanSpectralRolloff],
analysis[AnalysisIndex::StdDeviationSpectralRolloff], analysis[AnalysisIndex::MeanSpectralFlatness], analysis[AnalysisIndex::StdDeviationSpectralFlatness], analysis[AnalysisIndex::MeanLoudness], analysis[AnalysisIndex::StdDeviationLoudness], analysis[AnalysisIndex::StdDeviationSpectralRolloff], analysis[AnalysisIndex::MeanSpectralFlatness], analysis[AnalysisIndex::StdDeviationSpectralFlatness], analysis[AnalysisIndex::MeanLoudness], analysis[AnalysisIndex::StdDeviationLoudness],
@ -58,7 +61,23 @@ pub fn write_analysis(track: &String, analysis: &Analysis) {
// Store analysis results // Store analysis results
tag.push(TagItem::new(ANALYSIS_TAG, ItemValue::Text(value))); tag.push(TagItem::new(ANALYSIS_TAG, ItemValue::Text(value)));
let now = SystemTime::now();
let mut mod_time = now;
if preserve_mod_times {
if let Ok(fmeta) = fs::metadata(track) {
if let Ok(time) = fmeta.modified() {
mod_time = time;
}
}
}
let _ = tag.save_to_path(Path::new(track), WriteOptions::default()); let _ = tag.save_to_path(Path::new(track), WriteOptions::default());
if preserve_mod_times {
if mod_time<now {
if let Ok(f) = File::open(track) {
let _ = f.set_modified(mod_time);
}
}
}
} }
} }