''' This script cuts an audio file into multiple files when you provide the timestamps and titles for each. > mp3slice bigfile.mp3 00:00-01:00 part1.mp3 01:00-02:00 part2.mp3 ''' import argparse import os import sys from voussoirkit import bytestring def parse_rules(lines): rules = [] for (times, title) in lines[::-1]: rule = {'title': title} (start, end) = hyphen_range(times) if start is None: raise ValueError('Null start') rule['start'] = start if end is None and len(rules) > 0: end = rules[-1]['start'] rule['end'] = end rules.append(rule) rules.sort(key=lambda x: x.get('start')) return rules def read_rulefile(filename): text = None for encoding in [None, 'utf-8']: try: with open(filename, 'r', encoding=encoding) as handle: text = handle.read() break except UnicodeError: pass else: raise UnicodeError() lines = [l.strip() for l in text.strip().splitlines()] lines = [l for l in lines if l] rules = [l.split(maxsplit=1) for l in lines] return parse_rules(rules) def chunk_sequence(sequence, chunk_length, allow_incomplete=True): ''' Given a sequence, divide it into sequences of length `chunk_length`. allow_incomplete: If True, allow the final chunk to be shorter if the given sequence is not an exact multiple of `chunk_length`. If False, the incomplete chunk will be discarded. ''' (complete, leftover) = divmod(len(sequence), chunk_length) if not allow_incomplete: leftover = 0 chunk_count = complete + min(leftover, 1) chunks = [] for x in range(chunk_count): left = chunk_length * x right = left + chunk_length chunks.append(sequence[left:right]) return chunks def hyphen_range(s): ''' Given a string like '1-3', return numbers (1, 3) representing lower and upper bounds. Supports bytestring.parsebytes and hh:mm:ss format, for example '1k-2k', '10:00-20:00', '4gib-' ''' s = s.strip() s = s.replace(' ', '') if not s: return (None, None) parts = s.split('-') parts = [part.strip() or None for part in parts] if len(parts) == 1: low = parts[0] high = None elif len(parts) == 2: (low, high) = parts else: raise ValueError('Too many hyphens') low = _unitconvert(low) high = _unitconvert(high) if low is not None and high is not None and low > high: raise ValueError(s) return low, high def hms_to_seconds(hms): ''' Convert hh:mm:ss string to an integer seconds. ''' hms = hms.split(':') seconds = 0 if len(hms) == 3: seconds += int(hms[0])*3600 hms.pop(0) if len(hms) == 2: seconds += int(hms[0])*60 hms.pop(0) if len(hms) == 1: seconds += float(hms[0]) return seconds def _unitconvert(value): ''' When parsing hyphenated ranges, this function is used to convert strings like "1k" to 1024 and "1:00" to 60. ''' if value is None: return None if ':' in value: return hms_to_seconds(value) elif all(c in '0123456789.' for c in value): return float(value) else: return bytestring.parsebytes(value) def mp3slice_argparse(args): if len(args.rules) == 1 and os.path.isfile(args.rules[0]): rules = read_rulefile(args.rules[0]) else: rules = args.rules rules = chunk_sequence(rules, 2) if len(rules[-1]) != 2: raise ValueError('Odd-number parameters') rules = parse_rules(rules) extension = os.path.splitext(args.input_filename)[1] outputters = [] for rule in rules: outputter = [] if not rule['title'].endswith(extension): rule['title'] += extension outputter.append('-ss') outputter.append(str(rule['start'])) if rule['end'] is not None: outputter.append('-to') outputter.append(str(rule['end'])) outputter.append(' -c copy') outputter.append('"%s"' % rule['title']) print(outputter) outputter = ' '.join(outputter) outputters.append(outputter) outputters = ' '.join(outputters) command = 'ffmpeg -i "%s" %s' % (args.input_filename, outputters) print(command) os.system(command) return 0 def main(argv): parser = argparse.ArgumentParser() parser.add_argument('input_filename') parser.add_argument('rules', nargs='+', default=None) parser.set_defaults(func=mp3slice_argparse) args = parser.parse_args(argv) return args.func(args) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:]))