<?php
/////////////////////////////////////////////////////////////////
/// getID3() by James Heinrich <info@getid3.org>			//
// available at http://getid3.sourceforge.net				//
//			or http://www.getid3.org						//
/////////////////////////////////////////////////////////////////
// See readme.txt for more details							//
/////////////////////////////////////////////////////////////////
//															//
// module.tag.id3v1.php										//
// module for analyzing ID3v1 tags							//
// dependencies: NONE										//
//															///
/////////////////////////////////////////////////////////////////


class getid3_id3v1
{

	function getid3_id3v1(&$fd, &$ThisFileInfo) {

		if ($ThisFileInfo['filesize'] >= pow(2, 31)) {
			$ThisFileInfo['warning'][] = 'Unable to check for ID3v1 because file is larger than 2GB';
			return false;
		}

		fseek($fd, -256, SEEK_END);
		$preid3v1 = fread($fd, 128);
		$id3v1tag = fread($fd, 128);

		if (substr($id3v1tag, 0, 3) == 'TAG') {

			$ThisFileInfo['avdataend'] = $ThisFileInfo['filesize'] - 128;

			$ParsedID3v1['title'] = $this->cutfield(substr($id3v1tag, 3, 30));
			$ParsedID3v1['artist'] = $this->cutfield(substr($id3v1tag, 33, 30));
			$ParsedID3v1['album'] = $this->cutfield(substr($id3v1tag, 63, 30));
			$ParsedID3v1['year']	= $this->cutfield(substr($id3v1tag, 93, 4));
			$ParsedID3v1['comment'] =				substr($id3v1tag, 97, 30); // can't remove nulls yet, track detection depends on them
			$ParsedID3v1['genreid'] =			ord(substr($id3v1tag, 127, 1));

			// If second-last byte of comment field is null and last byte of comment field is non-null
			// then this is ID3v1.1 and the comment field is 28 bytes long and the 30th byte is the track number
			if (($id3v1tag{125} === "\x00") && ($id3v1tag{126} !== "\x00")) {
				$ParsedID3v1['track'] = ord(substr($ParsedID3v1['comment'], 29, 1));
				$ParsedID3v1['comment'] =	substr($ParsedID3v1['comment'], 0, 28);
			}
			$ParsedID3v1['comment'] = $this->cutfield($ParsedID3v1['comment']);

			$ParsedID3v1['genre'] = $this->LookupGenreName($ParsedID3v1['genreid']);
			if (!empty($ParsedID3v1['genre'])) {
				unset($ParsedID3v1['genreid']);
			}
			if (empty($ParsedID3v1['genre']) || (@$ParsedID3v1['genre'] == 'Unknown')) {
				unset($ParsedID3v1['genre']);
			}

			foreach ($ParsedID3v1 as $key => $value) {
				$ParsedID3v1['comments'][$key][0] = $value;
			}

			// ID3v1 data is supposed to be padded with NULL characters, but some taggers pad with spaces
			$GoodFormatID3v1tag = $this->GenerateID3v1Tag(
											$ParsedID3v1['title'],
											$ParsedID3v1['artist'],
											$ParsedID3v1['album'],
											$ParsedID3v1['year'],
											(isset($ParsedID3v1['genre']) ? $this->LookupGenreID($ParsedID3v1['genre']) : false),
											$ParsedID3v1['comment'],
											@$ParsedID3v1['track']);
			$ParsedID3v1['padding_valid'] = true;
			if ($id3v1tag !== $GoodFormatID3v1tag) {
				$ParsedID3v1['padding_valid'] = false;
				$ThisFileInfo['warning'][] = 'Some ID3v1 fields do not use NULL characters for padding';
			}

			$ParsedID3v1['tag_offset_end'] = $ThisFileInfo['filesize'];
			$ParsedID3v1['tag_offset_start'] = $ParsedID3v1['tag_offset_end'] - 128;

			$ThisFileInfo['id3v1'] = $ParsedID3v1;
		}

		if (substr($preid3v1, 0, 3) == 'TAG') {
			// The way iTunes handles tags is, well, brain-damaged.
			// It completely ignores v1 if ID3v2 is present.
			// This goes as far as adding a new v1 tag *even if there already is one*

			// A suspected double-ID3v1 tag has been detected, but it could be that
			// the "TAG" identifier is a legitimate part of an APE or Lyrics3 tag
			if (substr($preid3v1, 96, 8) == 'APETAGEX') {
				// an APE tag footer was found before the last ID3v1, assume false "TAG" synch
			} elseif (substr($preid3v1, 119, 6) == 'LYRICS') {
				// a Lyrics3 tag footer was found before the last ID3v1, assume false "TAG" synch
			} else {
				// APE and Lyrics3 footers not found - assume double ID3v1
				$ThisFileInfo['warning'][] = 'Duplicate ID3v1 tag detected - this has been known to happen with iTunes';
				$ThisFileInfo['avdataend'] -= 128;
			}
		}

		return true;
	}

