#!/usr/bin/env python2
"""
A simple Blu-ray ripping tool using eac3to, FFmpeg, MPlayer and x264.

Settings are queried interactively before main processing begins. They will
be stored so that all processing steps can be re-run if interrupted.

Input is a directory containing an unencrypted dump of the Blu-ray contents.
Audio tracks will be converted into AC3, the video track will be encoded using
x264, and subtitle tracks will be extracted so that they can be processed
further with tools like SupRip. Black bars at the top and bottom edges of the
image will be detected and cropped away automatically. During encoding, a live
preview (cropped to show the center of the image unscaled if the stream is
larger than the screen) is shown of the encoded results. A settings file for
mmg (mkvmerge GUI) will be generated so that after running this script, it's
just a matter of double-clicking that file to get a final Matroska file (unless
subtitles are wanted).
"""
__version__ = "1.0.2"
__author__ = "Martin J. Fiedler <martin.fiedler@gmx.net>"
__copyright__ = """
This software is published under the terms of KeyJ's Research License,
version 0.2. Usage of this software is subject to the following conditions:
0. There's no warranty whatsoever. The author(s) of this software can not
   be held liable for any damages that occur when using this software.
1. This software may be used freely for both non-commercial and commercial
   purposes.
2. This software may be redistributed freely as long as no fees are charged
   for the distribution and this license information is included.
3. This software may be modified freely except for this license information,
   which must not be changed in any way.
4. If anything other than configuration, indentation or comments have been
   altered in the code, the original author(s) must receive a copy of the
   modified code.
"""
import sys, os, re, subprocess, random, time, threading, optparse

AutoselectLanguages = set(["english"])
AudioFormats = set(["ac3 surround", "ac3", "e-ac3", "truehd/ac3", "dts", "dts hi-res", "dts master audio", "raw/pcm"])
ValidAudioBitrates = set([64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640])
DefaultAudioBitrate = 448
VideoBitrateGranularity = 50
X264BaseOptions = [
    ("--profile", "high"), ("--level", "4.1"),
    ("--no-interlaced", "--interlaced"), ("--sar", "1:1"),
    ("--range", "tv"), ("--colorprim", "bt709"), ("--transfer", "bt709"), ("--colormatrix", "bt709"),
    ("--demuxer", "lavf")]
