BPM functionality

This commit is contained in:
Roncero Blanco, Edgar
2025-05-26 17:20:55 +02:00
parent 63953f45e8
commit 76d25ebe5e
2 changed files with 45 additions and 23 deletions

View File

@@ -1,11 +1,12 @@
import os import os
import shutil import shutil
from pathlib import Path from pathlib import Path
from mutagen.id3 import ID3 from mutagen.id3 import ID3, TBPM, ID3NoHeaderError
from datetime import datetime from datetime import datetime
from collections import defaultdict from collections import defaultdict
import configparser import configparser
import sys import sys
import librosa
GROUP_SIZE = 5 GROUP_SIZE = 5
@@ -13,7 +14,6 @@ def load_config():
config_path = Path("config.ini") config_path = Path("config.ini")
default_config_path = Path("def_config.ini") default_config_path = Path("def_config.ini")
# If config.ini missing, copy def_config.ini
if not config_path.exists(): if not config_path.exists():
if default_config_path.exists(): if default_config_path.exists():
shutil.copy(default_config_path, config_path) 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.") print("Error: def_config.ini not found! Please create it and rerun.")
sys.exit(1) sys.exit(1)
# Read default config
default_config = configparser.ConfigParser() default_config = configparser.ConfigParser()
default_config.read(default_config_path) default_config.read(default_config_path)
# Read user config (overrides default)
user_config = configparser.ConfigParser() user_config = configparser.ConfigParser()
user_config.read(config_path) user_config.read(config_path)
# Merge configs: start with defaults, update with user settings
merged_config = configparser.ConfigParser() merged_config = configparser.ConfigParser()
merged_config.read_dict(default_config) merged_config.read_dict(default_config)
merged_config.read_dict(user_config) merged_config.read_dict(user_config)
return merged_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): def get_bpm(file_path):
try: try:
tags = ID3(file_path) tags = ID3(file_path)
bpm_tag = tags.get("TBPM") bpm_tag = tags.get("TBPM")
if bpm_tag: if bpm_tag:
return int(float(bpm_tag.text[0])) return int(float(bpm_tag.text[0]))
except Exception as e: except Exception:
print(f"Skipping {file_path.name} (no BPM): {e}") pass
return None return None
def split_evenly(lst, n): def split_evenly(lst, n):
@@ -54,13 +74,12 @@ def split_evenly(lst, n):
def main(): def main():
config = load_config() config = load_config()
# Use SplitFolderMB from config, convert MB to bytes
CD_SIZE = config.getint("Settings", "SplitFolderMB") * 1024 * 1024 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") 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_input = input("Drag and drop your music folder here, then press Enter: ").strip().strip('"')
source_media_path = Path(source_input) source_media_path = Path(source_input)
@@ -68,16 +87,26 @@ def main():
print(f"Error: {source_media_path} is not a valid directory.") print(f"Error: {source_media_path} is not a valid directory.")
sys.exit(1) sys.exit(1)
# Build destination path
parent = source_media_path.parent parent = source_media_path.parent
folder_name = source_media_path.name folder_name = source_media_path.name
dest_media_path = parent / f"[CDs-{run_date}]{folder_name}" dest_media_path = parent / f"[CDs-{run_date}]{folder_name}"
dest_media_path.mkdir(parents=True, exist_ok=True) dest_media_path.mkdir(parents=True, exist_ok=True)
# Collect all tracks recursively, group unknown BPM separately
all_tracks = [] all_tracks = []
for file in source_media_path.rglob("*.mp3"): for file in source_media_path.rglob("*.mp3"):
bpm = get_bpm(file) 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 size = file.stat().st_size
if bpm is None: if bpm is None:
bpm_range = "[Unknown BPM]" bpm_range = "[Unknown BPM]"
@@ -89,35 +118,28 @@ def main():
print("No MP3 files found.") print("No MP3 files found.")
sys.exit(0) sys.exit(0)
# Calculate total size and number of CDs needed
total_size = sum(t["size"] for t in all_tracks) total_size = sum(t["size"] for t in all_tracks)
num_cds = max(1, (total_size + CD_SIZE - 1) // CD_SIZE) 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") print(f"Total size: {total_size / 1024**2:.2f} MB, splitting into {num_cds} CDs")
# Group tracks by BPM range
bpm_groups = defaultdict(list) bpm_groups = defaultdict(list)
for track in all_tracks: for track in all_tracks:
bpm_groups[track["bpm_range"]].append(track) bpm_groups[track["bpm_range"]].append(track)
# Sort each bpm group
for bpm_range in bpm_groups: for bpm_range in bpm_groups:
bpm_groups[bpm_range].sort(key=lambda x: x["file"].name) bpm_groups[bpm_range].sort(key=lambda x: x["file"].name)
# Split each BPM group evenly into num_cds parts
bpm_chunks = {} bpm_chunks = {}
for bpm_range, tracks in bpm_groups.items(): for bpm_range, tracks in bpm_groups.items():
bpm_chunks[bpm_range] = split_evenly(tracks, num_cds) bpm_chunks[bpm_range] = split_evenly(tracks, num_cds)
# Prepare CDs
cd_contents = [[] for _ in range(num_cds)] cd_contents = [[] for _ in range(num_cds)]
# Fill CDs with balanced BPM chunks
for bpm_range in bpm_chunks: for bpm_range in bpm_chunks:
for i, chunk in enumerate(bpm_chunks[bpm_range]): for i, chunk in enumerate(bpm_chunks[bpm_range]):
cd_contents[i].extend(chunk) cd_contents[i].extend(chunk)
# Write CDs with BPM subfolders
for i, tracks in enumerate(cd_contents, start=1): for i, tracks in enumerate(cd_contents, start=1):
cd_folder = dest_media_path / f"CD-{i:02}" cd_folder = dest_media_path / f"CD-{i:02}"
cd_folder.mkdir(parents=True, exist_ok=True) cd_folder.mkdir(parents=True, exist_ok=True)
@@ -127,9 +149,9 @@ def main():
bpm_subfolder.mkdir(parents=True, exist_ok=True) bpm_subfolder.mkdir(parents=True, exist_ok=True)
shutil.copy(track["file"], bpm_subfolder / track["file"].name) shutil.copy(track["file"], bpm_subfolder / track["file"].name)
size_accum += track["size"] 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__": if __name__ == "__main__":
main() main()

View File

@@ -1,4 +1,4 @@
[Settings] [Settings]
bCheckNonPresentBPM = False bWriteNonPresentBPM = False
bReCheckPresentBPM = False bCheckAllTracksBPM = False
SplitFolderMB = 695 SplitFolderMB = 695