|
1 <?php |
|
2 ///////////////////////////////////////////////////////////////// |
|
3 /// getID3() by James Heinrich <info@getid3.org> // |
|
4 // available at http://getid3.sourceforge.net // |
|
5 // or http://www.getid3.org // |
|
6 ///////////////////////////////////////////////////////////////// |
|
7 // See readme.txt for more details // |
|
8 ///////////////////////////////////////////////////////////////// |
|
9 // // |
|
10 // module.tag.id3v1.php // |
|
11 // module for analyzing ID3v1 tags // |
|
12 // dependencies: NONE // |
|
13 // /// |
|
14 ///////////////////////////////////////////////////////////////// |
|
15 |
|
16 |
|
17 class getid3_id3v1 extends getid3_handler |
|
18 { |
|
19 |
|
20 public function Analyze() { |
|
21 $info = &$this->getid3->info; |
|
22 |
|
23 if (!getid3_lib::intValueSupported($info['filesize'])) { |
|
24 $info['warning'][] = 'Unable to check for ID3v1 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB'; |
|
25 return false; |
|
26 } |
|
27 |
|
28 fseek($this->getid3->fp, -256, SEEK_END); |
|
29 $preid3v1 = fread($this->getid3->fp, 128); |
|
30 $id3v1tag = fread($this->getid3->fp, 128); |
|
31 |
|
32 if (substr($id3v1tag, 0, 3) == 'TAG') { |
|
33 |
|
34 $info['avdataend'] = $info['filesize'] - 128; |
|
35 |
|
36 $ParsedID3v1['title'] = $this->cutfield(substr($id3v1tag, 3, 30)); |
|
37 $ParsedID3v1['artist'] = $this->cutfield(substr($id3v1tag, 33, 30)); |
|
38 $ParsedID3v1['album'] = $this->cutfield(substr($id3v1tag, 63, 30)); |
|
39 $ParsedID3v1['year'] = $this->cutfield(substr($id3v1tag, 93, 4)); |
|
40 $ParsedID3v1['comment'] = substr($id3v1tag, 97, 30); // can't remove nulls yet, track detection depends on them |
|
41 $ParsedID3v1['genreid'] = ord(substr($id3v1tag, 127, 1)); |
|
42 |
|
43 // If second-last byte of comment field is null and last byte of comment field is non-null |
|
44 // then this is ID3v1.1 and the comment field is 28 bytes long and the 30th byte is the track number |
|
45 if (($id3v1tag{125} === "\x00") && ($id3v1tag{126} !== "\x00")) { |
|
46 $ParsedID3v1['track'] = ord(substr($ParsedID3v1['comment'], 29, 1)); |
|
47 $ParsedID3v1['comment'] = substr($ParsedID3v1['comment'], 0, 28); |
|
48 } |
|
49 $ParsedID3v1['comment'] = $this->cutfield($ParsedID3v1['comment']); |
|
50 |
|
51 $ParsedID3v1['genre'] = $this->LookupGenreName($ParsedID3v1['genreid']); |
|
52 if (!empty($ParsedID3v1['genre'])) { |
|
53 unset($ParsedID3v1['genreid']); |
|
54 } |
|
55 if (isset($ParsedID3v1['genre']) && (empty($ParsedID3v1['genre']) || ($ParsedID3v1['genre'] == 'Unknown'))) { |
|
56 unset($ParsedID3v1['genre']); |
|
57 } |
|
58 |
|
59 foreach ($ParsedID3v1 as $key => $value) { |
|
60 $ParsedID3v1['comments'][$key][0] = $value; |
|
61 } |
|
62 |
|
63 // ID3v1 data is supposed to be padded with NULL characters, but some taggers pad with spaces |
|
64 $GoodFormatID3v1tag = $this->GenerateID3v1Tag( |
|
65 $ParsedID3v1['title'], |
|
66 $ParsedID3v1['artist'], |
|
67 $ParsedID3v1['album'], |
|
68 $ParsedID3v1['year'], |
|
69 (isset($ParsedID3v1['genre']) ? $this->LookupGenreID($ParsedID3v1['genre']) : false), |
|
70 $ParsedID3v1['comment'], |
|
71 (!empty($ParsedID3v1['track']) ? $ParsedID3v1['track'] : '')); |
|
72 $ParsedID3v1['padding_valid'] = true; |
|
73 if ($id3v1tag !== $GoodFormatID3v1tag) { |
|
74 $ParsedID3v1['padding_valid'] = false; |
|
75 $info['warning'][] = 'Some ID3v1 fields do not use NULL characters for padding'; |
|
76 } |
|
77 |
|
78 $ParsedID3v1['tag_offset_end'] = $info['filesize']; |
|
79 $ParsedID3v1['tag_offset_start'] = $ParsedID3v1['tag_offset_end'] - 128; |
|
80 |
|
81 $info['id3v1'] = $ParsedID3v1; |
|
82 } |
|
83 |
|
84 if (substr($preid3v1, 0, 3) == 'TAG') { |
|
85 // The way iTunes handles tags is, well, brain-damaged. |
|
86 // It completely ignores v1 if ID3v2 is present. |
|
87 // This goes as far as adding a new v1 tag *even if there already is one* |
|
88 |
|
89 // A suspected double-ID3v1 tag has been detected, but it could be that |
|
90 // the "TAG" identifier is a legitimate part of an APE or Lyrics3 tag |
|
91 if (substr($preid3v1, 96, 8) == 'APETAGEX') { |
|
92 // an APE tag footer was found before the last ID3v1, assume false "TAG" synch |
|
93 } elseif (substr($preid3v1, 119, 6) == 'LYRICS') { |
|
94 // a Lyrics3 tag footer was found before the last ID3v1, assume false "TAG" synch |
|
95 } else { |
|
96 // APE and Lyrics3 footers not found - assume double ID3v1 |
|
97 $info['warning'][] = 'Duplicate ID3v1 tag detected - this has been known to happen with iTunes'; |
|
98 $info['avdataend'] -= 128; |
|
99 } |
|
100 } |
|
101 |
|
102 return true; |
|
103 } |
|
104 |
|
105 public static function cutfield($str) { |
|
106 return trim(substr($str, 0, strcspn($str, "\x00"))); |
|
107 } |
|
108 |
|
109 public static function ArrayOfGenres($allowSCMPXextended=false) { |
|
110 static $GenreLookup = array( |
|
111 0 => 'Blues', |
|
112 1 => 'Classic Rock', |
|
113 2 => 'Country', |
|
114 3 => 'Dance', |
|
115 4 => 'Disco', |
|
116 5 => 'Funk', |
|
117 6 => 'Grunge', |
|
118 7 => 'Hip-Hop', |
|
119 8 => 'Jazz', |
|
120 9 => 'Metal', |
|
121 10 => 'New Age', |
|
122 11 => 'Oldies', |
|
123 12 => 'Other', |
|
124 13 => 'Pop', |
|
125 14 => 'R&B', |
|
126 15 => 'Rap', |
|
127 16 => 'Reggae', |
|
128 17 => 'Rock', |
|
129 18 => 'Techno', |
|
130 19 => 'Industrial', |
|
131 20 => 'Alternative', |
|
132 21 => 'Ska', |
|
133 22 => 'Death Metal', |
|
134 23 => 'Pranks', |
|
135 24 => 'Soundtrack', |
|
136 25 => 'Euro-Techno', |
|
137 26 => 'Ambient', |
|
138 27 => 'Trip-Hop', |
|
139 28 => 'Vocal', |
|
140 29 => 'Jazz+Funk', |
|
141 30 => 'Fusion', |
|
142 31 => 'Trance', |
|
143 32 => 'Classical', |
|
144 33 => 'Instrumental', |
|
145 34 => 'Acid', |
|
146 35 => 'House', |
|
147 36 => 'Game', |
|
148 37 => 'Sound Clip', |
|
149 38 => 'Gospel', |
|
150 39 => 'Noise', |
|
151 40 => 'Alt. Rock', |
|
152 41 => 'Bass', |
|
153 42 => 'Soul', |
|
154 43 => 'Punk', |
|
155 44 => 'Space', |
|
156 45 => 'Meditative', |
|
157 46 => 'Instrumental Pop', |
|
158 47 => 'Instrumental Rock', |
|
159 48 => 'Ethnic', |
|
160 49 => 'Gothic', |
|
161 50 => 'Darkwave', |
|
162 51 => 'Techno-Industrial', |
|
163 52 => 'Electronic', |
|
164 53 => 'Pop-Folk', |
|
165 54 => 'Eurodance', |
|
166 55 => 'Dream', |
|
167 56 => 'Southern Rock', |
|
168 57 => 'Comedy', |
|
169 58 => 'Cult', |
|
170 59 => 'Gangsta Rap', |
|
171 60 => 'Top 40', |
|
172 61 => 'Christian Rap', |
|
173 62 => 'Pop/Funk', |
|
174 63 => 'Jungle', |
|
175 64 => 'Native American', |
|
176 65 => 'Cabaret', |
|
177 66 => 'New Wave', |
|
178 67 => 'Psychedelic', |
|
179 68 => 'Rave', |
|
180 69 => 'Showtunes', |
|
181 70 => 'Trailer', |
|
182 71 => 'Lo-Fi', |
|
183 72 => 'Tribal', |
|
184 73 => 'Acid Punk', |
|
185 74 => 'Acid Jazz', |
|
186 75 => 'Polka', |
|
187 76 => 'Retro', |
|
188 77 => 'Musical', |
|
189 78 => 'Rock & Roll', |
|
190 79 => 'Hard Rock', |
|
191 80 => 'Folk', |
|
192 81 => 'Folk/Rock', |
|
193 82 => 'National Folk', |
|
194 83 => 'Swing', |
|
195 84 => 'Fast-Fusion', |
|
196 85 => 'Bebob', |
|
197 86 => 'Latin', |
|
198 87 => 'Revival', |
|
199 88 => 'Celtic', |
|
200 89 => 'Bluegrass', |
|
201 90 => 'Avantgarde', |
|
202 91 => 'Gothic Rock', |
|
203 92 => 'Progressive Rock', |
|
204 93 => 'Psychedelic Rock', |
|
205 94 => 'Symphonic Rock', |
|
206 95 => 'Slow Rock', |
|
207 96 => 'Big Band', |
|
208 97 => 'Chorus', |
|
209 98 => 'Easy Listening', |
|
210 99 => 'Acoustic', |
|
211 100 => 'Humour', |
|
212 101 => 'Speech', |
|
213 102 => 'Chanson', |
|
214 103 => 'Opera', |
|
215 104 => 'Chamber Music', |
|
216 105 => 'Sonata', |
|
217 106 => 'Symphony', |
|
218 107 => 'Booty Bass', |
|
219 108 => 'Primus', |
|
220 109 => 'Porn Groove', |
|
221 110 => 'Satire', |
|
222 111 => 'Slow Jam', |
|
223 112 => 'Club', |
|
224 113 => 'Tango', |
|
225 114 => 'Samba', |
|
226 115 => 'Folklore', |
|
227 116 => 'Ballad', |
|
228 117 => 'Power Ballad', |
|
229 118 => 'Rhythmic Soul', |
|
230 119 => 'Freestyle', |
|
231 120 => 'Duet', |
|
232 121 => 'Punk Rock', |
|
233 122 => 'Drum Solo', |
|
234 123 => 'A Cappella', |
|
235 124 => 'Euro-House', |
|
236 125 => 'Dance Hall', |
|
237 126 => 'Goa', |
|
238 127 => 'Drum & Bass', |
|
239 128 => 'Club-House', |
|
240 129 => 'Hardcore', |
|
241 130 => 'Terror', |
|
242 131 => 'Indie', |
|
243 132 => 'BritPop', |
|
244 133 => 'Negerpunk', |
|
245 134 => 'Polsk Punk', |
|
246 135 => 'Beat', |
|
247 136 => 'Christian Gangsta Rap', |
|
248 137 => 'Heavy Metal', |
|
249 138 => 'Black Metal', |
|
250 139 => 'Crossover', |
|
251 140 => 'Contemporary Christian', |
|
252 141 => 'Christian Rock', |
|
253 142 => 'Merengue', |
|
254 143 => 'Salsa', |
|
255 144 => 'Thrash Metal', |
|
256 145 => 'Anime', |
|
257 146 => 'JPop', |
|
258 147 => 'Synthpop', |
|
259 |
|
260 255 => 'Unknown', |
|
261 |
|
262 'CR' => 'Cover', |
|
263 'RX' => 'Remix' |
|
264 ); |
|
265 |
|
266 static $GenreLookupSCMPX = array(); |
|
267 if ($allowSCMPXextended && empty($GenreLookupSCMPX)) { |
|
268 $GenreLookupSCMPX = $GenreLookup; |
|
269 // http://www.geocities.co.jp/SiliconValley-Oakland/3664/alittle.html#GenreExtended |
|
270 // Extended ID3v1 genres invented by SCMPX |
|
271 // Note that 255 "Japanese Anime" conflicts with standard "Unknown" |
|
272 $GenreLookupSCMPX[240] = 'Sacred'; |
|
273 $GenreLookupSCMPX[241] = 'Northern Europe'; |
|
274 $GenreLookupSCMPX[242] = 'Irish & Scottish'; |
|
275 $GenreLookupSCMPX[243] = 'Scotland'; |
|
276 $GenreLookupSCMPX[244] = 'Ethnic Europe'; |
|
277 $GenreLookupSCMPX[245] = 'Enka'; |
|
278 $GenreLookupSCMPX[246] = 'Children\'s Song'; |
|
279 $GenreLookupSCMPX[247] = 'Japanese Sky'; |
|
280 $GenreLookupSCMPX[248] = 'Japanese Heavy Rock'; |
|
281 $GenreLookupSCMPX[249] = 'Japanese Doom Rock'; |
|
282 $GenreLookupSCMPX[250] = 'Japanese J-POP'; |
|
283 $GenreLookupSCMPX[251] = 'Japanese Seiyu'; |
|
284 $GenreLookupSCMPX[252] = 'Japanese Ambient Techno'; |
|
285 $GenreLookupSCMPX[253] = 'Japanese Moemoe'; |
|
286 $GenreLookupSCMPX[254] = 'Japanese Tokusatsu'; |
|
287 //$GenreLookupSCMPX[255] = 'Japanese Anime'; |
|
288 } |
|
289 |
|
290 return ($allowSCMPXextended ? $GenreLookupSCMPX : $GenreLookup); |
|
291 } |
|
292 |
|
293 public static function LookupGenreName($genreid, $allowSCMPXextended=true) { |
|
294 switch ($genreid) { |
|
295 case 'RX': |
|
296 case 'CR': |
|
297 break; |
|
298 default: |
|
299 if (!is_numeric($genreid)) { |
|
300 return false; |
|
301 } |
|
302 $genreid = intval($genreid); // to handle 3 or '3' or '03' |
|
303 break; |
|
304 } |
|
305 $GenreLookup = self::ArrayOfGenres($allowSCMPXextended); |
|
306 return (isset($GenreLookup[$genreid]) ? $GenreLookup[$genreid] : false); |
|
307 } |
|
308 |
|
309 public static function LookupGenreID($genre, $allowSCMPXextended=false) { |
|
310 $GenreLookup = self::ArrayOfGenres($allowSCMPXextended); |
|
311 $LowerCaseNoSpaceSearchTerm = strtolower(str_replace(' ', '', $genre)); |
|
312 foreach ($GenreLookup as $key => $value) { |
|
313 if (strtolower(str_replace(' ', '', $value)) == $LowerCaseNoSpaceSearchTerm) { |
|
314 return $key; |
|
315 } |
|
316 } |
|
317 return false; |
|
318 } |
|
319 |
|
320 public static function StandardiseID3v1GenreName($OriginalGenre) { |
|
321 if (($GenreID = self::LookupGenreID($OriginalGenre)) !== false) { |
|
322 return self::LookupGenreName($GenreID); |
|
323 } |
|
324 return $OriginalGenre; |
|
325 } |
|
326 |
|
327 public static function GenerateID3v1Tag($title, $artist, $album, $year, $genreid, $comment, $track='') { |
|
328 $ID3v1Tag = 'TAG'; |
|
329 $ID3v1Tag .= str_pad(trim(substr($title, 0, 30)), 30, "\x00", STR_PAD_RIGHT); |
|
330 $ID3v1Tag .= str_pad(trim(substr($artist, 0, 30)), 30, "\x00", STR_PAD_RIGHT); |
|
331 $ID3v1Tag .= str_pad(trim(substr($album, 0, 30)), 30, "\x00", STR_PAD_RIGHT); |
|
332 $ID3v1Tag .= str_pad(trim(substr($year, 0, 4)), 4, "\x00", STR_PAD_LEFT); |
|
333 if (!empty($track) && ($track > 0) && ($track <= 255)) { |
|
334 $ID3v1Tag .= str_pad(trim(substr($comment, 0, 28)), 28, "\x00", STR_PAD_RIGHT); |
|
335 $ID3v1Tag .= "\x00"; |
|
336 if (gettype($track) == 'string') { |
|
337 $track = (int) $track; |
|
338 } |
|
339 $ID3v1Tag .= chr($track); |
|
340 } else { |
|
341 $ID3v1Tag .= str_pad(trim(substr($comment, 0, 30)), 30, "\x00", STR_PAD_RIGHT); |
|
342 } |
|
343 if (($genreid < 0) || ($genreid > 147)) { |
|
344 $genreid = 255; // 'unknown' genre |
|
345 } |
|
346 switch (gettype($genreid)) { |
|
347 case 'string': |
|
348 case 'integer': |
|
349 $ID3v1Tag .= chr(intval($genreid)); |
|
350 break; |
|
351 default: |
|
352 $ID3v1Tag .= chr(255); // 'unknown' genre |
|
353 break; |
|
354 } |
|
355 |
|
356 return $ID3v1Tag; |
|
357 } |
|
358 |
|
359 } |