MaxPreviewSize = (1268, 732)
AnswerCacheFile = ".blurip.cache"
LanguageMap = """
    aar=afar abk=abkhazian ace=achinese ach=acoli ada=adangme ady=adyghe
    afa=afro-asiatic afh=afrihili afr=afrikaans ain=ainu aka=akan akk=akkadian
    alb=albanian ale=aleut alg=algonquian alt=southern_altai amh=amharic
    ang=old_english anp=angika apa=apache ara=arabic arc=aramaic arg=aragonese
    arm=armenian arn=mapudungun arp=arapaho art=artificial arw=arawak
    asm=assamese ast=asturian ath=athapascan aus=australian ava=avaric
    ave=avestan awa=awadhi aym=aymara aze=azerbaijani bad=banda bai=bamileke
    bak=bashkir bal=baluchi bam=bambara ban=balinese baq=basque bas=basa
    bat=baltic bej=beja bel=belarusian bem=bemba ben=bengali ber=berber
    bho=bhojpuri bih=bihari bik=bikol bin=bini bis=bislama bla=siksika
    bnt=bantu bos=bosnian bra=braj bre=breton btk=batak bua=buriat
    bug=buginese bul=bulgarian bur=burmese byn=blin cad=caddo
    cai=c.a._indian car=galibi_carib cat=catalan cau=caucasian ceb=cebuano
    cel=celtic cha=chamorro chb=chibcha che=chechen chg=chagatai chi=chinese
    chk=chuukese chm=mari chn=chinook cho=choctaw chp=chipewyan chr=cherokee
    chu=church_slavic chv=chuvash chy=cheyenne cmc=chamic cop=coptic
    cor=cornish cos=corsican cpe=creoles_english cpf=creoles_french
    cpp=creoles_portuguese cre=cree crh=crimean_tatar crp=creoles csb=kashubian
    cus=cushitic cze=czech dak=dakota dan=danish dar=dargwa day=land_dayak
    del=delaware den=slave dgr=dogrib din=dinka div=divehi doi=dogri
    dra=dravidian dsb=lower_sorbian dua=duala dum=middle_dutch dut=dutch
    dyu=dyula dzo=dzongkha efi=efik egy=egyptian eka=ekajuk elx=elamite
    eng=english enm=middle_english epo=esperanto est=estonian ewe=ewe
    ewo=ewondo fan=fang fao=faroese fat=fanti fij=fijian fil=filipino
    fin=finnish fiu=finno-ugrian fon=fon fre=french frm=middle_french
    fro=old_french frr=northern_frisian frs=eastern_frisian fry=western_frisian
    ful=fulah fur=friulian gaa=ga gay=gayo gba=gbaya gem=germanic geo=georgian
    ger=german gez=geez gil=gilbertese gla=gaelic gle=irish glg=galician
    glv=manx gmh=middle_german goh=old_german gon=gondi gor=gorontalo
    got=gothic grb=grebo grc=ancient_greek gre=modern_greek grn=guarani
    gsw=swiss_german guj=gujarati gwi=gwich'in hai=haida hat=haitian hau=hausa
    haw=hawaiian heb=hebrew her=herero hil=hiligaynon him=himachali hin=hindi
    hit=hittite hmn=hmong hmo=hiri_motu hsb=upper_sorbian hun=hungarian
    hup=hupa iba=iban ibo=igbo ice=icelandic ido=ido iii=sichuan_yi ijo=ijo
    iku=inuktitut ile=interlingue ilo=iloko ina=interlingua inc=indic
    ind=indonesian ine=indo-european inh=ingush ipk=inupiaq ira=iranian
    iro=iroquoian ita=italian jav=javanese jbo=lojban jpn=japanese
    jpr=judeo-persian jrb=judeo-arabic kaa=kara-kalpak kab=kabyle kac=kachin
    kal=kalaallisut kam=kamba kan=kannada kar=karen kas=kashmiri kau=kanuri
    kaw=kawi kaz=kazakh kbd=kabardian kha=khasi khi=khoisan khm=central_khmer
    kho=khotanese kik=kikuyu kin=kinyarwanda kir=kirghiz kmb=kimbundu
    kok=konkani kom=komi kon=kongo kor=korean kos=kosraean kpe=kpelle
    krc=karachay-balkar krl=karelian kro=kru kru=kurukh kua=kuanyama kum=kumyk
    kur=kurdish kut=kutenai lad=ladino lah=lahnda lam=lamba lao=lao lat=latin
    lav=latvian lez=lezghian lim=limburgan lin=lingala lit=lithuanian lol=mongo
    loz=lozi ltz=luxembourgish lua=luba-lulua lub=luba-katanga lug=ganda
    lui=luiseno lun=lunda luo=luo lus=lushai mac=macedonian mad=madurese
    mag=magahi mah=marshallese mai=maithili mak=makasar mal=malayalam
    man=mandingo mao=maori map=austronesian mar=marathi mas=masai may=malay
    mdf=moksha mdr=mandar men=mende mga=middle_irish mic=mi'kmaq
    min=minangkabau mis=uncoded mkh=mon-khmer mlg=malagasy mlt=maltese
    mnc=manchu mni=manipuri mno=manobo moh=mohawk mol=moldavian mon=mongolian
    mos=mossi mul=multiple mun=munda mus=creek mwl=mirandese mwr=marwari
    myn=mayan myv=erzya nah=nahuatl nai=n.a._indian nap=neapolitan nau=nauru
    nav=navajo nbl=south_ndebele nde=north_ndebele ndo=ndonga nds=lowgerman
    nep=nepali new=nepal_bhasa nia=nias nic=niger-kordofanian niu=niuean
    nno=norwegian nob=bokmal nog=nogai non=oldnorse nor=norwegian nqo=n'ko
    nso=pedi nub=nubian nwc=classical_newari nya=chichewa nym=nyamwezi
    nyn=nyankole nyo=nyoro nzi=nzima oci=occitan oji=ojibwa ori=oriya orm=oromo
    osa=osage oss=ossetian ota=ottoman oto=otomian paa=papuan pag=pangasinan
    pal=pahlavi pam=pampanga pan=panjabi pap=papiamento pau=palauan
    peo=oldpersian per=persian phi=philippine phn=phoenician pli=pali
    pol=polish pon=pohnpeian por=portuguese pra=prakrit pro=oldprovencal
    pus=pushto que=quechua raj=rajasthani rap=rapanui rar=rarotongan
    roa=romance roh=romansh rom=romany rum=romanian run=rundi rup=aromanian
    rus=russian sad=sandawe sag=sango sah=yakut sai=s.a._indian sal=salishan
    sam=samaritan_aramaic san=sanskrit sas=sasak sat=santali scc=serbian
    scn=sicilian sco=scots scr=croatian sel=selkup sem=semitic sga=oldirish
    sgn=sign shn=shan sid=sidamo sin=sinhala sio=siouan sit=sino-tibetan
    sla=slavic slo=slovak slv=slovenian sma=southern_sami sme=northern_sami
    smi=sami_languages smj=lule_sami smn=inari_sami smo=samoan sms=skolt_sami
    sna=shona snd=sindhi snk=soninke sog=sogdian som=somali son=songhai
    sot=southern_sotho spa=spanish srd=sardinian srn=sranan_tongo srr=serer
    ssa=nilo-saharan ssw=swati suk=sukuma sun=sundanese sus=susu sux=sumerian
    swa=swahili swe=swedish sve=swedish syc=classical_syriac syr=syriac
    tah=tahitian tai=tai tam=tamil tat=tatar tel=telugu tem=timne ter=tereno
    tet=tetum tgk=tajik tgl=tagalog tha=thai tib=tibetan tig=tigre tir=tigrinya
    tiv=tiv tkl=tokelau tlh=klingon tli=tlingit tmh=tamashek tog=tonga
    ton=tonga tpi=tokpisin tsi=tsimshian tsn=tswana tso=tsonga tuk=turkmen
    tum=tumbuka tup=tupi tur=turkish tut=altaic tvl=tuvalu twi=twi tyv=tuvinian
    udm=udmurt uga=ugaritic uig=uighur ukr=ukrainian umb=umbundu
    und=undetermined urd=urdu uzb=uzbek vai=vai ven=venda vie=vietnamese
    vol=volapuk vot=votic wak=wakashan wal=walamo war=waray was=washo wel=welsh
    wen=sorbian wln=walloon wol=wolof xal=kalmyk xho=xhosa yao=yao yap=yapese
    yid=yiddish yor=yoruba ypk=yupik zap=zapotec zbl=blissymbols zen=zenaga
    zha=zhuang znd=zande zul=zulu zun=zuni zza=zaza
"""
LanguageMap = dict([(lang.replace('_', ' '), code) for code,lang in [item.split('=') for item in LanguageMap.split() if item]])

Windows = (os.name == 'nt')
Unix = not Windows

