source: spip-zone/_core_/plugins/medias/lib/getid3/module.tag.apetag.php @ 113161

Last change on this file since 113161 was 113161, checked in by spip.franck@…, 5 months ago

Mise à jour de la lib getid en 1.9.16, nous étions en 1.9.14
https://github.com/JamesHeinrich/getID3/blob/master/changelog.txt

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