Skip to content

Instantly share code, notes, and snippets.

@postmodern
Last active September 16, 2024 00:09
Show Gist options
  • Save postmodern/482fa8a9d810c40353e2 to your computer and use it in GitHub Desktop.
Save postmodern/482fa8a9d810c40353e2 to your computer and use it in GitHub Desktop.
Script to automate ripping DVDs using Handbrake-CLI and mkvmerge
#!/usr/bin/env bash
#
# Author: postmodern
# Description:
# Rips a DVD to a H.264 MKV file, with chapters and tags. Ignores any
# bad blocks or sectors on the DVD.
# Dependencies:
# * gddrescue
# * handbrake-cli
# * mkvtoolnix
#
set -e
#set -x
shopt -s extglob
rip_dvd_version=0.12.0
dvd_device="/dev/dvd"
dvd_blocksize=2048
dvd_fps=30
video_codec="x264"
video_quality="20.0"
audio_codec="vorbis"
x264_tune="film"
x264_preset="veryslow"
x264_profile="high"
ddrescue_passes=3
cleanup="true"
handbrake_opts=()
function print_help()
{
cat <<USAGE
usage: rip_dvd [OPTIONS] "TITLE" [-- HANDBRAKE_OPTS]
Options:
-d,--device DEV The dvd device.
Default: $dvd_device
-t,--dvd-title NUM Rips the specific title from the DVD.
-c,--chapters N[-M] Rips the specific chapter(s)
--start-at [[HH:]MM:]SS Starts ripping at the absolute time.
--stop-at [[HH:]MM:]SS Stops ripping at the given time.
--x264-tune TUNE What type of video to tune for.
Default: $x264_tune
--x264-profile PROFILE Encoding profile
Default: $x264_profile
--x264-preset PRESET Encoding preset
Default: $x264_preset
--video-codec Video codec to encode with
Default: $video_codec
--video-quality Video quality
Default: $video_quality
--audio-codec Audio codec to encode with
Default: $audio_codec
-T,--content-type TYPE MKV Content-Type
-K,--keyowrds WORD,[...] MKV Keywords
--ddrescue-passes 0,1,2,3 The number of ddrescue passes
--[no-]cleanup Delete files after ripping
-V,--version Print the version
-h,--help Print this message
Examples:
rip_dvd "Stargate I"
rip_dvd --dvd-title 3 "Stargate I"
rip_dvd --dvd-title 3 --chapters 2-40 "Stargate I"
rip_dvd --dvd-title 3 --start-at 42 --stop-at 1:45:00.500 "Stargate I" -- --detelecine
USAGE
}
function parse_options()
{
while (( $# > 0 )); do
case "$1" in
-d|--device)
dvd_device="$2"
shift 2
;;
-t|--dvd-title)
dvd_title="$2"
shift 2
;;
-c|--chapters)
chapters="$2"
first_chapter="${2%%-*}"
last_chapter="${2##*-}"
shift 2
;;
--start-at)
start_at="$2"
shift 2
;;
--stop-at)
stop_at="$2"
shift 2
;;
--video-codec)
video_codec="$2"
shift 2
;;
--video-quality)
video_quality="$2"
shift 2
;;
--audio-codec)
audio_codec="$2"
shift 2
;;
--x264-profile)
x264_profile="$2"
shift 2
;;
--x264-tune)
x264_tune="$2"
shift 2
;;
--x264-preset)
x264_preset="$2"
shift 2
;;
-T|--content-type)
mkv_content_type="$2"
shift 2
;;
-K|--keywords)
mkv_keywords="$2"
shift 2
;;
--cleanup)
cleanup="true"
shift
;;
--ddrescue-passes)
ddrescue_passes="$1"
shift 2
;;
--no-cleanup)
cleanup="false"
shift
;;
-V|--version)
echo "rip_dvd $rip_dvd_version"
exit
;;
-h|--help)
print_help
exit
;;
--)
shift
handbrake_opts+=($@)
break
;;
-*)
error "unknown option $1"
exit -1
;;
*)
if [[ -n "$title" ]]; then
error "additional argument $1"
exit -1
fi
title="$1"
shift
;;
esac
done
if [[ -z "$title" ]]; then
error "no title argument given"
exit -1
fi
output="${title}.mkv"
working_dir="$title"
}
function log()
{
if [[ -t 1 ]]; then
echo -e "\x1b[1m\x1b[32m>>>\x1b[0m \x1b[1m$1\x1b[0m"
else
echo ">>> $1"
fi
}
function warn()
{
if [[ -t 1 ]]; then
echo -e "\x1b[1m\x1b[33m***\x1b[0m \x1b[1m$1\x1b[0m" >&2
else
echo "*** $1" >&2
fi
}
function error()
{
if [[ -t 1 ]]; then
echo -e "\x1b[1m\x1b[31m!!!\x1b[0m \x1b[1m$1\x1b[0m" >&2
else
echo "!!! $1" >&2
fi
}
function fail()
{
error "$@"
exit -1
}
function load_dvd_info()
{
if [[ -z "$dvd_title" ]]; then
local line="$(lsdvd dvd.iso | tail -n 1)"
dvd_title="${line#Longest track: }"
fi
}
function time2s()
{
local time="$1"
local parts=(${time//:/ })
for i in $(seq 0 $(( ${#parts[@]} - 1 ))); do
parts[$i]=${parts[$i]#0}
done
case "${#parts[@]}" in
3) echo -n $(( ${parts[0]} * 3600 + ${parts[1]} * 60 + ${parts[2]} )) ;;
2) echo -n $(( ${parts[0]} * 60 + ${parts[1]} )) ;;
1) echo -n ${parts[0]} ;;
0) echo -n 0 ;;
esac
}
function time2frame()
{
local s="$(time2s "$1")"
echo -n $(( s * dvd_fps ))
}
function chapter2time()
{
local chapter="${1#\#}"
local line="$(dvdxchap -t "$dvd_title" "$dvd_device" | grep "CHAPTER0*${chapter}=")"
local time="${line#*=}"
echo -n "${time%.*}"
}
function chapter2frame()
{
time2frame "$(chapter2time "$1")"
}
function backup_file()
{
local file="$1"
if [[ -f "$file" ]]; then
cp "$file" "${file}.bak"
fi
}
function enter_working_directory()
{
mkdir -p "$working_dir"
pushd "$working_dir" > /dev/null
output="../$output"
}
function copy_dvd()
{
if [[ -f dvd.iso ]]; then
return
fi
ddrescue -b "$dvd_blocksize" -r 0 -n "$dvd_device" dvd.iso.part dvd.log
if (( ddrescue_passes > 1 )); then
ddrescue -b "$dvd_blocksize" -d "$dvd_device" -r 0 dvd.iso.part dvd.log
fi
if (( ddrescue_passes > 2 )); then
ddrescue -b "$dvd_blocksize" -d -R "$dvd_device" -r 0 dvd.iso.part dvd.log
fi
mv dvd.iso.part dvd.iso
}
function rip_dvd_title()
{
if [[ -f video.mkv ]]; then
return
fi
local options=(
--input dvd.iso
--format mkv
--output video.mkv.part
--markers
--use-opencl
)
if [[ -n "$dvd_title" ]]; then
options+=(--title "$dvd_title")
else
options+=(--main-feature)
fi
# seeking options
if [[ -n "$chapters" ]]; then
options+=(--chapters "$chapters")
fi
local start_at_frame stop_at_frame
if [[ -n "$start_at" ]]; then
if [[ "$start_at" == "#"* ]]; then
start_at_frame="$(chapter2frame "$start_at")"
else
start_at_frame="$(time2frame "$start_at")"
fi
options+=(--start-at "frame:${start_at_frame}")
fi
if [[ -n "$stop_at" ]]; then
if [[ "$stop_at" == "#"* ]]; then
stop_at_frame="$(chapter2frame "$stop_at")"
else
stop_at_frame="$(time2frame "$stop_at")"
fi
if [[ -n "$start_at_frame" ]]; then
stop_at_frame=$(( stop_at_frame - start_at_frame ))
fi
options+=(--stop-at "frame:${stop_at_frame}")
fi
# video options
options+=(
--encoder "$video_codec"
--quality "$video_quality"
--loose-anamorphic
)
case "$video_codec" in
x264)
options+=(
--x264-preset "$x264_preset"
--x264-profile "$x264_profile"
--x264-tune "$x264_tune"
)
;;
esac
# audio options
options+=(--aencoder "$audio_codec")
HandBrakeCLI "${options[@]}" "${handbrake_opts[@]}"
mv video.mkv.part video.mkv
}
function generate_tags_xml()
{
backup_file tags.xml
cat > tags.xml <<EOS
<Tags>
<Tag>
<Targets>
<TargetTypeValue>50</TargetTypeValue>
</Targets>
<Simple>
<Name>TITLE</Name>
<String>$title</String>
</Simple>
</Tag>
<Tag>
<Targets>
<TargetTypeValue>50</TargetTypeValue>
</Targets>
<Simple>
<Name>ORIGINAL_MEDIA_TYPE</Name>
<String>DVD</String>
</Simple>
</Tag>
EOS
if [[ -n "$mkv_content_type" ]]; then
cat >> tags.xml <<EOS
<Tag>
<Targets>
<TargetTypeValue>50</TargetTypeValue>
</Targets>
<Simple>
<Name>CONTENT_TYPE</Name>
<String>$mkv_content_type</String>
</Simple>
</Tag>
EOS
fi
if [[ -n "$mkv_keywords" ]]; then
cat >> tags.xml <<EOS
<Tag>
<Targets>
<TargetTypeValue>50</TargetTypeValue>
</Targets>
<Simple>
<Name>KEYWORDS</Name>
<String>$mkv_keywords</String>
</Simple>
</Tag>
EOS
fi
cat >> tags.xml <<EOS
<Tag>
<Targets>
<TargetTypeValue>50</TargetTypeValue>
</Targets>
<Simple>
<Name>RIP_DVD_VERSION</Name>
<String>$rip_dvd_version</String>
</Simple>
</Tag>
EOS
if [[ -n "$dvd_title" ]]; then
cat >> tags.xml <<EOS
<Tag>
<Targets>
<TargetTypeValue>50</TargetTypeValue>
</Targets>
<Simple>
<Name>RIP_DVD_TITLE</Name>
<String>$dvd_title</String>
</Simple>
</Tag>
EOS
fi
if [[ -n "$chapters" ]]; then
cat >> tags.xml <<EOS
<Tag>
<Targets>
<TargetTypeValue>50</TargetTypeValue>
</Targets>
<Simple>
<Name>RIP_DVD_CHAPTERS</Name>
<String>$chapters</String>
</Simple>
</Tag>
EOS
fi
if [[ -n "$start_at" ]]; then
cat >> tags.xml <<EOS
<Tag>
<Targets>
<TargetTypeValue>50</TargetTypeValue>
</Targets>
<Simple>
<Name>RIP_DVD_START_AT</Name>
<String>$start_at</String>
</Simple>
</Tag>
EOS
fi
if [[ -n "$stop_at" ]]; then
cat >> tags.xml <<EOS
<Tag>
<Targets>
<TargetTypeValue>50</TargetTypeValue>
</Targets>
<Simple>
<Name>RIP_DVD_STOP_AT</Name>
<String>$stop_at</String>
</Simple>
</Tag>
EOS
fi
if (( ${#handbrake_opts[@]} > 0 )); then
cat >> tags.xml <<EOS
<Tag>
<Targets>
<TargetTypeValue>50</TargetTypeValue>
</Targets>
<Simple>
<Name>RIP_DVD_HANDBRAKE_OPTS</Name>
<String>${handbrake_opts[@]}</String>
</Simple>
</Tag>
EOS
fi
cat >> tags.xml <<EOS
</Tags>
EOS
mkvpropedit video.mkv -t global:tags.xml >/dev/null
}
function leave_working_directory()
{
popd >/dev/null
output="${output#../}"
if [[ "$cleanup" == "true" ]]; then
mv "$working_dir/video.mkv" "$output"
rm -r "$working_dir"
fi
}
parse_options "$@"
enter_working_directory
log "Copying DVD $dvd_device ..."
copy_dvd
load_dvd_info
log "Ripping DVD \"$title\" ${start_at:+@$start_at${stop_at:+-$stop_at} }${chapters:+Chapters $chapters }..."
rip_dvd_title
log "DVD ripped!"
log "Adding tags"
generate_tags_xml
leave_working_directory
@dryaf
Copy link

dryaf commented Apr 21, 2018

thx for sharing. unfortunately it' didn't work on osx.
error:
ddrescue: Direct disc access not available

@lazind
Copy link

lazind commented Oct 8, 2022

Hello, I'm trying to run this on a raspberry pi. I had to comment out the --use-opencl because it would throw an error (unknown option). The other issue is that I see a bynch of ac3 frame sync errors. Is this any cause for concern?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment