Skip to content

Instantly share code, notes, and snippets.

@eduard-sukharev
Forked from Theldus/cue_to_flac.py
Last active May 1, 2024 21:11
Show Gist options
  • Save eduard-sukharev/b58ded287e27f8df311d6f57b3d8d699 to your computer and use it in GitHub Desktop.
Save eduard-sukharev/b58ded287e27f8df311d6f57b3d8d699 to your computer and use it in GitHub Desktop.
CUE splitter using ffmpeg (to flac)
import os
import subprocess
import sys
from itertools import chain
import argparse
parser = argparse.ArgumentParser(description="Split audio file by .cue metadata.")
parser.add_argument(
"cue-files",
help="CUE file to be parsed. Multiple files can be passed. If no files provided, reads from stdin",
nargs="*",
default=sys.stdin,
)
parser.add_argument(
"-d",
"--output-dir",
type=str,
help="Specify dir to be used as basedir for split files. By default split files are placed in the same dir as "
"original CUE file",
)
parser.add_argument(
"-s",
"--sample-rate-limit",
type=int,
help="Transcode files, limiting samplerate if original file exceeds this value, in Hz.",
)
parser.add_argument(
"-r",
"--bitrate-limit",
type=int,
help="Transcode files, limiting bitrate if original file exceeds this value.",
)
parser.add_argument(
"-b",
"--bits-per-sample-limit",
type=int,
help="Transcode files, setting bits per sample.",
)
parser.add_argument(
"-pa",
"--prefix-by-artist",
action='store_true',
help="When used with --output-dir, create directory per Artist.",
)
parser.add_argument(
"-pb",
"--prefix-by-album",
action='store_true',
help="When used with --output-dir, create directory per Album.",
)
parser.add_argument(
"-py",
"--prefix-by-year",
action='store_true',
help="When used with --output-dir, create directory per Year.",
)
parser.add_argument(
"-n",
"--skip-existing",
action='store_true',
help="Do not overwrite existing files.",
)
parser.add_argument(
"-f",
"--force-overwrite",
action='store_true',
help="Do split/transcoding even if the target file exists.",
)
args = vars(parser.parse_args())
# Look to the path of your current working directory
working_directory = os.getcwd()
cue_files = []
for file in args.get("cue-files"):
cue_files.append(os.path.join(working_directory, file).strip("\n"))
max_sample_rate = args.get("sample_rate_limit")
max_bitrate = args.get("bitrate_limit")
max_bits_per_sample = args.get("bits_per_sample_limit")
def parse_cue_file(cue_file):
cue_directory = os.path.dirname(cue_file)
general = {}
tracks = []
current_file = None
d = open(cue_file).read().splitlines()
for line in d:
if line.startswith("REM GENRE "):
general["genre"] = " ".join(line.split(" ")[2:])
if line.startswith("REM DATE "):
general["date"] = " ".join(line.split(" ")[2:])
if line.startswith("PERFORMER "):
general["artist"] = " ".join(line.split(" ")[1:]).replace('"', "")
if line.startswith("TITLE "):
general["album"] = " ".join(line.split(" ")[1:]).replace('"', "")
if line.startswith("FILE "):
current_file = os.path.join(cue_directory, " ".join(line.split(" ")[1:-1]).replace('"', ""))
process = subprocess.run(
["ffprobe", "-show_streams", current_file],
capture_output=True,
check=True,
)
for line in str(process.stdout).split("\\n"):
if line.startswith("sample_rate"):
general["sample_rate"] = int(line.split("=")[1])
if line.startswith("bit_rate"):
if line.split("=")[1] != "N/A":
general["bitrate"] = int(line.split("=")[1])
if line.startswith("sample_fmt"):
general["bits_per_sample"] = int(line.split("=")[1][1:])
if line.startswith(" TRACK "):
track = general.copy()
track["track"] = int(line.strip().split(" ")[1], 10)
tracks.append(track)
if line.startswith(" TITLE "):
tracks[-1]["title"] = " ".join(line.strip().split(" ")[1:]).replace('"', "").replace("/", "-")
if line.startswith(" PERFORMER "):
tracks[-1]["artist"] = " ".join(line.strip().split(" ")[1:]).replace('"', "").replace("/", "-")
if line.startswith(" INDEX 01 "):
t = list(
map(
int,
" ".join(line.strip().split(" ")[2:]).replace('"', "").split(":"),
)
)
tracks[-1]["start"] = 60 * t[0] + t[1] + t[2] / 100.0
for i in range(len(tracks)):
if i != len(tracks) - 1:
tracks[i]["duration"] = tracks[i + 1]["start"] - tracks[i]["start"]
return current_file, tracks
def split_track(source_file, track, metadata, output_directory, force_overwrite, skip_existing):
track_filepath = os.path.join(
output_directory,
f'{track["track"]:02d} - {track["artist"]} - {track["title"]}.flac',
)
if skip_existing and os.path.exists(track_filepath):
print(f'File {track_filepath} already exist, skipping due to -n option')
return
cmd = ["ffmpeg"]
cmd += ["-i", f"{source_file}"]
if force_overwrite:
cmd += ['-y']
cmd += [
"-ss",
"%.2d:%.2d:%.2d"
% (
track["start"] / 60 / 60,
track["start"] / 60 % 60,
int(track["start"] % 60),
),
]
if "duration" in track:
cmd += [
"-t",
"%.2d:%.2d:%.2d"
% (
track["duration"] / 60 / 60,
track["duration"] / 60 % 60,
int(track["duration"] % 60),
),
]
metadata_cmd = [("-metadata", f'{k.upper()}="{v}"') for (k, v) in list(metadata.items())]
cmd += [v for v in chain(*metadata_cmd)]
if max_sample_rate and track["sample_rate"] > max_sample_rate:
cmd += ["-ar", str(max_sample_rate)]
if max_bitrate and track.get("bitrate", 0) > max_bitrate:
cmd += ["-ab", str(max_bitrate)]
if max_bits_per_sample and track["bits_per_sample"] > max_bits_per_sample:
cmd += ["-sample_fmt", f"s{max_bits_per_sample}"]
cmd += ["-compression_level", "8"]
cmd.append(
track_filepath
)
print(cmd)
subprocess.run(cmd, check=True)
for cue_file in cue_files:
try:
original_file, tracks = parse_cue_file(cue_file)
except:
print(f'Skipping {cue_file} due to errors')
continue
output_directory = os.path.dirname(cue_file)
skip_existing = args.get('skip_existing')
force_overwrite = args.get('force_overwrite')
for track in tracks:
if args.get('output_dir'):
output_directory = args.get("output_dir")
if args.get('prefix_by_artist'):
output_directory = os.path.join(output_directory, track.get("artist"))
if args.get('prefix_by_year'):
output_directory = os.path.join(output_directory, track.get("year"))
if args.get('prefix_by_album'):
output_directory = os.path.join(output_directory, track.get("album"))
if not os.path.exists(output_directory):
os.makedirs(output_directory)
metadata = {
k: v
for k, v in {
"artist": track.get("artist"),
"title": track.get("title"),
"album": track.get("album"),
"track": str(track.get("track")) + "/" + str(len(tracks)),
"genre": track.get("genre"),
"date": track.get("date"),
}.items()
if v
}
split_track(original_file, track, metadata, output_directory, force_overwrite, skip_existing)
@milofultz
Copy link

Thank you for sharing this! Worked like a charm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment