#!/usr/bin/perl

# $Id: ARCue.pl 40 2007-05-21 10:44:04Z Chris $

###############################################################################
#                                                                             #
#                                  ARCue.pl                                   #
#                                                                             #
#                      Christopher Key <cjk32@cam.ac.uk>                      #
#                                                                             #
#                                                                             #
# Many thanks to Mr. Spoon for his kind permission to use the AccurateRip     #
# technology and database.  Access to AccurateRip is regulated, see           #
# http://www.accuraterip.com/3rdparty-access.htm for details.                 #
#                                                                             #
#                                                                             #
# This script allows the usage of AccurateRip's extensive track checksum      #
# database to verify the accuracy of whole CD single file rips. To use,       #
# simply run:                                                                 #
#                                                                             #
# ARCue.pl  cd1.cue  cd2.cue  etc...                                          #
#                                                                             #
###############################################################################

use strict;

use LWP;
use Carp;

my $lwpUserAgent = LWP::UserAgent->new;

foreach my $cueFile (@ARGV) {

	# Parse each cue file, and extract track offsets
	# and the relevant wav file.
	print $cueFile . ":\n\n";

	my $FH;
	open($FH, "<", $cueFile) or croak("Failed to open $cueFile: $!");

	my ($wavFile, $trackCount, $curTrack);
	my @trackOffsets;
	while (<$FH>) {
		chomp;
		if (m/^FILE\s+\"(.+)\"\s+(.+)/) {
			if ($2 ne "WAVE") { croak("Failed to open $cueFile: $!") }
			$wavFile = $1;
		}elsif (m/\s+TRACK\s+(\d+)\s+(.+)/) {
			$curTrack = $1 + 0;
			# What to do with non AUDIO tracks ($2)?
		}elsif (m/\s+INDEX\s+01\s+(\d+):(\d+):(\d+)/) {
			$trackOffsets[$curTrack-1] = ($1*60+$2)*75+$3;
		}
	}
	$trackCount = $curTrack;

	close ($FH);


	# Open the reference wav file, and make sure it looks
	# like one.
	open ($FH, "<", $wavFile) or croak("Failed to open $wavFile: $!");
	binmode $FH;

	my ($chunkID, $chunkSize, $chunkFormat);

	read($FH, $chunkID, 4);
	read($FH, $chunkSize, 4);
	read($FH, $chunkFormat, 4);

	if ($chunkID ne "RIFF" || $chunkFormat ne "WAVE") { croak("$wavFile doesn't appear to be a valid wav file.") }


	# Find the offset to the data in the wav file, and also the start of the leadout
	my ($subChunkID, $subChunkSize, $dataOffset);

	while (!eof($FH)){
		read($FH, $subChunkID, 4);
		read($FH, $subChunkSize, 4);
		$subChunkSize = unpack("V", $subChunkSize);
		if ($subChunkID eq "data") {
			$trackOffsets[$trackCount] = $subChunkSize / 2352; #leadout location
			$dataOffset = tell($FH);
			last;
		}
	}


	# Calculate the length of each track
	my @trackLengths = ();

	for (my $trackNo = 0; $trackNo < $trackCount; $trackNo++) {
		$trackLengths[$trackNo] = $trackOffsets[$trackNo+1] - $trackOffsets[$trackNo];
	}

	# Calculate the three disc ids used by AR
	my ($discId1, $discId2, $cddbDiscId) = (0, 0, 0);

	{
		use integer;

		for (my $trackNo = 0; $trackNo <= $trackCount; $trackNo++) {
			my $trackOffset = $trackOffsets[$trackNo];

			$discId1 += $trackOffset;
			$discId2 += ($trackOffset ? $trackOffset : 1) * ($trackNo + 1);
			if ($trackNo < $trackCount) {
				$cddbDiscId = $cddbDiscId + sumDigits(int($trackOffset/75) + 2);
			}
		}

		$cddbDiscId =
		  (($cddbDiscId % 255) << 24) +
		  ((int($trackOffsets[$trackCount]/75) - int($trackOffsets[0]/75)) << 8) +
		  $trackCount;

		$discId1 &= 0xFFFFFFFF;
		$discId2 &= 0xFFFFFFFF;
		$cddbDiscId &= 0xFFFFFFFF;
	}

  print "Checking AccurateRip database\n\n";

	# See if we can find the disc in the database
	my $arUrl = sprintf("http://www.accuraterip.com/accuraterip/%.1x/%.1x/%.1x/dBAR-%.3d-%.8x-%.8x-%.8x.bin", 
		$discId1 & 0xF, $discId1>>4 & 0xF, $discId1>>8 & 0xF, $trackCount, $discId1, $discId2, $cddbDiscId);

	my $arDiscNotInDb = 0;
	my $arNetworkFailed = 0;

	my $response = $lwpUserAgent->get($arUrl);

	if (!$response->is_success) {
		if ($response->status_line =~ m/^404/) {
			$arDiscNotInDb = 1;
		}else{
			$arNetworkFailed = $response->status_line;
		}
	}

	# Extract CRCs from response data
	my $arCrcCount = 0;
	my @arTrackConfidences = ();
	my @arTrackCRCs = ();

	if (!($arDiscNotInDb || $arNetworkFailed)) {
		my $arCrcData = $response->content;
		my $ptr = 0;

		while ($ptr < length($arCrcData)) {
			my ($chunkTrackCount, $chunkDiscId1, $chunkDiscId2, $chunkCddbDiscId);

			# Force perl to interpret these values as signed integers
			{
				use integer;

				$chunkTrackCount = unpack("c",substr($arCrcData,$ptr,1));
				$chunkDiscId1 = unpack("V",substr($arCrcData,$ptr+1,4)) + 0;
				$chunkDiscId2 = unpack("V",substr($arCrcData,$ptr+5,4)) + 0;
	 			$chunkCddbDiscId = unpack("V",substr($arCrcData,$ptr+9,4)) + 0;
			}

			$ptr +=13;

			if ( $chunkTrackCount != $trackCount
				|| $chunkDiscId1 != $discId1
				|| $chunkDiscId2 != $discId2
				|| $chunkCddbDiscId != $cddbDiscId ) {

				croak("Track count or Disc IDs don't match.");
			}

			# How if it flagged that a track is not in the database?
			for (my $track = 0; $track < $trackCount; $track++) {
				my ($trackConfidence, $trackCrc);

				# Force perl to interpret these values as signed integers
				{
					use integer;

					$trackConfidence = unpack("c",substr($arCrcData,$ptr,1));
					$trackCrc = unpack("V",substr($arCrcData,$ptr+1,4)) + 0;
					$ptr += 9;
				}

				if ($arCrcCount == 0){
					$arTrackConfidences[$track] = [];
					$arTrackCRCs[$track] = [];
				}

				$arTrackConfidences[$track]->[$arCrcCount] = $trackConfidence;
				$arTrackCRCs[$track]->[$arCrcCount] = $trackCrc;
			}
			$arCrcCount++;
		}
	}

	printf "Track\tRipping Status\t\t[Disc ID: %08x-%08x]\n\n", $discId1, $cddbDiscId;

	# Calculate a CRC for each track
	my @trackCRCs = ();
	my ($accuratelyRipped, $notAccuratelyRipped, $notInDatabase) = (0, 0, 0);
	for (my $trackNo = 0; $trackNo < $trackCount; $trackNo++) {
		seek($FH, $dataOffset + $trackOffsets[$trackNo] * 2352, 0);

		my ($frame, $CRC);
		$CRC = 0;
		for (my $frameNo = 0; $frameNo < $trackLengths[$trackNo]; $frameNo++) {
			if (read($FH, $frame, 2352) != 2352) { croak ("read failed.") };

			{
				use integer;
				$CRC += processFrame($frame, $frameNo, $trackLengths[$trackNo], $trackNo == 0, $trackNo == $trackCount - 1);
			}
		}

		{
			use integer;
			$trackCRCs[$trackNo] = $CRC & 0xFFFFFFFF;
		}


		if ($arDiscNotInDb) {
			printf " %d\tTrack not present in database.    [%08x]\n", 
				$trackNo + 1, $trackCRCs[$trackNo];

			 $notInDatabase++;
		}	elsif ($arNetworkFailed) {
			printf " %d\t    [%08x]\n", 
				$trackNo + 1, $trackCRCs[$trackNo];

		} else {

			my $foundCrc = 0;
			my $foundCrcMatch = 0;

			for (my $arCrcNo=0; $arCrcNo < $arCrcCount; $arCrcNo++) {
				if ($arTrackConfidences[$trackNo]->[$arCrcNo] != 0){
					$foundCrc = 1;

					if ($arTrackCRCs[$trackNo]->[$arCrcNo] == $trackCRCs[$trackNo]) {
						printf " %d\tAccurately Ripped    (confidence %d)     [%08x]\n", 
							$trackNo + 1, $arTrackConfidences[$trackNo]->[$arCrcNo], $arTrackCRCs[$trackNo]->[$arCrcNo];

						$accuratelyRipped++;

						$foundCrcMatch = 1;
						last;
					}
				}
			}
			if (!$foundCrc) {
					printf " %d\tTrack not present in database.    [%08x]\n", 
					$trackNo + 1, $trackCRCs[$trackNo];

				$notInDatabase++;
			}elsif (!$foundCrcMatch) {
				printf " %d\t** Rip not accurate **   (confidence %d)     [%08x] [%08x]\n", 
					$trackNo + 1, $arTrackConfidences[$trackNo]->[0], $arTrackCRCs[$trackNo]->[0], $trackCRCs[$trackNo];

				$notAccuratelyRipped++;
			}
		}
	}

	print "\n_______________________\n\n";

	my $errLevel = 0;

	if ($arDiscNotInDb) {
			print "Disc not present in AccurateRip database.\n";
			$errLevel = 2;
	} elsif ($arNetworkFailed) {
			print "Failed to get $arUrl : " . $arNetworkFailed . "\n";
			$errLevel = 3;
	} elsif ($accuratelyRipped == $trackCount) {
		print "All Tracks Accurately Ripped.\n";
	} else {
		if ($notAccuratelyRipped >= 3) {
			 print "Your CD disc is possibly a different pressing to the one(s) stored in AccurateRip.\n"
		}

		printf "Track(s) Accurately Ripped: %d\n", $accuratelyRipped;
		printf "**** Track(s) Not Ripped Accurately: %d ****\n", $notAccuratelyRipped;
		printf "Track(s) Not in Database: %d\n", $notInDatabase;

		$errLevel = 1;
	}

	print "\n\n\n";

	exit $errLevel;
}

sub processFrame {
	use integer;
	my ($frame, $frameNo, $trackLength, $firstTrack, $lastTrack) = @_;

	if ($firstTrack && $frameNo<4) {
		return 0;
	} elsif ($lastTrack && $trackLength - $frameNo < 6) {
		return 0;
	} elsif ($firstTrack && $frameNo == 4) {
		my $sample = unpack("V", substr($frame,2348,4));
		return ($sample * 588 * 5);
	} else {

		my $CRC = 0;
		my $frameOffset = $frameNo * 588;

		foreach (unpack("V588", $frame)) {
			$CRC += $_ * (++$frameOffset);
		}

		return $CRC;
	}
}

sub sumDigits {
	my $n = shift;
	my $r = 0;
	while ($n > 0) {
		$r = $r + ($n % 10);
		$n = int($n / 10);
	}
	return $r;
}
