<?php
/////////////////////////////////////////////////////////////////
/// getID3() by James Heinrich <info@getid3.org>               //
//  available at http://getid3.sourceforge.net                ///
//            or http://www.getid3.org                        ///
/////////////////////////////////////////////////////////////////
//                                                             //
// getid3.ape.php - part of getID3()                           //
// See getid3.readme.txt for more details                      //
//                                                             //
/////////////////////////////////////////////////////////////////

function getAPEtagFilepointer(&$fd, &$ThisFileInfo, $overrideendoffset=0) {
	$id3v1tagsize     = 128;
	$apetagheadersize = 32;

	if ($overrideendoffset == 0) {

		fseek($fd, 0 - $id3v1tagsize - $apetagheadersize, SEEK_END);
		$APEfooterID3v1 = fread($fd, $id3v1tagsize + $apetagheadersize);
		if ((substr($APEfooterID3v1, 0, strlen('APETAGEX')) == 'APETAGEX') && (substr($APEfooterID3v1, $apetagheadersize, strlen('TAG')) == 'TAG')) {

			// APE tag found before ID3v1
			//$APEfooterData = substr($APEfooterID3v1, 0, $apetagheadersize);
			//$APEfooterOffset = 0 - $apetagheadersize - $id3v1tagsize;
			$ThisFileInfo['ape']['tag_offset_end'] = $ThisFileInfo['filesize'] - $id3v1tagsize;

		} elseif (substr($APEfooterID3v1, $id3v1tagsize, strlen('APETAGEX')) == 'APETAGEX') {

			// APE tag found, no ID3v1
			//$APEfooterData = substr($APEfooterID3v1, $id3v1tagsize, $apetagheadersize);
			//$APEfooterOffset = 0 - $apetagheadersize;
			$ThisFileInfo['ape']['tag_offset_end'] = $ThisFileInfo['filesize'];

		}

	} else {

		fseek($fd, $overrideendoffset - $apetagheadersize, SEEK_SET);
		if (fread($fd, 8) == 'APETAGEX') {
			$ThisFileInfo['ape']['tag_offset_end'] = $overrideendoffset;
		}

	}

	if (!isset($ThisFileInfo['ape']['tag_offset_end'])) {

		// APE tag not found
		unset($ThisFileInfo['ape']);
		return false;

	}

	//if (empty($ThisFileInfo['fileformat'])) {
	//	$ThisFileInfo['fileformat'] = 'ape';
	//}

	fseek($fd, $ThisFileInfo['ape']['tag_offset_end'] - $apetagheadersize, SEEK_SET);
	$APEfooterData = fread($fd, 32);
	if (!($ThisFileInfo['ape']['footer'] = parseAPEheaderFooter($APEfooterData))) {
		$ThisFileInfo['error'] .= "\n".'Error parsing APE footer at offset '.$ThisFileInfo['ape']['tag_offset_end'];
		return false;
	}

	if (isset($ThisFileInfo['ape']['footer']['flags']['header']) && $ThisFileInfo['ape']['footer']['flags']['header']) {
		//fseek($fd, $APEfooterOffset - $ThisFileInfo['ape']['footer']['raw']['tagsize'] + $apetagheadersize - $apetagheadersize, SEEK_END);
		fseek($fd, $ThisFileInfo['ape']['tag_offset_end'] - $ThisFileInfo['ape']['footer']['raw']['tagsize'] - $apetagheadersize, SEEK_SET);
		$ThisFileInfo['ape']['tag_offset_start'] = ftell($fd);
		$APEtagData = fread($fd, $ThisFileInfo['ape']['footer']['raw']['tagsize'] + $apetagheadersize);
	} else {
		//fseek($fd, $APEfooterOffset - $ThisFileInfo['ape']['footer']['raw']['tagsize'] + $apetagheadersize, SEEK_END);
		$ThisFileInfo['ape']['tag_offset_start'] = $ThisFileInfo['ape']['tag_offset_end'] - $ThisFileInfo['ape']['footer']['raw']['tagsize'];
		fseek($fd, $ThisFileInfo['ape']['tag_offset_start'], SEEK_SET);
		$APEtagData = fread($fd, $ThisFileInfo['ape']['footer']['raw']['tagsize']);
	}
	$ThisFileInfo['avdataend'] = $ThisFileInfo['ape']['tag_offset_start'];
	$offset = 0;
	if (isset($ThisFileInfo['ape']['footer']['flags']['header']) && $ThisFileInfo['ape']['footer']['flags']['header']) {
		if ($ThisFileInfo['ape']['header'] = parseAPEheaderFooter(substr($APEtagData, 0, $apetagheadersize))) {
			$offset += $apetagheadersize;
		} else {
			$ThisFileInfo['error'] .= "\n".'Error parsing APE header at offset '.$ThisFileInfo['ape']['tag_offset_start'];
			return false;
		}
	}

	for ($i = 0; $i < $ThisFileInfo['ape']['footer']['raw']['tag_items']; $i++) {
		$value_size = LittleEndian2Int(substr($APEtagData, $offset, 4));
		$offset += 4;
		$item_flags = LittleEndian2Int(substr($APEtagData, $offset, 4));
		$offset += 4;
		if (strstr(substr($APEtagData, $offset), chr(0)) === false) {
			$ThisFileInfo['error'] .= "\n".'Cannot find null-byte (0x00) seperator between ItemKey #'.$i.' and value. ItemKey starts '.$offset.' bytes into the APE tag, at file offset '.($ThisFileInfo['ape']['tag_offset_start'] + $offset);
			return false;
		}
		$ItemKeyLength = strpos($APEtagData, chr(0), $offset) - $offset;
		$item_key      = strtolower(substr($APEtagData, $offset, $ItemKeyLength));
		$offset += ($ItemKeyLength + 1); // skip 0x00 terminator
		$ThisFileInfo['ape']['items']["$item_key"]['data'] = substr($APEtagData, $offset, $value_size);
		$offset += $value_size;

		$ThisFileInfo['ape']['items']["$item_key"]['flags'] = parseAPEtagFlags($item_flags);
		switch ($ThisFileInfo['ape']['items']["$item_key"]['flags']['item_contents_raw']) {
			case 0: // UTF-8
			case 3: // Locator (URL, filename, etc), UTF-8 encoded
				$ThisFileInfo['ape']['items']["$item_key"]['data'] = explode(chr(0), trim($ThisFileInfo['ape']['items']["$item_key"]['data']));
				foreach ($ThisFileInfo['ape']['items']["$item_key"]['data'] as $key => $value) {
					$ThisFileInfo['ape']['items']["$item_key"]['data_ascii'][$key] = RoughTranslateUnicodeToASCII($value, 3);
				}
				break;

			default: // binary data
				//$ThisFileInfo['ape']['items']["$item_key"]['data_ascii'] = null;
				break;
		}

		switch ($item_key) {
			case 'replaygain_track_gain':
				$ThisFileInfo['replay_gain']['radio']['adjustment']      = (float) $ThisFileInfo['ape']['items']["$item_key"]['data_ascii'][0];
				$ThisFileInfo['replay_gain']['radio']['originator']      = 'unspecified';
				break;

			case 'replaygain_track_peak':
				$ThisFileInfo['replay_gain']['radio']['peak']            = (float) $ThisFileInfo['ape']['items']["$item_key"]['data_ascii'][0];
				$ThisFileInfo['replay_gain']['radio']['originator']      = 'unspecified';
				break;

			case 'replaygain_album_gain':
				$ThisFileInfo['replay_gain']['audiophile']['adjustment'] = (float) $ThisFileInfo['ape']['items']["$item_key"]['data_ascii'][0];
				$ThisFileInfo['replay_gain']['audiophile']['originator'] = 'unspecified';
				break;

			case 'replaygain_album_peak':
				$ThisFileInfo['replay_gain']['audiophile']['peak']       = (float) $ThisFileInfo['ape']['items']["$item_key"]['data_ascii'][0];
				$ThisFileInfo['replay_gain']['audiophile']['originator'] = 'unspecified';
				break;

			default:
				foreach ($ThisFileInfo['ape']['items']["$item_key"]['data_ascii'] as $comment) {
					$ThisFileInfo['ape']['comments'][strtolower($item_key)][] = $comment;
				}
				break;

		}

	}

	return true;
}

