#!/usr/bin/env python3 from __future__ import unicode_literals from io import open class AutoMKVChapters: class Template: def __init__(self): from random import randint self.uid = randint(10**4,10**6) self.num_editions = 1 self.lang = ['eng'] self.country = ['us'] self.fps = '30' self.ofps = '24' self.qpf = '0' self.idr = False self.trims = None self.kframes = None def toxml(self,chapfile): chf = open(chapfile+'.xml','w',encoding='utf-8') head = '\n\n' chf.write(head+'\n') if self.num_editions > 1: tagf = open(chapfile+'tags.xml','w') tagf.write(head+'\n') else: tagf = False for ed in self.editions: chf.write('\t\n') chf.write('\t\t{0:d}\n'.format(ed.hidden) if ed.hidden else '') chf.write('\t\t{0:d}\n'.format(ed.default) if ed.default else '') chf.write('\t\t{0:d}\n'.format(ed.ordered) if ed.ordered else '') chf.write('\t\t{0:d}\n'.format(ed.uid)) if tagf: tagf.write('\t\n\t\t\n') tagf.write('\t\t\t{0:d}\n'.format(ed.uid)) tagf.write('\t\t\t50\n\t\t\n') num_names = len(ed.name) if len(ed.name) < len(self.lang) else len(self.lang) for i in range(num_names): tagf.write('\t\t\n\t\t\tTITLE\n') tagf.write('\t\t\t{0}\n'.format(ed.name[i] if ed.name[i] != '' else ed.name[i-1])) tagf.write('\t\t\t{0}\n'.format(self.lang[i])) tagf.write('\t\t\t{0:d}\n'.format(1 if i == 0 else 0)) tagf.write('\t\t\n') tagf.write('\t\n') for ch in ed.chapters: chf.write('\t\t\n') num_names = len(ch.name) if len(ch.name) < len(self.lang) else len(self.lang) for i in range(num_names): chf.write('\t\t\t\n') chf.write('\t\t\t\t{0}\n'.format(ch.name[i] if ch.name[i] != '' else ch.name[i-1])) chf.write('\t\t\t\t{0}\n'.format(self.lang[i]) if self.lang[i] != 'eng' else '') chf.write('\t\t\t\t{0}\n'.format(self.country[i]) if i < len(self.country) else '') chf.write('\t\t\t\n') chf.write('\t\t\t{0:d}\n'.format(ch.uid)) chf.write('\t\t\t{0}\n'.format(ch.start)) chf.write('\t\t\t{0}\n'.format(ch.end) if ch.end else '') chf.write('\t\t\t{0:d}\n'.format(ch.hidden) if ch.hidden != 0 else '') chf.write('\t\t\t{0:d}\n'.format(ch.enabled) if ch.enabled != 1 else '') chf.write('\t\t\t{0}\n'.format(ch.suid) if ch.suid else '') chf.write('\t\t\n') chf.write('\t\n') chf.write('\n') if tagf: tagf.write('\n') tagf.close() if self.qpf != '0' and self.kframes: from vfr import write_qpfile if self.qpf != '1': qpfile = self.qpf else: qpfile = chapfile+'.qpfile' write_qpfile(qpfile, self.kframes, self.idr) def connect_with_vfr(self,avs,label=None,clip=None): """ Connects templates.py with vfr.py, enabling its use outside of vfr.py. Uses the same quirks as AMkvC but only for 24 and 30 fps. Ex: inputfps=30 is understood as being '30*1000/1001' """ from vfr import parse_trims, fmt_time # compensate for amkvc's fps assumption if self.fps in ('24','30'): fps = self.fps + '/1.001' else: fps = str(self.fps) if self.ofps and self.ofps in ('24','30'): ofps = self.ofps + '/1.001' else: ofps = str(self.ofps) Trims2, Trims2ts = parse_trims(avs, fps, ofps, label=label, clip=clip)[2:4] Trims2ts = [(fmt_time(i[0]),fmt_time(i[1]) if i[1] != 0 else None) for i in Trims2ts] self.trims = Trims2ts self.kframes = Trims2 def parse_mkv(self, path): """Parse a Matroska file for SegmentUID and Duration""" import binascii import struct def get_data_len(byte): """Get the length (bytes) of the element data""" n = ord(byte) mask = 0b10000000 while not n & mask: mask >>= 1 return n & ~mask suid = tcscale = duration = 0 with open(path, 'rb') as file: if file.read(4) != b'\x1A\x45\xDF\xA3': # not a Matroska file return suid, duration chunk_size = 100000 # 100 kB i = 0 while True: if suid and tcscale and duration: break bin = file.read(chunk_size) if not bin: break suid_pos = bin.find(b'\x73\xA4\x90') # \x90 -> 16 bytes if suid_pos != -1: suid_pos = 4 + i * chunk_size + suid_pos + 3 file.seek(suid_pos) suid = binascii.hexlify(file.read(16)).decode() tcscale_pos = bin.find(b'\x2A\xD7\xB1') if tcscale_pos != -1: tcscale_pos = 4 + i * chunk_size + tcscale_pos + 3 file.seek(tcscale_pos) tcscale_len = get_data_len(file.read(1)) tcscale = int(binascii.hexlify(file.read(tcscale_len)), 16) duration_pos = bin.find(b'\x44\x89\x84') # float (4 bytes) if duration_pos != -1: duration_pos = 4 + i * chunk_size + duration_pos + 3 file.seek(duration_pos) duration = struct.unpack('>f', file.read(4))[0] if not duration: # double (8 bytes) duration_pos = bin.find(b'\x44\x89\x88') if duration_pos != -1: duration_pos = 4 + i * chunk_size + duration_pos + 3 file.seek(duration_pos) duration = struct.unpack('>d', file.read(8))[0] if bin.find(b'\x1F\x43\xB6\x75') != -1: # segment info should be before the clusters break i += 1 duration = duration * tcscale / 1000000 return suid, duration class Edition: def __init__(self): self.default = 0 self.name = ['Default'] self.hidden = 0 self.ordered = 0 self.num_chapters = 1 self.uid = 0 class Chapter: def __init__(self): self.name = ['Chapter'] self.chapter = False self.start = False self.end = False self.suid = False self.hidden = 0 self.uid = 0 self.enabled = 1 def __init__(self, templatefile, output=None, avs=None, trims=None, kframes=None, uid=None, label=None, ifps=None, clip=None, idr=False): try: import configparser except ImportError: import ConfigParser as configparser from io import open # Init config config = configparser.ConfigParser() template = open(templatefile, encoding='utf-8') # Read template config.readfp(template) template.close() # Template defaults self = self.Template() self.editions = [] self.uid = uid if uid else self.uid # Set mkvinfo path from vfr import mkvmerge, parse_with_mkvmerge, fmt_time from os.path import dirname, join, isfile # Set placeholder for mkvinfo output mkv_globbed = False mkvinfo = {} for k, v in config.items('info'): if k == 'lang': self.lang = v.split(',') elif k == 'country': self.country = v.split(',') elif k == 'inputfps': self.fps = v elif k == 'outputfps': self.ofps = v elif k == 'createqpfile': self.qpf = v elif k == 'uid': self.uid = int(v) elif k == 'editions': self.num_editions = int(v) if avs and not ifps: self.connect_with_vfr(avs, label, clip) elif trims: self.trims = trims self.kframes = kframes else: self.trims = False self.idr = idr for i in range(self.num_editions): from re import compile ed = self.Edition() ed.uid = self.uid * 100 self.uid += 1 cuid = ed.uid ed.num = i+1 ed.chapters = [] stuff = {} for k, v in config.items('edition{0:d}'.format(ed.num)): if k == 'default': ed.default = int(v) elif k == 'name': ed.name = v.split(',') elif k == 'ordered': ed.ordered = int(v) elif k == 'hidden': ed.hidden = int(v) elif k == 'chapters': ed.num_chapters = int(v) for i in range(ed.num_chapters): stuff[i+1] = [] elif k == 'uid': ed.uid = int(v) else: opt_re = compile('(\d+)(\w+)') ret = opt_re.search(k) if ret: stuff[int(ret.group(1))].append((ret.group(2),v)) for j in range(ed.num_chapters): ch = self.Chapter() cuid += 1 ch.uid = cuid ch.num = j+1 for k, v in stuff[j+1]: if k == 'name': ch.name = v.split(',') elif k == 'chapter': ch.chapter = int(v) elif k == 'start': ch.start = v elif k == 'end': ch.end = v elif k == 'suid': ch.suid = v.strip() if ret else 0 elif k == 'hidden': ch.hidden = int(v) elif k == 'enabled': ch.enabled = int(v) if ch.suid and not isfile(ch.suid): ch.suid = ch.suid.replace('0x','').lower().replace(' ','') if ch.chapter and not (ch.start and ch.end): ch.start, ch.end = self.trims[ch.chapter-1] if self.trims else (ch.start, ch.end) elif ch.suid: mkvfiles = [] if isfile(ch.suid): mkvfiles = [ch.suid] elif not mkv_globbed: from glob import glob mkvfiles = glob('*.mkv') + glob(join(dirname(avs),'*.mkv')) mkv_globbed = True if mkvfiles: if parse_with_mkvmerge: from subprocess import check_output import json for file in mkvfiles: info = check_output([mkvmerge, '-i', '-F', 'json', '--output-charset', 'utf-8', file]).decode('utf-8') try: props = json.loads(info).get("container", {}).get("properties", {}) ch.suid = props.get("segment_uid", 0) duration = props.get("duration", 0) mkvinfo[ch.suid] = {'file': file, 'duration': fmt_time(duration * 10**6) if duration else 0} except Exception: pass else: for file in mkvfiles: ch.suid, duration = self.parse_mkv(file) mkvinfo[ch.suid] = {'file': file, 'duration': fmt_time(duration * 10**6) if duration else 0} if not (ch.start or ch.end): ch.start = fmt_time(0) if not ch.start else ch.start ch.end = mkvinfo[ch.suid]['duration'] if not ch.end and (ch.suid in mkvinfo) else ch.end ed.chapters.append(ch) self.editions.append(ed) if output: self.toxml(output) def main(args): template = args[0] output = args[1] avs = args[2] if len(args) == 3 else None chaps = AutoMKVChapters(template,output,avs) if __name__ == '__main__': from sys import argv, exit if len(argv) > 1: main(argv[1:]) else: exit("templates.py