Add rip-py project

This is a backup from 2020-08-19 - the oldest version I still have
around.
This commit is contained in:
MasterofJOKers 2023-12-05 23:50:04 +01:00
parent ae52d4962a
commit 32edd53cfb
3 changed files with 304 additions and 0 deletions

285
rip-py/rip.py Executable file
View File

@ -0,0 +1,285 @@
#!/usr/bin/env python3
from datetime import datetime
import math
import re
import os
import subprocess
import tempfile
import click
FILE_TYPE = click.Path(file_okay=True, dir_okay=False, exists=True)
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):
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)]
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.
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
@with_fifo
def _rip_title(title, output_dir, fifo_path, device=None, media_type=None,
keep_audio=False):
out_file = os.path.join(output_dir, '{}.mkv'.format(title))
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)
ffmpeg_cmd = ['ffmpeg',
'-probesize', '101M',
'-analyzeduration', '150M',
'-i', fifo_path,
'-map', '0:v:0',
'-c:a', 'copy',
'-c:v', 'h264',
'-crf', '18',
'-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)
])
else:
ffmpeg_cmd.extend(['-map', '0:a:0', '-metadata:s:a:0', 'language=de'])
if media_type:
ffmpeg_cmd.extend(['-tune:v', media_type])
ffmpeg_cmd.append(out_file)
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)
def _get_title_chapters(title=None, mpv_output=None, device=None):
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)]
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)
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):
mpv_cmd = ['mpv', '--vo', 'null', '--quiet', '--frames=0', 'dvdread://']
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)
@click.option('--media-type', type=click.Choice(['film', 'animation']))
@click.option('--keep-audio', is_flag=True)
def all(output_dir, device=None, media_type=None, keep_audio=False):
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)
@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)
@click.option('--media-type', type=click.Choice(['film', 'animation']))
@click.option('--keep-audio', is_flag=True)
def single(title, output_dir, device=None, media_type=None, keep_audio=False):
_rip_title(title, output_dir, device=device, media_type=media_type,
keep_audio=keep_audio)
@cli.command('get-chapters')
@click.option('-t', '--title', type=int, required=True)
@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE)
def get_chapters(title, device=None):
print(','.join(_get_title_chapters(title=title, 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)
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)
@click.option('--end-title', type=int)
@click.option('-d', '--device', type=DEVICE_OR_DIRECTORY_TYPE)
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')
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.')
if __name__ == '__main__':
cli()

15
rip-py/setup.cfg Normal file
View File

@ -0,0 +1,15 @@
[metadata]
name = rip-py
version = 0.1
description = CD/DVD/BluRay ripping and re-encoding toolkit
[options]
scripts = rip.py
install_requires =
click
[flake8]
max-line-length = 120
exclude = .git,__pycache__,*.egg-info,*lib/python*
ignore = E241,E741,W503,W504

4
rip-py/setup.py Normal file
View File

@ -0,0 +1,4 @@
import setuptools
setuptools.setup()