11 Commits

Author SHA1 Message Date
Roncero Blanco, Edgar
189645254b Cleaner code, it works pretty fine 2025-05-26 18:26:30 +02:00
Roncero Blanco, Edgar
a46d810d0a Fix: Only create destination folder if MP3 files exist in source directory 2025-05-26 17:31:58 +02:00
Roncero Blanco, Edgar
1f83bb8517 Fix DeprecationWarning in BPM rounding; ensure BPM tags are correctly written and updated 2025-05-26 17:24:00 +02:00
Roncero Blanco, Edgar
76d25ebe5e BPM functionality 2025-05-26 17:20:55 +02:00
Roncero Blanco, Edgar
63953f45e8 First step for a config file 2025-05-26 15:33:07 +02:00
Roncero Blanco, Edgar
dfe237d3ea Simplify timestamp format in output folder name to YYYY-MM-DD_HHMMSS 2025-05-26 13:59:32 +02:00
Roncero Blanco, Edgar
52edae428c Support recursive search of MP3 files in subfolders; update input prompt message 2025-05-26 13:57:08 +02:00
Roncero Blanco, Edgar
9000ff1680 Fix input path parsing by stripping quotes from user input 2025-05-26 13:53:52 +02:00
Roncero Blanco, Edgar
53cad05cd9 Add handling for files without BPM: group into [Unknown BPM] folder; keep balanced CD splits and timestamped output folder 2025-05-26 13:51:24 +02:00
Roncero Blanco, Edgar
cbe4c5badb Include minutes and seconds in run_date to avoid destination folder conflicts 2025-05-26 13:39:11 +02:00
Roncero Blanco, Edgar
e8cf98708b Add run_date variable, prompt for source path, auto-generate destination folder with date-based name 2025-05-26 13:35:48 +02:00
8 changed files with 190 additions and 103 deletions

3
.gitignore vendored
View File

@@ -180,3 +180,6 @@ cython_debug/
.cursorignore
.cursorindexingignore
# ed1337x
config.ini
Run.ps1

158
Program/app.py Normal file
View File

@@ -0,0 +1,158 @@
import os
import shutil
from pathlib import Path
from mutagen.id3 import ID3, TBPM, ID3NoHeaderError
from datetime import datetime
from collections import defaultdict
import configparser
import sys
import librosa
GROUP_SIZE = 5
def load_config():
config_path = Path("config.ini")
default_config_path = Path("def_config.ini")
if not config_path.exists():
if default_config_path.exists():
shutil.copy(default_config_path, config_path)
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()
default_config.read(default_config_path)
user_config = configparser.ConfigParser()
user_config.read(config_path)
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:
pass
return None
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)]
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)
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]"
else:
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})
if not all_tracks:
print("No MP3 files found.")
sys.exit(0)
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)
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")
bpm_groups = defaultdict(list)
for track in all_tracks:
bpm_groups[track["bpm_range"]].append(track)
for bpm_range in bpm_groups:
bpm_groups[bpm_range].sort(key=lambda x: x["file"].name)
bpm_chunks = {}
for bpm_range, tracks in bpm_groups.items():
bpm_chunks[bpm_range] = split_evenly(tracks, num_cds)
cd_contents = [[] for _ in range(num_cds)]
for bpm_range in bpm_chunks:
for i, chunk in enumerate(bpm_chunks[bpm_range]):
cd_contents[i].extend(chunk)
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)
size_accum = 0
for track in tracks:
bpm_subfolder = cd_folder / track["bpm_range"]
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("\n✓ Done!")
if __name__ == "__main__":
main()

4
Program/def_config.ini Normal file
View File

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

25
Run.cmd Normal file
View File

@@ -0,0 +1,25 @@
@echo off
setlocal
REM Define paths
set VENV_DIR=venv
set APP_DIR=Program
set PYTHON=%VENV_DIR%\Scripts\python.exe
REM Check for venv
if not exist %VENV_DIR% (
echo Creating virtual environment...
python -m venv %VENV_DIR%
echo Installing dependencies...
%PYTHON% -m pip install --upgrade pip
%PYTHON% -m pip install -r requirements.txt
) else (
echo Virtual environment found, assuming dependencies are satisfied.
)
REM Run the app
cd /d %~dp0%APP_DIR%
..\%VENV_DIR%\Scripts\python.exe app.py
endlocal

BIN
requirements.txt Normal file

Binary file not shown.

View File

@@ -1,78 +0,0 @@
import os
import shutil
from pathlib import Path
from mutagen.id3 import ID3
CD_SIZE = 700 * 1024 * 1024 # 700 MB
GROUP_SIZE = 5
dir_a = Path(r"C:\Users\Edgar\Desktop\Peak Time BOF (ed1337x)\Already where in MP3")
dir_b = Path(r"C:\Users\Edgar\Desktop\Peak Time BOF (ed1337x)\CD")
dir_b.mkdir(parents=True, exist_ok=True)
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}")
return None
# Collect all tracks
all_tracks = []
for file in dir_a.glob("*.mp3"):
bpm = get_bpm(file)
if bpm is None:
continue
size = file.stat().st_size
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})
# 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) # ceiling division
print(f"Total size: {total_size / 1024**2:.2f} MB, splitting into {num_cds} CDs")
# Group tracks by BPM range
from collections import defaultdict
bpm_groups = defaultdict(list)
for track in all_tracks:
bpm_groups[track["bpm_range"]].append(track)
# Sort each bpm group by filename (or size if you want)
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
def split_evenly(lst, n):
"""Split list lst into n chunks as evenly as possible by count."""
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)
# Prepare CDs
cd_contents = [[] for _ in range(num_cds)]
# Fill CDs with chunks from each BPM range
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 = dir_b / f"CD-{i:02}"
cd_folder.mkdir(parents=True, exist_ok=True)
size_accum = 0
for track in tracks:
bpm_subfolder = cd_folder / track["bpm_range"]
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")

View File

@@ -1,13 +0,0 @@
@echo off
REM Create Python virtual environment named "venv"
python -m venv venv
REM Activate the virtual environment
call venv\Scripts\activate.bat
REM Upgrade pip
python -m pip install --upgrade pip
REM Install dependencies
pip install mutagen

View File

@@ -1,12 +0,0 @@
# Create Python virtual environment named "venv"
python -m venv venv
# Activate the virtual environment
& .\venv\Scripts\Activate.ps1
# Upgrade pip
python -m pip install --upgrade pip
# Install dependencies
pip install mutagen