def set_binary(f):
    pass
if Windows:
    try:
        import msvcrt
        def set_binary(f):
            msvcrt.setmode(f.fileno(), os.O_BINARY)
    except ImportError:
        pass
    try:
        import ctypes
        SM = ctypes.windll.user32.GetSystemMetrics
        MaxPreviewSize = (SM(16) - 2 * SM(32), SM(17) - 2 * SM(33))
        # (CXFULLSCREEN - 2 * CXFRAME, CYFULLSCREEN - 2 * CYFRAME)
    except ImportError:
        pass

def uw(for_unix, for_windows):
    if Unix: return for_unix
    if Windows: return for_windows


AnswerCache = {}

def store_answer(key=None, value=None):
    global AnswerCache
    if key:
        if value is None:
            value = ""
        AnswerCache[key] = str(value)
    try:
        f = open(AnswerCacheFile, "w")
        for key, value in AnswerCache.iteritems():
            print >>f, key, '=', value
        f.close()
    except IOError:
        pass

def my_input(prompt="", default=None, cache_key=None):
    global AnswerCache
    if cache_key and AnswerCache.get(cache_key):
        default = str(AnswerCache[cache_key])
    if default:
        prompt += " [default: %s]" % default
    try:
        res = raw_input(prompt.strip() + " => ")
    except (KeyboardInterrupt, IOError, EOFError):
        print "Aborted."
        sys.exit(0)
    if not(res) and default:
        res = default
    if cache_key:
        store_answer(cache_key, res)
    return res


def find_binary(name, override=None, required=True):
    if override and os.path.isfile(override):
        print "Using %s: %s" % (name, override)
        return override
    for path in os.getenv("PATH").split(os.path.pathsep):
        fullpath = os.path.join(path, name)
        if Unix:
            if os.path.isfile(fullpath):
                print "Using %s: %s" % (name, fullpath)
                return fullpath
        if os.path.isfile(fullpath + ".exe"):
            print "Using %s: %s.exe" % (name, fullpath)
            return fullpath + ".exe"
        if (Windows and os.path.isfile(fullpath + ".bat")) \
        or (Unix and os.path.isfile(fullpath + ".sh")):
            call = None
            try:
                f = open(fullpath + uw(".sh", ".bat"), "r")
                for line in f:
                    line = line.strip()
                    if not line: continue
                    if Unix and line.startswith('#'): continue
                    if Windows and (line[0] in "':"): continue
                    if Windows and line.startswith('@'): line = line[1:]
                    if Windows and line.lower().startswith("rem"): continue
                    if line.lower().startswith("echo") and not('>' in line): continue
                    sep = line[0]
                    if sep in '"\'':
                        cmd, args = line[1:].split(sep, 1)
                        args = filter(None, args.split())
                    else:
                        args = filter(None, line.split())
                        cmd = args[0]
                        args = args[1:]
                    if not([None for arg in args if not(arg.startswith(uw('$', '%')))]):
                        if call:
                            call = None
                            break  # ambiguous call
                        else:
                            call = cmd
                f.close()
            except IOError:
                pass
            if os.path.isfile(call):
                print "Using %s: %s" % (name, call)
                return call
            if Windows and os.path.isfile(call + ".exe"):
                print "Using %s: %s.exe" % (name, call)
                return call + ".exe"
    if required:
        print "Error:", name, "binary not found"
        sys.exit(1)


def fmt_time(t):
    t = int(t + 0.9)
    return "%d:%02d:%02d" % (t / 3600, (t / 60) % 60, t % 60)


def delete_file(filename, wait=0):
    while wait >= 0:
        try:
            os.unlink(filename)
            return
        except OSError:
            pass
        if wait <= 0:
            return
        time.sleep(1)
        wait -= 1


def arglist(cmdline):
    def escape(arg):
        if type(arg) != str:
            return str(arg)
        if ' ' in arg:
            return '"%s"' % arg
        return arg
    return ' '.join(map(escape, cmdline))


def cmd_err(cmdline, e=None):
    print "Error: failed to execute"
    print "   ", arglist(cmdline)
    if e:
        print "Error message is:", e
    sys.exit(1)


def get_output(cmdline):
    try:
        data = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()[0]
    except (IOError, OSError), e:
        cmd_err(cmdline, e)
    return [line.rstrip().lstrip('\x08') for line in data.split('\n')]


def black_bar_check(stream):
    # clean up
    if not stream:
        try:
            os.unlink("black_bar_check.pgm")
        except OSError:
            pass
        return

    # run decoder
    try:
        decoder = subprocess.Popen([Path_ffmpeg, "-y", "-loglevel", "fatal", "-i", "-", "-an", "-sn", "-vf", "select='gte(n,100)'", "-vframes", "1", "black_bar_check.pgm"], stdin=subprocess.PIPE)
    except (IOError, OSError):
        cmd_err(cmdline, e)
    while True:
        try:
            packet = stream.read(192)
            decoder.stdin.write(packet[4:])
        except IOError:
            break
    try:
        decoder.stdin.close()
    except IOError:
        pass
    decoder.wait()

    # read PGM file
    f = open("black_bar_check.pgm", "rb")
    x = f.read(1)
    assert x == 'P'
    def readnum(f):
        n = 0
        c = ord(f.read(1))
        while c in (13, 10, 32):
            c = ord(f.read(1))
        while (c >= 48) and (c < 58):
            n = 10 * n + c - 48
            c = ord(f.read(1))
        assert c in (13, 10, 32)
        return n
    x = readnum(f)
    assert x == 5
    width = readnum(f)
    height = readnum(f)
    x = readnum(f)
    assert x == 255
    lines = [max(map(ord, f.read(width))) for y in xrange(height)]
    f.close()

    # evaluate bars
    def get_bar(lines):
        if lines[0] >= 32:
            return (0, lines[0])  # no bar at all
        for y in xrange(1, len(lines)):
            confidence = lines[y] - (sum(lines[:y]) / y)
            if confidence > 32:
                return (y, confidence)
        return (0, 0)  # no idea
    upper, c1 = get_bar(lines)
    lines.reverse()
    lower, c2 = get_bar(lines)
    return (width, height, upper, lower, min(c1, c2))