function parseAPEheaderFooter($APEheaderFooterData) {
	// http://www.uni-jena.de/~pfk/mpp/sv8/apeheader.html
	$headerfooterinfo['raw']['footer_tag']   =                  substr($APEheaderFooterData,  0, 8);
	if ($headerfooterinfo['raw']['footer_tag'] != 'APETAGEX') {
		return false;
	}
	$headerfooterinfo['raw']['version']      = LittleEndian2Int(substr($APEheaderFooterData,  8, 4));
	$headerfooterinfo['raw']['tagsize']      = LittleEndian2Int(substr($APEheaderFooterData, 12, 4));
	$headerfooterinfo['raw']['tag_items']    = LittleEndian2Int(substr($APEheaderFooterData, 16, 4));
	$headerfooterinfo['raw']['global_flags'] = LittleEndian2Int(substr($APEheaderFooterData, 20, 4));
	$headerfooterinfo['raw']['reserved']     =                  substr($APEheaderFooterData, 24, 8);

	$headerfooterinfo['tag_version']         = $headerfooterinfo['raw']['version'] / 1000;
	if ($headerfooterinfo['tag_version'] >= 2) {
		$headerfooterinfo['flags'] = parseAPEtagFlags($headerfooterinfo['raw']['global_flags']);
	}
	return $headerfooterinfo;
}

