rip-py: Add all the latest things

This is the latest version having new commands and support for dvdread
to reach chapters, because the mpv output was unreliable (iirc it forgot
to output the last chapter) and slower and harder to parse.
This commit is contained in:
MasterofJOKers 2023-12-06 00:04:29 +01:00
parent a95febccc3
commit 074b368b82
1 changed files with 581 additions and 85 deletions

View File

@ -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',
# 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),
'-pix_fmt', 'yuv420p',
#'-x264-params', 'keyint=240:min-keyint=20',
'-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 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)
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 media_type:
ffmpeg_cmd.extend(['-tune:v', media_type])
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)
])
ffmpeg_cmd.append(out_file)
# 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')
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,
@ -148,6 +308,14 @@ def _convert_chapters_to_mkvmerge_format(chapters):
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):
@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)
for title in range(0, titles):
_rip_title(title, output_dir, device=device, media_type=media_type,
keep_audio=keep_audio, quality=quality)
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()
@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)
@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,7 +593,8 @@ 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()
title_chapter_lengths = {
title: lengths for title, lengths in title_chapter_lengths.items()
if len(lengths) == len(set(lengths))}
if title_chapter_lengths:
@ -291,27 +609,205 @@ def find_right_title(start_title, end_title, device):
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',
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 += ['-to', end]
ffmpeg_cmd.extend(['-to', end])
ffmpeg_cmd += [
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')