Exemplo n.º 1
0
 def remux_callback(path):
     cwd = platform.getcwd()
     os.chdir(src_directory)
     platform.execute('eac3to {}) {}'.format(
         movie_id.rstrip(')'),
         cmd.quote(os.path.join(path, 'video.mkv'))),
                      capture_output=False)
     os.chdir(cwd)
     return True
Exemplo n.º 2
0
 def handle_response(text):
     mpv_opts = {
         TrackType.AUD: u'--audio-file {}',
         TrackType.SUB: u'--sub-file {}'
     }
     if text == 'p':
         # TODO add chosen audio tracks when choosing subtitles
         # TODO somehow consider track delays
         command = [
             u'mpv {}'.format(
                 cmd.quote(
                     list(movie.tracks(TrackType.VID))[0].source_file()))
         ]
         for track in sorted(candidates,
                             key=lambda t: movie.track_index_in_type(t)):
             if track.is_single():
                 command.append(mpv_opts[track.type()].format(
                     cmd.quote(track.source_file())))
         platform.execute(u'{} >nul 2>&1'.format(u' '.join(command)),
                          capture_output=False)
     return text
Exemplo n.º 3
0
 def remux_callback(path):
     platform.execute(makemkv_cmd.format(cmd.quote(path)),
                      capture_output=False)
     return True
Exemplo n.º 4
0
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('src',
                        type=cli.argparse_path,
                        help='path to source folder')
    parser.add_argument('dst',
                        type=cli.argparse_path,
                        help='path to destination folder')
    args = parser.parse_args()

    dvds = set()
    blurays = set()
    for root, folders, files in os.walk(args.src):
        for entry in folders:
            if entry == 'BDMV':
                blurays.add(root)
        for entry in files:
            _, ext = os.path.splitext(entry.lower())
            if ext == '.vob':
                dvds.add(root)

    eac3to_ids = {}
    for src_directory in sorted(blurays):
        platform.print_string(u'=== Processing "{}" ==='.format(src_directory))
        movie_id = ''
        while not movie_id.isdigit() and movie_id != 'no':
            movie_id = raw_input('Enter target movie id: ')
            if movie_id.isdigit():
                eac3to_ids[src_directory] = movie_id

    for src_directory, movie_id in sorted(eac3to_ids.iteritems()):

        def remux_callback(path):
            cwd = platform.getcwd()
            os.chdir(src_directory)
            platform.execute('eac3to {}) {}'.format(
                movie_id.rstrip(')'),
                cmd.quote(os.path.join(path, 'video.mkv'))),
                             capture_output=False)
            os.chdir(cwd)
            return True

        dst_directory = os.path.join(
            args.dst, u'remux_{}'.format(os.path.basename(src_directory)))
        remux_movie(src_directory, dst_directory, remux_callback)

    for src_directory in dvds:
        makemkv_opts = [
            u'--noscan',
            u'--messages=-stderr',
            u'--progress=-stderr',
        ]
        makemkv_cmd = u'makemkvcon {opts} mkv file:{src} all {{}}'.format(
            opts=' '.join(makemkv_opts), src=cmd.quote(src_directory))

        def remux_callback(path):
            platform.execute(makemkv_cmd.format(cmd.quote(path)),
                             capture_output=False)
            return True

        dst_name = os.path.basename(src_directory)
        if dst_name == 'VIDEO_TS':
            dst_name = os.path.basename(os.path.dirname(src_directory))
        dst_directory = os.path.join(args.dst, u'remux_{}'.format(dst_name))
        remux_movie(src_directory, dst_directory, remux_callback)

        parent_directory = os.path.dirname(src_directory)
        if os.listdir(parent_directory) == ['VIDEO_TS']:
            shutil.rmtree(parent_directory)

    return 0
