Created
March 8, 2026 22:08
-
-
Save paigeadelethompson/4554cc80179306ca5aff3982f089355f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import os | |
| import sys | |
| import warnings | |
| import numpy as np | |
| import librosa | |
| from mutagen.easyid3 import EasyID3 | |
| # Suppress librosa UserWarnings and FutureWarnings | |
| warnings.filterwarnings("ignore", category=UserWarning) | |
| warnings.filterwarnings("ignore", category=FutureWarning) | |
| def detect_bpm(file_path): | |
| """Detect BPM of an audio file using librosa.""" | |
| try: | |
| y, sr = librosa.load(file_path, sr=None) | |
| except Exception as e: | |
| print(f"[ERROR] Failed to load '{file_path}': {e}") | |
| return None | |
| if y is None or y.size == 0: | |
| print(f"[SKIP] '{file_path}' contains no audio data.") | |
| return None | |
| try: | |
| tempo, _ = librosa.beat.beat_track(y=y, sr=sr) | |
| # Handle cases where tempo is returned as an array | |
| if isinstance(tempo, np.ndarray): | |
| tempo = float(tempo[0]) if tempo.size > 0 else None | |
| if tempo is None or tempo <= 0: | |
| print(f"[SKIP] Could not detect BPM for '{file_path}'") | |
| return None | |
| bpm = int(np.round(tempo)) | |
| return bpm | |
| except Exception as e: | |
| print(f"[ERROR] BPM detection failed for '{file_path}': {e}") | |
| return None | |
| def write_bpm_to_id3(file_path, bpm): | |
| """Write BPM to the ID3 tags of an MP3 file.""" | |
| try: | |
| audio = EasyID3(file_path) | |
| except Exception: | |
| # Create new ID3 tags if none exist | |
| from mutagen.id3 import ID3 | |
| audio = ID3() | |
| audio.save(file_path) | |
| audio = EasyID3(file_path) | |
| try: | |
| audio['bpm'] = str(bpm) | |
| audio.save() | |
| print(f"[OK] BPM {bpm} written to '{file_path}'") | |
| except Exception as e: | |
| print(f"[ERROR] Failed to write BPM to '{file_path}': {e}") | |
| def process_file(file_path): | |
| """Process a single MP3 file.""" | |
| if not file_path.lower().endswith('.mp3'): | |
| print(f"[SKIP] Not an MP3 file: '{file_path}'") | |
| return | |
| bpm = detect_bpm(file_path) | |
| if bpm is not None: | |
| write_bpm_to_id3(file_path, bpm) | |
| def process_folder(folder_path): | |
| """Recursively process all MP3 files in a folder.""" | |
| for root, _, files in os.walk(folder_path): | |
| for file in files: | |
| file_path = os.path.join(root, file) | |
| process_file(file_path) | |
| if __name__ == "__main__": | |
| if len(sys.argv) != 2: | |
| print("Usage: python bpm_tag.py <file_or_folder>") | |
| sys.exit(1) | |
| target = sys.argv[1] | |
| if os.path.isfile(target): | |
| process_file(target) | |
| elif os.path.isdir(target): | |
| process_folder(target) | |
| else: | |
| print(f"[ERROR] '{target}' is not a valid file or folder") | |
| sys.exit(1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment