#!/bin/sh
# Copyright (c) 1998,1999 Robert Woodcock <rcw@debian.org>
# This code is hereby licensed for public consumption under either the
# GNU GPL v2 or greater, or Larry Wall's Artistic license - your choice.
#
# 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.

VERSION=0.7.6

usage ()
{
echo "This is cdgrab v$VERSION."
echo "Usage: cdgrab [options] [tracks]"
echo "Options:"
echo "-d    Specify CDROM device to grab"
echo "-e    Edit returned CDDB information before using"
echo "-h    This help information"
echo "-j    Number of encoder processes to run at once"
echo "-l [num] Low disk space handling option"
echo "      1 = normal parallelization"
echo "      2 = disk space conservation"
echo "-p    Create m3u playlist"
echo "-P    Only create m3u playlist (do no CD ripping)"
echo "Tracks is a space-delimited list of tracks to grab."
echo "No wildcards accepted yet."
}

# Builtin defaults
CDDBURL="http://cddb.cddb.com/~cddb/cddb.cgi"
HELLOINFO="`whoami`+`hostname`+cdgrab+$VERSION"
CDROM=/dev/cdrom
CDROMREADERSYNTAX=cdparanoia
ENCODERSYNTAX=lame
OUTPUTFORMAT='${ARTISTFILE}/${TRACKFILE}.mp3'
PLAYLISTFORMAT='${ARTISTFILE}_-_${ALBUMFILE}.m3u'
LOWDISK=1

# program paths - defaults to checking your $PATH
LAME=lame
BLADEENC=bladeenc
L3ENC=l3enc
ID3=id3
CDPARANOIA=cdparanoia
CDDA2WAV=cdda2wav
WGET=wget
CDDISCID=cd-discid

# Options for programs called from cdgrab
LAMEOPTS=
BLADEENCOPTS=
L3ENCOPTS=
ID3OPTS=
CDPARANOIAOPTS=
CDDA2WAVOPTS=
WGETOPTS=

# Default to one process if -j isn't specified
MAXPROCS=1

# Momma always taught me not to use no double negatives :)
# If NOCDDB is set to y, no CDDB read is done
# If NOID3 is set to y, no ID3 tagging is done
NOCDDB=n
NOID3=n

if [ "$OUTPUTDIR" = "" ]; then
	OUTPUTDIR=`pwd`
fi
# Load system defaults
if [ -r /etc/cdgrab.conf ]; then
	. /etc/cdgrab.conf
fi
# Load user preference defaults
if [ -r $HOME/.cdgrab.conf ]; then
	. $HOME/.cdgrab.conf
fi

# Parse command line options
while getopts d:ehj:l:pP opt ; do
	case "$opt" in
		d) CDROM="$OPTARG" ;;
		e) EDITCDDB="y" ;;
		j) MAXPROCS="$OPTARG" ;;
		h) usage; exit ;;
		l) LOWDISK="$OPTARG" ;;
		p) PLAYLIST="y" ;;
		P) PLAYLISTONLY="y"; PLAYLIST="y" ;;
		?) usage; exit ;;
	esac
done

if [ $LOWDISK -lt 1 ] || [ $LOWDISK -gt 2 ]; then
	echo    "cdgrab: invalid lowdisk option argument."
	echo    "Lowdisk options:"
	echo -e "1:\tDefaults (Parallelize entire CD)"
	echo -e "2:\tLowest disk usage (No parallelization)"
	echo -e "3:\tRip timing prediction (best case parallelization, not yet implemented)"
	exit 1
fi

shift $(($OPTIND - 1))

while [ $# -gt 0 ]; do
	TRACKQUEUE=`echo "$TRACKQUEUE" $1`
	shift
done

# Decide which CDROM reader we're gonna use
case "$CDROMREADERSYNTAX" in
	cdparanoia|debug)
		CDROMREADER="$CDPARANOIA"
		CDROMREADEROPTS="$CDPARANOIAOPTS"
		;;
	cdda2wav)
		CDROMREADER="$CDDA2WAV"
		CDROMREADEROPTS="$CDDA2WAVOPTS"
		;;
esac

# and which encoder
case "$ENCODERSYNTAX" in
	lame)
		ENCODEROPTS="$LAMEOPTS"
		ENCODER="$LAME"
		;;
	bladeenc)
		ENCODEROPTS="$BLADEENCOPTS"
		ENCODER="$BLADEENC"
		;;
	l3enc)
		ENCODEROPTS="$L3ENCOPTS"
		ENCODER="$L3ENC"
		;;
esac
	
# Make sure a buncha things exist
for X in $CDROMREADER $CDDISCID $ID3 $ENCODER $WGET
do
	# Cut off the command-line options we just added in
	X=`echo $X | cut -d' ' -f2`
	if [ "`which $X`" = "" ]; then
		echo "cdgrab error: $X is not in your path."
		exit 1
	elif [ \! -x `which $X` ]; then
		echo "cdgrab error: $X is not executable."
		exit 1
	fi 
done

CDROMREADER="$CDROMREADER $CDROMREADEROPTS"

# Query the CD to get the track info
echo -n "Getting CD track info... "
TRACKINFO=`$CDDISCID $CDROM`

# Make sure there's a CD in there by checking cd-discid's return code
if [ "$?" = "1" ]; then
	echo "cdgrab error: CD could not be read. Perhaps there's no CD in the drive?"
	exit 1
fi

# Get a full enumeration of tracks, sort it, and put it in the TRACKQUEUE.

TRACKS=`echo $TRACKINFO | cut -f2 -d' '`

if [ "$TRACKQUEUE" = "" ]; then
	echo -n "Grabbing entire CD - tracks: "
	X=0
	while [ "$X" != "$TRACKS" ]
	do
		X=`expr $X + 1`
		TRACKQUEUE=`echo "$TRACKQUEUE" $X`
	done
	echo $TRACKQUEUE
else
	TRACKQUEUE=`(for X in $TRACKQUEUE; do echo $X; done) | sort | uniq | xargs`
	echo Grabbing tracks: "$TRACKQUEUE"
fi

# TRACKQUEUE has now been sorted - get the last variable in the list
for X in $TRACKQUEUE; do :; done
# get the number of digits to pad TRACKNUM with - we'll use this down below
TRACKNUMPADDING=`echo -n $X | wc -c | xargs`
TRACKINFOPLUS=`echo $TRACKINFO | sed 's/ /+/g'`
echo -n "Looking up CD name.."
# Make CDDB query - protocol level = 4
CDINFO=`$WGET $WGETOPTS -O - "$CDDBURL?cmd=cddb+query+$TRACKINFOPLUS\&hello=$HELLOINFO\&proto=4" 2>/dev/null`
echo -n ".."
RESPONSECODE=`echo $CDINFO | cut -f1 -d' '`
DISCID=`echo $TRACKINFO | cut -f1 -d' '`
# Deal with inexact matches, multiple exact matches, or no match here
# RESPONSECODE contains CDDB response code
# need to set CDINFO to <responsecode> <genre> <discid> <artist> / <albumtitle>
# In other words, don't muck with it.
case $RESPONSECODE in
200)
	# Do nothing
	;;
202)	# No match
	echo -e ".\nCDDB Error: No match found."
	echo -n "Preparing new template file..."
	CDDBDATA=`mktemp $OUTPUTDIR/cdgrab.XXXXXX` || exit 1
	(
		echo "202 misc $DISCID CD database entry follows (until terminating '.')"
		echo "# xmcd CD database file"
		echo "#"
		echo "# Track frame offsets:"
		X=0
		while [ "$X" != "$TRACKS" ]
		do
			X=`expr $X + 1`
			echo -ne "#\t"
			echo $TRACKINFO | cut -f`expr $X + 2` -d' '
		done
		echo "#"
		echo -n "# Disc length: "
		echo $TRACKINFO | cut -f`expr $X + 3` -d' '
		echo "# Revision: 0"
		echo "# Submitted via: cdgrab $VERSION"
		echo "#"
		echo "DISCID=$DISCID"
		echo "DTITLE=Unknown Artist / Unknown Album"
		X=0
		while [ "$X" != "$TRACKS" ]
		do
			X=`expr $X + 1`
			echo TTITLE`expr $X - 1`=Track $X
		done
		echo "."
	) > $CDDBDATA
	echo "done."
	NOCDDB=y
	NOID3=y
	# Ok now that we have this template file let's ask the user if they
	# want to put something in it
	echo "Would you like to edit this blank template file?"
	echo "If you choose no, your tracks will be named like this:"
	echo -e "\tUnknown_Artist/Track_01.mp3"
	echo "and will not be ID3 tagged."
	echo -n "Edit CDDB template? [y/n] "
	read YESNO
	while [ "$YESNO" != "y" ] && [ "$YESNO" != "n" ] && [ "$YESNO" != "Y" ] && [ "$YESNO" != "N" ]
	do
		echo -n 'Invalid selection. Please answer "y" or "n": '
		read YESNO
	done
	if [ "$YESNO" == "y" ] || [ "$YESNO" == "Y" ]; then
		EDITCDDB=y
		NOID3=0
	fi
	;;
210|211)
	# Multiple or inexact matches
	CDCHOICES=`echo "$CDINFO" | tail +2 | grep -v ^[.]`
	NUMCDCHOICES=`echo "$CDCHOICES" | wc -l | xargs`
	if [ "$NUMCDCHOICES" = "1" ]; then
		CDCHOICENUM=1
		echo -n "Inexact match: "			
	else
		echo -n "Multiple "
		if [ "$RESPONSECODE" = "211" ]; then echo -n "inexact "; fi
		echo "matches found, please select one."
		echo "Number of CD choices: $NUMCDCHOICES"
		X=0
		while [ "$X" != "$NUMCDCHOICES" ]
		do
			X=`expr $X + 1`
			echo $X: `echo "$CDCHOICES" | head -$X | tail -1`
		done
		echo -n "Selection [1-$NUMCDCHOICES]: "
		read CDCHOICE
		# Make sure we get a valid choice
		CDCHOICENUM=`echo $CDCHOICE | xargs printf %d 2>/dev/null`
		while [ $CDCHOICENUM -lt 1 ] || [ $CDCHOICENUM -gt $NUMCDCHOICES ]
		do
			echo "Invalid selection. Please choose a number between 1 and $NUMCDCHOICES."
			echo -n "Selection [1-$NUMCDCHOICES]: "
			read CDCHOICE
			CDCHOICENUM=`echo $CDCHOICE | xargs printf %d 2>/dev/null`
		done
		echo -n "Selected: "
	fi
	# Selection is valid, use it
	CDINFO="$RESPONSECODE `echo "$CDCHOICES" | head -$CDCHOICENUM | tail -1`"
	DISCID=`echo $CDCHOICES | cut -f2 -d' '`
	echo $CDINFO
	;;
403)
	echo "CDDB error: database entry is corrupt. Sorry."
	exit
	;;
409)
	echo "CDDB error: No handshake. Sorry."
	exit
	;;
esac
CDDBGENRE=`echo $CDINFO | cut -f2 -d' '`
if [ "$NOCDDB" != "y" ]; then
	# Create a tempfile
	CDDBDATA=`mktemp $OUTPUTDIR/cdgrab.XXXXXX` || exit 1
	# Make CDDB read - protocol level = 4
	$WGET $WGETOPTS -O $CDDBDATA "$CDDBURL?cmd=cddb+read+$CDDBGENRE+$DISCID\&hello=$HELLOINFO\&proto=4" 2>/dev/null
	echo ". done."
	RESPONSECODE=`cat $CDDBDATA | head -1 | cut -f1 -d' '`
	if [ $RESPONSECODE -gt 399 ]; then
		echo "cdgrab: CDDB error: `cat $CDDBDATA | head -1`"
		rm -f $CDDBDATA
		exit 1
	fi
fi
# Let user edit CDDB data if they requested such a thing
if [ "$EDITCDDB" = "y" ]; then
	# Try to load the preferred editor, starting with their
	# EDITOR variable
	if [ -x "$EDITOR" ]; then
		$EDITOR $CDDBDATA
	# If that fails, check for a vi
	elif [ -x /usr/bin/vi ]; then
		/usr/bin/vi $CDDBDATA
	# ae should be on all debian systems
	elif [ -x /bin/ae ]; then
		/bin/ae $CDDBDATA
	# bomb out
	else
		echo "No editor available. Check your EDITOR environment variable."
		exit 1
	fi
	# delete editor backup file if it exists
	if [ -w $CDDBDATA~ ]; then
		rm -f $CDDBDATA~
	fi