function parseAPEtagFlags($rawflagint) {
	// "Note: APE Tags 1.0 do not use any of the APE Tag flags.
	// All are set to zero on creation and ignored on reading."
	// http://www.uni-jena.de/~pfk/mpp/sv8/apetagflags.html
	$flags['header']            = (bool) ($rawflagint & 0x80000000);
	$flags['footer']            = (bool) ($rawflagint & 0x40000000);
	$flags['this_is_header']    = (bool) ($rawflagint & 0x20000000);
	$flags['item_contents_raw'] =        ($rawflagint & 0x00000006) >> 1;
	$flags['read_only']         = (bool) ($rawflagint & 0x00000001);

	$flags['item_contents']     = APEcontentTypeFlagLookup($flags['item_contents_raw']);

	return $flags;
}

function APEcontentTypeFlagLookup($contenttypeid) {
	static $APEcontentTypeFlagLookup = array();
	if (empty($APEcontentTypeFlagLookup)) {
		$APEcontentTypeFlagLookup[0]  = 'utf-8';
		$APEcontentTypeFlagLookup[1]  = 'binary';
		$APEcontentTypeFlagLookup[2]  = 'external';
		$APEcontentTypeFlagLookup[3]  = 'reserved';
	}
	return (isset($APEcontentTypeFlagLookup[$contenttypeid]) ? $APEcontentTypeFlagLookup[$contenttypeid] : 'invalid');
}

function APEtagItemIsUTF8Lookup($itemkey) {
	static $APEtagItemIsUTF8Lookup = array();
	if (empty($APEtagItemIsUTF8Lookup)) {
		$APEtagItemIsUTF8Lookup[]  = 'title';
		$APEtagItemIsUTF8Lookup[]  = 'subtitle';
		$APEtagItemIsUTF8Lookup[]  = 'artist';
		$APEtagItemIsUTF8Lookup[]  = 'album';
		$APEtagItemIsUTF8Lookup[]  = 'debut album';
		$APEtagItemIsUTF8Lookup[]  = 'publisher';
		$APEtagItemIsUTF8Lookup[]  = 'conductor';
		$APEtagItemIsUTF8Lookup[]  = 'track';
		$APEtagItemIsUTF8Lookup[]  = 'composer';
		$APEtagItemIsUTF8Lookup[]  = 'comment';
		$APEtagItemIsUTF8Lookup[]  = 'copyright';
		$APEtagItemIsUTF8Lookup[]  = 'publicationright';
		$APEtagItemIsUTF8Lookup[]  = 'file';
		$APEtagItemIsUTF8Lookup[]  = 'year';
		$APEtagItemIsUTF8Lookup[]  = 'record date';
		$APEtagItemIsUTF8Lookup[]  = 'record location';
		$APEtagItemIsUTF8Lookup[]  = 'genre';
		$APEtagItemIsUTF8Lookup[]  = 'media';
		$APEtagItemIsUTF8Lookup[]  = 'related';
		$APEtagItemIsUTF8Lookup[]  = 'isrc';
		$APEtagItemIsUTF8Lookup[]  = 'abstract';
		$APEtagItemIsUTF8Lookup[]  = 'language';
		$APEtagItemIsUTF8Lookup[]  = 'bibliography';
	}
	return in_array(strtolower($itemkey), $APEtagItemIsUTF8Lookup);
}


