diff --git a/rip-py/rip.py b/rip-py/rip.py index 93a520a..172fb3b 100755 --- a/rip-py/rip.py +++ b/rip-py/rip.py @@ -1,18 +1,27 @@ #!/usr/bin/env python3 -from datetime import datetime +from datetime import datetime, timedelta import itertools import math import re import os +import shlex import subprocess +import sys import tempfile import click +try: + import dvdread + HAS_DVDREAD = True +except ImportError: + HAS_DVDREAD = False FILE_TYPE = click.Path(file_okay=True, dir_okay=False, exists=True) +NEW_FILE_TYPE = click.Path(file_okay=True, dir_okay=False, exists=False) DIRECTORY_TYPE = click.Path(file_okay=False, dir_okay=True, exists=True) -DEVICE_OR_DIRECTORY_TYPE = click.Path(file_okay=True, dir_okay=True, exists=True) +DEVICE_OR_DIRECTORY_TYPE = click.Path(file_okay=True, dir_okay=True, + exists=True) @click.group() @@ -34,12 +43,14 @@ def with_fifo(func): def _get_audio_map(title=None, mpv_output=None, device=None): + # FIXME this doesn't always work. ffmpeg seems to order audio stream the + # other way around or something if (None, None) == (title, mpv_output): raise RuntimeError('Either `title` or `mpv_output` is required.') if mpv_output is None: mpv_cmd = ['mpv', '--quiet', '--frames=0', - 'dvdread://{}'.format(title)] + 'dvd://{}'.format(title)] if device: mpv_cmd.append('--dvd-device={}'.format(device)) mpv_output = subprocess.check_output(mpv_cmd, @@ -49,6 +60,10 @@ def _get_audio_map(title=None, mpv_output=None, device=None): audio_map = [] for line in mpv_output.splitlines(): # [dvd] audio stream: 0 format: mpeg1 (stereo) language: de aid: 0. + # FIXME use the aid as ffmpeg should support -map i:0x80, too + # or another one + # [dvd] audio stream: 0 format: ac3 (5.1) language: de aid: 128. + # [dvd] audio stream: 1 format: ac3 (5.1) language: en aid: 129. m = re.match(r'\[dvd\] audio stream: (\d+) .* language: (\S+) .*', line) if m: @@ -62,64 +77,209 @@ def _get_audio_map(title=None, mpv_output=None, device=None): return audio_map -@with_fifo -def _rip_title(title, output_dir, fifo_path, device=None, media_type=None, - keep_audio=False, quality=18): - out_file = os.path.join(output_dir, '{}.mkv'.format(title)) +def _get_audios(title, device): + if HAS_DVDREAD: + with dvdread.DVD(device) as d: + d.Open() - mpv_cmd = ['mpv', '--quiet', '--stream-dump={}'.format(fifo_path), - 'dvdread://{}'.format(title)] - if device: - mpv_cmd.append('--dvd-device={}'.format(device)) - mpv_proc = subprocess.Popen(mpv_cmd, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) + if not 0 <= title <= d.NumberOfTitles: + msg = 'Number of titles of of range. Use a number in [0, {}[' + raise click.ClickException(msg.format(d.NumberOfTitles)) - ffmpeg_cmd = ['ffmpeg', - '-probesize', '101M', - '-analyzeduration', '150M', - '-i', fifo_path, - '-map', '0:v:0', - #'-c:a', 'flac', - '-c:a', 'copy', - '-c:v', 'h264', - '-crf', '{}'.format(quality), - '-pix_fmt', 'yuv420p', - #'-x264-params', 'keyint=240:min-keyint=20', - '-preset:v', 'slower', - '-movflags', '+faststart', - #'-profile:v', 'baseline', - #'-level', '3.0', - ] - - if keep_audio: - audio_map = _get_audio_map(title, device=device) - for channel, lang in audio_map: - ffmpeg_cmd.extend([ - '-map', '0:a:{}'.format(channel), - '-metadata:s:a:{}'.format(channel), 'language={}'.format(lang) + # DvdRead starts counting titles at 1, we start at 0 + title += 1 + t = d.GetTitle(title) + audio_map = [] + for i in range(1, t.NumberOfAudios + 1): + a = t.GetAudio(i) + audio_map.append((128 + i - 1, a.LangCode)) + return audio_map + else: + # FIXME can this be _get_audio_map? + raise NotImplementedError( + 'Need to do this via ffmpeg/ffprobe/mpv ' + 'somehow. Make sure you make sense of mpv showing the audio ' + 'streams in another order than they appear in the file in the ' + 'end ..') + + +def _get_subtitles(title, device): + if HAS_DVDREAD: + with dvdread.DVD(device) as d: + d.Open() + + if not 0 <= title <= d.NumberOfTitles: + msg = 'Number of titles of of range. Use a number in [0, {}[' + raise click.ClickException(msg.format(d.NumberOfTitles)) + + # DvdRead starts counting titles at 1, we start at 0 + title += 1 + t = d.GetTitle(title) + subtitles = [] + for i in range(1, t.NumberOfSubpictures + 1): + s = t.GetSubpicture(i) + subtitles.append((0x20 + i - 1, s.LangCode)) + return subtitles + else: + raise NotImplementedError() + + +def _get_parameterized_ffmpeg_cmd(media_type, quality, extra_opts, use_gpu, + audio_map, subtitle_map, pre_read=1024): + ffmpeg_cmd = [] + ffmpeg_cmd.append(['ffmpeg', + '-probesize', '{}M'.format(pre_read), + '-analyzeduration', '{}M'.format(pre_read), + '-hwaccel', 'auto', + '-i']) + + ffmpeg_cmd.append(extra_opts) + ffmpeg_cmd[-1].extend(['-map', '0:v:0']) + if '-c:a' not in ffmpeg_cmd[-1]: + ffmpeg_cmd[-1].extend(['-c:a', 'copy']) + ffmpeg_cmd[-1].extend(['-c:s', 'copy']) + if use_gpu: + ffmpeg_cmd[-1] += [ + '-c:v', 'h264_nvenc', + '-rc', 'vbr_hq', + '-rc-lookahead', '20', + '-cq', '{}'.format(quality), + # '-maxrate:v', '3M', + # '-b:v', '8M', '-maxrate:v', '10M', + '-preset:v', 'slow'] + else: + ffmpeg_cmd[-1] += [ + '-c:v', 'h264', + '-crf', '{}'.format(quality), + '-preset:v', 'slower', + '-x264-params', 'opencl=true', + # '-x264-params', 'keyint=240:min-keyint=20', + ] + + ffmpeg_cmd[-1] += [ + '-pix_fmt', 'yuv420p', + '-movflags', '+faststart', + # '-profile:v', 'baseline', + # '-level', '3.0', + ] + + if audio_map: + for i, (channel, lang) in enumerate(audio_map): + if channel < 0x80: + map_target = '0:a:{}'.format(channel) + else: + map_target = 'i:0x{:x}'.format(channel) + ffmpeg_cmd[-1].extend([ + '-map', map_target, + '-metadata:s:a:{}'.format(i), 'language={}'.format(lang) ]) else: - ffmpeg_cmd.extend(['-map', '0:a:0', '-metadata:s:a:0', 'language=de']) + ffmpeg_cmd[-1].extend(['-map', '0:a:0', '-metadata:s:a:0', 'language=de']) + + if subtitle_map: + for i, (channel, lang) in enumerate(subtitle_map): + if channel < 0x20: + map_target = '0:s:{}'.format(channel) + else: + map_target = 'i:0x{:x}'.format(channel) + ffmpeg_cmd[-1].extend([ + '-map', map_target, + '-metadata:s:s:{}'.format(i), 'language={}'.format(lang) + ]) + + # TODO we are probably able to do forced subtitles. we probably want that + + # we can set a default stream. make this available somehow + # -disposition:a:1 default + + if media_type and not use_gpu: + ffmpeg_cmd[-1].extend(['-tune:v', media_type]) + + def parameterized(output_file, input_file): + return ffmpeg_cmd[0] + [input_file] + ffmpeg_cmd[1] + [output_file] + + return parameterized + + +def _get_ffmpeg_cmd(output_file, input_path, media_type, quality, extra_opts, + use_gpu, audio_map, subtitle_map): + cmd = _get_parameterized_ffmpeg_cmd(media_type, quality, extra_opts, + use_gpu, audio_map, subtitle_map) + cmd = cmd(output_file=output_file, input_file=input_path) + print(cmd) + return cmd - if media_type: - ffmpeg_cmd.extend(['-tune:v', media_type]) - ffmpeg_cmd.append(out_file) +def _get_mpv_dump_cmd(title, output_path, device=None): + cmd = ['mpv', '--quiet', '--stream-dump={}'.format(output_path), + 'dvd://{}'.format(title)] + if device: + cmd.append('--dvd-device={}'.format(device)) + + return cmd + + +@with_fifo +def _rip_title(title, output_dir, ffmpeg_cmd, fifo_path, device=None, + output_file_suffix='.mkv'): + filename = '{}{}'.format(title, output_file_suffix) + out_file = os.path.join(output_dir, filename) + + mpv_cmd = _get_mpv_dump_cmd(title, fifo_path, device) + mpv_proc = subprocess.Popen(mpv_cmd, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + ffmpeg_cmd = ffmpeg_cmd(output_file=out_file, input_file=fifo_path) + print(ffmpeg_cmd) subprocess.check_call(ffmpeg_cmd) mpv_output = mpv_proc.stdout.read().decode('utf-8') - chapters = _get_title_chapters(mpv_output=mpv_output) - if chapters: - _set_chapters(out_file, chapters) + + return out_file, mpv_output def _get_title_chapters(title=None, mpv_output=None, device=None): + if HAS_DVDREAD: + return _get_title_chapters_dvdread(title=title, device=device) + else: + return _get_title_chapters_mpv(title=title, mpv_output=mpv_output, + device=device) + + +def _get_title_chapters_dvdread(title, device='/dev/dvd'): + # we usually start at 0, but dvdread starts at 1 + title += 1 + with dvdread.DVD(device) as d: + d.Open() + + if title > d.NumberOfTitles: + msg = 'Given title {} doesn\'t exist on DVD (has {} titles).' + raise RuntimeError(msg.format(title, d.NumberOfTitles)) + + t = d.GetTitle(title) + + # we need to put in at least a microsecond to get the full + # 0:00:00.000001 format + td = timedelta(microseconds=1) + chapters = [str(td)] + + for i in range(1, t.NumberOfChapters + 1): + td += timedelta(milliseconds=t.GetChapter(i).Length) + chapters.append(str(td)) + + return chapters + + +def _get_title_chapters_mpv(title=None, mpv_output=None, device=None): if (None, None) == (title, mpv_output): raise RuntimeError('Either `title` or `mpv_output` is required.') + print('Getting chapters from mpv might miss some at the end. ' + 'Please install the dvdread python packages.', file=sys.stderr) + if mpv_output is None: mpv_cmd = ['mpv', '--quiet', '--frames=0', - 'dvdread://{}'.format(title)] + 'dvd://{}'.format(title)] if device: mpv_cmd.append('--dvd-device={}'.format(device)) mpv_output = subprocess.check_output(mpv_cmd, @@ -141,13 +301,21 @@ def _get_title_chapters(title=None, mpv_output=None, device=None): def _convert_chapters_to_mkvmerge_format(chapters): - lines = [] + lines = [] number_of_zeros = 0 if len(chapters) > 1: number_of_zeros = int(math.log10(len(chapters) - 1)) chapter_template = 'CHAPTER{{:0{}d}}'.format(number_of_zeros) for i, timestamp in enumerate(chapters): chapter_str = chapter_template.format(i) + # if we start with e.g. 0:21, we need another 0 to be compatible + if len(timestamp.split(':')[0]) == 1: + timestamp = '0' + timestamp + # if we have a higher resolution than 1 millisecond, we need to cut + # some chars + if len(timestamp.split('.')[-1]) > 3: + strip_length = len(timestamp.split('.')[-1]) - 3 + timestamp = timestamp[:-strip_length] lines.append('{}={}'.format(chapter_str, timestamp)) lines.append('{}NAME='.format(chapter_str)) return os.linesep.join(lines) @@ -167,7 +335,21 @@ def _set_chapters(mkv_file, chapters): def _get_dvd_titles(device=None): - mpv_cmd = ['mpv', '--vo', 'null', '--quiet', '--frames=0', 'dvdread://'] + if HAS_DVDREAD: + return _get_dvd_titles_dvdread(device=device) + else: + return _get_dvd_titles_mpv(device=device) + + +def _get_dvd_titles_dvdread(device='/dev/dvd'): + with dvdread.DVD(device) as d: + d.Open() + + return d.NumberOfTitles + + +def _get_dvd_titles_mpv(device=None): + mpv_cmd = ['mpv', '--vo', 'null', '--quiet', '--frames=0', 'dvd://'] if device: mpv_cmd.append('--dvd-device={}'.format(device)) out = subprocess.check_output(mpv_cmd, stderr=subprocess.STDOUT) @@ -189,42 +371,175 @@ def _get_dvd_titles(device=None): @cli.command() @click.option('-o', '--output-dir', type=DIRECTORY_TYPE, required=True) -@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE) +@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE, + default='/dev/dvd', show_default=True) @click.option('--media-type', type=click.Choice(['film', 'animation'])) @click.option('--keep-audio', is_flag=True) -@click.option('--quality', type=click.INT) -def all(output_dir, device=None, media_type=None, keep_audio=False, quality=18): - titles = _get_dvd_titles(device=device) - for title in range(0, titles): - _rip_title(title, output_dir, device=device, media_type=media_type, - keep_audio=keep_audio, quality=quality) +@click.option('--audio-map', multiple=True, type=(int, str), + help="Keep specific audio. Order is important. Takes first " + "audio stream and puts it at stream INT in the new file " + "with language STR.") +@click.option('--subtitle-map', multiple=True, type=(int, str), + help="Keep the specific subtitles. Order is important. Takes " + "stream defined as INT into place according to order with " + "language STR.") +@click.option('--quality', type=click.INT, default=22, show_default=True) +@click.option('--start-at-title', type=click.INT, default=0, show_default=True) +@click.option('--end-at-title', type=click.INT, default=-1) +@click.option('-t', '--title', 'titles', multiple=True, type=click.INT) +@click.option('--deinterlace', is_flag=True) +@click.option('--pre-read-size', type=click.INT, default=1024, show_default=True) +@click.option('--ffmpeg-opts') +def rip(output_dir, titles, device=None, media_type=None, keep_audio=False, + quality=18, start_at_title=0, end_at_title=-1, ffmpeg_opts=None, + deinterlace=False, audio_map=None, subtitle_map=None, pre_read_size=1024): + if end_at_title >= 0 and start_at_title > end_at_title: + raise click.ClickException('--start-at-title cannot be larger than --end-at-title.') + + if ffmpeg_opts is None: + ffmpeg_opts = [] + else: + ffmpeg_opts = shlex.split(ffmpeg_opts) + if deinterlace: + ffmpeg_opts.extend(['-vf', 'yadif']) -@cli.command() -@click.option('-t', '--title', type=int, required=True) -@click.option('-o', '--output-dir', type=DIRECTORY_TYPE, required=True) -@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE) + if titles: + titles = sorted(t for t in titles if t >= start_at_title) + else: + titles = _get_dvd_titles(device=device) + if end_at_title >= 0: + if end_at_title > titles: + raise click.ClickException('--end-at-title {} larger than number of titles {}' + .format(end_at_title, titles)) + # we'll put it into a Python range(), so we need to add 1 to make + # it include that title + end_at_title += 1 + else: + end_at_title = titles + titles = range(start_at_title, end_at_title) + + for title in titles: + if keep_audio: + audio_map = _get_audio_map(title, device=device) + + use_gpu = False + ffmpeg_cmd = _get_parameterized_ffmpeg_cmd(media_type, quality, + ffmpeg_opts, use_gpu, + audio_map, subtitle_map, + pre_read=pre_read_size) + + out_file, mpv_output = _rip_title(title, output_dir, ffmpeg_cmd, device=device) + + chapters = _get_title_chapters(title=title, mpv_output=mpv_output, + device=device) + if chapters: + _set_chapters(out_file, chapters) + + +@cli.command('convert') +@click.option('-i', '--input-file', type=FILE_TYPE, required=True) +@click.option('-o', '--output-file', type=NEW_FILE_TYPE, required=True) @click.option('--media-type', type=click.Choice(['film', 'animation'])) -@click.option('--keep-audio', is_flag=True) -@click.option('--quality', type=click.INT) -def single(title, output_dir, device=None, media_type=None, keep_audio=False, - quality=18): - _rip_title(title, output_dir, device=device, media_type=media_type, - keep_audio=keep_audio, quality=quality) +@click.option('--quality', type=click.INT, default=18, show_default=True) +@click.option('--deinterlace', is_flag=True) +@click.option('--ffmpeg-opts') +@click.option('--audio-map', multiple=True, type=(int, str), + help="Keep specific audio. Order is important. Takes first " + "audio stream and puts it at stream INT in the new file " + "with language STR.") +@click.option('--subtitle-map', multiple=True, type=(int, str), + help="Keep the specific subtitles. Order is important. Takes " + "stream defined as INT into place according to order with " + "language STR.") +@click.option('-c', '--chapters') +def convert(input_file, output_file, media_type, quality, deinterlace, + ffmpeg_opts, audio_map, subtitle_map, chapters): + if ffmpeg_opts is None: + ffmpeg_opts = [] + else: + ffmpeg_opts = shlex.split(ffmpeg_opts) + + if deinterlace: + ffmpeg_opts.extend(['-vf', 'yadif']) + + if chapters: + chapters = chapters.split(',') + + ffmpeg_cmd = _get_ffmpeg_cmd(output_file, input_file, media_type, + quality, ffmpeg_opts, False, audio_map, + subtitle_map) + + subprocess.check_call(ffmpeg_cmd) + + if chapters: + _set_chapters(output_file, chapters) @cli.command('get-chapters') @click.option('-t', '--title', type=int, required=True) -@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE) +@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE, + default='/dev/dvd', show_default=True) def get_chapters(title, device=None): print(','.join(_get_title_chapters(title=title, device=device))) +@cli.command() +@click.option('-o', '--output-dir', type=DIRECTORY_TYPE, required=True) +@click.option('-t', '--title', type=int, required=True) +@click.option('--pre-read-size', type=click.INT, default=1024, show_default=True) +@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE, + default='/dev/dvd', show_default=True) +def get_subtitles_file(output_dir, title, device=None, pre_read_size=1024): + # ffmpeg_cmd = ffmpeg_cmd(output_file=out_file, input_file=fifo_path) + ffmpeg_cmd = [] + ffmpeg_cmd.append(['ffmpeg', + '-probesize', '{}M'.format(pre_read_size), + '-analyzeduration', '{}M'.format(pre_read_size), + '-hwaccel', 'auto', + '-i']) + ffmpeg_cmd.append([ + '-map', '0:v:0', + '-c:v', 'h264', + '-crf', '75', + '-preset:v', 'veryfast', + '-x264-params', 'opencl=true', + # '-x264-params', 'keyint=240:min-keyint=20', + '-c:s', 'copy', + ]) + for i, (channel, lang) in enumerate(_get_subtitles(title, device)): + lang = '{} - 0x{:x}'.format(lang, channel) + ffmpeg_cmd[-1] += [ + '-map', 'i:0x{:x}'.format(channel), + '-metadata:s:s:{}'.format(i), 'language={}'.format(lang) + ] + + def parameterized(output_file, input_file): + return ffmpeg_cmd[0] + [input_file] + ffmpeg_cmd[1] + [output_file] + + out_file, mpv_output = _rip_title(title, output_dir, parameterized, device=device) + + +@cli.command() +@click.option('-o', '--output-dir', type=DIRECTORY_TYPE, required=True) +@click.option('-t', '--title', type=int, required=True) +@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE, + default='/dev/dvd', show_default=True) +def ffprobe(output_dir, title, device): + ffmpeg_cmd = [['ffprobe', '-i']] + + def parameterized(output_file, input_file): + return ffmpeg_cmd[0] + [input_file] + + out_file, mpv_output = _rip_title(title, output_dir, parameterized, device=device) + + @cli.command('set-chapters') @click.option('-o', '--output-file', type=FILE_TYPE, required=True) @click.option('-t', '--title', type=int) @click.option('-c', '--chapters') -@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE) +@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE, + default='/dev/dvd', show_default=True) def set_chapters(output_file, title=None, chapters=None, device=None): if (None, None) == (title, chapters): raise RuntimeError('One of `title` and `chapters` is required.') @@ -238,12 +553,14 @@ def set_chapters(output_file, title=None, chapters=None, device=None): @cli.command('find-right-title') -@click.option('--start-title', type=int, default=0) +@click.option('--start-title', type=int, default=0, show_default=True) @click.option('--end-title', type=int) -@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE) +@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE, + default='/dev/dvd', show_default=True) def find_right_title(start_title, end_title, device): if end_title and start_title > end_title: - raise click.ClickException('--start-title must not be bigger than --end-title') + msg = '--start-title must not be bigger than --end-title' + raise click.ClickException(msg) titles = _get_dvd_titles(device=device) if start_title > titles: @@ -276,8 +593,9 @@ def find_right_title(start_title, end_title, device): title_chapter_lengths[title] = lengths # remove titles with duplicate chapters by looking at the length - title_chapter_lengths = {title: lengths for title, lengths in title_chapter_lengths.items() - if len(lengths) == len(set(lengths))} + title_chapter_lengths = { + title: lengths for title, lengths in title_chapter_lengths.items() + if len(lengths) == len(set(lengths))} if title_chapter_lengths: print('The titles {} contain no duplicate chapter lengths.'.format( @@ -288,30 +606,208 @@ def find_right_title(start_title, end_title, device): @cli.command('split-file') @click.option('-c', '--chapters-file', type=FILE_TYPE, required=True, - help='File with timestamps separated by comma') + help='File with timestamps separated by comma') @click.option('-i', '--input-file', type=FILE_TYPE, required=True) @click.option('-o', '--output-dir', type=DIRECTORY_TYPE, required=True) -def split_file(output_dir, input_file, chapters_file): +@click.option('-n', '--number', type=click.INT, + help='Split out only one chapter given by this number.') +@click.option('--reencode', is_flag=True, + help='Don\'t use -c:v copy and -c:a copy') +@click.option('--media-type', type=click.Choice(['film', 'animation']), + help='Only relevant with --reencode') +@click.option('--quality', type=click.INT, default=18, show_default=True, + help='Only relevant with --reencode') +@click.option('--ffmpeg-opts', + help='Only relevant with --reencode') +@click.option('--audio-map', multiple=True, type=(int, str), + help="Keep specific audio. Only relevant with --reencode." + "Order is important. Takes first " + "audio stream and puts it at stream INT in the new file " + "with language STR.") +@click.option('--subtitle-map', multiple=True, type=(int, str), + help="Keep the specific subtitles. Order is important. Takes " + "stream defined as INT into place according to order with " + "language STR.") +def split_file(output_dir, input_file, chapters_file, number, reencode, + media_type, quality, ffmpeg_opts, audio_map, subtitle_map): + if ffmpeg_opts is None: + ffmpeg_opts = [] + else: + ffmpeg_opts = shlex.split(ffmpeg_opts) + extension = input_file.split('.')[-1] with open(chapters_file) as f: chapters = f.read().strip().split(',') - - for i, (start, end) in enumerate(itertools.zip_longest(chapters, chapters[1:])): - ffmpeg_cmd = ['ffmpeg', - '-ss', start] + + for i, (start, end) in enumerate(itertools.zip_longest(chapters, chapters[1:]), start=1): + if number is not None and i != number: + continue + + extra_opts = ffmpeg_opts + ['-ss', start] if end is not None: - ffmpeg_cmd += ['-to', end] + extra_opts.extend(['-to', end]) + + out_path = os.path.join(output_dir, '{}.{}'.format(i, extension)) + + if not reencode: + ffmpeg_cmd = ['ffmpeg'] + extra_opts + [ + '-i', input_file, + '-c:a', 'copy', + '-c:v', 'copy', + '-movflags', '+faststart', + out_path + ] + else: + use_gpu = False + ffmpeg_cmd = _get_ffmpeg_cmd(out_path, input_file, media_type, + quality, extra_opts, use_gpu, + audio_map, subtitle_map) + subprocess.check_call(ffmpeg_cmd) + + +@cli.command('titles') +@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE, + default='/dev/dvd', show_default=True) +def titles(device): + if HAS_DVDREAD: + with dvdread.DVD(device) as d: + d.Open() + + for i in range(1, d.NumberOfTitles + 1): + t = d.GetTitle(i) + click.echo('{}: {}'.format(i - 1, t.PlaybackTimeFancy)) + else: + click.echo(_get_dvd_titles(device=device)) + + +@cli.command() +@click.option('-t', '--title', type=int, required=True) +@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE, + default='/dev/dvd', show_default=True) +def audios(device, title): + """Retrieve the audio streams and their languages for a title""" + for i, (channel, lang) in enumerate(_get_audios(title, device)): + click.echo('{}: {} aid probably {} (0x{:x})' + .format(i, lang, channel, channel)) + + +@cli.command() +@click.option('-t', '--title', type=int, required=True) +@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE, + default='/dev/dvd', show_default=True) +def subtitles(device, title): + """Retrieve the subtitles streams and their languages for a title""" + for i, (channel, lang) in enumerate(_get_subtitles(title, device)): + print('{}: {} with id {} (0x{:x})' + .format(i, lang, channel, channel)) - ffmpeg_cmd += [ + +@cli.command() +@click.option('-t', '--title', type=int) +@click.option('-o', '--output-dir', type=DIRECTORY_TYPE, required=True) +@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE, + default='/dev/dvd', show_default=True) +@click.option('--no-dump', is_flag=True) +@click.option('--no-audios', is_flag=True) +@click.option('--no-subtitles', is_flag=True) +@click.option('--no-chapters', is_flag=True) +def dump(title, output_dir, device, no_dump, no_audios, no_subtitles, + no_chapters): + """Just dump the stream and additional infos""" + if title is None: + titles = range(0, _get_dvd_titles(device)) + else: + titles = [title] + + for title in titles: + if not no_dump: + dump_file = os.path.join(output_dir, '{}.dump'.format(title)) + mpv_cmd = _get_mpv_dump_cmd(title, dump_file, device) + + subprocess.check_call(mpv_cmd) + + if not no_audios: + audios_file = os.path.join(output_dir, '{}.audios'.format(title)) + with open(audios_file, 'w') as f: + audios = ['{}: {}'.format(c, l) + for c, l in _get_audios(title, device)] + f.write('\n'.join(audios)) + + if not no_subtitles: + subtitles_file = os.path.join(output_dir, '{}.subs'.format(title)) + with open(subtitles_file, 'w') as f: + subtitles = ['{}: {}'.format(c, l) + for c, l in _get_subtitles(title, device)] + f.write('\n'.join(subtitles)) + + if not no_chapters: + chapters_file = os.path.join(output_dir, '{}.chapters'.format(title)) + with open(chapters_file, 'w') as f: + chapters = _get_title_chapters(title, device=device) + f.write(','.join(chapters)) + + +@cli.command() +def has_dvdread(): + print(f"dvdread support: {'available' if HAS_DVDREAD else 'missing'}") + + +@cli.group() +def audio(): + ... + + +@audio.command() +@click.option('-o', '--output-dir', type=DIRECTORY_TYPE, required=True) +@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE, + default='/dev/dvd', show_default=True) +@click.option('-t', '--title', type=int, required=True) +def rip_dvd_title(device, output_dir, title): + """Rip the given title's audio from DVD""" + def partial(output_file, input_file): + return ['ffmpeg', + '-i', input_file, + '-map', '0:a:0', + '-c:a', 'flac', + output_file] + + _rip_title(title, output_dir, partial, device=device, + output_file_suffix='.flac') + + +@audio.command('split-file') +@click.option('-o', '--output-dir', type=DIRECTORY_TYPE, required=True) +@click.option('-c', '--chapters-file', type=FILE_TYPE, required=True, + help='File with timestamps separated by comma') +@click.option('-i', '--input-file', type=FILE_TYPE, required=True) +def audio_split_file(output_dir, chapters_file, input_file): + """Split an audio file into different flac files based on timestamps in the chapters file""" + with open(chapters_file) as f: + chapters = f.read().strip().split(',') + + times_ten = 0 + while len(chapters) // (10 ** times_ten) > 9: + times_ten += 1 + + name_template = '{:0' + str(times_ten + 1) + 'd}.flac' + for i, (start, end) in enumerate(itertools.zip_longest(chapters, chapters[1:]), start=1): + ffmpeg_cmd = [ + 'ffmpeg', + '-ss', start] + if end is not None: + ffmpeg_cmd.extend(['-to', end]) + + out_path = os.path.join(output_dir, name_template.format(i)) + + ffmpeg_cmd.extend([ '-i', input_file, '-c:a', 'flac', - #'-c:v', 'copy', - os.path.join(output_dir, '{}.{}'.format(i + 1, extension)) - ] - #print(ffmpeg_cmd) + '-movflags', '+faststart', + out_path]) + subprocess.check_call(ffmpeg_cmd) if __name__ == '__main__': - cli() + cli(complete_var='_RIP_PY_COMPLETE')