fi
# Get Artist and Album info from the CDDB response
DARTISTALBUM="`grep ^DTITLE= $CDDBDATA | cut -f2 -d= | sed 's- / -~-g'`"
DARTIST=`echo $DARTISTALBUM | cut -f1 -d~`
DALBUM=`echo $DARTISTALBUM | cut -f2 -d~ | tr -d \[:cntrl:\]`
echo Title: $DALBUM
echo Artist: $DARTIST
ARTISTFILE=`echo $DARTIST | tr \ / __ | tr -d \'\? | tr -d \[:cntrl:\]`
ALBUMFILE=`echo $DALBUM | tr \ / __ | tr -d \'\? | tr -d \[:cntrl:\]`
TRACKNAME=foo
OUTPUTFILE=`eval echo $OUTPUTFORMAT`
if [ "$PLAYLISTONLY" != "y" ]; then
	if [ \! -e `dirname $OUTPUTDIR/$OUTPUTFILE` ]; then
		mkdir -p `dirname $OUTPUTDIR/$OUTPUTFILE`;
	fi
fi
PLAYLISTFILE=`eval echo $PLAYLISTFORMAT`
if [ "$PLAYLIST" = "y" ]; then
	rm -f "$PLAYLISTFILE"
	touch "$PLAYLISTFILE"
fi
# Go through the tracks and list them all
# UTRACKNUM is the un-0-padded tracknum counter
for UTRACKNUM in $TRACKQUEUE
do
	CDDBTRACKNUM=`expr $UTRACKNUM - 1`
	TRACKNAME=`grep ^TTITLE$CDDBTRACKNUM= $CDDBDATA | head -1 | cut -f2 -d= | tr -d \[:cntrl:\]`
	echo Track $UTRACKNUM: $TRACKNAME
done
# For option #2, only one program is running at once so the encoder can be
# unsilenced right away
if [ "$LOWDISK" = "2" ]; then
	echo "cdgrab-control-unsilence" >> $CDDBDATA
fi

# This is where we split into two programs - one does the ripping, the other
# does the encoding, tagging, and deletion of all temporary files.
# Communication is via pipe on file descriptor #3. One filename per line.

export TRACKNUMPADDING CDDBDATA OUTPUTDIR OUTPUTFORMAT ENCODER ID3 ID3OPTS
export TRACKQUEUE DALBUM DARTIST PLAYLIST PLAYLISTONLY
(
# Start ripping in process #1.
for UTRACKNUM in $TRACKQUEUE
do
	# This entire section must be completely silent to stdout - all
	# output *must* go to stderr. Otherwise it'll get sent down the
	# pipe.
	TRACKNUM=`printf %0.${TRACKNUMPADDING}d ${UTRACKNUM}`
	CDDBTRACKNUM=`expr $UTRACKNUM - 1`
	TRACKNAME=`grep ^TTITLE$CDDBTRACKNUM= $CDDBDATA | head -1 | cut -f2 -d= | tr -d \[:cntrl:\]`
	if [ "$PLAYLISTONLY" != "y" ]; then
		echo "Grabbing track $UTRACKNUM: $TRACKNAME..." >&2
		WAVDATA=`mktemp -u $OUTPUTDIR/cdgrab.currenttrack.XXXXXX`.wav || exit 1
		case "$CDROMREADERSYNTAX" in
			cdparanoia) $CDPARANOIA -d $CDROM $UTRACKNUM $WAVDATA ;;
			cdda2wav) $CDDA2WAV -H -D $CDROM -t $UTRACKNUM $WAVDATA ;;
			debug) $CDPARANOIA -d $CDROM -w $UTRACKNUM-[:5] $WAVDATA ;;
		esac
	fi
	# The filename gets sent down the pipe - the encoder thread will
	# block on the pipe read until this is sent, automagically pausing things
	# nicely 
	echo $WAVDATA

	if [ "$PLAYLISTONLY" = "y" ]; then LOWDISK=1; fi # this is essentially a goto

	case $LOWDISK in
	1) # do nothing here - proceed straight to next track (default)
		;;
	2) # eat up as little disk space as possible - wait for this track
	   # to finish encoding
		while true
		do
			grep -q cdgrab-control-completed-track-$UTRACKNUM $CDDBDATA 2>/dev/null
			if [ "$?" = "2" ] || [ "$?" = "0" ]; then
				# grep hit some fatal error (like the CDDBDATA file
				# was cleaned by an exiting encoder thread),
				# or found a match, either way we exit
				break
			fi
			sleep 2
		done
		;;
	3) # Door #3 hasn't been built yet - tell the user that
		echo "Low Disk option #3 not yet available, defaulting to normal parallelization."
		# I would jump back to #1 to do nothing, but I figured I
		# could just do nothing right here instead. Code duplication
		# sucks but it does have its place.
		;;
	esac