	function cutfield($str) {
		return trim(substr($str, 0, strcspn($str, "\x00")));
	}

	function ArrayOfGenres($allowSCMPXextended=false) {
		static $GenreLookup = array(
			0	=> 'Blues',
			1	=> 'Classic Rock',
			2	=> 'Country',
			3	=> 'Dance',
			4	=> 'Disco',
			5	=> 'Funk',
			6	=> 'Grunge',
			7	=> 'Hip-Hop',
			8	=> 'Jazz',
			9	=> 'Metal',
			10 => 'New Age',
			11 => 'Oldies',
			12 => 'Other',
			13 => 'Pop',
			14 => 'R&B',
			15 => 'Rap',
			16 => 'Reggae',
			17 => 'Rock',
			18 => 'Techno',
			19 => 'Industrial',
			20 => 'Alternative',
			21 => 'Ska',
			22 => 'Death Metal',
			23 => 'Pranks',
			24 => 'Soundtrack',
			25 => 'Euro-Techno',
			26 => 'Ambient',
			27 => 'Trip-Hop',
			28 => 'Vocal',
			29 => 'Jazz+Funk',
			30 => 'Fusion',
			31 => 'Trance',
			32 => 'Classical',
			33 => 'Instrumental',
			34 => 'Acid',
			35 => 'House',
			36 => 'Game',
			37 => 'Sound Clip',
			38 => 'Gospel',
			39 => 'Noise',
			40 => 'Alt. Rock',
			41 => 'Bass',
			42 => 'Soul',
			43 => 'Punk',
			44 => 'Space',
			45 => 'Meditative',
			46 => 'Instrumental Pop',
			47 => 'Instrumental Rock',
			48 => 'Ethnic',
			49 => 'Gothic',
			50 => 'Darkwave',
			51 => 'Techno-Industrial',
			52 => 'Electronic',
			53 => 'Pop-Folk',
			54 => 'Eurodance',
			55 => 'Dream',
			56 => 'Southern Rock',
			57 => 'Comedy',
			58 => 'Cult',
			59 => 'Gangsta Rap',
			60 => 'Top 40',
			61 => 'Christian Rap',
			62 => 'Pop/Funk',
			63 => 'Jungle',
			64 => 'Native American',
			65 => 'Cabaret',
			66 => 'New Wave',
			67 => 'Psychedelic',
			68 => 'Rave',
			69 => 'Showtunes',
			70 => 'Trailer',
			71 => 'Lo-Fi',
			72 => 'Tribal',
			73 => 'Acid Punk',
			74 => 'Acid Jazz',
			75 => 'Polka',
			76 => 'Retro',
			77 => 'Musical',
			78 => 'Rock & Roll',
			79 => 'Hard Rock',
			80 => 'Folk',
			81 => 'Folk/Rock',
			82 => 'National Folk',
			83 => 'Swing',
			84 => 'Fast-Fusion',
			85 => 'Bebob',
			86 => 'Latin',
			87 => 'Revival',
			88 => 'Celtic',
			89 => 'Bluegrass',
			90 => 'Avantgarde',
			91 => 'Gothic Rock',
			92 => 'Progressive Rock',
			93 => 'Psychedelic Rock',
			94 => 'Symphonic Rock',
			95 => 'Slow Rock',
			96 => 'Big Band',
			97 => 'Chorus',
			98 => 'Easy Listening',
			99 => 'Acoustic',
			100 => 'Humour',
			101 => 'Speech',
			102 => 'Chanson',
			103 => 'Opera',
			104 => 'Chamber Music',
			105 => 'Sonata',
			106 => 'Symphony',
			107 => 'Booty Bass',
			108 => 'Primus',
			109 => 'Porn Groove',
			110 => 'Satire',
			111 => 'Slow Jam',
			112 => 'Club',
			113 => 'Tango',
			114 => 'Samba',
			115 => 'Folklore',
			116 => 'Ballad',
			117 => 'Power Ballad',
			118 => 'Rhythmic Soul',
			119 => 'Freestyle',
			120 => 'Duet',
			121 => 'Punk Rock',
			122 => 'Drum Solo',
			123 => 'A Cappella',
			124 => 'Euro-House',
			125 => 'Dance Hall',
			126 => 'Goa',
			127 => 'Drum & Bass',
			128 => 'Club-House',
			129 => 'Hardcore',
			130 => 'Terror',
			131 => 'Indie',
			132 => 'BritPop',
			133 => 'Negerpunk',
			134 => 'Polsk Punk',
			135 => 'Beat',
			136 => 'Christian Gangsta Rap',
			137 => 'Heavy Metal',
			138 => 'Black Metal',
			139 => 'Crossover',
			140 => 'Contemporary Christian',
			141 => 'Christian Rock',
			142 => 'Merengue',
			143 => 'Salsa',
			144 => 'Trash Metal',
			145 => 'Anime',
			146 => 'JPop',
			147 => 'Synthpop',

			255 => 'Unknown',

			'CR' => 'Cover',
			'RX' => 'Remix'
		);

		static $GenreLookupSCMPX = array();
		if ($allowSCMPXextended && empty($GenreLookupSCMPX)) {
			$GenreLookupSCMPX = $GenreLookup;
			// http://www.geocities.co.jp/SiliconValley-Oakland/3664/alittle.html#GenreExtended
			// Extended ID3v1 genres invented by SCMPX
			// Note that 255 "Japanese Anime" conflicts with standard "Unknown"
			$GenreLookupSCMPX[240] = 'Sacred';
			$GenreLookupSCMPX[241] = 'Northern Europe';
			$GenreLookupSCMPX[242] = 'Irish & Scottish';
			$GenreLookupSCMPX[243] = 'Scotland';
			$GenreLookupSCMPX[244] = 'Ethnic Europe';
			$GenreLookupSCMPX[245] = 'Enka';
			$GenreLookupSCMPX[246] = 'Children\'s Song';
			$GenreLookupSCMPX[247] = 'Japanese Sky';
			$GenreLookupSCMPX[248] = 'Japanese Heavy Rock';
			$GenreLookupSCMPX[249] = 'Japanese Doom Rock';
			$GenreLookupSCMPX[250] = 'Japanese J-POP';
			$GenreLookupSCMPX[251] = 'Japanese Seiyu';
			$GenreLookupSCMPX[252] = 'Japanese Ambient Techno';
			$GenreLookupSCMPX[253] = 'Japanese Moemoe';
			$GenreLookupSCMPX[254] = 'Japanese Tokusatsu';
			//$GenreLookupSCMPX[255] = 'Japanese Anime';
		}

		return ($allowSCMPXextended ? $GenreLookupSCMPX : $GenreLookup);
	}

