Skip to content

Instantly share code, notes, and snippets.

@mitchellrj
Created November 24, 2015 15:44
Show Gist options
  • Save mitchellrj/8c713974413d76ab3e9a to your computer and use it in GitHub Desktop.
Save mitchellrj/8c713974413d76ab3e9a to your computer and use it in GitHub Desktop.
Script to split a single audio file into multiple files using a Cue file.
#!env python3
import argparse
import datetime
import enum
import math
import os.path
import shlex
import shutil
import subprocess
import sys
import tempfile
class Context(enum.Enum):
ALBUM = 0
TRACK = 1
def parse_cue_context(lines, context=Context.ALBUM, **defaults):
result = {'genre': None, 'date': None, 'input_file': None, 'title': None, 'performer': None}
if context == Context.ALBUM:
result['tracks'] = []
result.update(defaults)
while lines:
line = lines.pop(0)
if line.startswith('REM GENRE '):
result['genre'] = shlex.split(line[10:])[0]
elif line.startswith('REM DATE '):
result['date'] = shlex.split(line[9:])[0]
elif line.startswith('PERFORMER '):
result['performer'] = shlex.split(line[10:])[0]
elif line.startswith('TITLE '):
result['title'] = shlex.split(line[6:])[0]
elif line.startswith('FILE '):
result['input_file'] = shlex.split(line[5:])[0]
elif line.startswith('POSTGAP '):
result['postgap'] = parse_time(shlex.split(line[8:])[0])
elif line.startswith('PREGAP '):
result['pregap'] = parse_time(shlex.split(line[7:])[0])
elif line.startswith('INDEX '):
result['index_type'], result['start_time'] = shlex.split(line[6:])
result['index_type'] = int(result['index_type'])
result['start_time'] = parse_time(result['start_time'])
elif line.startswith(' TRACK '):
track_number, track_type = shlex.split(line[8:])
if track_type == 'AUDIO':
track_lines = []
while lines:
if not lines[0].startswith(' '):
break
track_lines.append(lines.pop(0)[4:])
result['tracks'].append(parse_cue_context(track_lines, context=Context.TRACK, genre=result['genre'], date=result['date'], performer=result.get('performer', 'Unknown Artist'), title='Unknown Track', input_file=result['input_file']))
return result
def parse_time(time):
minutes, seconds, frames = tuple(map(int, time.split(':')))
return datetime.timedelta(
minutes=minutes,
seconds=seconds,
milliseconds=frames / 0.75 * 1000
)
def split_cue(cue_data, no_gap):
filename = None
start_time = datetime.timedelta()
end_time = None
tracks = cue_data['tracks']
for i in range(len(tracks)):
is_first_track = i == 0
is_last_track = i == (len(tracks) - 1)
track = tracks[i]
start_time = track['start_time']
if not is_first_track:
tracks[i - 1]['end_time'] = start_time
if track['input_file'] != filename:
tracks[i - 1]['end_time'] = None
if is_last_track:
track['end_time'] = None
filename = track['input_file']
track_number = 1
output_track = {'track_number': track_number}
for track in tracks:
if track['index_type'] == 0 and not no_gap:
output_track.setdefault('start_time', track['start_time'])
elif track['index_type'] >= 1:
for k, v in track.items():
output_track.setdefault(k, v)
yield output_track
track_number += 1
output_track = {'track_number': track_number}
def extract_track(cue_data, track_data, total_tracks, no_gap, dry_run):
ext = os.path.splitext(track_data['input_file'])[-1]
cmd = [
'avconv',
'-i', track_data['input_file'],
'-c:a', 'copy',
'-ss',
str(track_data['start_time'].total_seconds()),
]
if track_data['end_time'] is not None:
duration = track_data['end_time'] - track_data['start_time']
cmd.extend([
'-t',
str(duration.total_seconds()),
])
cmd.extend([
'-metadata', 'TALB={}'.format(cue_data['title']),
'-metadata', 'TDAT={}'.format(track_data['date']),
'-metadata', 'TIT1={}'.format(track_data['title']),
'-metadata', 'TPE1={}'.format(track_data['performer']),
'-metadata', 'TPE2={}'.format(cue_data['performer']),
'-metadata', 'TRCK={}/{}'.format(track_data['track_number'], total_tracks),
])
output_filename = '{:02d} - {} - {}{}'.format(
track_data['track_number'],
track_data['performer'],
track_data['title'],
ext
).replace(os.path.sep, '_')
cmd.append(output_filename)
if not dry_run:
proc = subprocess.Popen(cmd)
stdout, stderr = proc.communicate()
if proc.returncode:
raise RuntimeError(stderr)
else:
print(' '.join(map(shlex.quote, cmd)))
if not no_gap:
if not track_data.get('pregap') and not track_data.get('postgap'):
return
fd, filename = tempfile.mkstemp(suffix=ext)
os.close(fd)
cmd = [
'sox',
output_filename,
filename,
'pad',
str(track_data.get('pregap', datetime.timedelta()).total_seconds()),
str(track_data.get('postgap', datetime.timedelta()).total_seconds()),
]
if not dry_run:
proc = subprocess.Popen(cmd)
stdout, stderr = proc.communicate()
if proc.returncode:
raise RuntimeError(stderr)
shutil.move(filename, output_filename)
else:
print(' '.join(map(shlex.quote, cmd)))
print('Move {} to {}'.format(filename, output_filename))
def main(argv=None):
if argv is None:
argv = sys.argv[1:]
parser = argparse.ArgumentParser()
parser.add_argument('cuefile')
parser.add_argument('-G, --no-gap', default=False, action='store_true', help='Ignore audio gaps', dest='no_gap')
parser.add_argument('-E, --encoding', default='UTF8', help='The text encoding of the CUE file', dest='encoding')
parser.add_argument('--dry-run', default=False, action='store_true', dest='dry_run')
parser.add_argument('-t', default=[], action='append', dest='track_numbers')
args = parser.parse_args(argv)
with open(args.cuefile, encoding=args.encoding) as cuefile:
cue = parse_cue_context(list(cuefile))
tracks = list(split_cue(cue, args.no_gap))
total_tracks = len(tracks)
track_numbers = map(int, args.track_numbers)
for track in tracks:
if track_numbers and track['track_number'] not in track_numbers:
continue
extract_track(cue, track, total_tracks, args.no_gap, args.dry_run)
if __name__ == '__main__':
main()
@mortalis13
Copy link

Hi. What is '-t' option? Thanks.

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