def run_encoder(cmdline, outfile, width, height):
    xcrop = (max(0, (width - MaxPreviewSize[0]) / 2) + 1) & 0x7FFFFFFE
    ycrop = (max(0, (height - MaxPreviewSize[1]) / 2) + 1) & 0x7FFFFFFE
    player_cmd = [Path_mplayer, "-really-quiet", "-benchmark", "-demuxer", "h264es"]
    if xcrop or ycrop:
        player_cmd += ["-vf", "crop=%d:%d" % (width - 2 * xcrop, height - 2 * ycrop)]
    player_cmd += ["-"]
    class PreviewThread(threading.Thread):
        def __init__(self):
            threading.Thread.__init__(self)
            self.stopping = False
        def run(self):
            if self.stopping: return
            try:
                player = subprocess.Popen(player_cmd, stdin=subprocess.PIPE)
            except (IOError, OSError), e:
                print >>sys.stderr, "Warning: failed to run preview -", e
                return
            set_binary(player.stdin)
            pos = 0
            while not self.stopping:
                while not self.stopping:
                    try:
                        f = open(outfile, "rb")
                        break
                    except IOError:
                        time.sleep(1)
                if self.stopping: break
                f.seek(0, 2)
                size = f.tell()
                if size <= pos:
                    f.close()
                    time.sleep(1)
                    if self.stopping: break
                    continue
                f.seek(pos)
                while not self.stopping:
                    block = f.read(4096)
                    if not block: break
                    try:
                        player.stdin.write(block)
                    except IOError:
                        self.stopping = True
                pos = f.tell()
                f.close()
                if self.stopping: break
                continue
            player.stdin.close()
            player.wait()
        def stop(self):
            self.stopping = True
    t0 = time.time()
    print
    print "Running", arglist(cmdline)
    sys.stdout.flush()
    preview = PreviewThread()
    try:
        encoder = subprocess.Popen(cmdline)
        preview.start()
        encoder.wait()
        preview.stop()
        preview.join()
    except (IOError, OSError), e:
        print "Error: could not run x264 -", e
        sys.exit(1)
    except KeyboardInterrupt:
        print "Aborted."
        sys.exit(0)
    print "Time taken:", fmt_time(time.time() - t0)


