You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

814 lines
29 KiB

#!/usr/bin/env python3
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)
@click.group()
def cli():
pass
def with_fifo(func):
def f(*args, **kwargs):
with tempfile.TemporaryDirectory() as tmpdir:
fifo_path = os.path.join(tmpdir, 'rip.fifo')
try:
os.mkfifo(fifo_path)
except FileExistsError:
pass
kwargs['fifo_path'] = fifo_path
return func(*args, **kwargs)
return f
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',
'dvd://{}'.format(title)]
if device:
mpv_cmd.append('--dvd-device={}'.format(device))
mpv_output = subprocess.check_output(mpv_cmd,
stderr=subprocess.DEVNULL)
mpv_output = mpv_output.decode('utf-8')
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:
audio_map.append((int(m.group(1)), m.group(2)))
if not audio_map:
print('#' * 15)
print('WARNING: Could not find any audio streams.')
print('#' * 15)
return audio_map
def _get_audios(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)
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[-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
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')
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',
'dvd://{}'.format(title)]
if device:
mpv_cmd.append('--dvd-device={}'.format(device))
mpv_output = subprocess.check_output(mpv_cmd,
stderr=subprocess.DEVNULL)
mpv_output = mpv_output.decode('utf-8')
for line in mpv_output.splitlines():
m = re.match(r'\[dvd\] CHAPTERS: (\S*)', line)
if m:
chapters = m.group(1)
break
else:
raise RuntimeError('Could not find CHAPTERS in mpv output.')
chapters = chapters.split(',')
chapters = [c for c in chapters if c] # filter empty/last
return chapters
def _convert_chapters_to_mkvmerge_format(chapters):
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)
def _set_chapters(mkv_file, chapters):
if not os.path.exists(mkv_file):
raise RuntimeError('MKV file does not exist.')
with tempfile.NamedTemporaryFile() as f:
mkvmerge_chapters = _convert_chapters_to_mkvmerge_format(chapters)
f.write(mkvmerge_chapters.encode('utf-8'))
f.flush()
mkvpropedit_cmd = ['mkvpropedit', mkv_file, '--chapters', f.name]
subprocess.check_call(mkvpropedit_cmd)
def _get_dvd_titles(device=None):
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)
out = out.decode('utf-8')
for line in out.splitlines():
m = re.match(r'^\[dvd\] There are (\d+) titles on this DVD.', line)
# next line doesn't work on DVDs with one titleset having multiple
# titles
# m = re.match(r'^libdvdread: Found (\d+) VTS', line)
if m:
titles = int(m.group(1))
break
else:
raise RuntimeError('Could not find the number of VTS in mpv output.')
return titles
@cli.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('--media-type', type=click.Choice(['film', 'animation']))
@click.option('--keep-audio', is_flag=True)
@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'])
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('--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,
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,
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.')
if chapters is None:
chapters = _get_title_chapters(title=title, device=device)
else:
chapters = chapters.split(',')
_set_chapters(output_file, chapters)
@cli.command('find-right-title')
@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,
default='/dev/dvd', show_default=True)
def find_right_title(start_title, end_title, device):
if end_title and start_title > 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:
raise RuntimeError('DVD has less titles than start title.')
if end_title:
end_title = min(titles - 1, end_title)
else:
end_title = titles - 1
title_chapter_lengths = {}
for title in range(start_title, end_title + 1):
chapters = _get_title_chapters(title, device=device)
timestamps = []
for timestamp in chapters:
timestamp, microseconds = timestamp.split('.')
dt = datetime.strptime(timestamp, '%H:%M:%S')
dt = dt.replace(microsecond=int(microseconds) * 1000)
timestamps.append(dt)
lengths = []
if len(timestamps) > 1:
for i, timestamp in enumerate(timestamps[1:]):
lengths.append(timestamp - timestamps[i])
if not lengths:
continue
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))}
if title_chapter_lengths:
print('The titles {} contain no duplicate chapter lengths.'.format(
', '.join(str(i) for i in title_chapter_lengths.keys())))
else:
print('No title without duplicates found.')
@cli.command('split-file')
@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)
@click.option('-o', '--output-dir', type=DIRECTORY_TYPE, required=True)
@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:]), start=1):
if number is not None and i != number:
continue
extra_opts = ffmpeg_opts + ['-ss', start]
if end is not None:
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))
@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',
'-movflags', '+faststart',
out_path])
subprocess.check_call(ffmpeg_cmd)
if __name__ == '__main__':
cli(complete_var='_RIP_PY_COMPLETE')