done
# This is downright evil, but elegant nonetheless (it's either this or
# another tempfile) - we tack a keyword at the end of the CDDBDATA tempfile
# that signals that the encoder/tagger can produce output.
if [ "$PLAYLISTONLY" != "y" ]; then
	echo cdgrab-control-unsilence >> $CDDBDATA
	echo Finishing encoding... >&2
fi
) | (
# The variables down here live in a kind of alternate reality so some of
# them need to be recreated.
SILENCE='2>/dev/null'
NUMPROCS=0
for UTRACKNUM in $TRACKQUEUE
do
	# get next file to process
	read WAVDATA
	# quit if we're all done
	if [ "$?" != "0" ]; then exit; fi
	TRACKNUM=`printf %0.${TRACKNUMPADDING}d ${UTRACKNUM}`
	CDDBTRACKNUM=`expr $UTRACKNUM - 1`
	TRACKNAME=`grep ^TTITLE$CDDBTRACKNUM= $CDDBDATA | head -1 | cut -f2 -d= | tr -d \[:cntrl:\]`
	TRACKFILE=`echo $TRACKNAME | sed 's- -_-g' | tr -d \'\?/`
	OUTPUTFILE=`eval echo $OUTPUTFORMAT`
	if [ "$PLAYLIST" = "y" ]; then
		echo "$OUTPUTFILE" >> "$PLAYLISTFILE"
	fi
	if [ "$PLAYLISTONLY" = "y" ]; then continue; fi
	if grep -q ^cdgrab-control-unsilence$ $CDDBDATA >/dev/null; then
		unset SILENCE
	else
		SILENCE='2>/dev/null'
	fi
	NUMPROCS=`expr $NUMPROCS + 1`
	if [ "$NUMPROCS" = "$MAXPROCS" ]
	then
		# Don't background this one - reset the counter, encode the
		# mp3, lather, rinse, repeat.
		NUMPROCS=0
		eval "echo Encoding track" '"$UTRACKNUM: $TRACKNAME..."' "$SILENCE >&2"
		case "$ENCODERSYNTAX" in
		lame)
			eval "$ENCODER" "$ENCODEROPTS" "$WAVDATA" '"$OUTPUTDIR/$OUTPUTFILE"' "$SILENCE >&2"
			;;
		bladeenc|l3enc)
			eval "$ENCODER" "$WAVDATA" '"$OUTPUTDIR/$OUTPUTFILE"' "$ENCODEROPTS" "$SILENCE >&2"
			;;
		esac
		rm -f "$WAVDATA"
		if [ "$NOID3" != "y" ]; then
			eval "echo Tagging track" '"$UTRACKNUM: $TRACKNAME..."' "$SILENCE >&2"
			eval "$ID3" "$ID3OPTS" -A '"$DALBUM"' -a '"$DARTIST"' \
				-t '"$TRACKNAME"' '"$OUTPUTDIR/$OUTPUTFILE"' "$SILENCE >&2"
		fi
		echo "cdgrab-control-completed-track-$UTRACKNUM" >> $CDDBDATA
	else
		# Background this one, so it immediately goes through the
		# for loop and finds more stuff to run
		( SILENCE='2>/dev/null'
		eval "echo Encoding track" '"$UTRACKNUM: $TRACKNAME..."' "$SILENCE >&2"
		case "$ENCODERSYNTAX" in
		lame)
			eval "$ENCODER" "$ENCODEROPTS" "$WAVDATA" '"$OUTPUTDIR/$OUTPUTFILE"' "$SILENCE >&2"
			;;
		bladeenc|l3enc)
			eval "$ENCODER" "$WAVDATA" '"$OUTPUTDIR/$OUTPUTFILE"' "$ENCODEROPTS" "$SILENCE >&2"
			;;
		esac
		rm -f "$WAVDATA"
		if [ "$NOID3" != "y" ]; then
			eval "echo Tagging track" '"$UTRACKNUM: $TRACKNAME..."' "$SILENCE >&2"
			eval "$ID3" "$ID3OPTS" -A '"$DALBUM"' -a '"$DARTIST"' \
				-t '"$TRACKNAME"' '"$OUTPUTDIR/$OUTPUTFILE"' "$SILENCE >&2"
		fi
		echo "cdgrab-control-completed-track-$UTRACKNUM" >> $CDDBDATA
		) &
	fi		
done
echo "Finished."
rm -f $CDDBDATA
)

