0
|
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 |
} |