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.apetag.php // |
---|
11 | // module for analyzing APE tags // |
---|
12 | // dependencies: NONE // |
---|
13 | // /// |
---|
14 | ///////////////////////////////////////////////////////////////// |
---|
15 | |
---|
16 | class getid3_apetag extends getid3_handler |
---|
17 | { |
---|
18 | public $inline_attachments = true; // true: return full data for all attachments; false: return no data for all attachments; integer: return data for attachments <= than this; string: save as file to this directory |
---|
19 | public $overrideendoffset = 0; |
---|
20 | |
---|
21 | public function Analyze() { |
---|
22 | $info = &$this->getid3->info; |
---|
23 | |
---|
24 | if (!getid3_lib::intValueSupported($info['filesize'])) { |
---|
25 | $info['warning'][] = 'Unable to check for APEtags because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB'; |
---|
26 | return false; |
---|
27 | } |
---|
28 | |
---|
29 | $id3v1tagsize = 128; |
---|
30 | $apetagheadersize = 32; |
---|
31 | $lyrics3tagsize = 10; |
---|
32 | |
---|
33 | if ($this->overrideendoffset == 0) { |
---|
34 | |
---|
35 | fseek($this->getid3->fp, 0 - $id3v1tagsize - $apetagheadersize - $lyrics3tagsize, SEEK_END); |
---|
36 | $APEfooterID3v1 = fread($this->getid3->fp, $id3v1tagsize + $apetagheadersize + $lyrics3tagsize); |
---|
37 | |
---|
38 | //if (preg_match('/APETAGEX.{24}TAG.{125}$/i', $APEfooterID3v1)) { |
---|
39 | if (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $id3v1tagsize - $apetagheadersize, 8) == 'APETAGEX') { |
---|
40 | |
---|
41 | // APE tag found before ID3v1 |
---|
42 | $info['ape']['tag_offset_end'] = $info['filesize'] - $id3v1tagsize; |
---|
43 | |
---|
44 | //} elseif (preg_match('/APETAGEX.{24}$/i', $APEfooterID3v1)) { |
---|
45 | } elseif (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $apetagheadersize, 8) == 'APETAGEX') { |
---|
46 | |
---|
47 | // APE tag found, no ID3v1 |
---|
48 | $info['ape']['tag_offset_end'] = $info['filesize']; |
---|
49 | |
---|
50 | } |
---|
51 | |
---|
52 | } else { |
---|
53 | |
---|
54 | fseek($this->getid3->fp, $this->overrideendoffset - $apetagheadersize, SEEK_SET); |
---|
55 | if (fread($this->getid3->fp, 8) == 'APETAGEX') { |
---|
56 | $info['ape']['tag_offset_end'] = $this->overrideendoffset; |
---|
57 | } |
---|
58 | |
---|
59 | } |
---|
60 | if (!isset($info['ape']['tag_offset_end'])) { |
---|
61 | |
---|
62 | // APE tag not found |
---|
63 | unset($info['ape']); |
---|
64 | return false; |
---|
65 | |
---|
66 | } |
---|
67 | |
---|
68 | // shortcut |
---|
69 | $thisfile_ape = &$info['ape']; |
---|
70 | |
---|
71 | fseek($this->getid3->fp, $thisfile_ape['tag_offset_end'] - $apetagheadersize, SEEK_SET); |
---|
72 | $APEfooterData = fread($this->getid3->fp, 32); |
---|
73 | if (!($thisfile_ape['footer'] = $this->parseAPEheaderFooter($APEfooterData))) { |
---|
74 | $info['error'][] = 'Error parsing APE footer at offset '.$thisfile_ape['tag_offset_end']; |
---|
75 | return false; |
---|
76 | } |
---|
77 | |
---|
78 | if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) { |
---|
79 | fseek($this->getid3->fp, $thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize'] - $apetagheadersize, SEEK_SET); |
---|
80 | $thisfile_ape['tag_offset_start'] = ftell($this->getid3->fp); |
---|
81 | $APEtagData = fread($this->getid3->fp, $thisfile_ape['footer']['raw']['tagsize'] + $apetagheadersize); |
---|
82 | } else { |
---|
83 | $thisfile_ape['tag_offset_start'] = $thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize']; |
---|
84 | fseek($this->getid3->fp, $thisfile_ape['tag_offset_start'], SEEK_SET); |
---|
85 | $APEtagData = fread($this->getid3->fp, $thisfile_ape['footer']['raw']['tagsize']); |
---|
86 | } |
---|
87 | $info['avdataend'] = $thisfile_ape['tag_offset_start']; |
---|
88 | |
---|
89 | if (isset($info['id3v1']['tag_offset_start']) && ($info['id3v1']['tag_offset_start'] < $thisfile_ape['tag_offset_end'])) { |
---|
90 | $info['warning'][] = 'ID3v1 tag information ignored since it appears to be a false synch in APEtag data'; |
---|
91 | unset($info['id3v1']); |
---|
92 | foreach ($info['warning'] as $key => $value) { |
---|
93 | if ($value == 'Some ID3v1 fields do not use NULL characters for padding') { |
---|
94 | unset($info['warning'][$key]); |
---|
95 | sort($info['warning']); |
---|
96 | break; |
---|
97 | } |
---|
98 | } |
---|
99 | } |
---|
100 | |
---|
101 | $offset = 0; |
---|
102 | if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) { |
---|
103 | if ($thisfile_ape['header'] = $this->parseAPEheaderFooter(substr($APEtagData, 0, $apetagheadersize))) { |
---|
104 | $offset += $apetagheadersize; |
---|
105 | } else { |
---|
106 | $info['error'][] = 'Error parsing APE header at offset '.$thisfile_ape['tag_offset_start']; |
---|
107 | return false; |
---|
108 | } |
---|
109 | } |
---|
110 | |
---|
111 | // shortcut |
---|
112 | $info['replay_gain'] = array(); |
---|
113 | $thisfile_replaygain = &$info['replay_gain']; |
---|
114 | |
---|
115 | for ($i = 0; $i < $thisfile_ape['footer']['raw']['tag_items']; $i++) { |
---|
116 | $value_size = getid3_lib::LittleEndian2Int(substr($APEtagData, $offset, 4)); |
---|
117 | $offset += 4; |
---|
118 | $item_flags = getid3_lib::LittleEndian2Int(substr($APEtagData, $offset, 4)); |
---|
119 | $offset += 4; |
---|
120 | if (strstr(substr($APEtagData, $offset), "\x00") === false) { |
---|
121 | $info['error'][] = 'Cannot find null-byte (0x00) seperator between ItemKey #'.$i.' and value. ItemKey starts '.$offset.' bytes into the APE tag, at file offset '.($thisfile_ape['tag_offset_start'] + $offset); |
---|
122 | return false; |
---|
123 | } |
---|
124 | $ItemKeyLength = strpos($APEtagData, "\x00", $offset) - $offset; |
---|
125 | $item_key = strtolower(substr($APEtagData, $offset, $ItemKeyLength)); |
---|
126 | |
---|
127 | // shortcut |
---|
128 | $thisfile_ape['items'][$item_key] = array(); |
---|
129 | $thisfile_ape_items_current = &$thisfile_ape['items'][$item_key]; |
---|
130 | |
---|
131 | $thisfile_ape_items_current['offset'] = $thisfile_ape['tag_offset_start'] + $offset; |
---|
132 | |
---|
133 | $offset += ($ItemKeyLength + 1); // skip 0x00 terminator |
---|
134 | $thisfile_ape_items_current['data'] = substr($APEtagData, $offset, $value_size); |
---|
135 | $offset += $value_size; |
---|
136 | |
---|
137 | $thisfile_ape_items_current['flags'] = $this->parseAPEtagFlags($item_flags); |
---|
138 | switch ($thisfile_ape_items_current['flags']['item_contents_raw']) { |
---|
139 | case 0: // UTF-8 |
---|
140 | case 3: // Locator (URL, filename, etc), UTF-8 encoded |
---|
141 | $thisfile_ape_items_current['data'] = explode("\x00", trim($thisfile_ape_items_current['data'])); |
---|
142 | break; |
---|
143 | |
---|
144 | default: // binary data |
---|
145 | break; |
---|
146 | } |
---|
147 | |
---|
148 | switch (strtolower($item_key)) { |
---|
149 | case 'replaygain_track_gain': |
---|
150 | $thisfile_replaygain['track']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! |
---|
151 | $thisfile_replaygain['track']['originator'] = 'unspecified'; |
---|
152 | break; |
---|
153 | |
---|
154 | case 'replaygain_track_peak': |
---|
155 | $thisfile_replaygain['track']['peak'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! |
---|
156 | $thisfile_replaygain['track']['originator'] = 'unspecified'; |
---|
157 | if ($thisfile_replaygain['track']['peak'] <= 0) { |
---|
158 | $info['warning'][] = 'ReplayGain Track peak from APEtag appears invalid: '.$thisfile_replaygain['track']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")'; |
---|
159 | } |
---|
160 | break; |
---|
161 | |
---|
162 | case 'replaygain_album_gain': |
---|
163 | $thisfile_replaygain['album']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! |
---|
164 | $thisfile_replaygain['album']['originator'] = 'unspecified'; |
---|
165 | break; |
---|
166 | |
---|
167 | case 'replaygain_album_peak': |
---|
168 | $thisfile_replaygain['album']['peak'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! |
---|
169 | $thisfile_replaygain['album']['originator'] = 'unspecified'; |
---|
170 | if ($thisfile_replaygain['album']['peak'] <= 0) { |
---|
171 | $info['warning'][] = 'ReplayGain Album peak from APEtag appears invalid: '.$thisfile_replaygain['album']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")'; |
---|
172 | } |
---|
173 | break; |
---|
174 | |
---|
175 | case 'mp3gain_undo': |
---|
176 | list($mp3gain_undo_left, $mp3gain_undo_right, $mp3gain_undo_wrap) = explode(',', $thisfile_ape_items_current['data'][0]); |
---|
177 | $thisfile_replaygain['mp3gain']['undo_left'] = intval($mp3gain_undo_left); |
---|
178 | $thisfile_replaygain['mp3gain']['undo_right'] = intval($mp3gain_undo_right); |
---|
179 | $thisfile_replaygain['mp3gain']['undo_wrap'] = (($mp3gain_undo_wrap == 'Y') ? true : false); |
---|
180 | break; |
---|
181 | |
---|
182 | case 'mp3gain_minmax': |
---|
183 | list($mp3gain_globalgain_min, $mp3gain_globalgain_max) = explode(',', $thisfile_ape_items_current['data'][0]); |
---|
184 | $thisfile_replaygain['mp3gain']['globalgain_track_min'] = intval($mp3gain_globalgain_min); |
---|
185 | $thisfile_replaygain['mp3gain']['globalgain_track_max'] = intval($mp3gain_globalgain_max); |
---|
186 | break; |
---|
187 | |
---|
188 | case 'mp3gain_album_minmax': |
---|
189 | list($mp3gain_globalgain_album_min, $mp3gain_globalgain_album_max) = explode(',', $thisfile_ape_items_current['data'][0]); |
---|
190 | $thisfile_replaygain['mp3gain']['globalgain_album_min'] = intval($mp3gain_globalgain_album_min); |
---|
191 | $thisfile_replaygain['mp3gain']['globalgain_album_max'] = intval($mp3gain_globalgain_album_max); |
---|
192 | break; |
---|
193 | |
---|
194 | case 'tracknumber': |
---|
195 | if (is_array($thisfile_ape_items_current['data'])) { |
---|
196 | foreach ($thisfile_ape_items_current['data'] as $comment) { |
---|
197 | $thisfile_ape['comments']['track'][] = $comment; |
---|
198 | } |
---|
199 | } |
---|
200 | break; |
---|
201 | |
---|
202 | case 'cover art (artist)': |
---|
203 | case 'cover art (back)': |
---|
204 | case 'cover art (band logo)': |
---|
205 | case 'cover art (band)': |
---|
206 | case 'cover art (colored fish)': |
---|
207 | case 'cover art (composer)': |
---|
208 | case 'cover art (conductor)': |
---|
209 | case 'cover art (front)': |
---|
210 | case 'cover art (icon)': |
---|
211 | case 'cover art (illustration)': |
---|
212 | case 'cover art (lead)': |
---|
213 | case 'cover art (leaflet)': |
---|
214 | case 'cover art (lyricist)': |
---|
215 | case 'cover art (media)': |
---|
216 | case 'cover art (movie scene)': |
---|
217 | case 'cover art (other icon)': |
---|
218 | case 'cover art (other)': |
---|
219 | case 'cover art (performance)': |
---|
220 | case 'cover art (publisher logo)': |
---|
221 | case 'cover art (recording)': |
---|
222 | case 'cover art (studio)': |
---|
223 | // list of possible cover arts from http://taglib-sharp.sourcearchive.com/documentation/2.0.3.0-2/Ape_2Tag_8cs-source.html |
---|
224 | list($thisfile_ape_items_current['filename'], $thisfile_ape_items_current['data']) = explode("\x00", $thisfile_ape_items_current['data'], 2); |
---|
225 | $thisfile_ape_items_current['data_offset'] = $thisfile_ape_items_current['offset'] + strlen($thisfile_ape_items_current['filename']."\x00"); |
---|
226 | $thisfile_ape_items_current['data_length'] = strlen($thisfile_ape_items_current['data']); |
---|
227 | |
---|
228 | $thisfile_ape_items_current['image_mime'] = ''; |
---|
229 | $imageinfo = array(); |
---|
230 | $imagechunkcheck = getid3_lib::GetDataImageSize($thisfile_ape_items_current['data'], $imageinfo); |
---|
231 | $thisfile_ape_items_current['image_mime'] = image_type_to_mime_type($imagechunkcheck[2]); |
---|
232 | |
---|
233 | do { |
---|
234 | if ($this->inline_attachments === false) { |
---|
235 | // skip entirely |
---|
236 | unset($thisfile_ape_items_current['data']); |
---|
237 | break; |
---|
238 | } |
---|
239 | if ($this->inline_attachments === true) { |
---|
240 | // great |
---|
241 | } elseif (is_int($this->inline_attachments)) { |
---|
242 | if ($this->inline_attachments < $thisfile_ape_items_current['data_length']) { |
---|
243 | // too big, skip |
---|
244 | $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' is too large to process inline ('.number_format($thisfile_ape_items_current['data_length']).' bytes)'; |
---|
245 | unset($thisfile_ape_items_current['data']); |
---|
246 | break; |
---|
247 | } |
---|
248 | } elseif (is_string($this->inline_attachments)) { |
---|
249 | $this->inline_attachments = rtrim(str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->inline_attachments), DIRECTORY_SEPARATOR); |
---|
250 | if (!is_dir($this->inline_attachments) || !is_writable($this->inline_attachments)) { |
---|
251 | // cannot write, skip |
---|
252 | $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$this->inline_attachments.'" (not writable)'; |
---|
253 | unset($thisfile_ape_items_current['data']); |
---|
254 | break; |
---|
255 | } |
---|
256 | } |
---|
257 | // if we get this far, must be OK |
---|
258 | if (is_string($this->inline_attachments)) { |
---|
259 | $destination_filename = $this->inline_attachments.DIRECTORY_SEPARATOR.md5($info['filenamepath']).'_'.$thisfile_ape_items_current['data_offset']; |
---|
260 | if (!file_exists($destination_filename) || is_writable($destination_filename)) { |
---|
261 | file_put_contents($destination_filename, $thisfile_ape_items_current['data']); |
---|
262 | } else { |
---|
263 | $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$destination_filename.'" (not writable)'; |
---|
264 | } |
---|
265 | $thisfile_ape_items_current['data_filename'] = $destination_filename; |
---|
266 | unset($thisfile_ape_items_current['data']); |
---|
267 | } else { |
---|
268 | if (!isset($info['ape']['comments']['picture'])) { |
---|
269 | $info['ape']['comments']['picture'] = array(); |
---|
270 | } |
---|
271 | $info['ape']['comments']['picture'][] = array('data'=>$thisfile_ape_items_current['data'], 'image_mime'=>$thisfile_ape_items_current['image_mime']); |
---|
272 | } |
---|
273 | } while (false); |
---|
274 | break; |
---|
275 | |
---|
276 | default: |
---|
277 | if (is_array($thisfile_ape_items_current['data'])) { |
---|
278 | foreach ($thisfile_ape_items_current['data'] as $comment) { |
---|
279 | $thisfile_ape['comments'][strtolower($item_key)][] = $comment; |
---|
280 | } |
---|
281 | } |
---|
282 | break; |
---|
283 | } |
---|
284 | |
---|
285 | } |
---|
286 | if (empty($thisfile_replaygain)) { |
---|
287 | unset($info['replay_gain']); |
---|
288 | } |
---|
289 | return true; |
---|
290 | } |
---|
291 | |
---|
292 | public function parseAPEheaderFooter($APEheaderFooterData) { |
---|
293 | // http://www.uni-jena.de/~pfk/mpp/sv8/apeheader.html |
---|
294 | |
---|
295 | // shortcut |
---|
296 | $headerfooterinfo['raw'] = array(); |
---|
297 | $headerfooterinfo_raw = &$headerfooterinfo['raw']; |
---|
298 | |
---|
299 | $headerfooterinfo_raw['footer_tag'] = substr($APEheaderFooterData, 0, 8); |
---|
300 | if ($headerfooterinfo_raw['footer_tag'] != 'APETAGEX') { |
---|
301 | return false; |
---|
302 | } |
---|
303 | $headerfooterinfo_raw['version'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 8, 4)); |
---|
304 | $headerfooterinfo_raw['tagsize'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 12, 4)); |
---|
305 | $headerfooterinfo_raw['tag_items'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 16, 4)); |
---|
306 | $headerfooterinfo_raw['global_flags'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 20, 4)); |
---|
307 | $headerfooterinfo_raw['reserved'] = substr($APEheaderFooterData, 24, 8); |
---|
308 | |
---|
309 | $headerfooterinfo['tag_version'] = $headerfooterinfo_raw['version'] / 1000; |
---|
310 | if ($headerfooterinfo['tag_version'] >= 2) { |
---|
311 | $headerfooterinfo['flags'] = $this->parseAPEtagFlags($headerfooterinfo_raw['global_flags']); |
---|
312 | } |
---|
313 | return $headerfooterinfo; |
---|
314 | } |
---|
315 | |
---|
316 | public function parseAPEtagFlags($rawflagint) { |
---|
317 | // "Note: APE Tags 1.0 do not use any of the APE Tag flags. |
---|
318 | // All are set to zero on creation and ignored on reading." |
---|
319 | // http://www.uni-jena.de/~pfk/mpp/sv8/apetagflags.html |
---|
320 | $flags['header'] = (bool) ($rawflagint & 0x80000000); |
---|
321 | $flags['footer'] = (bool) ($rawflagint & 0x40000000); |
---|
322 | $flags['this_is_header'] = (bool) ($rawflagint & 0x20000000); |
---|
323 | $flags['item_contents_raw'] = ($rawflagint & 0x00000006) >> 1; |
---|
324 | $flags['read_only'] = (bool) ($rawflagint & 0x00000001); |
---|
325 | |
---|
326 | $flags['item_contents'] = $this->APEcontentTypeFlagLookup($flags['item_contents_raw']); |
---|
327 | |
---|
328 | return $flags; |
---|
329 | } |
---|
330 | |
---|
331 | public function APEcontentTypeFlagLookup($contenttypeid) { |
---|
332 | static $APEcontentTypeFlagLookup = array( |
---|
333 | 0 => 'utf-8', |
---|
334 | 1 => 'binary', |
---|
335 | 2 => 'external', |
---|
336 | 3 => 'reserved' |
---|
337 | ); |
---|
338 | return (isset($APEcontentTypeFlagLookup[$contenttypeid]) ? $APEcontentTypeFlagLookup[$contenttypeid] : 'invalid'); |
---|
339 | } |
---|
340 | |
---|
341 | public function APEtagItemIsUTF8Lookup($itemkey) { |
---|
342 | static $APEtagItemIsUTF8Lookup = array( |
---|
343 | 'title', |
---|
344 | 'subtitle', |
---|
345 | 'artist', |
---|
346 | 'album', |
---|
347 | 'debut album', |
---|
348 | 'publisher', |
---|
349 | 'conductor', |
---|
350 | 'track', |
---|
351 | 'composer', |
---|
352 | 'comment', |
---|
353 | 'copyright', |
---|
354 | 'publicationright', |
---|
355 | 'file', |
---|
356 | 'year', |
---|
357 | 'record date', |
---|
358 | 'record location', |
---|
359 | 'genre', |
---|
360 | 'media', |
---|
361 | 'related', |
---|
362 | 'isrc', |
---|
363 | 'abstract', |
---|
364 | 'language', |
---|
365 | 'bibliography' |
---|
366 | ); |
---|
367 | return in_array(strtolower($itemkey), $APEtagItemIsUTF8Lookup); |
---|
368 | } |
---|
369 | |
---|
370 | } |
---|