#!/bin/bash err() { printf 'error: %b\n' "$*" 1>&2; exit 1; } usage() { cat <<-EOF backup-dvd [options] [commands] Options: -i Defaults to ${dev} -o Defaults to ${out} -n Defaults to volume on disk Commands: --errors Image the raw disc first w/ddrescue --backup Back up the disc --modify Tweak runtime settings (needs -n) --mkiso Create an iso (needs -n) If no commands are specified, then all are run. EOF exit ${1:-0} } dev=/dev/cdrom out=${PWD} vol= unset doit_{backup,modify,mkiso} doit_errors=false eval set -- `getopt -l errors,backup,modify,mkiso -- hi:n:o: "$@"` while [[ -n $1 ]] ; do case $1 in -h) usage;; -i) dev=$2; shift 2;; -o) out=$2; shift 2;; -n) vol=$2; shift 2;; --errors) doit_errors=true; shift;; --backup) doit_backup=true; shift;; --modify) doit_modify=true; shift;; --mkiso) doit_mkiso=true; shift;; --) shift; break;; -*) usage 1;; *) usage 2; break;; esac done doit_all() { : ${doit_backup:=$1} : ${doit_modify:=$1} : ${doit_mkiso:=$1} } if [[ -z ${doit_backup}${doit_modify}${doit_mkiso} ]] ; then doit_all true else doit_all false fi # iso-info mkisofs Application= # -A Preparer= # -p Publisher= # -publisher System= # -sysid Volume=${vol} # -V Volume_Set= # -volset if ${doit_backup} ; then if [[ -z ${Volume} ]] ; then info=$(iso-info ${dev}) || exit 1 eval $(echo "${info}" | awk -F: ' (NF > 1 && $1 !~ /image/) { sub(/ *$/, "", $1); sub(/ /, "_", $1); sub(/^ */, "", $2); print $1 "=\"" $2 "\""; }') fi if [[ -z ${Volume} ]] ; then echo "Unable to parse Volume out of ISO" iso-info ${dev} fi for v in Application Preparer Publisher System Volume Volume_Set ; do echo "${v}='${!v}'" done > "${out}/.${Volume}.vars.sh" else [[ -z ${Volume} ]] && err "Need to specify -n with --modify/--mkiso" . "${out}/.${Volume}.vars.sh" || exit 1 fi e() { for a ; do [[ ${a} == *" "* || ${#a} == 0 ]] && fmt='"%s"' || fmt='%s' printf "${fmt} " "${a}" done echo "$@" } # # Mirror the disc w/ddrescue which tolerates errors. # raw_read_dvd() { echo "Imaging disc" local raw=".${Volume}.raw" local log="${raw}.log" local opts=( -v -b 2048 ${dev} "${raw}" "${log}" ) ddrescue -p -n "${opts[@]}" ddrescue -d -r 3 "${opts[@]}" ddrescue -d -r 3 -R "${opts[@]}" dev=${raw} } # # Backup the disc. # backup_dvd() { echo "Backing up: ${Volume}" local opts=( -i ${dev} -o "${out}" -n "${Volume}" ) sync if ! e dvdbackup -M "${opts[@]}" ; then e dvdbackup -F "${opts[@]}" || exit 1 fi sync } # # Modify some of the runtime settings. # bytes_get() { local pfx='0x' sep while [[ $# -gt 3 ]]; do case $1 in "-p") pfx=$2;; "-s") sep=$2;; *) break;; esac shift 2 done printf '%b' "${pfx}" local file=$1 off=$2 num=${3:-1} hexdump -v -n ${num} -s $((${off})) -e "1/1 \"%02x${sep}\"" "${file}" } bytes_set() { local file=$1 off=$2 shift 2 (for b; do printf "\x${b#0x}"; done) | \ dd of="${file}" \ bs=1 seek=$((${off})) \ conv=notrunc status=none } dvdsec() { # DVDs have sectors of 2048 bytes (2^11). local file=$1 off=$2 num=${3:-4} echo $(( $(bytes_get "${file}" "${off}" "${num}") << 11 )) } _check_ifo() { # Back up the .IFO files before we edit them. local ifo=$1 magic=$2 [[ -e ${ifo} ]] || return 1 if [[ $(bytes_get "${ifo}" 0 12) != "0x${magic}" ]] ; then echo "not a VMG IFO file: ${ifo}" return 1 fi if [[ ! -e ${ifo}.bak ]] ; then cp -a "${ifo}" "${ifo}.bak" fi return 0 } check_changes() { [[ -e ifodump ]] || return 0 local file=$1 ./ifodump -f "${file}.bak" > "${file}.bak.dmp" ./ifodump -f "${file}" > "${file}.dmp" vapier-diff "${file}.bak.dmp" "${file}.dmp" | sed -e 1d -e 2d > "${file}.diff" local out=$( grep '^[+-]' "${file}.diff" | \ grep -v \ -e '^-VMG Category:.*' \ -e '^+VMG Category: 00000000 (Region Code=ff)' \ -e '^[-+].*Title playback type:' \ -e '^-.*Title or time play:1' \ -e '^+.*Title or time play:0' \ -e '^-Prohibited user operations:' \ -e '^+Prohibited user operations: Angle Change, $' \ -e '^+Prohibited user operations: None$' \ | egrep -v -e '^[-+]([0-9a-f]{2} )+$' ) rm "${file}".{{,bak.}dmp,diff} if [[ -z ${out} ]] ; then return 0 else echo "error in ifo modification:" echo "${out}" return 1 fi } process_pgci() { # http://dvdnav.mplayerhq.hu/dvdinfo/pgc.html local file=$1 pgci_off=$2 indent=$3 local num_pgcs p pgc_off off num_pgcs=$(bytes_get "${file}" ${pgci_off} 2) for (( p = 0; p < num_pgcs; ++p )) ; do pgc_off=$(( pgci_off + $(bytes_get "${file}" $(( pgci_off + 8 + 8 * p + 4 )) 4) )) off=$(( pgc_off + 8 )) pgc_puo=$(bytes_get "${file}" ${off} 4) printf '%bpgc #%2i @ %#06x: %s: ' "${indent}" ${p} ${vmgm_pgc_off} ${pgc_puo} new_pgc_puo=$(( pgc_puo & (1 << 22) )) # Keep angle field. if [[ ${pgc_puo} -ne ${new_pgc_puo} ]] ; then bytes_set "${file}" ${off} 0 $(printf '%x' $(( new_pgc_puo >> 16 ))) 0 0 echo "cleared" else echo "nothing to do" fi done } process_pgci_ut() { # http://dvdnav.mplayerhq.hu/dvdinfo/ifo_vts.html#pgciut local file=$1 pgci_ut_off=$2 local num_langs l off num_langs=$(bytes_get "${file}" ${pgci_ut_off} 2) for (( l = 0; l < num_langs; ++l )) ; do off=$(( pgci_ut_off + $(bytes_get "${file}" $(( pgci_ut_off + 8 + 8 * l + 4 )) 4) )) printf '\tclearing lang #%i @ %#06x\n' ${l} ${off} process_pgci "${file}" ${off} '\t\t' done } process_vmgi() { # Back up the .IFO files before we edit them. local vmgi=$1 _check_ifo "${vmgi}" "445644564944454f2d564d47" || return 1 echo "${vmgi}" # Clear the region code. # http://dvdnav.mplayerhq.hu/dvdinfo/ifo.html # 0x23 - region code restrict byte - set to 0x00 for region free. echo "clearing region code" bytes_set "${vmgi}" 0x23 0x00 # Clear all Prohibited User Operations (PUOs) in TT_SRPT (Uop1 & Uop0). # http://dvdnav.mplayerhq.hu/dvdinfo/ifo_vmg.html echo "clearing PUOs in TT_SRPT" tt_srpt_sec=$(dvdsec "${vmgi}" 0xC4) num_titles=$(bytes_get "${vmgi}" ${tt_srpt_sec} 2) for (( t = 0; t < num_titles; ++t )) ; do off=$(( tt_srpt_sec + 8 + (t * 12) )) printf "\ttitle type #%2i @ %#06x: " "${t}" "${off}" title_type=$(bytes_get "${vmgi}" ${off} 1) uop0=$(( title_type & 1 )) uop1=$(( title_type & 2 )) printf "%s (uop1:%i uop0:%i): " ${title_type} ${uop1} ${uop0} new_title_type=$(( title_type & ~3 )) if [[ ${title_type} -ne ${new_title_type} ]] ; then bytes_set "${vmgi}" ${off} $(printf '%x' ${new_title_type}) echo "cleared" else echo "nothing to do" fi done # Clear all the PUOs in the PGCs. But Preserve the angle bit. # Note: Removal of the angle PUO can cause certain standalone players to # display the "angle" icon during playback. Removal of the angle PUO # whilst permitted is therefore not recommended. # http://dvdnav.mplayerhq.hu/dvdinfo/uops.html #fp_pgc_addr=$(bytes_get "${vmgi}" 0x84 4) #num_pgcs=$(bytes_get "${vmgi}" $(( fp_pgc_sec + 1 )) 1) echo "clearing PUOs in PGCs" vmgm_pgci_ut_sec=$(dvdsec "${vmgi}" 0xC8) process_pgci_ut "${vmgi}" ${vmgm_pgci_ut_sec} } process_vtsi() { # Back up the .IFO files before we edit them. local vtsi=$1 _check_ifo "${vtsi}" "445644564944454f2d565453" || return 1 echo "${vtsi}" echo "clearing PUOs in PGCs" vts_pgci_sec=$(dvdsec "${vtsi}" 0xCC) process_pgci "${vtsi}" ${vts_pgci_sec} '\t' vtsm_pgci_ut_sec=$(dvdsec "${vtsi}" 0xD0) process_pgci_ut "${vtsi}" ${vtsm_pgci_ut_sec} } modify_dvd() { local out_vts_dir="${out}/${Volume}/VIDEO_TS" local vmgi vtsi if ${doit_errors} ; then echo "skipping modifications due to possible errors in files" return fi for vmgi in "${out_vts_dir}"/VIDEO_TS.{IFO,BUP} ; do process_vmgi "${vmgi}" check_changes "${vmgi}" || exit 1 done for vtsi in "${out_vts_dir}"/VTS_??_?.{IFO,BUP} ; do process_vtsi "${vtsi}" check_changes "${vtsi}" || exit 1 done } # # Finally, create a new iso. # mkiso_dvd() { set -- mkisofs -quiet -dvd-video -no-bak \ -A "${Application}" \ -p "${Preparer}" \ -publisher "${Publisher}" \ -sysid "${System}" \ -V "${Volume}" \ -volset "${Volume_Set}" \ -o "${Volume}.iso" "${Volume}" sh="${out}/.${Volume}.sh" ( echo "#!/bin/sh" echo "cd '${out}' || exit 1" echo ". './.${Volume}.vars.sh'" printf 'set -- ' printf '%q ' "$@" echo echo 'echo "$@"; exec "$@"' ) > "${sh}" chmod a+x "${sh}" "${sh}" || exit sync md5sum "${out}/${Volume}.iso" > "${out}/${Volume}.md5" du -h "${out}/${Volume}.iso" sudo chattr +i "${out}/${Volume}.iso" sync } ${doit_errors} && raw_read_dvd ${doit_backup} && backup_dvd ${doit_modify} && modify_dvd ${doit_mkiso} && mkiso_dvd ${doit_backup} && eject exit 0