Compare commits
5 Commits
dfe237d3ea
...
1.2.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
189645254b | ||
![]() |
a46d810d0a | ||
![]() |
1f83bb8517 | ||
![]() |
76d25ebe5e | ||
![]() |
63953f45e8 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -180,3 +180,6 @@ cython_debug/
|
|||||||
.cursorignore
|
.cursorignore
|
||||||
.cursorindexingignore
|
.cursorindexingignore
|
||||||
|
|
||||||
|
# ed1337x
|
||||||
|
config.ini
|
||||||
|
Run.ps1
|
||||||
|
158
Program/app.py
Normal file
158
Program/app.py
Normal 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
4
Program/def_config.ini
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[Settings]
|
||||||
|
bWriteNonPresentBPM = False
|
||||||
|
bCheckAllTracksBPM = False
|
||||||
|
SplitFolderMB = 695
|
25
Run.cmd
Normal file
25
Run.cmd
Normal 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
BIN
requirements.txt
Normal file
Binary file not shown.
98
src/app.py
98
src/app.py
@@ -1,98 +0,0 @@
|
|||||||
import os
|
|
||||||
import shutil
|
|
||||||
from pathlib import Path
|
|
||||||
from mutagen.id3 import ID3
|
|
||||||
from datetime import datetime
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
CD_SIZE = 695 * 1024 * 1024 # 695 MB
|
|
||||||
GROUP_SIZE = 5
|
|
||||||
|
|
||||||
# Store the current date
|
|
||||||
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)
|
|
||||||
|
|
||||||
if not source_media_path.exists() or not source_media_path.is_dir():
|
|
||||||
print(f"Error: {source_media_path} is not a valid directory.")
|
|
||||||
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)
|
|
||||||
|
|
||||||
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 recursively, grouping unknown BPM separately
|
|
||||||
all_tracks = []
|
|
||||||
for file in source_media_path.rglob("*.mp3"): # <-- recursive glob
|
|
||||||
bpm = get_bpm(file)
|
|
||||||
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.")
|
|
||||||
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
|
|
||||||
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)
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
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!")
|
|
||||||
|
|
@@ -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
|
|
||||||
|
|
@@ -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
|
|
||||||
|
|
Reference in New Issue
Block a user