As threatened, I have an update for my HandBrake wrapper script. On a Mac or Linux system, the script examines one or more DVD folders (I rip mine with MacTheRipper), and converts the DVD MPEG2 content to MP4 files suitable for Apple TV (by default) or iPhone (with an iphone argument, e.g., hb.sh iphone). It's easy to hack up for different preferences, and will require changing some variables to match your preferred media folder layout. Thanks to Brian Beardmore for the original GPL script.

The required customizations are these two lines:

inputSearchDir="$HOME/tivo-inspector/input/$mySuffix"
outputDir="$HOME/tivo-inspector/output"

The main difference in v1.0.7 of the script is that it uses HandBrake 0.9.3's new improved presets, rather than hard-coding my custom settings. I've found the new presets give excellent quality in considerably less time.

If you have trouble compiling the Linux CLI, see my earlier post about v0.9.3 (I didn't try the GUI).

You can download hb.sh v1.0.7 (recommended), or copy & paste, but might have to fix line wrapping & remove blog cruft if you do.


#!/bin/bash

# hb.sh uses HandBrakeCLI to convert whole DVDs automatically.
# Copyright (C) 2007  Brian Beardmore
# Copyright (C) 2008  Chris Pepper

#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

# This script requires HandBrakeCLI <http://handbrake.fr/>.

# This script searches the entire input directory directory tree looking
# for TS_VIDEO directories looking for DVDs to encode.  The output mp4
# filename is the DVD volume name.  When multiple tracks are to encoded 
# from a DVD, the output filename is appended with the track number.
# For example, 'Monsters, Inc.' DVD has both standard and wide screen tracks
# so there are 'Monsters, Inc.1.mp4' & 'Monsters, Inc.10.mp4' files created.
# For addition information on this script contact the script's original author,
# Brian Beardmore at medfk(at)realisticsoftware(dot)com or
# http://www.realisticsoftware.com
# Or contact Chris Pepper <http://www.extrapepperoni.com/category/television/>.

# Examples
# $ ./appletv.sh
#   [ encodes all DVDs mounted and encodes any tracks longer than 60min ]
#   [ to the mp4 folder in the users Movies folder ]
# $ appletv.sh -i ~/Movies -o toPod -b 1000 -w 576 --minGetTime 50
#   [ encodes all DVDs found in the users Movies folder and encodes any ]
#   [ tracks longer than 50min, with a 1000kb/s bitrate and scales the output ]
#   [ movie to a width of 576 pixels and puts the resulting mp4 files in the ]
#   [ toPod folder in the current directory that the command was executed from ]

# Syntax: HandBrakeCLI [options] -i <device> -o <file>
# 
# ### General Handbrake Options------------------------------------------------
# 
#     -h, --help              Print help
#     -u, --update            Check for updates and exit
#     -v, --verbose <#>       Be verbose (optional argument: logging level)
#     -C, --cpu               Set CPU count (default: autodetected)
#     -Z. --preset <string>   Use a built-in preset. Capitalization matters, and
#                             if the preset name has spaces, surround it with
#                             double quotation marks
#     -z, --preset-list       See a list of available built-in presets
# 
# ### Source Options-----------------------------------------------------------
# 
#     -i, --input <string>    Set input device
#     -t, --title <number>    Select a title to encode (0 to scan only,
#                             default: 1)
#     -L, --longest           Select the longest title
#     -c, --chapters <string> Select chapters (e.g. "1-3" for chapters
#                             1 to 3, or "3" for chapter 3 only,
#                             default: all chapters)
# 
# ### Destination Options------------------------------------------------------
# 
#     -o, --output <string>   Set output file name
#     -f, --format <string>   Set output format (avi/mp4/ogm/mkv, default:
#                             autodetected from file name)
#     -m, --markers           Add chapter markers (mp4 and mkv output formats only)
#     -4, --large-file        Use 64-bit mp4 files that can hold more than
#                             4 GB. Note: Breaks iPod, PS3 compatibility.
#     -O, --optimize          Optimize mp4 files for HTTP streaming
#     -I, --ipod-atom         Mark mp4 files so 5.5G iPods will accept them
# 
# ### Video Options------------------------------------------------------------
# 
#     -e, --encoder <string>  Set video library encoder (ffmpeg,xvid,
#                             x264,theora default: ffmpeg)
#     -x, --x264opts <string> Specify advanced x264 options in the
#                             same style as mencoder:
#                             option1=value1:option2=value2
#     -q, --quality <float>   Set video quality (0.0..1.0)
#     -Q, --cqp               Use with -q for CQP instead of CRF
#     -S, --size <MB>         Set target size
#     -b, --vb <kb/s>         Set video bitrate (default: 1000)
#     -2, --two-pass          Use two-pass mode
#     -T, --turbo             When using 2-pass use the turbo options
#                             on the first pass to improve speed
#                             (only works with x264, affects PSNR by about 0.05dB,
#                             and increases first pass speed two to four times)
#     -r, --rate              Set video framerate (5/10/12/15/23.976/24/25/29.97)
#                             Be aware that not specifying a framerate lets
#                             HandBrake preserve a source's time stamps,
#                             potentially creating variable framerate video
# 
# ### Audio Options-----------------------------------------------------------
# 
#     -a, --audio <string>    Select audio track(s), separated by commas
#                             More than one output track can be used for one
#                             input.
#                             ("none" for no audio, "1,2,3" for multiple
#                              tracks, default: first one)
#     -E, --aencoder <string> Audio encoder(s) (faac/lame/vorbis/ac3) 
#                             ac3 meaning passthrough
#                             Separated by commas for more than one audio track.
#                             (default: guessed)
#     -B, --ab <kb/s>         Set audio bitrate(s)  (default: 160)
#                             Separated by commas for more than one audio track.
#     -6, --mixdown <string>  Format(s) for surround sound downmixing
#                             Separated by commas for more than one audio track.
#                             (mono/stereo/dpl1/dpl2/6ch, default: dpl2)
#     -R, --arate             Set audio samplerate(s) (22.05/24/32/44.1/48 kHz)
#                             Separated by commas for more than one audio track.
#     -D, --drc <float>       Apply extra dynamic range compression to the audio,
#                             making soft sounds louder. Range is 1.0 to 4.0
#                             (too loud), with 1.5 - 2.5 being a useful range.
#                             Separated by commas for more than one audio track.
#     -A, --aname <string>    Audio track name(s),
#                             Separated by commas for more than one audio track.
# 
# ### Picture Settings---------------------------------------------------------
# 
#     -w, --width <number>    Set picture width
#     -l, --height <number>   Set picture height
#         --crop <T:B:L:R>    Set cropping values (default: autocrop)
#     -Y, --maxHeight <#>     Set maximum height
#     -X, --maxWidth <#>      Set maximum width
#     -p, --pixelratio        Store pixel aspect ratio in video stream
#     -P, --loosePixelratio   Store pixel aspect ratio with specified width
#           <MOD:PARX:PARY>   Takes as optional arguments what number you want
#                             the dimensions to divide cleanly by (default 16)
#                             and the pixel ratio to use (default autodetected)
#     -M  --color-matrix      Set the color space signaled by the output
#           <601 or 709>      (Bt.601 is mostly for SD content, Bt.709 for HD,
#                              default: set by resolution)
# 
# ### Filters---------------------------------------------------------
# 
#     -d, --deinterlace       Deinterlace video with yadif/mcdeint filter
#           <YM:FD:MM:QP>     (default 0:-1:-1:1)
#            or
#           <fast/slow/slower>
#     -5, --decomb            Selectively deinterlaces when it detects combing
#           <MO:ME:MT:ST:BT:BX:BY>     (default: 1:2:6:9:80:16:16)
#     -9, --detelecine        Detelecine (ivtc) video with pullup filter
#                             Note: this filter drops duplicate frames to
#                             restore the pre-telecine framerate, unless you
#                             specify a constant framerate (--rate 29.97)
#           <L:R:T:B:SB:MP>   (default 1:1:4:4:0:0)
#     -8, --denoise           Denoise video with hqdn3d filter
#           <SL:SC:TL:TC>     (default 4:3:6:4.5)
#            or
#           <weak/medium/strong>
#     -7, --deblock           Deblock video with pp7 filter
#           <QP:M>            (default 5:2)
#     -g, --grayscale         Grayscale encoding
# 
# ### Subtitle Options------------------------------------------------------------
# 
#     -s, --subtitle <number> Select subtitle (default: none)
#     -U, --subtitle-scan     Scan for subtitles in an extra 1st pass, and choose
#                             the one that's only used 10 percent of the time
#                             or less. This should locate subtitles for short
#                             foreign language segments. Best used in conjunction
#                             with --subtitle-forced.
#     -F, --subtitle-forced   Only display subtitles from the selected stream if
#                             the subtitle has the forced flag set. May be used in
#                             conjunction with --subtitle-scan to auto-select
#                             a stream if it contains forced subtitles.
#     -N, --native-language   Select subtitles with this language if it does not
#           <string>          match the Audio language. Provide the language's
#                             iso639-2 code (fre, eng, spa, dut, et cetera)

# Revision history:
# 1.0.7, 2008/12 -- Update for HandBrake 0.9.3, and use presets.
# 1.0.6 -- never released.
# 1.0.5, 2008/08 -- Require bash, per ebb.
# 1.0.4, 2008/06 -- Use different suffices so different flavors can coexist.
# 1.0.3, 2008/05 -- Add basic argument processing, so "appletv" and "iphone" as argument #1 produce different output.
# 1.0.2, 2008/05 -- hacks by Pepper to use HandBrakeCLI and optimize for AppleTV.
# 1.0.1, 2008/02/24 -- hacks by Pepper to use HandBrakeCLI and optimize for iPhone.
# 0.20070329.0 - initial release


#############################################################################
# globals

# const global variables
scriptName=`basename "$0"`
scriptVers="1.0.7"
scriptPID=$$
saveDVDinfo=1       # the DVD track info is also saved as .txt file when set
skipDuplicates=1    # if this option is off, overwrite existing files
E_BADARGS=65

if [[ $# = 0 ]]
 then
  myArgs='$HANDBRAKE_ARGS -Z AppleTV'
elif [[ $1 = "appletv" ]]
 then
  myArgs='$HANDBRAKE_ARGS -Z AppleTV'
  mySuffix=AppleTV
elif [[ $1 = "AppleTV" ]]
 then
  myArgs='$HANDBRAKE_ARGS -Z AppleTV'
  mySuffix=AppleTV
elif [[ $1 = "iphone" ]]
 then
  myArgs='$HANDBRAKE_ARGS -Z "iPhone & iPod Touch"'
  mySuffix=iPhone
elif [[ $1 = "iPhone" ]]
 then
  myArgs='$HANDBRAKE_ARGS -Z "iPhone & iPod Touch"'
  mySuffix=iPhone
else
 echo "Unknown format -- aborting!"
 exit 1
fi

# set the global variables to defaults
toolName="HandBrakeCLI"
toolPath="$HOME/bin/$toolName"
toolTrackArgs="-t 0"
toolArgs="-v"
inputSearchDir="$HOME/tivo-inspector/input/$mySuffix"
outputDir="$HOME/tivo-inspector/output"
minTrackTime="3"    # in minutes


# Pepper's sometime options:
# -s 1 # first subtitle
# -a 2 # second audio track ??
# For example (best within screen): HANDBRAKE_ARGS="-s1 -t11" iphone.sh


#############################################################################
# functions

parseProcessInArgs()
{
    if [ -z "$1" ]; then
        return
    fi

    toolArgs=""

    while [ ! -z "$1" ]
    do
        case "$1" in
            -h) displayUsageExit ;;
            --help) displayUsageExit ;;
            -i) inputSearchDir=$2
                shift ;;
            --input) inputSearchDir=$2
                shift ;;
            -o) outputDir=$2
                shift ;;
            --output) outputDir=$2
                shift ;;
            --minGetTime) minTrackTime=$2
                shift ;;
            *) toolArgs="$toolArgs $1" ;;
        esac

        shift
    done
}

