4. Renaming the MIDI Files
4. Renaming the MIDI Files
Deterministic filename generation and internal metadata repair
Once the metadata has been extracted (Step 1) and normalized + human‑curated (Steps 2–3), the final stage of the workflow is to apply the new metadata to the actual MIDI files. This renamer script is the engine that transforms a chaotic library into a clean, consistent, future‑proof collection.
The script uses the curated metadata spreadsheet as the single source of truth and performs two major tasks:
- Generate clean, deterministic filenames
- Rewrite internal MIDI metadata safely and consistently
This step is fully automated, but intentionally includes a dry‑run mode so you can preview all changes before committing them.
What the Renamer Does
✔ Builds filenames using a consistent pattern
The script generates filenames in the form:
composer--title.mid
Composer and title are normalized separately, ensuring the double dash (--) remains a semantic separator rather than being collapsed by normalization.
✔ Ensures collision‑safe naming
If two files normalize to the same base name, the script automatically appends:
_filename_1.mid
_filename_2.mid
This prevents accidental overwrites.
✔ Rewrites internal MIDI metadata
The script updates:
- Track name
- Text meta events
- Copyright
- Comments
All text is passed through a Latin‑1‑safe filter so Mido can write it without errors.
✔ Produces a rename report
Every run generates:
rename_report.csv
mapping:
old_filename,new_filename
This makes the process reversible and auditable.
✔ Supports dry‑run mode
Before making any changes, you can run:
DRY_RUN = True
to preview all renames without touching any files.
When to Use This Script
Run the renamer only after:
- Raw metadata has been extracted
- Metadata has been normalized
- You have manually reviewed and corrected the metadata
You have saved the curated file as:
normalized_metadata_edit_fixed.csv
This curated file becomes the authoritative source for renaming.
✅ FULL RENAMER SCRIPT
(Copy/paste directly into your Contraptions page)
import csv
import re
import mido
from mido import MetaMessage
from pathlib import Path
from openpyxl import load_workbook
# Base name only — script auto-detects .csv or .xlsx
METADATA_FILE = "normalized_metadata_edit_fixed"
MIDI_FOLDER = Path(".")
REPORT_PATH = "rename_report.csv"
DRY_RUN = True # ← Set to False to actually rename + modify files
# ------------------------------------------------------------
# Encoding cleanup
# ------------------------------------------------------------
def fix_encoding(s):
if not isinstance(s, str):
return ""
try:
return s.encode("cp1252").decode("utf-8")
except:
return s
def latin1_safe(s):
"""Convert text to something Mido can encode (Latin-1)."""
if not isinstance(s, str):
return ""
replacements = {
"\u2018": "'", # left single quote
"\u2019": "'", # right single quote
"\u201C": '"', # left double quote
"\u201D": '"', # right double quote
"\u2013": "-", # en dash
"\u2014": "-", # em dash
"\u2026": "...", # ellipsis
}
for bad, good in replacements.items():
s = s.replace(bad, good)
# Final fallback: replace anything Latin-1 can't encode
return s.encode("latin-1", "replace").decode("latin-1")
# ------------------------------------------------------------
# Normalization helpers
# ------------------------------------------------------------
def normalize_filename(s):
s = fix_encoding(s)
s = re.sub(r"[^\w]+", "_", s)
s = re.sub(r"_+", "_", s)
s = s.strip("_")
return s.lower()
def extract_primary_composer(composer):
"""Use only the first composer name."""
composer = fix_encoding(composer)
parts = re.split(r"[\/,&]| and ", composer, flags=re.IGNORECASE)
primary = parts[0].strip()
return primary
def ensure_unique_name(base_name, used_names):
name = base_name
counter = 1
while name in used_names:
name = f"{base_name}_{counter}"
counter += 1
used_names.add(name)
return name
# ------------------------------------------------------------
# MIDI metadata helpers
# ------------------------------------------------------------
def replace_or_insert(track, meta_type, text):
"""Replace or insert a meta message of a given type, using correct attribute."""
uses_name = meta_type in ("track_name", "instrument_name", "device_name")
for msg in track:
if msg.type == meta_type:
if uses_name:
msg.name = text
else:
msg.text = text
return
# Insert new message at top
if uses_name:
new_msg = MetaMessage(meta_type, name=text, time=0)
else:
new_msg = MetaMessage(meta_type, text=text, time=0)
track.insert(0, new_msg)
def append_comment_safely(track, comments):
if comments:
replace_or_insert(track, "text", comments)
# ------------------------------------------------------------
# File reader (auto-detect CSV vs XLSX)
# ------------------------------------------------------------
def load_metadata_rows(base_name):
csv_path = Path(base_name + ".csv")
xlsx_path = Path(base_name + ".xlsx")
if csv_path.exists():
# FIX: read as UTF-8
with open(csv_path, newline="", encoding="utf-8") as f:
return list(csv.DictReader(f))
if xlsx_path.exists():
wb = load_workbook(xlsx_path)
ws = wb.active
rows = list(ws.iter_rows(values_only=True))
headers = rows[0]
return [dict(zip(headers, row)) for row in rows[1:]]
raise FileNotFoundError("Neither CSV nor XLSX found.")
# ------------------------------------------------------------
# Main processing
# ------------------------------------------------------------
def process_midi_file(old_path, title, composer, comments, used_names):
title = fix_encoding(title)
composer = fix_encoding(composer)
comments = fix_encoding(comments)
primary_composer = extract_primary_composer(composer)
# Normalize separately
norm_title = normalize_filename(title)
norm_composer = normalize_filename(primary_composer)
# Prevent composer == title duplication
if norm_composer == norm_title or norm_composer == "unknown":
base = norm_title
else:
base = f"{norm_composer}--{norm_title}"
base = ensure_unique_name(base, used_names)
new_name = base + ".mid"
new_path = old_path.with_name(new_name)
if DRY_RUN:
return new_name, False
mid = mido.MidiFile(old_path)
track = mid.tracks[0]
safe_title = latin1_safe(title)
safe_composer = latin1_safe(primary_composer)
safe_comments = latin1_safe(comments)
replace_or_insert(track, "track_name", safe_title)
replace_or_insert(track, "text", safe_title)
replace_or_insert(track, "copyright", safe_composer)
append_comment_safely(track, safe_comments)
mid.save(new_path)
if new_path != old_path:
old_path.unlink()
return new_name, True
def main():
used_names = set()
report_rows = []
rows = load_metadata_rows(METADATA_FILE)
for row in rows:
filename = str(row.get("filename", "")).strip()
title = str(row.get("title", "")).strip()
composer = str(row.get("composer", "")).strip()
comments = str(row.get("comments", "")).strip()
midi_path = MIDI_FOLDER / filename
if not midi_path.exists():
report_rows.append([filename, "MISSING"])
print(f"Missing file: {filename}")
continue
new_name, changed = process_midi_file(
midi_path, title, composer, comments, used_names
)
report_rows.append([filename, new_name])
print(f"{'(dry-run)' if DRY_RUN else ''} {filename} → {new_name}")
with open(REPORT_PATH, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["old_filename", "new_filename"])
writer.writerows(report_rows)
print("\nReport written to:", REPORT_PATH)
print("Dry-run mode is", DRY_RUN)
print("\nAll operations completed successfully.")
if __name__ == "__main__":
main()
Summary
The renamer script is the final, transformative step in the normalization workflow.
It takes your curated metadata and produces a clean, deterministic, collision‑safe set of filenames while repairing internal MIDI metadata. With dry‑run mode and a detailed rename report, the process is transparent, reversible, and safe.
Once this step is complete, your MIDI library is fully normalized and ready for archival, playback, or further processing.