-
-
Save eduard-sukharev/b58ded287e27f8df311d6f57b3d8d699 to your computer and use it in GitHub Desktop.
CUE splitter using ffmpeg (to flac)
This file contains 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
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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you for sharing this! Worked like a charm