@ -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 ' ,
' dvd read ://{} ' . 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 ' )