function WriteAPEtag($filename, $data, $writeid3v1=true, $dataisUTF8=false) {
	// $filename is the filename to write the APE tag to

	// $data is a 2-dimensional array of values:
	//   $data['artist'][0] = 'Artist 1';
	//   $data['artist'][1] = 'Artist 2';
	//   $data['title'][0]  = 'Song Title';

	// $writeid3v1 is a boolean value that specifies if a blank ID3v1 tag
	// is added to the end of the file. Existing ID3v1 tags are not added
	// or removed, regardless of this setting

	// NOTE: All data passed to this function must be either ISO-8859-1 or
	// UTF-8 format, not Unicode.

	$ThisFileInfo = GetAllFileInfo($filename);
	if (isset($ThisFileInfo['ape']['tag_offset_start']) && isset($ThisFileInfo['lyrics3']['tag_offset_end'])) {
		if ($ThisFileInfo['ape']['tag_offset_start'] >= $ThisFileInfo['lyrics3']['tag_offset_end']) {
			// Current APE tag between Lyrics3 and ID3v1/EOF
			// This break Lyrics3 functionality
			if (!DeleteAPEtag($filename)) {
				return false;
			}
			$ThisFileInfo = GetAllFileInfo($filename);
		}
	}

	if ($APEtag = GenerateAPEtag($data, $dataisUTF8)) {
		if ($fp = @fopen($filename, 'a+b')) {
			$oldignoreuserabort = ignore_user_abort(true);
			flock($fp, LOCK_EX);

			$PostAPEdataOffset = $ThisFileInfo['avdataend'];
			if (isset($ThisFileInfo['ape']['tag_offset_end'])) {
				$PostAPEdataOffset = max($PostAPEdataOffset, $ThisFileInfo['ape']['tag_offset_end']);
			}
			if (isset($ThisFileInfo['lyrics3']['tag_offset_start'])) {
				$PostAPEdataOffset = max($PostAPEdataOffset, $ThisFileInfo['lyrics3']['tag_offset_start']);
			}
			fseek($fp, $PostAPEdataOffset, SEEK_SET);
			$PostAPEdata = fread($fp, $ThisFileInfo['filesize'] - $PostAPEdataOffset);

			fseek($fp, $PostAPEdataOffset, SEEK_SET);
			if (isset($ThisFileInfo['ape']['tag_offset_start'])) {
				fseek($fp, $ThisFileInfo['ape']['tag_offset_start'], SEEK_SET);
			}
			ftruncate($fp, ftell($fp));
			fwrite($fp, $APEtag, strlen($APEtag));
			fwrite($fp, $PostAPEdata, strlen($PostAPEdata));
			if (!isset($ThisFileInfo['id3v1']['tag_offset_start']) && $writeid3v1) {
				require_once(GETID3_INCLUDEPATH.'getid3.id3v1.php');
				fwrite($fp, GenerateID3v1Tag(@$data['title'][0], @$data['artist'][0], @$data['album'][0], @$data['year'][0], @$data['genre'][0], @$data['comment'][0], @$data['track'][0]), 128);
			}
			flock($fp, LOCK_UN);
			fclose($fp);
			ignore_user_abort($oldignoreuserabort);
			return true;

		}
		return false;
	}
	return false;
}

