From 76d25ebe5edc0cd358c3fd47a89e6f847c52ee1f Mon Sep 17 00:00:00 2001 From: "Roncero Blanco, Edgar" Date: Mon, 26 May 2025 17:20:55 +0200 Subject: [PATCH] BPM functionality --- src/app.py | 64 +++++++++++++++++++++++++++++++--------------- src/def_config.ini | 4 +-- 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/src/app.py b/src/app.py index 9f8c560..58d4776 100644 --- a/src/app.py +++ b/src/app.py @@ -1,11 +1,12 @@ import os import shutil from pathlib import Path -from mutagen.id3 import ID3 +from mutagen.id3 import ID3, TBPM, ID3NoHeaderError from datetime import datetime from collections import defaultdict import configparser import sys +import librosa GROUP_SIZE = 5 @@ -13,7 +14,6 @@ def load_config(): config_path = Path("config.ini") default_config_path = Path("def_config.ini") - # If config.ini missing, copy def_config.ini if not config_path.exists(): if default_config_path.exists(): shutil.copy(default_config_path, config_path) @@ -22,29 +22,49 @@ def load_config(): print("Error: def_config.ini not found! Please create it and rerun.") sys.exit(1) - # Read default config default_config = configparser.ConfigParser() default_config.read(default_config_path) - # Read user config (overrides default) user_config = configparser.ConfigParser() user_config.read(config_path) - # Merge configs: start with defaults, update with user settings merged_config = configparser.ConfigParser() merged_config.read_dict(default_config) merged_config.read_dict(user_config) return merged_config +def analyze_bpm_librosa(file_path): + try: + y, sr = librosa.load(file_path, mono=True) + tempo, _ = librosa.beat.beat_track(y=y, sr=sr) + return int(round(float(tempo))) + except Exception as e: + print(f"Error analyzing BPM for {file_path.name}: {e}") + return None + +def write_bpm_tag(file_path, bpm): + try: + try: + tags = ID3(file_path) + except ID3NoHeaderError: + tags = ID3() + + tags.delall("TBPM") + tags.add(TBPM(encoding=3, text=str(bpm))) + tags.save(file_path) + print(f"Written BPM={bpm} to {file_path.name}") + except Exception as e: + print(f"Failed to write BPM tag for {file_path.name}: {e}") + def get_bpm(file_path): try: tags = ID3(file_path) bpm_tag = tags.get("TBPM") if bpm_tag: return int(float(bpm_tag.text[0])) - except Exception as e: - print(f"Skipping {file_path.name} (no BPM): {e}") + except Exception: + pass return None def split_evenly(lst, n): @@ -54,13 +74,12 @@ def split_evenly(lst, n): def main(): config = load_config() - # Use SplitFolderMB from config, convert MB to bytes CD_SIZE = config.getint("Settings", "SplitFolderMB") * 1024 * 1024 + bWriteNonPresentBPM = config.getboolean("Settings", "bWriteNonPresentBPM") + bCheckAllTracksBPM = config.getboolean("Settings", "bCheckAllTracksBPM") - # Store the current date for folder naming run_date = datetime.now().strftime("%Y-%m-%d_%H%M%S") - # Ask for source path, strip quotes if pasted source_input = input("Drag and drop your music folder here, then press Enter: ").strip().strip('"') source_media_path = Path(source_input) @@ -68,16 +87,26 @@ def main(): print(f"Error: {source_media_path} is not a valid directory.") sys.exit(1) - # Build destination path parent = source_media_path.parent folder_name = source_media_path.name dest_media_path = parent / f"[CDs-{run_date}]{folder_name}" dest_media_path.mkdir(parents=True, exist_ok=True) - # Collect all tracks recursively, group unknown BPM separately all_tracks = [] for file in source_media_path.rglob("*.mp3"): bpm = get_bpm(file) + + if bpm is None and bWriteNonPresentBPM: + bpm = analyze_bpm_librosa(file) + if bpm is not None: + write_bpm_tag(file, bpm) + + elif bpm is not None and bCheckAllTracksBPM: + new_bpm = analyze_bpm_librosa(file) + if new_bpm is not None and new_bpm != bpm: + bpm = new_bpm + write_bpm_tag(file, bpm) + size = file.stat().st_size if bpm is None: bpm_range = "[Unknown BPM]" @@ -89,35 +118,28 @@ def main(): print("No MP3 files found.") sys.exit(0) - # Calculate total size and number of CDs needed total_size = sum(t["size"] for t in all_tracks) num_cds = max(1, (total_size + CD_SIZE - 1) // CD_SIZE) print(f"Total size: {total_size / 1024**2:.2f} MB, splitting into {num_cds} CDs") - # Group tracks by BPM range bpm_groups = defaultdict(list) for track in all_tracks: bpm_groups[track["bpm_range"]].append(track) - # Sort each bpm group for bpm_range in bpm_groups: bpm_groups[bpm_range].sort(key=lambda x: x["file"].name) - # Split each BPM group evenly into num_cds parts bpm_chunks = {} for bpm_range, tracks in bpm_groups.items(): bpm_chunks[bpm_range] = split_evenly(tracks, num_cds) - # Prepare CDs cd_contents = [[] for _ in range(num_cds)] - # Fill CDs with balanced BPM chunks for bpm_range in bpm_chunks: for i, chunk in enumerate(bpm_chunks[bpm_range]): cd_contents[i].extend(chunk) - # Write CDs with BPM subfolders for i, tracks in enumerate(cd_contents, start=1): cd_folder = dest_media_path / f"CD-{i:02}" cd_folder.mkdir(parents=True, exist_ok=True) @@ -127,9 +149,9 @@ def main(): bpm_subfolder.mkdir(parents=True, exist_ok=True) shutil.copy(track["file"], bpm_subfolder / track["file"].name) size_accum += track["size"] - print(f"[WRITE] CD-{i:02}: {len(tracks)} tracks, approx {size_accum/1024**2:.2f} MB") + print(f"[WRITE] CD-{i:02}: {len(tracks)} tracks, approx {size_accum / 1024**2:.2f} MB") - print("\n✅ Done!") + print("\n✓ Done!") if __name__ == "__main__": main() diff --git a/src/def_config.ini b/src/def_config.ini index 9e41dfc..98d5071 100644 --- a/src/def_config.ini +++ b/src/def_config.ini @@ -1,4 +1,4 @@ [Settings] -bCheckNonPresentBPM = False -bReCheckPresentBPM = False +bWriteNonPresentBPM = False +bCheckAllTracksBPM = False SplitFolderMB = 695