added coniuga.py (with deps) to doc/ - contains usage paths for ffmpeg -map that might be implemented one day

This commit is contained in:
Overwatch 2014-08-29 11:48:06 +02:00
parent ae18e7a6ef
commit 65b0fe6144
2 changed files with 1333 additions and 0 deletions

967
doc/coniuga.py Executable file
View file

@ -0,0 +1,967 @@
#! /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("<C.>", "", 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 <CB>%s<C/>" %(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: <CB>%s<C/>" %(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 <Cr>roundto-all<C/> must be either <CB>2<C/> or <CB>16<C/>.", 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 <CB>%s<C/> type <Cr>%s<C/> from file <CB>%s<C/>" %(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(<CB>%s<C/>)" %(' '.join(track.proc_opts))
else:
proc_opts = ""
if track.cont_opts:
cont_opts = "mkvmerge(<CB>%s<C/>)" %(' '.join(track.cont_opts))
else:
cont_opts = ""
if track.active:
flags = ""
if track.language:
flags += "lang:<Cb>%s<C/> " %(track.language)
if track.name:
flags += "name:<Cb>%s<C/> " %(track.name)
if track.type == 0:
flags += "bitrate:<Cb>%s<C/>:<Cb>%s<C/>:<Cb>%s<C/>" %(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 += ":<Cr>low<C/>:<Cr>+%s<C/> " %(int(minimal_bitrate - track_bitrate))
elif int(track_bitrate) - minimal_bitrate > 600: # excessive
flags += ":<Cy>high<C/>:<Cy>-%s<C/> "%(int(track_bitrate - minimal_bitrate))
else:
flags += ":<Cg>OK<C/> "
flags += "fps:<Cb>%s<C/> " %(track.fps)
if track.resolution:
x, y = track.orig_resolution[0], track.orig_resolution[1]
bad = False
if x % 2 == 1:
x = "<Cr>%s<C/>" %(x)
if y % 2 == 1:
y = "<Cr>%s<C/>" %(y)
flags += "size:<Cb>%s<C/>x<Cb>%s<C/> " %(x, y)
if track.crop:
flags += "crop:<Cb>%s<C/>:<Cb>%s<C/>:<Cb>%s<C/>:<Cb>%s<C/> " %(track.crop)
if track.resize or track.crop:
flags += "resize:<Cb>%s<C/>x<Cb>%s<C/> " %(track.resolution[0], track.resolution[1])
if track.aspect:
flags += "aspect:<Cb>%s<C/> " %(track.aspect)
elif track.type == 1:
flags += "quality:<Cb>%s<C/> " %(track.quality)
if track.normalize:
flags += "<Cb>normalize<C/> "
elif track.type == 2:
if track.charset:
flags += "char:<Cb>%s<C/> " %(track.charset)
if track.type in [0, 1]:
if track.cut:
flags += "cut:<Cb>%s<C/>:<Cb>%s<C/> " %(track.cut)
else:
flags = "<Cr>TRACK IGNORED<C/>"
output = "<Cy>%s<C/>: <CB>%s<C/> from <CB>%s<C/> <Cb>%s<C/>/<Cb>%s<C/> %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("<Cy>TID<C/>: <CB>type<C/> from <CB>filename<C/> <Cb>position in file<C/>/<Cb>without subtitles<C/> 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 <Cr>%s<C/> is not defined." %(tid), 3)
print
if argc.chapters:
ellib.say("Reading chapters from file <CB>%s<C/>." %(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 <CB>%s<C/>." %(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("<Cr>%s<C/> is not a valid Track ID." %(tid), 3)
sys.exit(1)
try:
track = world.tracks[tid]
except IndexError:
ellib.say("Track <Cr>%s<C/> is not defined." %(tid), 3)
sys.exit(1)
if track.type not in accepted:
accept_names = []
for type in accepted:
accept_names.append("<CB>%s<C/>" %(world.types[type]))
accept_names = ' or '.join(accept_names)
ellib.say("Track <Cr>%s<C/> 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 <Cr>%s<C/> 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 <CB>%s<C/> 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 <CB>%s<C/> 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 <Cr>%s<C/> of track <CB>%s<C/> 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 <Cr>%s<C/> of track <CB>%s<C/> 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 <CB>%s<C/> 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 <Cr>%s<C/> must be either <CB>16<C/> or <CB>2<C/>." %(tid), 3)
for tid in argc.ignore:
try:
tid = int(tid)
except ValueError:
ellib.say("<Cr>%s<C/> 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 <CB>%s<C/>" %(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("<CB>Transcoding<C/> <Cy>video<C/> track <Cy>%s<C/>, <Cb>pass %s<C/>..." %(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("<CB>Extracting<C/> <Cy>audio<C/> track <Cy>%s<C/>..." %(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("<CB>Normalizing<C/> <Cy>audio<C/> track <Cy>%s<C/>..." %(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("<CB>Encoding<C/> <Cy>audio<C/> track <Cy>%s<C/>..." %(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 <CB>%s<C/>" %(file), 1)
except OSError:
ellib.say("Expected temp file <CB>%s<C/> 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 <CB>%s<C/>" %(world.temp), 1)
except OSError:
ellib.say("Expected temp <Cr>dir<C/> <CB>%s<C/> 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 <CB>%s<C/>." %(final_size)
ellib.say(final_size_str)
time_taken_str = "Process took <CB>%s<C/> hours, <CB>%s<C/> minutes and <CB>%s<C/> seconds." %(strftime(time_taken, False))
ellib.say(time_taken_str, 2)
log("")
log(time_taken_str, final_size_str)
write_log()
print

366
doc/ellib.py Executable file
View file

@ -0,0 +1,366 @@
#! /bin/env python
# -*- coding: utf-8 -*-
import os, pickle, re, sys, time, traceback
__version = '1.9.2'
class CmdParser:
class config:
pass
def __init__(self, mapping, source = sys.argv[1:]):
# pools for variable quadruples
p_long = []
p_short = []
p_type = []
p_plural = {}
p_default = {}
# Legacy checks for pre-1.7 or pre-1.9 programs -- it's a CRAP, so i had them neutered :]
#l_mapping = len(mapping)
#if l_mapping%3 == 0 and l_mapping%4 != 0:
#maptype = 3
#say("ellib.CmdParser: Pre-1.7 or 1.9 program detected, running in legacy mode", timestamp = True, icon = "DBG")
#else:
#maptype = 5
maptype = 5
for i in range(0, len(mapping), maptype):
p_long.append(mapping[i])
p_short.append(mapping[i+1])
p_type.append(mapping[i+2])
p_plural[mapping[i]] = mapping[i+3]
p_default[mapping[i]] = mapping[i+4]
# Switches and targets arrays.
self.switches = {}
for name in p_long:
if p_plural[name]:
self.switches[name] = []
else:
self.switches[name] = None
self.targets = []
self.errors = []
# Behold, the Parser !!!
delka = len(source)
mapa = range(delka)
for i in range(delka):
if i in mapa:
arg = source[i]
if arg[:2] == '--' and arg[:5] != '--no-':
if arg[2:] in p_long:
if p_type[p_long.index(arg[2:])] == 0:
if p_plural[arg[2:]]:
self.switches[arg[2:]].append(True)
else:
self.switches[arg[2:]] = True
else:
try:
if p_plural[arg[2:]]:
self.switches[arg[2:]].append(source[i+1])
else:
self.switches[arg[2:]] = source[i+1]
mapa.remove(i+1)
except:
self.errors.append(arg)
else:
self.errors.append(arg)
elif arg[:5] == '--no-':
if arg[5:] in p_long:
if p_type[p_long.index(arg[5:])] == 0:
if p_plural[arg[5:]]:
self.switches[arg[5:]].append(False)
else:
self.switches[arg[5:]] = False
else:
self.errors.append(arg)
elif arg[0] in ['-', '+']:
for x in arg[1:]:
if x in p_short:
longname = p_long[p_short.index(x)]
if p_type[p_short.index(x)] == 0:
if arg[0] == '-':
value = False
else:
value = True
if p_plural[longname]:
self.switches[longname].append(value)
else:
self.switches[longname] = value
else:
try:
if p_plural[longname]:
self.switches[longname].append(source[i+1])
else:
self.switches[longname] = source[i+1]
mapa.remove(i+1)
except:
self.errors.append(arg[0]+x)
else:
self.errors.append(arg[0]+x)
else:
self.targets.append(arg)
# Look for empty fields and fill them with default values if possible
for name in self.switches:
if name in p_default.keys() and p_default[name] != None and self.switches[name] in [[], None]:
if type(p_default[name]) == list:
self.switches[name] = p_default[name]
else:
if p_plural[name]:
self.switches[name] = [p_default[name]]
else:
self.switches[name] = p_default[name]
# Fill the self.switches dictionary into self.config variables for easier access
name2 = name.replace(' ', '_').replace('-', '_').replace('+', '_')
setattr(self.config, name2, self.switches[name])
# if there's a "--debug" somewhere on the line, run a command dump
if '--debug' in source:
_debug = True
say("Command dump", 1)
for name in self.switches:
say("%s: %s" %(name, self.switches[name]))
say("Command dump", 2)
class Db:
"""Třída velmi špatné databáze:
* při inicializaci se vytvoří databáze se jménem name, pokud neexistuje. Otevře databázi.
* metoda read() - přečte databázi
* metoda write() - zapíše (přepíše) databázi
* metoda remove() - odstraní řádek
"""
def __init__(self, filename):
"""Otevře databázi, pokud neexistuje, vytvoří ji."""
self.filename = filename
if os.path.isfile(self.filename):
try: file = open(self.filename, 'r+')
except IOError: file = open(self.filename, 'r')
else:
say("Vytvářím databázi %s \r" %(self.filename), 1, 1)
file = open(self.filename, 'w')
file.write('None')
say("Databáze %s vytvořena." %(self.filename), 2)
file.close()
try: file = open(self.filename, 'r+')
except IOError: file = open(self.filename, 'r')
self.file = file
def read(self):
"""Low level funkce; vrátí databázi"""
self.file.seek(0)
try: return pickle.load(self.file)
except:
return []
def write(self, object):
"""Low level funkce; zapíše databázi"""
self.file.seek(0)
retval = pickle.dump(object, self.file)
self.file.flush()
return retval
def remove(self, ID):
"""Odstraní řádek z databáze."""
database = self.read()
del database[ID]
return self.write(database)
def add(self, line):
"""Zapíše řádek do databáze"""
database = self.read()
database.append(line)
return self.write(database)
class Help:
def __init__(self, rawtext):
self.rawtext = rawtext+'\n[[' # a kinky workaround indeed :)
self.blocks = {}
self.order = []
self.mkindex()
def mkindex(self):
chunk = re.findall('\[\[(.+?)\]\]\n(.+?)(?=\[\[)', self.rawtext, re.S)
for section in chunk:
if len(section) == 2: # caption and text
self.order.append(section[0])
self.blocks[section[0]] = section[1]
else:
say("Help section is of wrong lenght, please report a bug (provide the original help text if possible)", timestamp = True, icon = "<Cr>FAIL<C/>")
def __call__(self, caption = None):
if caption in self.blocks.keys():
return style(self.blocks[caption])
else:
tosend = ""
for cap in self.order:
tosend += "[<CB>%s<C/>]\n%s" %(cap, self.blocks[cap])
return style(tosend)
def help(self, caption = None):
return self.__call__(caption)
# output engine
colortags = {
'<Cg>': '\x1b[32;01m',
'<Cy>': '\x1b[33;01m',
'<Cr>': '\x1b[31;01m',
'<Cb>': '\x1b[34;01m',
'<CB>': '\x1b[01m',
'<C/>': '\x1b[39;49;00m'
}
badchars = {
'\"': '\\"',
'\x00': '',
'/': '-'
}
def charfilter(text, sada = badchars):
for badchar in sada.keys():
text = text.replace(badchar, sada[badchar])
return text
# R.I.P, you served us well. May this line make us remember you for ages.
# Eram, non sum, non misero...
#def style(text):
#return re.compile('<C.>').sub(lambda text: colortags[text.group()], text)
def style(text):
tags = re.findall('<C.>', text)
for tag in tags:
try:
text = text.replace(tag, colortags[tag])
except KeyError:
pass
return text
def say(text, mode = 0, breakline = True, timestamp = False, icon = ""):
if isinstance(text, unicode) and sys.stdout.encoding:
text = text.encode(sys.stdout.encoding)
text = str(text)
if _debug:
timestamp = True
icons = _icons_debug
else:
icons = _icons
if mode in range(1, len(icons)) and not icon:
icon = icons[mode]
elif not icon:
icon = icons[0]
if breakline == 1:
br = '\n'
else:
br = ''
if timestamp:
text = style(time.strftime('%Y-%m-%d %H:%M:%S ')+icon+' -- '+text)+br
else:
text = style(icon+' '+text)+br
if _log: log(text)
sys.stdout.write(text)
sys.stdout.flush()
def countdown(units, text = "", secs = 1):
ticks = range(units)
ticks.reverse()
try:
for i in ticks:
say(text+str(i+1)+' \r', 1, 0)
time.sleep(secs)
say(text+'0 ', 2)
return True
except (EOFError, KeyboardInterrupt):
say(text+'0 ', 3)
return False
def log(text, logfile = None):
if not logfile:
logfile = _logfile
if not text:
return False
if text[-1] != '\n':
text += '\n'
fd = open(logfile, 'a')
fd.write(text)
fd.close()
def join(array, separator=' '):
return separator.join(array)
def logger(status, prefix = '.'):
global _log, _logfile
_prefix = prefix
if status:
_log = True
# Check if prefix is a directory: if it's a file, log into ".", if it doesn't exist, create it.
if not os.path.isdir(_prefix) and os.path.exists(_prefix):
_prefix = '.'
elif not os.path.exists(_prefix):
os.mkdir(_prefix)
_logfile = _prefix + '/' + time.strftime('%Y-%m-%d %H:%M:%S')+'.log'
if prefix != _prefix: say("%s exists, but is not a directory." %(prefix), 3)
say("Logging into %s" %(_logfile), 1)
else:
say("Logging into %s" %(_logfile), 2)
_log = False
class File:
def __init__(self, name, mode = 'r'):
self.name = name
self.mode = mode
self.fd = open(name, mode)
self.data = ''
def read(self):
self.data = self.fd.read()
def write(self):
self.fd.write(self.data)
def close(self):
self.fd.close()
def coredump(loc):
import code
c = code.InteractiveConsole(loc)
c.interact()
class Autoloader:
def __init__(self):
self.original_handler = sys.excepthook
def register(self, deact=False):
if not deact:
sys.excepthook = self
else:
sys.excepthook = self.original_handler
def __call__(self, exctype, value, trace):
if exctype == NameError:
module_name = charfilter(value.args[0], {"name '": "", "' is not defined": ""})
#retval = _exec("import %s" % module_name, trace)
command_locals = trace.tb_frame.f_locals
command_globals = trace.tb_frame.f_globals
try: # retval relates to import success only
exec "import %s" % module_name in command_locals, command_globals
retval = True
say("Autoloaded module <Cg>%s<C/>" %(module_name), timestamp=True, icon="INFO")
exec trace.tb_frame.f_code in command_locals, command_globals
retval = True
except ImportError:
retval = False
except:
traceback.print_exc()
if exctype != NameError or not retval:
traceback.print_exception(exctype, value, trace)
autoloader = Autoloader()
# Ellib setup
_debug = False
_log = False
_icons = [" ", ">>> ", "<Cg> * <C/>", "<Cr> * <C/>"]
_icons_debug = ["INFO", "<CB>EXEC<C/>", "<Cg>DONE<C/>", "<Cr>FAIL<C/>"]
_logfile = time.strftime('%Y-%m-%d %H:%M:%S')+".log"