verifyFindCLTool()
{
    # attempt to find the HandBrakeCLI if the script toolPath is not good
    if [ ! -x "$toolPath" ];
    then
        toolPathTMP=`PATH=.:/Applications:/:/usr/bin:/usr/local/bin:$HOME:$PATH which $toolName | sed '/^[^\/]/d' | sed 's/\S//g'`

        if [ ! -z $toolPathTMP ]; then 
            toolPath=$toolPathTMP
        fi
    fi  
}

displayUsageExit()
{
    echo "Usage: $scriptName [options]"
    echo ""
    echo "    -h, --help              Print help"
    echo "    -i, --input <string>    Set input directory to process all DVDs in it (default: /Volumes/)"
    echo "    -o, --output <string>   Set output directory for all output files (default: ~/Movies/mp4/)"
    echo "    --minGetTime <number>   Set the minimum time (mins) of the track/s to encode (default: 60)"

    if [ -x "$toolPath" ];
    then
        echo "   $toolName possible options"
        hBrakeHelp=`$toolPath --help 2>&1`
        hBrakeHelpPt=`printf "$hBrakeHelp" | egrep -v '( --input| --output| --help|Syntax: |^$)'`
        printf "$hBrakeHelpPt\n"
    else
        echo "    The options available to HandBrakeCLI except -o  and -i"
        if [ -e "$toolPath" ];
        then
            echo "    ERROR: $toolName command tool is not set up to execute"
            echo "    ERROR: attempting to use tool at $toolPath"
        else
            echo "    ERROR: $toolName command tool could not be found"
            echo "    ERROR: $toolName can be installed in ./ /usr/local/bin/ /usr/bin/ ~/ or /Applications/"
        fi
    fi

    echo ""

    exit $E_BADARGS
}