function DeleteAPEtag($filename) {
	$ThisFileInfo = GetAllFileInfo($filename);
	if (isset($ThisFileInfo['ape']['tag_offset_start']) && isset($ThisFileInfo['ape']['tag_offset_end'])) {

		if ($fp = @fopen($filename, 'a+b')) {

			flock($fp, LOCK_EX);
			$oldignoreuserabort = ignore_user_abort(true);

			fseek($fp, $ThisFileInfo['ape']['tag_offset_end'], SEEK_SET);
			$DataAfterAPE = fread($fp, $ThisFileInfo['filesize'] - $ThisFileInfo['ape']['tag_offset_end']);

			ftruncate($fp, $ThisFileInfo['ape']['tag_offset_start']);
			fseek($fp, $ThisFileInfo['ape']['tag_offset_start'], SEEK_SET);

			fwrite($fp, $DataAfterAPE, strlen($DataAfterAPE));

			flock($fp, LOCK_UN);
			fclose($fp);
			ignore_user_abort($oldignoreuserabort);

			return true;

		}
	}
	return false;
}


function GenerateAPEtag($data, $dataisUTF8=false) {
	// NOTE: All data passed to this function must be either ISO-8859-1 or
	// UTF-8 format, not Unicode.

	$items = array();
	if (!is_array($data)) {
		return false;
	}
	foreach ($data as $key => $arrayofvalues) {
		if (!is_array($arrayofvalues)) {
			return false;
		}

		$valuestring = '';
		foreach ($arrayofvalues as $value) {
			$valuestring .= str_replace(chr(0), '', $value).chr(0);
		}
		$valuestring = rtrim($valuestring, chr(0));
		if (!$dataisUTF8) {
			$valuestring = utf8_encode($valuestring);
		}

		// Length of the assigned value in bytes
		$tagitem  = LittleEndian2String(strlen($valuestring), 4);

		//$tagitem .= GenerateAPEtagFlags(true, true, false, 0, false);
		$tagitem .= "\x00\x00\x00\x00";

		$tagitem .= CleanAPEtagItemKey($key)."\x00";
		$tagitem .= $valuestring;

		$items[] = $tagitem;

	}

	return GenerateAPEtagHeaderFooter($items, true).implode('', $items).GenerateAPEtagHeaderFooter($items, false);
}

function GenerateAPEtagHeaderFooter(&$items, $isheader=false) {
	$tagdatalength = 0;
	foreach ($items as $itemdata) {
		$tagdatalength += strlen($itemdata);
	}

	$APEheader  = 'APETAGEX';
	$APEheader .= LittleEndian2String(2000, 4);
	$APEheader .= LittleEndian2String(32 + $tagdatalength, 4);
	$APEheader .= LittleEndian2String(count($items), 4);
	$APEheader .= GenerateAPEtagFlags(true, true, $isheader, 0, false);
	$APEheader .= str_repeat(chr(0), 8);

	return $APEheader;
}

function GenerateAPEtagFlags($header=true, $footer=true, $isheader=false, $encodingid=0, $readonly=false) {
	$APEtagFlags = array_fill(0, 4, 0);
	if ($header) {
		$APEtagFlags[0] |= 0x80; // Tag contains a header
	}
	if (!$footer) {
		$APEtagFlags[0] |= 0x40; // Tag contains no footer
	}
	if ($isheader) {
		$APEtagFlags[0] |= 0x20; // This is the header, not the footer
	}

	// 0: Item contains text information coded in UTF-8
	// 1: Item contains binary information °)
	// 2: Item is a locator of external stored information °°)
	// 3: reserved
	$APEtagFlags[3] |= ($encodingid << 1);

	if ($readonly) {
		$APEtagFlags[3] |= 0x01; // Tag or Item is Read Only
	}

	return chr($APEtagFlags[3]).chr($APEtagFlags[2]).chr($APEtagFlags[1]).chr($APEtagFlags[0]);
}

function CleanAPEtagItemKey($itemkey) {
	$itemkey = eregi_replace("[^\x20-\x7E]", '', $itemkey);

	// http://www.personal.uni-jena.de/~pfk/mpp/sv8/apekey.html
	switch (strtoupper($itemkey)) {
		case 'EAN/UPC':
		case 'ISBN':
		case 'LC':
		case 'ISRC':
			$itemkey = strtoupper($itemkey);
			break;

		default:
			$itemkey = ucwords($itemkey);
			break;
	}
	return $itemkey;

}


?>