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

function getAPEtagFilepointer(&$fd, &$ThisFileInfo) {
	$id3v1tagsize     = 128;
	$apetagheadersize = 32;
	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;

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

		// APE tag found, no ID3v1
		$APEfooterData = substr($APEfooterID3v1, $id3v1tagsize, $apetagheadersize);
		$APEfooterOffset = 0 - $apetagheadersize;

	} else {

		// APE tag not found
		return false;

	}

	$ThisFileInfo['ape']['tag_offset_end'] = $ThisFileInfo['filesize'] - ($APEfooterOffset + $apetagheadersize);
	if (empty($ThisFileInfo['fileformat'])) {
		$ThisFileInfo['fileformat'] = 'ape';
	}
	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);
		$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'] = ftell($fd);
		$APEtagData = fread($fd, $ThisFileInfo['ape']['footer']['raw']['tagsize']);
	}
	$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;

		if ($ThisFileInfo['ape']['footer']['tag_version'] >= 2) {
			$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), $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'];
				$ThisFileInfo['replay_gain']['radio']['originator']      = 'unspecified';
				break;

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

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

			case 'replaygain_album_peak':
				$ThisFileInfo['replay_gain']['audiophile']['peak']       = (float) $ThisFileInfo['ape']['items']["$item_key"]['data_ascii'];
				$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;

		}

	}
	if (isset($ThisFileInfo['ape']['comments'])) {
		CopyFormatCommentsToRootComments($ThisFileInfo['ape']['comments'], $ThisFileInfo, true, true, true);
	}

	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=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


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

			$APEwritingOffset = 0;

			fseek($fp, -128, SEEK_END);
			$ID3v1 = fread($fp, 128);
			if (substr($ID3v1, 0, 3) == 'TAG') {

				// existing ID3v1 tag
				$APEwritingOffset -= 128;

			} else {

				// no ID3v1 tag found
				if ($writeid3v1) {
					$ID3v1 = 'TAG'.str_repeat(chr(0), 124).chr(255);
				} else {
					unset($ID3v1);
				}

			}

			fseek($fp, $APEwritingOffset - 32, SEEK_END);
			$APEfooterTest = fread($fp, 32);
			if (substr($APEfooterTest, 0, 8) == 'APETAGEX') {

				// existing APE tag found
				$OldAPEversion = LittleEndian2Int(substr($APEfooterTest,  8, 4)) / 1000;
				$OldAPEsize    = LittleEndian2Int(substr($APEfooterTest, 12, 4));
				switch ($OldAPEversion) {
					case 1:
						$APEwritingOffset -= $OldAPEsize;
						break;

					case 2:
						fseek($fp, $APEwritingOffset - $OldAPEsize - 32, SEEK_END);
						$APEheaderTest = fread($fp, 32);
						if (substr($APEheaderTest, 0, 8) == 'APETAGEX') {

							// APE header present
							$APEwritingOffset -= ($OldAPEsize + 32);

						} else {

							// no APE header present
							$APEwritingOffset -= $OldAPEsize;

						}
						break;

					default:
						flock($fp, LOCK_UN);
						fclose($fp);
						ignore_user_abort($oldignoreuserabort);
						return false;
						break;
				}

			} else {

				// no existing APE tag found
				// nothing further to do

			}
			fseek($fp, $APEwritingOffset, SEEK_END);
			ftruncate($fp, ftell($fp));
			fwrite($fp, $APEtag, strlen($APEtag));
			if (!empty($ID3v1)) {
				fwrite($fp, $ID3v1, 128);
			}
			flock($fp, LOCK_UN);
			fclose($fp);
			ignore_user_abort($oldignoreuserabort);
			return true;

		}
		return false;
	}
	return false;
}

function GenerateAPEtag($data) {
	$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), '', $arrayofvalues);
		}
		// 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 ($itemkey) {
		case 'EAN/UPC':
		case 'ISBN':
		case 'LC':
		case 'ISRC':
			$itemkey = strtoupper($itemkey);
			break;

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

}


?>