	function LookupGenreName($genreid, $allowSCMPXextended=true) {
		switch ($genreid) {
			case 'RX':
			case 'CR':
				break;
			default:
				$genreid = intval($genreid); // to handle 3 or '3' or '03'
				break;
		}
		$GenreLookup = getid3_id3v1::ArrayOfGenres($allowSCMPXextended);
		return (isset($GenreLookup[$genreid]) ? $GenreLookup[$genreid] : false);
	}

	function LookupGenreID($genre, $allowSCMPXextended=false) {
		$GenreLookup = getid3_id3v1::ArrayOfGenres($allowSCMPXextended);
		$LowerCaseNoSpaceSearchTerm = strtolower(str_replace(' ', '', $genre));
		foreach ($GenreLookup as $key => $value) {
			foreach ($GenreLookup as $key => $value) {
				if (strtolower(str_replace(' ', '', $value)) == $LowerCaseNoSpaceSearchTerm) {
					return $key;
				}
			}
			return false;
		}
		return (isset($GenreLookup[$genreid]) ? $GenreLookup[$genreid] : false);
	}

	function StandardiseID3v1GenreName($OriginalGenre) {
		if (($GenreID = getid3_id3v1::LookupGenreID($OriginalGenre)) !== false) {
			return getid3_id3v1::LookupGenreName($GenreID);
		}
		return $OriginalGenre;
	}

	function GenerateID3v1Tag($title, $artist, $album, $year, $genreid, $comment, $track='') {
		$ID3v1Tag = 'TAG';
		$ID3v1Tag .= str_pad(trim(substr($title, 0, 30)), 30, "\x00", STR_PAD_RIGHT);
		$ID3v1Tag .= str_pad(trim(substr($artist, 0, 30)), 30, "\x00", STR_PAD_RIGHT);
		$ID3v1Tag .= str_pad(trim(substr($album, 0, 30)), 30, "\x00", STR_PAD_RIGHT);
		$ID3v1Tag .= str_pad(trim(substr($year, 0, 4)), 4, "\x00", STR_PAD_LEFT);
		if (!empty($track) && ($track > 0) && ($track <= 255)) {
			$ID3v1Tag .= str_pad(trim(substr($comment, 0, 28)), 28, "\x00", STR_PAD_RIGHT);
			$ID3v1Tag .= "\x00";
			if (gettype($track) == 'string') {
				$track = (int) $track;
			}
			$ID3v1Tag .= chr($track);
		} else {
			$ID3v1Tag .= str_pad(trim(substr($comment, 0, 30)), 30, "\x00", STR_PAD_RIGHT);
		}
		if (($genreid < 0) || ($genreid > 147)) {
			$genreid = 255; // 'unknown' genre
		}
		switch (gettype($genreid)) {
			case 'string':
			case 'integer':
				$ID3v1Tag .= chr(intval($genreid));
				break;
			default:
				$ID3v1Tag .= chr(255); // 'unknown' genre
				break;
		}

		return $ID3v1Tag;
	}

}


?>