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