Ethan Dalool
4a9051e617
With pathclass.glob_many, we can clean up and feel more confident about many programs that use pipeable to take glob patterns. Added return 0 to all programs that didn't have it, so we have consistent and explicit command line return values. Other linting and whitespace.
170 lines
4.7 KiB
Python
170 lines
4.7 KiB
Python
'''
|
|
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:]))
|