getTrackListLongerThan()
{
    # Two input arguments are are need. 
    #   arg1 is the time in minutes selector
    #   arg2 is the raw text stream from the track 0 call to HandBrake
    #   returns: a list of track numbers of tracks longer than the selector

    if [ $# -lt 2 ]; then
        return ""
    fi

    minTime="$1"
    shift
    allTrackText="$*"
    aReturn=""

    trackList=`eval "echo \"$allTrackText\" | egrep '(^\+ title |\+ duration\:)' | sed -e 's/^[^+]*+ //'g -e 's/title \([0-9]*\):/\1-/'g -e 's/duration: //'g"`

    trackNumber=""
    for aline in $trackList
    do
        trackLineFlag=`echo $aline | sed 's/[0-9]*-$/-/'`
        if [ $trackLineFlag = "-" ];
        then
            trackNumber=`echo $aline | sed 's/\([0-9]*\)-/\1/'`
        else
            set -- `echo $aline | sed -e 's/(^[:0-9])//g' -e 's/:/ /g'`
            if [ $3 -gt 29 ];
            then let trackTime=($1*60)+$2+1
            else let trackTime=($1*60)+$2
            fi

            if [ $trackTime -gt $minTime ];
            then aReturn="$aReturn $trackNumber"
            fi
        fi
    done

    echo "$aReturn"
}

makeFullPath()
{
    aReturn=""
    currentPath=`pwd`

    if [ $# -gt 0 ]; then
        inPath="$*"

        # put full path in front of path if needed
        aReturn=`echo "$inPath" | sed -e "s!~!$currentPath/!" -e "s!^./!$currentPath/!" -e "s!^\([^/]\)!$currentPath/\1!" -e "s!^../!$currentPath/../!"`

        # remove ../ from path - only goes 4 deep
        aReturn=`echo "$aReturn" | sed -e 's!/[^\.^/]*/\.\./!/!g' | sed -e 's!/[^\.^/]*/\.\./!/!g' | sed -e 's!/[^\.^/]*/\.\./!/!g' | sed -e 's!/[^\.^/]*/\.\./!/!g'`

        # cleanup by removing //
        aReturn=`echo "$aReturn" | sed -e 's!//!/!g'`
    fi

    echo "$aReturn"
}

isPIDRunning()
{
    aResult=0

    if [ $# -gt 0 ]; then
        txtResult="`ps ax | egrep \"^[ \t]*$1\" | sed -e 's/.*/1/'`"
        if [ -z "$txtResult" ];
        then aResult=0
        else aResult=1
        fi
    fi

    echo $aResult
}

#############################################################################
# MAIN SCRIPT

# initialization functions
verifyFindCLTool
parseProcessInArgs $*
# see if the output directory needs to be created
if [ ! -e $outputDir ]; then
    mkdir -p "$outputDir"
fi

# sanity checks
if [[ ! -x $toolPath || ! -d $inputSearchDir || ! -d $outputDir || -z "$toolArgs" ]]
 then

  if [[ ! -x $toolPath ]]
   then echo "ERROR: $toolPath is not executable!"
  fi
  if [[ ! -d $inputSearchDir ]]
   then echo "ERROR: $inputSearchDir is not a valid input directory!"
  fi
  if [[ ! -d $outputDir ]]
   then echo "ERROR: $outputDir is not a valid output directory!"
  fi
  if [[ ! -z "$toolArgs" ]]
   then echo "ERROR: $toolArgs is unset!"
  fi

    displayUsageExit
fi

# fix input and output paths to be full paths
inputSearchDir=`makeFullPath $inputSearchDir`
outputDir=`makeFullPath $outputDir`

# display the basic setup information
echo "$scriptName v$scriptVers"
echo "  Start: `date`"
echo "  Input directory: $inputSearchDir"
echo "  Output directory: $outputDir"
echo "  Minimum get track time: $minTrackTime mins"
echo "  Tool path: $toolPath"
echo "  Tool args: $toolArgs"
echo "  My args: $myArgs"
echo "  - - - - - - - - - - - - - - - -"

# find all the DVD videos in the input search directory tree
# spaces in file path temporarily become /008 and paths are separated with spaces
dvdTSVidList=`find $inputSearchDir -name VIDEO_TS -print0 | tr ' ' '\007' | tr '\000' ' '`

# process each DVD video found
for dvdTSDir in $dvdTSVidList
do
    # correct the tmp char back to spaces in the DVD file paths
    dvdTSDir=`echo $dvdTSDir | tr '\007' ' '`

    # get the DVD's name and path to root of the DVD
    dvdVolPath=`dirname "$dvdTSDir"`
    dvdName=`basename "$dvdVolPath"`
    dvdNameALNUM=`basename "$dvdVolPath" | sed 's/[^[:alnum:]^-^_]//g'`

    # display information
    echo "  * Processing DVD '$dvdName'"

    # create tmp link to the dvdVolPath to workaround a problem that
    # the HandBrakeCLI tool has a problem with spaces in the input
    # file paths in a script
    tmpNoSpacePath="/tmp/dvdVol-$dvdNameALNUM-$scriptPID"
    ln -s "$dvdVolPath" $tmpNoSpacePath

    # get the track list information from the DVD
    cmd="$toolPath -i $tmpNoSpacePath $toolTrackArgs /dev/null 2>&1"
    dvdTrackInfo=`eval $cmd`
    # save the DVD info
    outputFilePath="$outputDir/${dvdName}.txt"
    if [ $saveDVDinfo -eq 1 ]; then
        if [[ ! -e  $outputFilePath || skipDuplicates -eq 0 ]]; then
            echo "$dvdTrackInfo" | egrep '[ \t]*\+' > "$outputFilePath"
        fi
    fi
    # get the track number of tracks which are longer then the time desired
    trackFetchList=`getTrackListLongerThan $minTrackTime "$dvdTrackInfo"`
    if [ ! -z "$trackFetchList" ];
    then
        echo "   Will encode the following tracks: `echo $trackFetchList | sed 's/ /, /g'` "
    else
        echo "   No tracks on this DVD are longer than the minimum track time setting"
    fi

    trackCount=`echo $trackFetchList | wc -w`
    for aTrack in $trackFetchList
    do
        if [ $trackCount -gt 1 ]
            then outputFilePath="$outputDir/${dvdName}-${aTrack}.$mySuffix.m4v"
            else outputFilePath="$outputDir/${dvdName}.$mySuffix.m4v"
        # .m4v is important for AppleTV
        fi
        cmd="$toolPath $myArgs -i $tmpNoSpacePath $toolArgs -t $aTrack -o \"$outputFilePath\" > /tmp/${dvdNameALNUM}Results.txt 2>&1"

        if [[ ! -e  $outputFilePath || skipDuplicates -eq 0 ]];
        then
            # simple command execution
            #ripResult=`eval $cmd`

            # background command execution with some status

            echo "Command is: $cmd"

            eval $cmd &
            cmdPID=$!
            while [ `isPIDRunning $cmdPID` -eq 1 ]; do
                cmdStatusTxt="`tail -n 1 /tmp/${dvdNameALNUM}Results.txt | grep 'Encoding: '`"
                if [ ! -z "$cmdStatusTxt" ]; then
                    echo -n "$cmdStatusTxt"
                fi
                sleep 1s
            done
            echo ""
            wait $cmdPID

        else
            echo "   Output file SKIPPED because it ALREADY EXISTS"
        fi

        if [ -e /tmp/${dvdNameALNUM}Results.txt ]; then
            rm /tmp/${dvdNameALNUM}Results.txt
        fi
    done

    rm $tmpNoSpacePath
done

echo "  - - - - - - - - - - - - - - - -"
echo "  End: `date`"

exit 0