Two real, working scripts: an Ansible playbook run-menu (the same pattern used identically across both the fleet-config and infra- provisioning repos), and cuesplit, a frame-accurate FLAC/.cue splitting utility. Sanitized paths only, logic untouched. README gets a new "Scripts & Tooling" section pointing at it, placed right after "What's Running" alongside the other repo-structure pointer.
125 lines
3.5 KiB
Python
Executable File
125 lines
3.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
cuesplit.py — split FLAC files using a .cue sheet, via ffmpeg.
|
|
Handles multi-FILE cues, frame-accurate (MM:SS:FF) timestamps, and uses
|
|
INDEX 01 (track start, skipping pregap) as the split point.
|
|
|
|
Usage:
|
|
python3 cuesplit.py "album.cue" [output_dir]
|
|
|
|
Output dir defaults to "split".
|
|
"""
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def parse_cue(cue_path):
|
|
"""Returns list of (filename, [(track_num, index01_time_str), ...])"""
|
|
files = []
|
|
current_file = None
|
|
current_tracks = None
|
|
pending_track = None
|
|
|
|
with open(cue_path, encoding="utf-8", errors="replace") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
|
|
m = re.match(r'FILE\s+"([^"]+)"', line, re.IGNORECASE)
|
|
if m:
|
|
if current_file is not None:
|
|
files.append((current_file, current_tracks))
|
|
current_file = m.group(1)
|
|
current_tracks = []
|
|
continue
|
|
|
|
m = re.match(r'TRACK\s+(\d+)\s+AUDIO', line, re.IGNORECASE)
|
|
if m:
|
|
pending_track = int(m.group(1))
|
|
continue
|
|
|
|
m = re.match(r'INDEX\s+01\s+(\d+:\d+:\d+)', line, re.IGNORECASE)
|
|
if m and pending_track is not None:
|
|
current_tracks.append((pending_track, m.group(1)))
|
|
pending_track = None
|
|
continue
|
|
|
|
if current_file is not None:
|
|
files.append((current_file, current_tracks))
|
|
|
|
return files
|
|
|
|
|
|
def cue_time_to_seconds(t):
|
|
mm, ss, ff = map(int, t.split(":"))
|
|
return mm * 60 + ss + ff / 75.0
|
|
|
|
|
|
def seconds_to_ffmpeg_ts(total_seconds):
|
|
mm = int(total_seconds // 60)
|
|
ss = total_seconds - mm * 60
|
|
return f"{mm:02d}:{ss:06.3f}"
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python3 cuesplit.py <cuefile> [output_dir]")
|
|
sys.exit(1)
|
|
|
|
cue_path = Path(sys.argv[1])
|
|
out_dir = Path(sys.argv[2]) if len(sys.argv) > 2 else Path("split")
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
if not cue_path.exists():
|
|
print(f"Cue file not found: {cue_path}")
|
|
sys.exit(1)
|
|
|
|
files = parse_cue(cue_path)
|
|
if not files:
|
|
print("No FILE/TRACK entries found in cue — check the cue format.")
|
|
sys.exit(1)
|
|
|
|
track_counter = 1
|
|
|
|
for flac_name, tracks in files:
|
|
flac_path = cue_path.parent / flac_name
|
|
if not flac_path.exists():
|
|
print(f"WARNING: referenced audio file not found: {flac_path}")
|
|
continue
|
|
|
|
if not tracks:
|
|
print(f"WARNING: no tracks found for {flac_name}")
|
|
continue
|
|
|
|
starts = [cue_time_to_seconds(t) for _, t in tracks]
|
|
|
|
for i, start in enumerate(starts):
|
|
out_file = out_dir / f"{track_counter:02d}.flac"
|
|
ss = seconds_to_ffmpeg_ts(start)
|
|
|
|
cmd = ["ffmpeg", "-y", "-i", str(flac_path), "-ss", ss]
|
|
|
|
if i + 1 < len(starts):
|
|
to = seconds_to_ffmpeg_ts(starts[i + 1])
|
|
cmd += ["-to", to]
|
|
|
|
cmd += [str(out_file)]
|
|
|
|
print(f"Track {track_counter}: {flac_name} [{ss}"
|
|
f"{' -> ' + to if i + 1 < len(starts) else ' -> end'}]")
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
if result.returncode != 0:
|
|
print(f" FAILED: {result.stderr[-500:]}")
|
|
else:
|
|
print(f" OK -> {out_file}")
|
|
|
|
track_counter += 1
|
|
|
|
print(f"\nDone. {track_counter - 1} tracks written to {out_dir}/")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|