Compare commits

..

No commits in common. "1f83bb8517e49a92062315684c03a4740cba9acd" and "dfe237d3eabfca5a2a2668cbb68020bf22382f0c" have entirely different histories.

3 changed files with 69 additions and 135 deletions

2
.gitignore vendored
View File

@ -180,5 +180,3 @@ cython_debug/
.cursorignore .cursorignore
.cursorindexingignore .cursorindexingignore
# ed1337x
config.ini

View File

@ -1,61 +1,29 @@
import os import os
import shutil import shutil
from pathlib import Path from pathlib import Path
from mutagen.id3 import ID3, TBPM, ID3NoHeaderError from mutagen.id3 import ID3
from datetime import datetime from datetime import datetime
from collections import defaultdict from collections import defaultdict
import configparser
import sys
import librosa
CD_SIZE = 695 * 1024 * 1024 # 695 MB
GROUP_SIZE = 5 GROUP_SIZE = 5
def load_config(): # Store the current date
config_path = Path("config.ini") run_date = datetime.now().strftime("%Y-%m-%d_%H%M%S")
default_config_path = Path("def_config.ini")
if not config_path.exists(): # Ask for source path, strip quotes if pasted
if default_config_path.exists(): source_input = input("Drag and drop your music folder here, then press Enter: ").strip().strip('"')
shutil.copy(default_config_path, config_path) source_media_path = Path(source_input)
print("config.ini created from def_config.ini with default settings.")
else:
print("Error: def_config.ini not found! Please create it and rerun.")
sys.exit(1)
default_config = configparser.ConfigParser() if not source_media_path.exists() or not source_media_path.is_dir():
default_config.read(default_config_path) print(f"Error: {source_media_path} is not a valid directory.")
exit(1)
user_config = configparser.ConfigParser() # Build destination path
user_config.read(config_path) parent = source_media_path.parent
folder_name = source_media_path.name
merged_config = configparser.ConfigParser() dest_media_path = parent / f"[CDs-{run_date}]{folder_name}"
merged_config.read_dict(default_config) dest_media_path.mkdir(parents=True, exist_ok=True)
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[0])))
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:
@ -63,50 +31,14 @@ def get_bpm(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: except Exception as e:
pass print(f"Skipping {file_path.name} (no BPM): {e}")
return None return None
def split_evenly(lst, n): # Collect all tracks recursively, grouping unknown BPM separately
k, m = divmod(len(lst), n) all_tracks = []
return [lst[i*k + min(i, m):(i+1)*k + min(i+1, m)] for i in range(n)] for file in source_media_path.rglob("*.mp3"): # <-- recursive glob
def main():
config = load_config()
CD_SIZE = config.getint("Settings", "SplitFolderMB") * 1024 * 1024
bWriteNonPresentBPM = config.getboolean("Settings", "bWriteNonPresentBPM")
bCheckAllTracksBPM = config.getboolean("Settings", "bCheckAllTracksBPM")
run_date = datetime.now().strftime("%Y-%m-%d_%H%M%S")
source_input = input("Drag and drop your music folder here, then press Enter: ").strip().strip('"')
source_media_path = Path(source_input)
if not source_media_path.exists() or not source_media_path.is_dir():
print(f"Error: {source_media_path} is not a valid directory.")
sys.exit(1)
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)
all_tracks = []
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]"
@ -114,33 +46,44 @@ def main():
bpm_range = f"{(bpm // GROUP_SIZE) * GROUP_SIZE}-to-{((bpm // GROUP_SIZE) * GROUP_SIZE) + GROUP_SIZE - 1}" bpm_range = f"{(bpm // GROUP_SIZE) * GROUP_SIZE}-to-{((bpm // GROUP_SIZE) * GROUP_SIZE) + GROUP_SIZE - 1}"
all_tracks.append({"file": file, "size": size, "bpm_range": bpm_range}) all_tracks.append({"file": file, "size": size, "bpm_range": bpm_range})
if not all_tracks: if not all_tracks:
print("No MP3 files found.") print("No MP3 files found.")
sys.exit(0) exit(0)
total_size = sum(t["size"] for t in all_tracks) # Calculate total size and number of CDs needed
num_cds = max(1, (total_size + CD_SIZE - 1) // CD_SIZE) 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") print(f"Total size: {total_size / 1024**2:.2f} MB, splitting into {num_cds} CDs")
bpm_groups = defaultdict(list) # Group tracks by BPM range
for track in all_tracks: bpm_groups = defaultdict(list)
for track in all_tracks:
bpm_groups[track["bpm_range"]].append(track) bpm_groups[track["bpm_range"]].append(track)
for bpm_range in bpm_groups: # Sort each bpm group
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)
bpm_chunks = {} # Split each BPM group evenly into num_cds parts
for bpm_range, tracks in bpm_groups.items(): def split_evenly(lst, n):
k, m = divmod(len(lst), n)
return [lst[i*k + min(i, m):(i+1)*k + min(i+1, m)] for i in range(n)]
bpm_chunks = {}
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)
cd_contents = [[] for _ in range(num_cds)] # Prepare CDs
cd_contents = [[] for _ in range(num_cds)]
for bpm_range in bpm_chunks: # Fill CDs with balanced 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)
for i, tracks in enumerate(cd_contents, start=1): # 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 = dest_media_path / f"CD-{i:02}"
cd_folder.mkdir(parents=True, exist_ok=True) cd_folder.mkdir(parents=True, exist_ok=True)
size_accum = 0 size_accum = 0
@ -149,10 +92,7 @@ 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__":
main()

View File

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