Exemplo n.º 5
0
def main():
    parser = argparse.ArgumentParser()
    # TODO support sources as wildcards
    parser.add_argument('sources',
                        type=cli.argparse_path,
                        nargs='+',
                        help='paths to source directories/files')
    parser.add_argument('dst',
                        type=cli.argparse_path,
                        help='path to destination directory')

    # TODO add argument groups
    parser.add_argument('-vk',
                        default=False,
                        action='store_true',
                        help='keep source video')
    parser.add_argument('-vr',
                        default=False,
                        action='store_true',
                        help='recode video')
    parser.add_argument('-vc',
                        choices=VideoCodec.get_names(
                            [VideoCodec.H264, VideoCodec.H265]),
                        default=VideoCodec.H264.name,
                        help='set video encoding codec')
    add_enum_argument(parser, '-vt', InputTune, None,
                      'set video encoding tune')
    add_enum_argument(parser, '-vq', InputQuality, None,
                      'set video encoding quality')
    parser.add_argument('-va',
                        default=None,
                        choices=['16:9'],
                        help='set video display aspect ratio')
    parser.add_argument('-vs',
                        default=None,
                        choices=['720p', '1080p', '1440p'],
                        help='scale video')
    parser.add_argument('-ks',
                        default=False,
                        action='store_true',
                        help='keep current colorspace')

    parser.add_argument('-cr',
                        default=False,
                        action='store_true',
                        help='crop video')
    parser.add_argument('-cf',
                        type=cli.argparse_path,
                        default=None,
                        metavar='<path>',
                        help='path to crop map file')
    parser.add_argument('-sc',
                        default=False,
                        action='store_true',
                        help='use same crop values for all files')

    parser.add_argument(
        '-al',
        nargs='*',
        type=cli.argparse_lang,
        default=[],
        metavar='lang',
        help='ordered list of audio 3-letter language codes to keep')
    parser.add_argument('-ar',
                        default=False,
                        action='store_true',
                        help='recode audio')
    parser.add_argument('-ad',
                        default='5.1',
                        choices=CHANNEL_SCHEMES.keys(),
                        help='downmix to N channels')
    parser.add_argument('-aw',
                        default=False,
                        action='store_true',
                        help='convert audio to wavfile before encoding')

    parser.add_argument(
        '-sl',
        nargs='*',
        type=cli.argparse_lang,
        default=[],
        metavar='lang',
        help='ordered list of full subtitle 3-letter language codes to keep')
    parser.add_argument(
        '-fl',
        nargs='*',
        type=cli.argparse_lang,
        default=[],
        metavar='lang',
        help='ordered list of forced subtitle  3-letter language codes to keep'
    )
    parser.add_argument('-fo',
                        default=False,
                        action='store_true',
                        help='make forced subtitles optional')

    parser.add_argument('-nf',
                        type=cli.argparse_path,
                        default=None,
                        metavar='<path>',
                        help='path to names map file')
    parser.add_argument('-tv',
                        default=None,
                        metavar='<name>',
                        help='TV series name')
    parser.add_argument('-il',
                        default=False,
                        action='store_true',
                        help='Ignore track language data')
    parser.add_argument('-xx',
                        default=False,
                        action='store_true',
                        help='Remove original files after processing')
    parser.add_argument('-ma',
                        default=False,
                        action='store_true',
                        help='Append mux file instead of overwrite')
    parser.add_argument('-ds',
                        default=False,
                        action='store_true',
                        help='Disable movie sattelites detection')
    parser.add_argument('-sd',
                        default=False,
                        action='store_true',
                        help='Securely delete files using sdelete utility')

    args = parser.parse_args()
    if args.cf and args.sc:
        parser.error(u'Use -cf or -sc, not both')
    if args.nf and args.tv:
        parser.error(u'Use -nf or -tv, not both')
    if args.vk and (args.vr or args.vt):
        parser.error(u'Use -vk or -vr/-vt, not both')
    if not args.vk and (not args.vt or not args.vq):
        parser.error(u'Set -vt and -vq')

    args.vc = VideoCodec.get_definition(args.vc)
    if args.vt:
        args.vt = InputTune.get_definition(args.vt)
    if args.vq:
        args.vq = InputQuality.get_definition(args.vq)

    def read_movie_path(path):
        path = os.path.normpath(path.strip())
        if is_media_file_path(path):
            path = os.path.splitext(path)[0]
        return path

    def read_crop_args(s):
        s = s.strip()
        if s.lower() == 'no':
            return False
        return [int(x) for x in s.split(':')]

    filenames_map = read_map_file(args.nf, read_movie_path, read_movie_path)
    raw_crops_map = read_map_file(args.cf, read_movie_path, read_crop_args)

    tvdb = None
    if args.tv:
        tvdb = tvdb_api.Tvdb()

    movies = {}
    crop_args_map = None if raw_crops_map is None else {}
    for argspath in args.sources:
        for movie_object in find_movies(argspath, args.il, not args.ds):
            cur_path = os.path.normpath(
                os.path.relpath(movie_object.main_path(), platform.getcwd()))
            if raw_crops_map is not None:
                crop_args_map[movie_object.main_path()] = raw_crops_map[
                    os.path.splitext(cur_path)[0]]
            if args.tv:
                movie_name = os.path.basename(movie_object.main_path())
                src_season = int(
                    re.match(r'.*s(\d+)', movie_name, re.IGNORECASE).group(1))
                src_episodes = [
                    int(x)
                    for x in re.findall(r'e(\d+)', movie_name, re.IGNORECASE)
                ]
                ep_numbers = []
                ep_names = set()
                for src_episode in src_episodes:
                    ep_info = tvdb[args.tv][src_season][src_episode]
                    epn_dvd = ep_info['dvdEpisodeNumber']
                    epn_air = ep_info['airedEpisodeNumber']
                    if epn_dvd is not None and epn_dvd != epn_air:
                        epn_dvd_int = int(float(epn_dvd))
                        epn_dvd_frc = float(epn_dvd) - epn_dvd_int
                        assert int(epn_air) == int(
                            round(epn_dvd_int +
                                  (epn_dvd_frc - 0.1) * 10)), 'd{} a{}'.format(
                                      epn_dvd, epn_air)
                    ep_numbers.append(int(epn_air))
                    ep_names.add(
                        re.sub(r'\(\d\)$', '', ep_info['episodename']).strip())
                assert len(ep_numbers) > 0 and len(ep_names) == 1, ep_names
                cur_path = u'{} {}.mkv'.format(
                    '-'.join(u'{:02d}'.format(epn)
                             for epn in sorted(ep_numbers)),
                    list(ep_names)[0])
            elif filenames_map is not None:
                raw_new_name_string = filenames_map[os.path.splitext(cur_path)
                                                    [0]]
                cur_path = None
                if raw_new_name_string == 'NO': continue
                elif raw_new_name_string == 'KEEP': cur_path = cur_path
                else: cur_path = raw_new_name_string
            if is_media_file_path(cur_path):
                cur_path = os.path.splitext(cur_path)[0]
            new_name = u'{}.mkv'.format(
                platform.clean_filename(os.path.basename(cur_path)))
            new_path = os.path.join(os.path.abspath(args.dst),
                                    os.path.dirname(cur_path), new_name)
            assert new_path not in movies, new_path
            movies[new_path] = movie_object

    output_track_specs = collections.OrderedDict([
        ((TrackType.VID, False), ['und']),
        ((TrackType.AUD, False), args.al),
        ((TrackType.SUB, False), args.sl),
        ((TrackType.SUB, True), args.fl),
    ])

    if not (args.ma and os.path.isfile(MUX_BODY)):
        try:
            os.remove(MUX_BODY)
        except:
            pass
        shutil.copyfile(MUX_HEAD, MUX_BODY)

    created_directories = set()
    # TODO catch some of my exceptions, report skipped file, ask for action, log skipped file
    common_crop_args = None
    for target_path, movie in sorted(movies.iteritems(),
                                     key=lambda m: m[1].main_path()):
        platform.print_string(u'=== {} ==='.format(movie.main_path()))
        output_tracks = {}
        for (track_type, _) in output_track_specs.iterkeys():
            output_tracks[track_type] = []
        used_tracks = set()
        reference_duration = movie.reference_duration() or 0
        duration_threshold = reference_duration / 100.0 * 20.0
        for (track_type,
             search_forced), lang_list in output_track_specs.iteritems():
            forced_string = 'Forced' if search_forced else 'Full'
            for target_lang in lang_list:
                candidates = {}
                for track in movie.tracks(track_type):
                    if track.qualified_id() in used_tracks: continue
                    if track.language() not in (
                            target_lang, 'und') and target_lang != 'und':
                        continue
                    if any(s in track.name().lower()
                           for s in [u'comment', u'коммент']):
                        continue
                    if track.is_forced() is not None:
                        if track.is_forced() != search_forced: continue
                        if not track.is_forced() and track.duration(
                        ) is not None:
                            if reference_duration - track.duration(
                            ) > duration_threshold:
                                continue
                    candidates[track.qualified_id()] = track
                if not candidates:
                    if search_forced and args.fo: continue
                    raise cli.Error(u'{} {} {} not found'.format(
                        forced_string, track_type, target_lang))

                chosen_track_id = None
                if len(candidates) == 1:
                    chosen_track_id = list(candidates.keys())[0]

                sorted_candidates = sorted(candidates.itervalues(),
                                           key=lambda t: t.qualified_id())
                if chosen_track_id not in candidates:
                    candidates_by_index = {}
                    for track in sorted_candidates:
                        candidates_by_index[movie.track_index_in_type(
                            track)] = track.qualified_id()
                    header = u'--- {}, {}, {} ---'.format(
                        track_type, target_lang.upper(), forced_string)
                    # TODO if tv AND if tracks ids, names and codecs same as before then choose track automatically
                    chosen_track_index = ask_to_select_tracks(
                        movie, track_type, sorted_candidates, header)
                    chosen_track_id = candidates_by_index[chosen_track_index]

                used_tracks.add(chosen_track_id)
                chosen_track = candidates[chosen_track_id]
                chosen_track.set_language(target_lang)
                chosen_track.set_forced(search_forced)
                output_tracks[track_type].append(chosen_track)

        assert len(output_tracks[TrackType.VID]) == 1
        video_track = output_tracks[TrackType.VID][0]

        def track_sort_key(t):
            lng_idx = output_track_specs[(t.type(),
                                          t.is_forced())].index(t.language())
            return lng_idx + 1000 * int(t.is_forced())

        track_sources = {}
        for track_type, track_list in output_tracks.iteritems():
            track_list.sort(key=track_sort_key)
            for track in track_list:
                track_sources[track.qualified_id()] = [
                    track.source_file(), track.id()
                ]

        result_commands = [u'echo {}'.format(cmd.quote(movie.main_path()))]
        mux_temporary_files = []

        target_directory = os.path.dirname(target_path)
        if not os.path.isdir(target_directory
                             ) and target_directory not in created_directories:
            result_commands.extend(cmd.gen_create_dir(target_directory))
            created_directories.add(target_directory)

        def make_single_track_file(track,
                                   stream_id,
                                   file_ext=None,
                                   ffmpeg_opts=None,
                                   prefer_ffmpeg=True):
            if file_ext is None:
                file_ext = track.get_single_track_file_extension()
            if ffmpeg_opts is None:
                ffmpeg_opts = ['-c:{} copy'.format(stream_id)]
            if file_ext == platform.file_ext(
                    track.source_file()) and track.is_single():
                return track.source_file(), False
            tmp_path = platform.make_temporary_file(file_ext)
            if not prefer_ffmpeg and platform.file_ext(
                    track.source_file()) == '.mkv':
                command = cmd.gen_mkvtoolnix_extract_track(
                    track.source_file(), tmp_path, track.id())
            else:
                command = cmd.gen_ffmpeg_extract_track(track.source_file(),
                                                       tmp_path, track.id(),
                                                       [], ffmpeg_opts)
            result_commands.extend(command)
            return tmp_path, True

        # TODO move to software abstraction
        source_container_supported_by_mkvmerge = video_track.container_format(
        ) not in {FileFormat.x3GP, FileFormat.SMK, FileFormat.WMV}

        source_video_codec = video_track.codec()
        source_video_crf = video_track.crf()
        source_video_profile = video_track.profile()
        source_video_level = video_track.level()

        target_video_codec = args.vc
        target_video_profile, target_video_level = CODEC_FFMPEG_PARAMETERS[
            target_video_codec]

        encoded_ok = source_video_codec == target_video_codec and \
            source_video_crf is not None and \
            source_video_profile == target_video_profile and \
            source_video_level == target_video_level
        if args.vr or args.vs or not encoded_ok and not args.vk:
            ffmpeg = Ffmpeg()
            target_crf, target_tune = CODEC_TUNES[target_video_codec][args.vt][
                args.vq]

            # TODO check out rutracker manuals for dvd rip filters and stuff
            ffmpeg_filters = []

            assert video_track.field_order() is not None
            if video_track.field_order() in (FieldOrder.INTERLACED_BOT,
                                             FieldOrder.INTERLACED_TOP):
                # TODO consider bwdif
                ffmpeg_filters.append('yadif=1:-1:1')

            if args.va:
                ffmpeg_filters.append('setdar=dar={}'.format(args.va))

            crop_args = None
            if args.cr or args.cf:
                if common_crop_args is not None:
                    crop_args = common_crop_args
                if crop_args_map is not None:
                    crop_args = crop_args_map[video_track.source_file()]
                if crop_args is None:
                    os.system('ffmpegyag')
                    while crop_args is None:
                        try:
                            crop_args = [
                                int(x) for x in raw_input(
                                    'Enter crop parameters (w:h:x:y): ').strip(
                                    ).split(':')
                            ]
                        except:
                            pass
                    if args.sc:
                        common_crop_args = crop_args
            if crop_args is None or not crop_args:
                crop_args = [video_track.width(), video_track.height(), 0, 0]
            dw, dh, dx, dy = crop_args
            if not VideoTrack.dimensions_correct(dw, dh):
                platform.print_string(u'Adjusting crop by {}x{}'.format(
                    dw % 16, dh % 8))
                dw, dh, dx, dy = VideoTrack.correct_dimensions(dw, dh, dx, dy)
            assert VideoTrack.dimensions_correct(dw, dh)
            if dx > 0 or dy > 0 or dw != video_track.width(
            ) or dh != video_track.height():
                ffmpeg_filters.append('crop={w}:{h}:{x}:{y}'.format(w=dw,
                                                                    h=dh,
                                                                    x=dx,
                                                                    y=dy))

            # TODO support different resolutions
            # TODO forbid upscale
            if args.vs == '720p':
                ffmpeg_filters.append('scale=1280:-8')
            elif args.vs == '1080p':
                ffmpeg_filters.append('scale=1920:-8')
            elif args.vs == '1440p':  # TODO !!!!!!!!!!
                ffmpeg_filters.append('scale=-16:1440')

            src_colors = video_track.colors()
            dst_color_space = src_colors.correct_space()
            if args.ks:
                dst_color_space = src_colors.space()
            if src_colors.space() != dst_color_space:
                raise cli.Error(
                    u'Colorspace conversion from {} to {} not implemented'.
                    format(src_colors.space(), dst_color_space))
                # TODO specify input/output color_range
                # TODO specify each input component separately
                # TODO The input transfer characteristics, color space, color primaries and color range should be set on the input data
                # TODO clarify iall=all= format string
                # ffmpeg_filters.append('colorspace=iall={}:all={}'.format(src_color_space, dst_color_space))

            ffmpeg_src_options = []

            src_colors_range = src_colors.range()
            if src_colors_range is not None:
                ffmpeg_src_options.append('-color_range {}'.format(
                    ffmpeg.build_color_range_argument(src_colors_range)))

            ffmpeg_dst_options = ['-an', '-sn', '-dn']
            if ffmpeg_filters:
                ffmpeg_dst_options.append('-filter:v {}'.format(
                    ','.join(ffmpeg_filters)))
            ffmpeg_dst_options.extend([
                '-c:v {}'.format(
                    ffmpeg.build_video_encoding_library_argument(
                        target_video_codec)),
                '-preset veryslow',
                '-pix_fmt {}'.format(
                    ffmpeg.build_picture_format_argument(
                        PictureFormat.YUV420P)),
                '-crf {}'.format(target_crf),
                '-map_metadata -1',
                '-map_chapters -1',
            ])

            arg_profile = ffmpeg.build_video_codec_profile_argument(
                target_video_codec, target_video_profile)
            arg_level = ffmpeg.build_video_codec_level_argument(
                target_video_codec, target_video_level)
            if target_video_codec == VideoCodec.H264:
                ffmpeg_dst_options.extend([
                    '-profile:v {}'.format(arg_profile),
                    '-level:v {}'.format(arg_level)
                ])
            elif target_video_codec == VideoCodec.H265:
                ffmpeg_dst_options.append(
                    '-x265-params "profile={}:level={}"'.format(
                        arg_profile, arg_level))

            if target_tune is not None:
                ffmpeg_dst_options.append('-tune {}'.format(target_tune))

            if dst_color_space is not None and (
                    video_track.is_hd() or src_colors.space() is not None):
                ffmpeg_dst_options.extend([
                    # TODO "16-235 is a typical NTSC luma range. PAL always uses 0-255 luma range."
                    '-color_range {}'.format(
                        ffmpeg.build_color_range_argument(src_colors.range())),
                    '-color_primaries {}'.format(
                        ffmpeg.build_color_primaries_argument(
                            dst_color_space)),
                    '-color_trc {}'.format(
                        ffmpeg.build_color_trc_argument(dst_color_space)),
                    '-colorspace {}'.format(
                        ffmpeg.build_color_space_argument(dst_color_space)),
                ])
            else:
                assert not video_track.is_hd()

            new_video_path = platform.make_temporary_file('.mkv')
            result_commands.extend(
                cmd.gen_ffmpeg_convert(video_track.source_file(),
                                       ffmpeg_src_options, new_video_path,
                                       ffmpeg_dst_options))
            track_sources[video_track.qualified_id()] = [new_video_path, 0]
            mux_temporary_files.append(new_video_path)
        elif not source_container_supported_by_mkvmerge:
            new_video_path, _ = make_single_track_file(
                video_track, Ffmpeg.STREAM_ARGUMENT_VID, '.mkv')
            track_sources[video_track.qualified_id()] = [new_video_path, 0]
            mux_temporary_files.append(new_video_path)

        # TODO move to software abstraction
        audio_codecs_to_denorm = {AudioCodec.AC3, AudioCodec.DTS}
        audio_codecs_to_uncompress = {
            AudioCodec.AAC_HE,
            AudioCodec.AAC_HE_V2,
            AudioCodec.AAC_LC,
            AudioCodec.AMR,
            AudioCodec.OPUS,
            AudioCodec.SPEEX,
            AudioCodec.COOK,
            AudioCodec.ASAO,
            AudioCodec.ADPCM_SWF,
            AudioCodec.PCM_MULAW,
            AudioCodec.PCM_S16B,
            AudioCodec.VORBIS,
            AudioCodec.SMK,
            AudioCodec.WMA_PRO,
            AudioCodec.WMA_V2,
        }
        audio_codecs_to_recode = {
            AudioCodec.AMR, AudioCodec.ASAO, AudioCodec.OPUS, AudioCodec.SPEEX,
            AudioCodec.COOK, AudioCodec.EAC3, AudioCodec.DTS_ES,
            AudioCodec.DTS_HRA, AudioCodec.DTS_MA, AudioCodec.TRUE_HD,
            AudioCodec.ADPCM_IMA, AudioCodec.ADPCM_MS, AudioCodec.ADPCM_SWF,
            AudioCodec.PCM_MULAW, AudioCodec.PCM_S16B, AudioCodec.PCM_S16L,
            AudioCodec.PCM_S24L, AudioCodec.FLAC, AudioCodec.MP2,
            AudioCodec.VORBIS, AudioCodec.SMK, AudioCodec.WMA_PRO,
            AudioCodec.WMA_V2
        }

        max_audio_channels = CHANNEL_SCHEMES[args.ad]
        for track in output_tracks[TrackType.AUD]:
            need_extract = not source_container_supported_by_mkvmerge
            need_denorm = track.codec() in audio_codecs_to_denorm
            need_downmix = track.channels() > max_audio_channels
            need_recode = need_downmix or track.codec(
            ) in audio_codecs_to_recode or args.ar and track.codec(
            ) != AudioCodec.AAC_LC
            need_uncompress = track.codec(
            ) in audio_codecs_to_uncompress or args.aw

            if need_extract or need_denorm or need_downmix or need_recode:

                stf_ext = None
                stf_ffmpeg_opts = None
                if need_uncompress:
                    stf_ext = '.wav'
                    stf_ffmpeg_opts = ['-f wav', '-rf64 auto']
                src_track_file, is_src_track_file_temporary = make_single_track_file(
                    track, Ffmpeg.STREAM_ARGUMENT_AUD, stf_ext,
                    stf_ffmpeg_opts)

                eac_track_file = src_track_file
                if need_denorm or need_downmix or need_recode:
                    eac_track_file = platform.make_temporary_file(
                        '.wav' if need_recode else platform.
                        file_ext(src_track_file))
                    eac_opts = []
                    if need_downmix:
                        if max_audio_channels == 1:
                            pass  # will be processed later
                        elif max_audio_channels == 2:
                            eac_opts.append('-downStereo')
                        elif max_audio_channels == 6:
                            eac_opts.append('-down6')
                        else:
                            raise cli.Error(
                                u'Unhandled channels num {}'.format(
                                    max_audio_channels))
                    if track.delay() != 0:
                        eac_opts.append('{}{}ms'.format(
                            '+' if track.delay() > 0 else '-',
                            abs(track.delay())))
                    result_commands.append(u'call eac3to {} {} {}'.format(
                        cmd.quote(src_track_file), cmd.quote(eac_track_file),
                        ' '.join(eac_opts)))
                    if is_src_track_file_temporary:
                        result_commands.extend(
                            cmd.gen_del_files(args.sd, src_track_file))

                dst_track_file = eac_track_file
                if need_downmix and max_audio_channels == 1:
                    mono_track_file = platform.make_temporary_file('.wav')
                    result_commands.extend(
                        cmd.gen_ffmpeg_convert(eac_track_file, [],
                                               mono_track_file, ['-ac 1']))
                    result_commands.extend(
                        cmd.gen_del_files(args.sd, eac_track_file))
                    dst_track_file = mono_track_file

                if need_recode:
                    m4a_track_file = platform.make_temporary_file('.m4a')
                    qaac_opts = [
                        '--tvbr 91', '--quality 2', '--rate keep', '--no-delay'
                    ]
                    qaac = u'qaac64 {} {} -o {}'.format(
                        u' '.join(qaac_opts), cmd.quote(dst_track_file),
                        cmd.quote(m4a_track_file))
                    result_commands.append(qaac)
                    result_commands.extend(
                        cmd.gen_del_files(args.sd, dst_track_file))
                    dst_track_file = m4a_track_file

                mux_temporary_files.append(dst_track_file)
                track_sources[track.qualified_id()] = [dst_track_file, 0]

        for track in output_tracks[TrackType.SUB]:
            if track.is_text():
                ffmpeg_opts = None
                if track.codec() == SubtitleCodec.MOV:
                    ffmpeg_opts = []
                track_file, is_track_file_temporary = make_single_track_file(
                    track, Ffmpeg.STREAM_ARGUMENT_SUB, ffmpeg_opts=ffmpeg_opts)
                srt_file = platform.make_temporary_file('.srt')
                result_commands.append(
                    u'{python} {script} {src_path} {dst_path}'.format(
                        python=sys.executable,
                        script=cmd.quote(
                            os.path.join(os.path.dirname(__file__),
                                         'any2srt.py')),
                        src_path=cmd.quote(track_file),
                        dst_path=cmd.quote(srt_file)))
                track_sources[track.qualified_id()] = [srt_file, 0]
                track.set_encoding(lang.norm_encoding('utf-8'))
                mux_temporary_files.append(srt_file)
                if is_track_file_temporary:
                    result_commands.extend(
                        cmd.gen_del_files(args.sd, track_file))
            elif track.codec() == SubtitleCodec.PGS:
                track_file, is_track_file_temporary = make_single_track_file(
                    track, Ffmpeg.STREAM_ARGUMENT_SUB, prefer_ffmpeg=False)
                idx_file = platform.make_temporary_file('.idx')
                sub_file = u'{}.sub'.format(os.path.splitext(idx_file)[0])
                result_commands.extend(
                    cmd.gen_bdsup2sub(track_file, idx_file,
                                      lang.alpha2(track.language())))
                track_sources[track.qualified_id()] = [idx_file, 0]
                mux_temporary_files.extend([idx_file, sub_file])
                if is_track_file_temporary:
                    result_commands.extend(
                        cmd.gen_del_files(args.sd, track_file))

        mux_path = platform.make_temporary_file('.mkv')

        # TODO add cover to files
        mux = ['call', 'mkvmerge']
        mux.extend(['--output', cmd.quote(mux_path)])
        mux.extend([
            '--no-track-tags', '--no-global-tags',
            '--disable-track-statistics-tags'
        ])

        track_ids_by_files = {}
        for qualified_id, (source_file,
                           source_file_track_id) in track_sources.iteritems():
            track_ids_by_files.setdefault(
                source_file, {})[qualified_id] = source_file_track_id
        if source_container_supported_by_mkvmerge:
            track_ids_by_files.setdefault(video_track.source_file(), {})

        # TODO tracks need to be extracted from 3gp and wmv containers before passing to mkvmerge
        source_file_ids = {}
        for i, (source_file,
                track_ids_map) in enumerate(track_ids_by_files.iteritems()):
            source_file_ids[source_file] = i
            for track_type, (tracks_flags_yes,
                             tracks_flag_no) in Track.TYPE_FLAGS.iteritems():
                cur_file_tracks = [
                    track for track in output_tracks[track_type]
                    if track.qualified_id() in track_ids_map
                ]
                if cur_file_tracks:
                    if tracks_flags_yes:
                        mux.append('{} {}'.format(
                            tracks_flags_yes, ','.join(
                                str(track_ids_map[track.qualified_id()])
                                for track in cur_file_tracks)))
                    for track in cur_file_tracks:
                        default = track.qualified_id(
                        ) == output_tracks[track_type][0].qualified_id()
                        file_track_id = track_ids_map[track.qualified_id()]
                        mux.append('--track-name {0}:""'.format(file_track_id))
                        if track_type == TrackType.SUB and track.encoding(
                        ) is not None:
                            mux.append('--sub-charset {0}:{1}'.format(
                                file_track_id, track.encoding()))
                        mux.append('--language {0}:{1}'.format(
                            file_track_id, track.language()))
                        mux.append('--default-track {0}:{1}'.format(
                            file_track_id, 'yes' if default else 'no'))
                        if track.is_forced():
                            mux.append(
                                '--forced-track {0}:yes'.format(file_track_id))
                elif tracks_flag_no:
                    mux.append(tracks_flag_no)
            file_flags = [
                '--no-track-tags', '--no-attachments', '--no-buttons',
                '--no-global-tags'
            ]
            if source_file != video_track.source_file():
                file_flags.append('--no-chapters')
            mux.append(u'{} {}'.format(u' '.join(file_flags),
                                       cmd.quote(source_file)))

        mux.append('--title ""')

        track_order = []
        for track_type in [TrackType.VID, TrackType.AUD, TrackType.SUB]:
            for track in output_tracks[track_type]:
                source_file, source_file_track_id = track_sources[
                    track.qualified_id()]
                track_order.append('{}:{}'.format(source_file_ids[source_file],
                                                  source_file_track_id))
        mux.append('--track-order {}'.format(','.join(track_order)))

        result_commands.append(u' '.join(mux))
        if len(mux_temporary_files) > 0:
            result_commands.extend(
                cmd.gen_del_files(args.sd, *sorted(set(mux_temporary_files))))

        # TODO mark mkv file with mkvexport version
        if movie.chapters_path() is not None:
            result_commands.append(u'mkvpropedit --chapters {} {}'.format(
                cmd.quote(movie.chapters_path()), cmd.quote(mux_path)))

        clean_mux_path = platform.make_temporary_file('.mkv')
        result_commands.append(u'call mkclean {} {}'.format(
            cmd.quote(mux_path), cmd.quote(clean_mux_path)))
        result_commands.extend(cmd.gen_del_files(args.sd, mux_path))
        result_commands.extend(
            cmd.gen_move_file(clean_mux_path, target_path, args.sd))

        if args.xx:
            result_commands.extend(
                cmd.gen_del_files(
                    args.sd,
                    *sorted(
                        set(media_file.path()
                            for media_file in movie.media_files()))))

        allowed_exit_codes = {'robocopy': 1, 'mkvmerge': 1}
        with codecs.open(MUX_BODY, 'a', 'utf-8') as body_file:
            for command in result_commands:
                fail_exit_code = 1
                for program, code in allowed_exit_codes.iteritems():
                    if program in command.lower():
                        fail_exit_code = code + 1
                stop_statement = u'call :stop {}'.format(
                    misc.random_printable(8))
                if fail_exit_code == 1:
                    prepared_commands = [
                        u'{} || {}'.format(command.strip(), stop_statement)
                    ]
                else:
                    prepared_commands = [
                        command.strip(),
                        u'if errorlevel {} {}'.format(fail_exit_code,
                                                      stop_statement)
                    ]
                for prep_command in prepared_commands:
                    body_file.write(u'{}\r\n'.format(prep_command))
            body_file.write(u'\r\n')

    return 0