if __name__ == "__main__":
    parser = optparse.OptionParser(
        usage="%prog [OPTIONS...] [<BASEDIR> [<BASENAME>]]",
        description=__doc__.strip(),
        version=__version__
    )
    parser.add_option("--skip-eac3to", action='store_true',
                      help="don't execute eac3to for audio and subtitles, even if not all files are present")
    parser.add_option("--skip-x264", action='store_true',
                      help="don't execute x264 to encode video, only create a " + uw("shell script", "batch file"))
    parser.add_option("--select-playlist", action='store_true',
                      help="disable automatic stream/playlist selection")
    parser.add_option("--preview-size", action='store', metavar='<W>x<H>',
                      help="set maximum size of encoding preview window [default: %dx%d]" % MaxPreviewSize)
    parser.add_option("--black-bars", action='store', metavar='<TOP>/<BOTTOM>',
                      help="explicitly set black bar size")
    parser.add_option("--mplayer-path", action='store', metavar='PATH',
                      help="specify full path of the MPlayer executable")
    parser.add_option("--ffmpeg-path", action='store', metavar='PATH',
                      help="specify full path of the FFmpeg executable")
    parser.add_option("--eac3to-path", action='store', metavar='PATH',
                      help="specify full path of the eac3to executable")
    parser.add_option("--x264-path", action='store', metavar='PATH',
                      help="specify full path of the x264 executable")
    opts, args = parser.parse_args()
    if len(args) > 2:
        parser.error("too many arguments")
    if opts.preview_size:
        try:
            w, h = [int(s.strip()) for s in opts.preview_size.lower().split('x', 1)]
        except (ValueError, IndexError):
            parser.error("invalid preview size '%s'" % opts.preview_size)
        MaxPreviewSize = (w, h)
    Path_mplayer = find_binary("mplayer", opts.mplayer_path)
    Path_ffmpeg = find_binary("ffmpeg", opts.ffmpeg_path)
    Path_eac3to = find_binary("eac3to", opts.eac3to_path)
    Path_x264 = find_binary("x264_64", opts.x264_path, False) or find_binary("x264", opts.x264_path, not(opts.skip_x264)) or "x264"

    # load answer cache
    try:
        f = open(AnswerCacheFile, "r")
        for line in f:
            if not('=' in line):
                continue
            key, value = line.split('=', 1)
            AnswerCache[key.strip().lower()] = value.strip()
        f.close()
    except IOError:
        pass

    # get base directory
    print
    if args:
        basedir = args[0]
    else:
        basedir = my_input("Input base directory", AnswerCache.get('basedir', os.getcwd()))
    cwd = os.getcwd().split(os.path.sep)
    comp = os.path.normpath(os.path.abspath(basedir)).split(os.path.sep)
    if comp[:len(cwd)] == cwd:
        basedir = os.path.sep.join(comp[len(cwd):])
    if basedir != AnswerCache.get('basedir', "_dummy"):
        AnswerCache = { 'basedir': basedir }
        store_answer()
    print "Base directory is", basedir or '.'

    # get the stream directory
    streamdir = os.path.join(os.path.join(basedir, "BDMV"), "STREAM")
    if not os.path.isdir(streamdir):
        print "Error:", streamdir, "is not a valid directory"
        sys.exit(1)
    print "Stream directory is", streamdir

    # get project base name
    if len(args) > 1:
        basename = args[1]
        print "Output base name is", basename
        store_answer('basename', basename)
    else:
        basename = my_input("Output base name", os.path.basename(basedir or os.getcwd()).lower().replace(' ', '_'), 'basename')

    # search for the largest stream or playlist
    playlist = None
    streams = [os.path.join(streamdir, item) for item in os.listdir(streamdir) if os.path.splitext(item)[-1].lower() in (".mts", ".m2ts")]
    streams = [(os.stat(stream).st_size, stream) for stream in streams]
    streams.sort(reverse=True)
    if not streams:
        print "Error: no streams found in", streamdir
        sys.exit(1)
    if not(opts.select_playlist) and ((len(streams) == 1) or (streams[0][0] > (streams[1][0] * 2))):
        streams = streams[:1]
    else:
        # run eac3to to get the list of playlists
        output = get_output([Path_eac3to, basedir])
        # Note: this regexp ignores alternate angles, but we don't want those anyway
        re_playlist = re.compile(r'(?P<id>\d+)\)\s*\d+\.m?pls?,\s*((?P<stream>\d+\.m2?ts),\s*)?(?P<h>\d+):(?P<m>\d+):(?P<s>\d+)$', re.I)
        re_streams = re.compile(r'\s*\[([0-9+]+)\]\.m2?ts')
        # build a list of playlists
        playlists = {}
        durations = []
        plid = 0
        for line in output:
            m = re_playlist.match(line)
            if m:
                plid = int(m.group('id'))
                durations.append((int(m.group('h')) * 3600 + int(m.group('m')) * 60 + int(m.group('s')), plid))
                ss = m.group('stream')
                if ss:
                    ss = ss.lower()
                    playlists[plid] = filter(lambda s: os.path.basename(s[1]).lower() == ss, streams)
                    plid = 0
                continue
            m = re_streams.match(line)
            if m and plid:
                slist = set(map(int, m.group(1).split('+')))
                playlists[plid] = filter(lambda s: int(os.path.basename(s[1]).split('.', 1)[0]) in slist, streams)
                plid = 0
        durations.sort(reverse=True)
        if not playlists:
            print "Error: no streams or playlists found in", basedir
            sys.exit(1)
        if not(opts.select_playlist) and ((len(playlists) == 1) or (durations[0][0] > (durations[1][0] * 2))):
            playlist = durations[0][1]  # single playlist or dominant playlist
        else:
            # the user has to pick the playlist himself
            print
            print "Select playlist to use:"
            re_simple_playlist = re.compile(r'(\d+)\)\s*')
            valid = False
            for line in output:
                if not line:
                    continue
                m = re_simple_playlist.match(line)
                if m:
                    valid = (int(m.group(1)) in playlists)
                if valid:
                    print line
            while True:
                playlist = my_input("Select playlist", cache_key='playlist')
                try:
                    playlist = int(playlist)
                except ValueError:
                    playlist = 0
                if playlist in playlists:
                    break
        streams = playlists[playlist]
    if len(streams) == 1:
        stream = streams[0][1]
        print "Using input stream", stream
        eac3to = [Path_eac3to, stream]
        playlist = None
    else:
        stream = None
    if playlist:
        print "Using playlist #%s" % playlist
        eac3to = [Path_eac3to, basedir, "%d)" % playlist]
    assert stream or playlist

    # get track information
    print
    print "Determining stream information ..."
    output = filter(None, get_output(eac3to))
    output[0] = output[0].lstrip("-\x08 ")
    print output[0]
    header = re.match(r'M2TS,.*?,\s*(\d+):(\d+):(\d+),\s*(\d+(\.\d+)?)([ip])(\s*/1\.001)?', output[0], re.I)
    if not header:
        print "Error: analysis failed"
        sys.exit(1)
    h, m, s, fps, frac, ip, ntsc = header.groups()
    if not frac:
        fps = int(fps)
    else:
        ffps = float(fps)
        fps = int(ffps + 0.5)
        if abs(ffps - fps) > 0.1:
            fps = 0
        elif abs(ffps - fps) > abs(ffps * 1.0001 - fps):
            ntsc = True
    if ip.lower() == 'i':
        fps /= 2
    if not(fps in (24, 25, 30)):
        print "WARNING: unusual frame rate detected, assuming 24p/1.001"
        fps = 24
        ntsc = True
    duration = int(h) * 3600 + int(m) * 60 + int(s)
    approx_frames = (duration + 1) * fps
    print "Approximate number of frames:", approx_frames
    audio_tracks = {}
    sub_tracks = {}
    video_track = None
    chapter_track = None
    re_track_generic = re.compile(r'(\d+):\s*(.*)')
    re_video_res = re.compile(r'(\d+)[ip]|(\d+)x(\d+)')
    print "Available tracks:"
    for raw_line in output[1:]:
        m = re_track_generic.match(raw_line)
        if not m:
            continue
        trackno = int(m.group(1))
        line = map(str.strip, m.group(2).lower().split(','))
        mark = ' '
        if line[0].startswith("chapters") and not(chapter_track):
            chapter_track = trackno
            mark = '>'
        elif (line[0].startswith("h264") or line[0].startswith("vc-1") or line[0].startswith("mpeg2")) and not(video_track):
            video_track = trackno
            m = re_video_res.match(line[1])
            if m.group(1):
                height = int(m.group(1))
                width = { 480:720, 576:720, 720:1280, 1080:1920 }.get(height, height * 16 / 9)
            elif m.group(2) and m.group(3):
                width = int(m.group(2))
                height = int(m.group(3))
            mark = '>'
        elif line[0].startswith("subtitle"):
            sub_tracks[trackno] = line[-1]
            mark = '>'
        elif line[0] in AudioFormats:
            audio_tracks[trackno] = line[1]
            mark = '>'
        print mark, raw_line
    if not(video_track) or not(width) or not(height):
        print "Error: no usable video track found"
        sys.exit(1)
    if not audio_tracks:
        print "Error: no usable audio track found"
        sys.exit(1)
    print "Using track", video_track, "(%dx%d)" % (width, height), "for video."
    if chapter_track:
        print "Using track", chapter_track, "for chapters."
    print "Found", len(audio_tracks), "usable audio track(s) and", len(sub_tracks), "usable subtitle track(s)."

    # pre-select tracks
    preselect = {}
    for trackno in sorted(audio_tracks.iterkeys()):
        lang = audio_tracks[trackno]
        if (lang in AutoselectLanguages) and not(lang in preselect):
            preselect[lang] = trackno
    preselect = [(trackno, ":%d" % DefaultAudioBitrate) for lang, trackno in preselect.iteritems()]
    for trackno, lang in sub_tracks.iteritems():
        if lang in AutoselectLanguages:
            preselect.append((trackno, ""))
    preselect.sort()
    preselect = ' '.join(["%d%s" % item for item in preselect])

    # select tracks
    print
    print "Select which audio and subtitle tracks shall be included in the rip"
    print "(space-separated list of track numbers; for audio tracks, append"
    print "':bitrate' to set a bitrate, e.g. '%d:%d')" % (audio_tracks.keys()[0], DefaultAudioBitrate)
    tracksel_str = my_input("Track selection", preselect, 'tracksel')
    tracksel = {}
    audio_track_order = []
    for sel in filter(None, tracksel_str.split()):
        try:
            if ':' in sel:
                trackno, bitrate = map(int, sel.split(':', 1))
            else:
                trackno = int(sel)
                bitrate = DefaultAudioBitrate
        except ValueError:
            print "Warning: invalid track selector '%s'" % sel
            continue
        if trackno in audio_tracks:
            if not(bitrate in ValidAudioBitrates):
                print "Warning: invalid audio bitrate of", bitrate, "kbps specified for in track", trackno
                bitrate = DefaultAudioBitrate
            tracksel[trackno] = bitrate
            audio_track_order.append(trackno)
        elif trackno in sub_tracks:
            tracksel[trackno] = 1
        else:
            print "Warning: invalid track number", trackno, "selected"
    if playlist:
        tracksel[video_track] = 0
    if chapter_track:
        tracksel[chapter_track] = 0
    audio_kbps = sum(tracksel.itervalues())
    tracksel = sorted(tracksel.iteritems())
    print "Total audio and subtitle bitrate: approx.", audio_kbps, "kbps."

    # get video bitrate and quality
    print
    print "Set video quality:"
    print "  1-52       = use CRF mode"
    print "  1000-99999 = 2-pass VBR with specified number of kilobits per second"
    print "  1M-9999M   = 2-pass VBR with specified target size in megabytes"
    quality = my_input("Video quality setting", "8500M", 'quality').strip().upper()
    if quality.endswith('G'):
        target_size_mode = True
        try:
            quality = int(1000 * float(quality[:-1].strip()))
        except ValueError:
            pass
    elif quality.endswith('M'):
        target_size_mode = True
        quality = quality[:-1].strip()
    else:
        target_size_mode = False
    try:
        quality = int(quality)
    except ValueError:
        print "Error: invalid quality specification '%s'" % quality
        sys.exit(1)
    crf = None
    video_bitrate = None
    if target_size_mode:
        video_bitrate = (quality * 8000) / duration - audio_kbps
        video_bitrate = (video_bitrate / VideoBitrateGranularity) * VideoBitrateGranularity
    elif quality < 100:
        crf = quality
    else:
        video_bitrate = quality
    if crf:
        print "Using CRF of %d." % crf
    else:
        print "Target video bitrate for 2-pass VBR encoding set to", video_bitrate, "kbps."
    print
    x264opts = my_input("x264 encoding quality options", "--preset veryslow", 'x264opts').split()

    # collect tracks and build eac3to command line
    print
    print "The following files will be generated:"
    eac3to_results = []
    for trackno, bitrate in tracksel:
        eac3to += ["%d:" % trackno]
        if trackno == video_track:
            filename = "%s-rawvideo.h264" % basename
            eac3to += [filename]
            eac3to_results.append(filename)
            print '>', filename, "(combined raw video stream)"
        elif trackno == chapter_track:
            filename = "%s-chapters.txt" % basename
            eac3to += [filename]
            eac3to_results.append(filename)
            print '>', filename, "(chapters in text format)"
        elif trackno in audio_tracks:
            filename = "%s-%s-%d.ac3" % (basename, audio_tracks[trackno], trackno)
            eac3to += [filename, "-%d" % bitrate, "-no2ndpass"]
            eac3to_results.append(filename)
            print '>', filename, "(audio, %d kbps)" % bitrate
        elif trackno in sub_tracks:
            filename = "%s-%s-%d.sup" % (basename, sub_tracks[trackno], trackno)
            eac3to += [filename]
            eac3to_results.append(filename)
            print '>', filename, "(subtitles)"
        else:
            assert False
    if chapter_track:
        print '>', "%s-chapters.xml" % basename, "(chapters in Matroska XML format)"
    print '>', "%s.mmg" % basename, "(mkvmerge GUI configuration file)"
    print '>', "%s-encode.%s" % (basename, uw("sh", "bat")), "(video encoding script)"
    print '>', "%s.264" % basename, "(encoded video)"
    if not(opts.skip_eac3to) and not(filter(lambda f: not(os.path.isfile(f)), eac3to_results)):
        print
        print "All files generated by eac3to seem to be already present."
        yesno = my_input("Run eac3to again (Y/N)?", "y")
        run_eac3to = (yesno.upper() != 'N')
    else:
        run_eac3to = not(opts.skip_eac3to)

    global_t0 = time.time()

    # determine black bar size
    print
    black_bars = opts.black_bars or AnswerCache.get('black_bars')
    if black_bars:
        try:
            upper, lower = [int(s.strip()) for s in black_bars.split('/', 1)]
        except (IndexError, ValueError):
            print "Warning: invalid black bar size '%s'" % black_bars
            black_bars = None
    if not black_bars:
        print "Determining black bars, please wait ..."
        f = open(streams[0][1], "rb")
        f.seek(0, 2)
        packets = f.tell() / 192
        results = {}
        try:
            for attempt in xrange(10):
                ssize, sfile = random.choice(streams)
                f = open(sfile, "rb")
                r = random.random() * 0.9
                print "Trying at %s, %d%%:" % (os.path.basename(sfile), int(r * 100)),
                f.seek(int((ssize / 192) * r) * 192)
                c_width, c_height, upper, lower, confidence = black_bar_check(f)
                if (c_width == width) and (c_height == height):
                    print "bar size %d/%d, confidence %d" % (upper, lower, confidence)
                    key = (upper, lower)
                    results[key] = results.get(key, 0) + confidence
                    if results[key] >= 256:
                        break
                else:
                    print "size mismatch (%dx%d)" % (c_width, c_height)
        except KeyboardInterrupt:
            print "Aborted."
            black_bar_check(None)
            sys.exit(1)
        black_bar_check(None)
        results = [(confidence, key) for key, confidence in results.iteritems()]
        results.sort(reverse=True)
        upper, lower = results[0][1]
        orig_out_height = height - upper - lower
        print "Black bar size seems to be %d/%d, resulting in a video size of %dx%d." % (upper, lower, width, orig_out_height)
        out_height = (orig_out_height & 0x7FFFFFF0) + { 0:0, 2:0, 4:0, 6:0, 8:8, 10:8, 12:12, 14:14 }[orig_out_height & 14]
        upper = (upper + (orig_out_height - out_height) / 2 + 1) & 0x7FFFFFFE
        lower = height - out_height - upper
    else:
        out_height = height - upper - lower
    store_answer('black_bars', "%d/%d" % (upper, lower))
    print "Using effective bar size %d/%d, resulting in a video size of %dx%d." % (upper, lower, width, out_height)

    # run eac3to
    if run_eac3to:
        print
        print "Running:", arglist(eac3to)
        sys.stdout.flush()
        try:
            subprocess.Popen(eac3to).wait()
        except (IOError, OSError), e:
            print "Error: could not run eac3to -", e
            sys.exit(1)
        except KeyboardInterrupt:
            print "Aborted."
            sys.exit(0)

    # convert chapters
    print
    if chapter_track:
        print "Converting chapter information to XML."
        fin = open("%s-chapters.txt" % basename, "r")
        fout = open("%s-chapters.xml" % basename, "w")
        print >>fout, '<?xml version="1.0" encoding="utf-8"?>'
        print >>fout, "<Chapters>"
        print >>fout, "  <EditionEntry>"
        print >>fout, "    <EditionFlagHidden>0</EditionFlagHidden>"
        print >>fout, "    <EditionFlagDefault>0</EditionFlagDefault>"
        for line in fin:
            line = line.strip().upper()
            if not(line.startswith("CHAPTER")) or not('=' in line):
                continue
            key, ctime = line.split('=', 1)
            if key.strip().endswith("NAME"):
                continue
            print >>fout, "    <ChapterAtom>"
            print >>fout, "      <ChapterDisplay>"
            print >>fout, "        <ChapterString></ChapterString>"
            print >>fout, "        <ChapterLanguage>und</ChapterLanguage>"
            print >>fout, "      </ChapterDisplay>"
            print >>fout, "      <ChapterTimeStart>%s</ChapterTimeStart>" % ctime.strip()
            print >>fout, "      <ChapterFlagHidden>0</ChapterFlagHidden>"
            print >>fout, "      <ChapterFlagEnabled>1</ChapterFlagEnabled>"
            print >>fout, "    </ChapterAtom>"
        print >>fout, "  </EditionEntry>"
        print >>fout, "</Chapters>"
        fin.close()
        fout.close()

    # create mkvmerge GUI script
    print "Generating mkvmerge GUI settings file."
    f = open("%s.mmg" % basename, "w")
    def mmg_path(x):
        return os.path.join(os.getcwd(), x).replace(' ', '\x09').replace('\\', '\\\\').replace('\x09', '\\ ')
    print >>f, "[mkvmergeGUI]"
    print >>f, "file_version=3"
    print >>f, "output_file_name=" + mmg_path("%s.mkv" % basename)
    print >>f, "[input]"
    print >>f, "number_of_files=%d" % (len(audio_track_order) + 1)
    print >>f, "[input/file\\ 0]"
    print >>f, "file_name=" + mmg_path("%s.264" % basename)
    print >>f, "container=4"
    print >>f, "number_of_tracks=1"
    print >>f, "[input/file\\ 0/track\\ 0]"
    print >>f, "type=v"
    print >>f, "id=0"
    print >>f, "enabled=1"
    print >>f, "content_type=MPEG-4 part 10 ES"
    print >>f, "default_track_2=1"
    print >>f, "forced_track=0"
    print >>f, "language=und"
    print >>f, "cues=default"
    print >>f, "aspect_ratio="
    print >>f, "cropping="
    print >>f, "display_width="
    print >>f, "display_height="
    print >>f, "fourcc="
    if ntsc:
        print >>f, "fps=%d000/1001p" % fps
    else:
        print >>f, "fps=%dp" % fps
    print >>f, "nalu_size_length=0"
    print >>f, "stereo_mode=0"
    print >>f, "compression=none"
    print >>f, "packetizer=mpeg4_p10_es_video"
    n = 0
    for trackno in audio_track_order:
        n += 1
        lang = audio_tracks[trackno]
        print >>f, "[input/file\\ %d]" % n
        print >>f, "file_name=" + mmg_path("%s-%s-%d.ac3" % (basename, lang, trackno))
        print >>f, "container=2"
        print >>f, "number_of_tracks=1"
        print >>f, "[input/file\\ %d/track\\ 0]" % n
        print >>f, "type=a"
        print >>f, "id=0"
        print >>f, "enabled=1"
        print >>f, "content_type=AC3"
        print >>f, "default_track_2=%d" % {1:1}.get(n, 2)
        print >>f, "forced_track=0"
        print >>f, "language=%s" % LanguageMap.get(lang, lang)
        print >>f, "cues=default"
        print >>f, "delay="
        print >>f, "stretch="
        print >>f, "compression=none"
    print >>f, "[global]"
    print >>f, "segment_title="
    print >>f, "enable_splitting=0"
    print >>f, "link=0"
    print >>f, "segment_uid="
    print >>f, "previous_segment_uid="
    print >>f, "next_segment_uid="
    if chapter_track:
        print >>f, "chapters=" + mmg_path("%s-chapters.xml" % basename)
    else:
        print >>f, "chapters="
    print >>f, "chapter_language="
    print >>f, "chapter_charset="
    print >>f, "cue_name_format="
    print >>f, "global_tags="
    print >>f, "segmentinfo="
    print >>f, "webm_mode=0"

    f.close()

    print "Deleting old x264 2-pass log files, if present."
    delete_file("x264_2pass.log")
    delete_file("x264_2pass.log.mbtree")
    delete_file("x264_2pass.log.temp")
    delete_file("x264_2pass.log.mbtree.temp")

    # run x264 / generate batch file
    run_x264 = not(opts.skip_x264)
    x264 = [Path_x264]
    for opt, val in X264BaseOptions:
        if opt in x264opts: continue
        if val and val.startswith('-') and (val in x264opts): continue
        if val and not(val.startswith('-')):
            x264 += [opt, val]
        else:
            x264 += [opt]
    x264 += x264opts
    if crf:
        x264 += ["--crf", str(crf)]
    if video_bitrate:
        x264 += ["--bitrate", str(video_bitrate)]
    if not("--fps" in x264):
        if ntsc:
            x264 += ["--fps", "%d000/1001" % fps]
        else:
            x264 += ["--fps", str(fps)]
    if upper or lower:
        filter_spec = "crop:0,%d,0,%d" % (upper, lower)
        try:
            vf_index = x264.index("--vf") + 1
            x264[vf_index] = filter_spec + '/' + x264[vf_index]
        except ValueError:
            x264 += ["--vf", filter_spec]
    if "--frames" in x264:
        approx_frames = []
    else:
        approx_frames = ["--frames", str(approx_frames)]
    outfile = "%s.264" % basename
    x264 += ["-o", outfile]
    if playlist:
        stream = "%s-rawvideo.h264" % basename
    delete_file(outfile)
    f = open("%s-encode.%s" % (basename, uw('sh', 'bat')), "w")
    if Unix:
        print >>f, "#!/bin/sh"
    if video_bitrate:
        pass1opts = approx_frames + ["--pass", "1", stream]
        pass2opts = ["--pass", "2", stream]
        if Unix:
            print >>f, "x264='%s'" % arglist(x264)
            print >>f, "$x264", arglist(pass1opts)
            print >>f, "$x264", arglist(pass2opts)
        else:
            print >>f, "set x264=%s" % arglist(x264)
            print >>f, "%x264%", arglist(pass1opts)
            print >>f, "%x264%", arglist(pass2opts)
        f.close()
        if run_x264:
            run_encoder(x264 + pass1opts, outfile, width, out_height)
            print
            print "Deleting pass 1 intermediate file."
            delete_file(outfile, 60)
            run_encoder(x264 + pass2opts, outfile, width, out_height)
    else:
        print >>f, arglist(x264 + approx_frames + [stream])
        f.close()
        if run_x264:
            run_encoder(x264 + approx_frames + [stream], outfile, width, out_height)
    print
    print "Done."
    if run_x264 or run_eac3to:
        print "All tasks finished on %s." % time.strftime("%Y-%m-%d %H:%M:%S")
        print "Total time taken: %s." % fmt_time(time.time() - global_t0)
