#! /bin/env python # encoding: utf-8 # # Res Tranceliī # # X - ducentī sexāgintā quattuor videōnis audiōnisque convertator # # Usage: coniuga.py files [options] # import ellib, os, re, shutil, stat, sys, time ellib._debug = True ### # Actus Nullus: Parse command line, define commands, classes and methods ### print args = ellib.CmdParser([ "title", "t", 1, False, None, "order", "O", 1, False, None, # semicolon-separated list of internal TIDs in the desired order "force-duration", "D", 1, False, None, # force lenght of the video, used in local calculations only "language", "l", 1, True, None, # TID:data format "track-name", "ř", 1, True, None, # TID:data format "sub-charset", "s", 1, True, None, # TID:data format "fps", "f", 1, True, None, # TID:data format "video-rate", "v", 1, True, None, # TID:data format "audio-quality","a", 1, True, None, # TID:data format "output", "o", 1, False, None, "normalize", "n", 1, True, None, # TID:data format "armed", "A", 0, False, False, "resize", "r", 1, True, None, # TID:WxH "crop", "C", 1, True, None, # TID:top:bottom:left:right (in px) "aspect", "r", 1, True, None, # TID:data format "recalc-aspect","+", 1, True, None, # track number to force aspect ratio recalculation for "ffm-option", "P", 1, True, None, # additional params for ffmpeg, TID:data format "downmix", "Ž", 1, True, None, # number of channels to downmix audio to, TID:data format "mkv-option", "P", 1, True, None, # additional params for mkvmerge, TID:data format "cut", "T", 1, False, None, # Start at:Duration format "roundto", "d", 1, True, None, # TID:data "normalize-all","N", 0, False, True, # "video-rate-all", "V", 1, False, "600:200:800", # default values (overriden by specific values) "audio-quality-all", "A", 1, False, "1", # "roundto-all", "R", 1, False, "2", # "cleanup", "c", 0, False, True, "command-only", "X", 0, False, False, "ignore", "e", 1, True, None, # track number to ignore "chapters", "H", 1, False, None, # str "log", "L", 0, False, True, # logging into ~/.coniuga/date - title.log ]) argc = args.config if args.errors: ellib.say("Exiting due to errors on command line.", 3) for error in args.errors: print " %s" %(error) sys.exit(1) get_demuxers_command = 'mkvmerge --list-types spoo' tracklist_command = 'mkvmerge -i "%s"' alternate_tracklist_command = 'ffmpeg -i "%s" -vn -an spoo' media_identify_command = 'mplayer -endpos 0 -vo null -ao null -identify "%s"' class world: """Global settings""" types = {0: 'video', 1: 'audio', 2: 'subtitle'} tracks = [] order = [] log = [] time = time.time() def log(*lines): """Add data to logs.""" for line in lines: world.log.append(re.sub("", "", line)) def write_log(): """Dump everything we know into the log.""" logdir = "%s/.coniuga" %(os.getenv("HOME")) if not os.path.isdir(logdir): ellib.say("Creating log directory %s" %(logdir), 1) os.mkdir(logdir) name = re.sub(".mkv$", "", argc.output) logfile = "%s/%s - %s.log" %(logdir, time.strftime("%Y%m%d.%H%M%S"), name) # FIXME log = "%s\n\n%s" %(' '.join(sys.argv), '\n'.join(world.log)) file = open(logfile, 'w') file.write(log) file.close() ellib.say("Log saved: %s" %(logfile), 1) def get_demuxers(): pipe = os.popen(get_demuxers_command) data = pipe.read() pipe.close() return re.findall(' (\w{2,4})\s{2,4}\w', data)[1:] class Track: def __init__(self, filename, id_in_file, type, original_filename=None): self.active = True self.filename = filename self.original_filename = original_filename self.id_in_file = id_in_file # (real, ffmpeg) self.name = "" self.language = None # str self.type = type # 0 = video, 1 = audio, 2 = subtitle self.duration = 0.0 self.proc_opts_hidden = [] # Primary options are added into these before a list is printed self.cont_opts_hidden = [] # then they are merged into those below self.proc_opts = [] self.cont_opts = [] self.cut = None # (start, duration) # flags, data, etc. temp_filename_basis = "%s_%s" %(self.filename.replace("/", "-"), self.id_in_file[0]) if self.type == 0: self.temp_files = ["video-pass1-%s.x264" %(temp_filename_basis), "video-pass2-%s.x264" %(temp_filename_basis)] self.resolution = None # [W, H] self.orig_resolution = None # (W, H) self.aspect = None # float self.ro_aspect = False # True == readonly, don't recalc self.fps = None # str self.bitrate = tuple(map(int, argc.video_rate_all.split(":"))) # (nominal, variance, maximal) self.resize = None # (W, H) self.crop = None # (T, B, L, R) if argc.roundto_all not in ["2", "16"]: ellib.say("Option roundto-all must be either 2 or 16.", 3) sys.exit(1) self.roundto = int(argc.roundto_all) elif self.type == 1: self.temp_files = ["audio-%s.pcm" %(temp_filename_basis), "audio-%s.vorbis" %(temp_filename_basis)] self.quality = float(argc.audio_quality_all) # float <-1; 10> self.normalize = argc.normalize_all # bool self.downmix = None # channels to downmix to; None means leave it be elif self.type == 2: self.charset = None # str self.temp_file = "subtitle-%s.text" %(temp_filename_basis) def __cmp__(self, other): return cmp(self.type, other.type) def get_tracks(filename): # ffmpeg's an idiot incapable of grasping the preposterous idea that some filenames just HAVE a colon in their names, so we gotta hack around it if ":" in filename and False: original_filename = filename filename = filename.replace(":", "_") while os.path.exists(filename): filename = "_" + filename os.symlink(original_filename, filename) else: original_filename = None suffix = filename.split(".")[-1].lower() pipes = os.popen3(alternate_tracklist_command %(filename)) ffmpeg_data = pipes[2].read() map(file.close, pipes) tracks = [] with_ffmpeg = True if suffix in get_demuxers(): # analyse with mkvmerge pipe = os.popen(tracklist_command %(filename)) data = pipe.read() pipe.close() for tid, type in re.findall('Track ID (\d+): (\w+)', data): with_ffmpeg = False tid = int(tid) if suffix.lower() in ["mkv", "mov"]: tid -= 1 if original_filename: tracks.append(Track(filename, (tid, tid), {'video': 0, 'audio': 1, 'subtitles': 2}[type], original_filename=original_filename)) else: tracks.append(Track(filename, (tid, tid), {'video': 0, 'audio': 1, 'subtitles': 2}[type])) # post pro: id_in_file for ffmpeg has to skip subtitles for track in tracks: if track.type == 2: track.id_in_file = (track.id_in_file[0], None) index = tracks.index(track) for following_track in tracks[index+1:]: following_track.id_in_file = (following_track.id_in_file[0], following_track.id_in_file[1] - 1) if with_ffmpeg: # analyse with ffmpeg for tid, type in re.findall(' Stream #\d+.(\d+)[^:]*: ([^:]+):', ffmpeg_data): if type in ['Audio', 'Video']: tracks.append(Track(filename, (int(tid), int(tid)), {'Video': 0, 'Audio': 1}[type])) else: ellib.say("Unknown track %s type %s from file %s" %(tid, type, filename)) for track in tracks: if track.type in [0, 1]: hours, mins, duration = re.findall("Duration: (.+?),", ffmpeg_data)[0].split(":") track.duration = float(duration) + int(mins)*60 + int(hours)*3600 if track.type == 0: #resolution, track.fps = re.findall("Stream #0.%s.+, (\d+x\d+).* (\d+.\d+)" %(track.id_in_file[1]), ffmpeg_data)[0] #resolution, track.fps = re.findall("Stream #0.%s.+, (\d+x\d+),.* (\d+.\d+) fps" %(track.id_in_file[1]), ffmpeg_data)[0] # =media-video/ffmpeg-0.4.9_p20081014 if "tbr" in ffmpeg_data and "tbn" in ffmpeg_data and "tbc" in ffmpeg_data: resolution, track.fps = re.findall("Stream #0.%s.+, (\d+x\d+).* ([0-9./]+) tbr" %(track.id_in_file[1]), ffmpeg_data)[0] else: resolution, track.fps = re.findall("Stream #0.%s.+, (\d+x\d+).* (\d+.\d+)" %(track.id_in_file[1]), ffmpeg_data)[0] if track.fps.startswith("29.9"): track.fps = "30000/1001" elif track.fps.startswith("23.9"): track.fps = "24000/1001" track.resolution = map(int, resolution.split("x")) # we still need that state-of-the-junk MPlayer to capture aspect ratio pipes = os.popen3(media_identify_command %(filename)) data = pipes[1].read() map(file.close, pipes) aspect = re.findall("ID_VIDEO_ASPECT=(.+)", data) if (aspect and float(aspect[-1]) > 0) and not (str(tracks.index(track)) in argc.recalc_aspect): track.aspect = float(aspect[-1]) else: track.aspect = float(track.resolution[0]) / track.resolution[1] return tracks def list_track(tid, track, indent): type = world.types[track.type].rjust(8) filename = track.filename.ljust(indent) id_in_file = track.id_in_file if track.proc_opts: proc_opts = "ffmpeg(%s)" %(' '.join(track.proc_opts)) else: proc_opts = "" if track.cont_opts: cont_opts = "mkvmerge(%s)" %(' '.join(track.cont_opts)) else: cont_opts = "" if track.active: flags = "" if track.language: flags += "lang:%s " %(track.language) if track.name: flags += "name:%s " %(track.name) if track.type == 0: flags += "bitrate:%s:%s:%s" %(track.bitrate) # check bitrate if sufficient if "/" in track.fps: fps = map(float, track.fps.split("/")) fps = fps[0] / fps[1] else: print print track.fps fps = float(track.fps) minimal_bitrate = (track.resolution[0] * track.resolution[1] * fps) / 18432 track_bitrate = (track.bitrate[0] + track.bitrate[1]) / 2 if minimal_bitrate - track_bitrate > 150: # insufficient flags += ":low:+%s " %(int(minimal_bitrate - track_bitrate)) elif int(track_bitrate) - minimal_bitrate > 600: # excessive flags += ":high:-%s "%(int(track_bitrate - minimal_bitrate)) else: flags += ":OK " flags += "fps:%s " %(track.fps) if track.resolution: x, y = track.orig_resolution[0], track.orig_resolution[1] bad = False if x % 2 == 1: x = "%s" %(x) if y % 2 == 1: y = "%s" %(y) flags += "size:%sx%s " %(x, y) if track.crop: flags += "crop:%s:%s:%s:%s " %(track.crop) if track.resize or track.crop: flags += "resize:%sx%s " %(track.resolution[0], track.resolution[1]) if track.aspect: flags += "aspect:%s " %(track.aspect) elif track.type == 1: flags += "quality:%s " %(track.quality) if track.normalize: flags += "normalize " elif track.type == 2: if track.charset: flags += "char:%s " %(track.charset) if track.type in [0, 1]: if track.cut: flags += "cut:%s:%s " %(track.cut) else: flags = "TRACK IGNORED" output = "%s: %s from %s %s/%s %s %s %s" %(str(tid).rjust(3), type, filename, id_in_file[0], id_in_file[1], flags, proc_opts, cont_opts) log(output) print ellib.style(output) def list_tracks(): ellib.say("Duration: %s" %(world.tracks[0].duration)) print print ellib.style("TID: type from filename position in file/without subtitles flags") filename_len = 0 for track in world.tracks: if len(track.filename) > filename_len: try: filename_len = len(track.filename.decode(sys.stdin.encoding)) except: filename_len = len(track.filename) if world.order: order = world.order else: order = range(len(world.tracks)) for tid in order: try: list_track(tid, world.tracks[tid], filename_len) except IndexError: ellib.say("Track %s is not defined." %(tid), 3) print if argc.chapters: ellib.say("Reading chapters from file %s." %(argc.chapters)) print # estimates (in kB) total_size = 0.0 for track in world.tracks: if not track.active: continue if track.type == 0: total_size += track.duration * (track.bitrate[0] + track.bitrate[2]) / 2 / 8 elif track.type == 1: total_size += track.duration * 10 # approximate kB/s for vorbis q=1 if total_size > 40960: total_size = "%s MB" %(int(total_size / 1024)) else: total_size = "%s kB" %(int(total_size)) total_size_str = "Estimated file size is %s." %(total_size) log("") log(total_size_str) ellib.say(total_size_str) print def parse_group(data, accepted=[0, 1, 2]): tid, data = data.split(":", 1) try: tid = int(tid) except ValueError: ellib.say("%s is not a valid Track ID." %(tid), 3) sys.exit(1) try: track = world.tracks[tid] except IndexError: ellib.say("Track %s is not defined." %(tid), 3) sys.exit(1) if track.type not in accepted: accept_names = [] for type in accepted: accept_names.append("%s" %(world.types[type])) accept_names = ' or '.join(accept_names) ellib.say("Track %s is not a valid %s track." %(tid, accept_names), 3) sys.exit(1) return (tid, track, data) def pad(n, padme = False, padto = 5): if type(n) == float: n = round(n, 2) n = str(n) if "." in n: # it's a float -> make sure it has two decimals. Pad if necessary. if len(n.split(".")[1]) == 1: n = n + "0" elif len(n) == 1: # clock n = "0" + n if padme: padtimes = padto - len(n) n = "0"*padtimes + n return n def strftime(secs, return_string=True): hours = int(secs) / 3600 secs = secs % 3600 mins = int(secs) / 60 secs = int(secs % 60) if return_string: return "%s:%s:%s" %(pad(hours), pad(mins), pad(secs)) else: return (hours, mins, secs) class ProgressBar: def __init__(self, lenght = 30): self.lenght = lenght - 3 # minus [, > and ] self.filesize = 0 self.buffer = [] # stores file sizes, used to compute ETA self.buffer_len = 8 self.start = time.time() self.last_size = 0 def draw(self, percent, filesize = None, additional = ""): # filesize == current filesize # self.filesize == projected filesize if not filesize or filesize > self.filesize: # assume 100% filesize = self.filesize if percent > 1: percent = 1.0 current = int(self.lenght * percent) # maintain a graph of speeds (kB/s for each update) curr_time = time.time() if not self.buffer: last_time = self.start else: last_time = self.buffer[-1][1] speed = (filesize - self.last_size) / (curr_time - last_time) self.buffer.append([speed, curr_time]) self.last_size = filesize if len(self.buffer) > self.buffer_len: self.buffer.pop(0) # delete the oldest one # average speed suma = 0.0 for i in range(len(self.buffer)): suma += self.buffer[i][0] average = suma / len(self.buffer) # come up with an ETA based on average speed if average > 0: remaining_size = self.filesize - filesize ETA = strftime(remaining_size / average) else: ETA = "never" if percent == 1: arrow = "===" else: arrow = ">" sys.stdout.write("\r%s%% [%s%s%s] %s/%sMB (%s kB/s) ETA: %s %s " %(pad(percent*100), current * "=", arrow, (self.lenght - current) * " ", pad(filesize/1048576.0), pad(self.filesize/1048576.0), pad(average/1024.0, True), ETA, additional)) sys.stdout.flush() def ffmpeg(command, temp_file, track): # purge temps, ffmpeg is kinda sensitive :o) try: os.remove(temp_file) except: pass pipes = os.popen3(command) bar = ProgressBar() while True: #time.sleep(1) chunk = pipes[2].read(256) if not chunk: break # we're done I guess # split output by '\r's, parse the one before last (it's surely complete and most up to date) try: line = chunk.split('\r')[-2] # data: (frame, size in kB, time in seconds, bitrate in kb/s) - all floats secs, bitrate = map(float, re.findall('size=\s*\d+kB time=\s*(\d+.\d+) bitrate=\s*(\d+.\d)kbits/s', line)[0]) # estimate target file size percent = secs / track.duration if percent == 0: percent = 0.01 current_size = os.lstat(temp_file)[stat.ST_SIZE] target_size = current_size / percent bar.filesize = target_size # draw progress bar additional = "Kbps: %s" %(bitrate) bar.draw(percent, current_size, additional) except IndexError: # it's not converting yet try: #print line pass except: pass try: bar.draw(1, target_size, bitrate) except: pass map(file.close, pipes) print def oggenc(command, temp_file): pipes = os.popen3(command) bar = ProgressBar() while True: chunk = pipes[2].read(128) if not chunk: break # we're done I guess try: line = chunk.split('\r')[-2] perc_units, perc_frac = re.findall("(\d+)[.,](\d+)%.+\d+m\d+s remaining", line)[0] if len(perc_frac) == 1: perc_frac += "0" percent = (int(perc_units) + float(perc_frac)/100) / 100 if percent == 0: percent = 0.01 current_size = os.lstat(temp_file)[stat.ST_SIZE] target_size = current_size / percent bar.filesize = target_size # draw progress bar bar.draw(percent, current_size) except IndexError: # it's not converting yet pass try: bar.draw(1, target_size) except: pass map(file.close, pipes) print ### # Actus Primus: Analyse input files, create track objects, interpret gathered intel, bring order into chaos ### # gather basic intel on our files for filename in args.targets: if os.path.exists(filename): world.tracks += get_tracks(filename) else: ellib.say("File %s does not exist." %(filename), 3) sys.exit(1) if not world.tracks: ellib.say("Specify at least one file containing video, audio, or subtitles.", 3) sys.exit(1) # sort video -> audio -> subs world.tracks.sort() # React on user input if argc.order: world.order = map(int, argc.order.split(":")) # Container options if not argc.title: if world.tracks[0].original_filename: filename = world.tracks[0].original_filename else: filename = world.tracks[0].filename if "." in filename: argc.title = '.'.join(filename.split(".")[:-1]) else: argc.title = filename ellib.say("Setting title to %s as none was specified." %(argc.title)) if not argc.output: argc.output = "%s.mkv" %(argc.title) argc.output = ellib.charfilter(argc.output, {'/': '-'}) ellib.say("Setting output filename to %s as none was specified." %(argc.output)) for opt in argc.language: tid, track, data = parse_group(opt) track.language = data for opt in argc.track_name: tid, track, data = parse_group(opt) track.name = data for opt in argc.sub_charset: tid, track, data = parse_group(opt, [2]) track.charset = data for opt in argc.fps: tid, track, data = parse_group(opt, [0]) track.fps = data track.proc_opts_hidden.append("-r %s" %(data)) # Processor options if argc.force_duration: for track in world.tracks: track.duration = float(argc.force_duration) if argc.cut: for track in world.tracks: if track.type in [0, 1]: track.cut = tuple(map(float, argc.cut.split(":"))) for opt in argc.normalize: if ":" in opt: tid, track, data = parse_group(opt, [1]) if data.lower() in ["true", "1", "yes"]: track.normalize = True else: track.normalize = False else: if opt.lower() in ["true", "1", "yes"]: data = True else: data = False for track in world.tracks: if track.type == 1: track.normalize = data for opt in argc.aspect: tid, track, data = parse_group(opt, [0]) if ":" in data or "/" in data: data = map(float, re.findall("(.+)[:/](.+)", data)[0]) aspect = data[0] / data[1] else: aspect = float(data) track.aspect = aspect track.ro_aspect = True # readonly, don't recalc for opt in argc.ffm_option: tid, track, data = parse_group(opt, [0, 1]) track.proc_opts.append(data) for opt in argc.downmix: tid, track, data = parse_group(opt, [1]) track.downmix = data for opt in argc.mkv_option: tid, track, data = parse_group(opt) track.cont_opts.append(data) for opt in argc.audio_quality: tid, track, data = parse_group(opt, [1]) track.quality = float(data) for opt in argc.video_rate: tid, track, data = parse_group(opt, [0]) track.bitrate = tuple(map(int, data.split(":"))) for opt in argc.resize: tid, track, data = parse_group(opt, [0]) if "x" not in data: ellib.say("Resize parameter %s of track %s is not valid." %(data, tid), 3) sys.exit(1) track.resize = tuple(map(int, data.split("x"))) for opt in argc.crop: tid, track, data = parse_group(opt, [0]) if data.count(":") is not 3: ellib.say("Crop parameter %s of track %s is not valid." %(data, tid), 3) sys.exit(1) track.crop = tuple(map(int, data.split(":"))) for crop in track.crop: if crop%2 == 1: ellib.say("Crop values from track %s are not divisible by 2." %(tid), 3) sys.exit(1) for opt in argc.roundto: tid, track, data = parse_group(opt, [0]) if data == "16": track.roundto = 16 else: ellib.say("Roundto parameter of track %s must be either 16 or 2." %(tid), 3) for tid in argc.ignore: try: tid = int(tid) except ValueError: ellib.say("%s is not a valid Track ID." %(tid), 3) sys.exit(1) if tid < len(world.tracks): world.tracks[tid].active = False if world.order: order = world.order else: order = range(len(world.tracks)) # understand and apply all parameters for tid in order: track = world.tracks[tid] if track.language: #track.cont_opts_hidden.append("--language %s:%s" %(track.id_in_file[0], track.language)) track.cont_opts_hidden.append("--language 0:%s" %(track.language)) if track.type in [0, 1]: if track.cut: track.proc_opts_hidden.append("-ss %s -t %s" %(track.cut)) if track.type == 0: ## Size and aspect ratio # 1) determine real size if abs(float(track.resolution[0]) / track.resolution[1] - track.aspect) > 0.05: if track.aspect >= 1: track.resolution[0] = int(round(track.aspect * track.resolution[1])) else: track.resolution[1] = int(round(track.resolution[0] / track.aspect)) # save original sizes for the list track.orig_resolution = tuple(track.resolution) # 2) determine percentages - we'll use them to resize later if track.resize: perc_x = float(track.resize[0]) / track.resolution[0] perc_y = float(track.resize[1]) / track.resolution[1] # 3) substract crop values (comes in TBLR format) if track.crop: track.resolution[0] -= sum(track.crop[2:4]) track.resolution[1] -= sum(track.crop[0:2]) # 4) resize them if necessary if track.resize: track.resolution[0] *= perc_x track.resolution[1] *= perc_y # if we were resizing or cropping, calculate a new aspect ratio if (track.resize or track.crop) and not track.ro_aspect: track.aspect = track.resolution[0] / float(track.resolution[1]) track.proc_opts_hidden.append("-aspect %s" %(track.aspect)) # 5) round sizes track.resolution[0] = int(round(track.resolution[0] / float(track.roundto))) * track.roundto track.resolution[1] = int(round(track.resolution[1] / float(track.roundto))) * track.roundto ## ffmpeg options # crop if track.crop: track.proc_opts_hidden.append('-vf crop=%d:%d:%d:%d' %(track.orig_resolution[0] - track.crop[2] - track.crop[3], track.orig_resolution[1] - track.crop[0] - track.crop[1], track.crop[2], track.crop[0])) # resize if track.resize: track.proc_opts_hidden.append("-s %sx%s" %(track.resolution[0], track.resolution[1])) # mkvmerge options if track.fps: #track.cont_opts_hidden.append("--default-duration %s:%sfps" %(track.id_in_file[0], track.fps)) track.cont_opts_hidden.append("--default-duration 0:%sfps" %(track.fps)) elif track.type == 2: if track.charset: #track.cont_opts_hidden.append("--sub-charset %s:%s" %(track.id_in_file[0], track.charset)) track.cont_opts_hidden.append("--sub-charset 0:%s" %(track.charset)) # print all we know list_tracks() if not argc.armed: # if we're just planning we don't wanna go any further. Exiting nicely. for track in world.tracks:# part 2A of our "Why ffmpeg's A Total Moron™" hack - remove the file if not armed if track.original_filename and os.path.exists(track.filename): # in case a previous iteration removed the file os.remove(track.filename) sys.exit(0) else: # merge hidden and additional options for track in world.tracks: track.proc_opts = track.proc_opts_hidden + track.proc_opts track.cont_opts = track.cont_opts_hidden + track.cont_opts ### # Actus Secundus: The hard part - transcode (crop, resize, ...) video, transcode audio ### ffmpeg_command = 'ffmpeg -i "%s" %s -map 0:%s:0 "%s"' # input parameters ffmpeg_stream_id output ffmpeg_video_parameters = '-pass %s %s -an -vcodec libx264 -nr 400 -b %sk -bt %sk -maxrate %sk -coder 1 -flags +loop -partitions +parti4x4+partp8x8+partb8x8 -me_method 5 -subq 1 -trellis 0 -me_range 16 -g 250 -keyint_min 25 -sc_threshold 40 -i_qfactor 0.71 -rc_eq "blurCplx^(1-qComp)" -qmin 10 -qmax 51 -qdiff 4 -f rawvideo' # passnum additional_options bitrate_nominal bitrate_variance bitrate_maximal ffmpeg_audio_parameters = '-vn -acodec pcm_s16le %s -f wav %s' audio_normalize_command = 'normalize "%s"' audio_encode_command = 'oggenc -q 1 "%s" -o "%s"' # make us a nice temp directory world.temp = "coniuga-temp_%s" %(argc.output) if argc.command_only: print 'mkdir "%s"' %(world.temp) print 'cd "%s"' %(world.temp) print else: ellib.say("Creating temp directory %s" %(world.temp), 1) os.mkdir(world.temp) os.chdir(world.temp) # adjust filenames to reflect the change of current working directory for track in world.tracks: track.filename = "../%s" %(track.filename) for tid in order: track = world.tracks[tid] if not track.active: continue if track.type == 2: # Subtitles do not require processing in this act. Just copy them over. if argc.command_only: print 'cp "%s" "%s"' %(track.filename, track.temp_file) print else: shutil.copy(track.filename, track.temp_file) if track.type == 0: # videō for passnum in ["1", "2"]: # compose the command additional_processor_parameters = ' '.join(track.proc_opts) # passnum additional_options bitrate_nominal bitrate_variance bitrate_maximal processor_parameters = ffmpeg_video_parameters %((passnum, additional_processor_parameters) + track.bitrate) main_command = ffmpeg_command %(track.filename, processor_parameters, track.id_in_file[1], track.temp_files[int(passnum) - 1]) if argc.command_only: print main_command print else: ellib.say("Transcoding video track %s, pass %s..." %(tid, passnum), 1) ffmpeg(main_command, track.temp_files[int(passnum) - 1], track) elif track.type == 1: # audiō additional_processor_parameters = ' '.join(track.proc_opts) if track.downmix: processor_parameters = ffmpeg_audio_parameters %("-ac %s" %(track.downmix), additional_processor_parameters) else: processor_parameters = ffmpeg_audio_parameters %("", additional_processor_parameters) main_command = ffmpeg_command %(track.filename, processor_parameters, track.id_in_file[1], track.temp_files[0]) if argc.command_only: print main_command print else: ellib.say("Extracting audio track %s..." %(tid), 1) ffmpeg(main_command, track.temp_files[0], track) if track.normalize: if argc.command_only: print audio_normalize_command %(track.temp_files[0]) print else: ellib.say("Normalizing audio track %s..." %(tid), 1) pipes = os.popen3(audio_normalize_command %(track.temp_files[0])) pipes[2].read() if argc.command_only: print audio_encode_command %(track.temp_files[0], track.temp_files[1]) print else: ellib.say("Encoding audio track %s..." %(tid), 1) oggenc(audio_encode_command %(track.temp_files[0], track.temp_files[1]), track.temp_files[1]) ### # Actus Tertius: Müxing. ### mkvmerge_command = 'mkvmerge -o "../%s" --title "%s"' # output, title, tracks merge_command = [mkvmerge_command %(argc.output, argc.title)] for tid in order: track = world.tracks[tid] if not track.active: continue if track.type in [0, 1]: file = track.temp_files[1] else: file = track.temp_file if track.cont_opts: track_command = '%s "%s"' %(' '.join(track.cont_opts), file) else: track_command = '"%s"' %(file) merge_command.append(track_command) if argc.chapters: merge_command.append('--chapters "../%s"' %(argc.chapters)) merge_command = ' '.join(merge_command) if argc.command_only: print merge_command print else: retval = os.system(merge_command) if retval != 0: ellib.say("mkvmerge, halting without cleanup (use manual merging)...", 3) sys.exit(2) ### # Actus Quartus: Cleanup. ### if argc.cleanup: files = ["ffmpeg2pass-0.log", "x264_2pass.log", "x264_2pass.log.mbtree"] for track in world.tracks: # part 2B of our "Why ffmpeg's A Total Moron™" hack - remove the file if armed if track.original_filename: try: os.remove("../"+track.filename) except: pass if not track.active: continue if track.type in [0, 1]: files.extend(track.temp_files) else: files.append(track.temp_file) to_remove = [] for file in files: if argc.command_only: to_remove.append(file) else: try: os.remove(file) ellib.say("Removed temp file %s" %(file), 1) except OSError: ellib.say("Expected temp file %s not present." %(file)) if argc.command_only: print 'rm -f "%s"' %('" "'.join(to_remove)) print 'cd ..' print 'rmdir "%s"' %(world.temp) else: os.chdir("..") try: os.rmdir(world.temp) ellib.say("Removed temp directory %s" %(world.temp), 1) except OSError: ellib.say("Expected temp dir %s not present, what's going on ???" %(file)) if not argc.command_only: time_taken = time.time() - world.time final_size = os.lstat(argc.output)[stat.ST_SIZE] / 1024 if final_size > 40960: final_size = "%s MB" %(int(final_size / 1024)) else: final_size = "%s kB" %(int(final_size)) final_size_str = "Resulting file size is %s." %(final_size) ellib.say(final_size_str) time_taken_str = "Process took %s hours, %s minutes and %s seconds." %(strftime(time_taken, False)) ellib.say(time_taken_str, 2) log("") log(time_taken_str, final_size_str) write_log() print