1 | <?php |
---|
2 | // +----------------------------------------------------------------------+ |
---|
3 | // | PHP version 5 | |
---|
4 | // +----------------------------------------------------------------------+ |
---|
5 | // | Copyright (c) 2002-2006 James Heinrich, Allan Hansen | |
---|
6 | // +----------------------------------------------------------------------+ |
---|
7 | // | This source file is subject to version 2 of the GPL license, | |
---|
8 | // | that is bundled with this package in the file license.txt and is | |
---|
9 | // | available through the world-wide-web at the following url: | |
---|
10 | // | http://www.gnu.org/copyleft/gpl.html | |
---|
11 | // +----------------------------------------------------------------------+ |
---|
12 | // | getID3() - http://getid3.sourceforge.net or http://www.getid3.org | |
---|
13 | // +----------------------------------------------------------------------+ |
---|
14 | // | Authors: James Heinrich <infoØgetid3*org> | |
---|
15 | // | Allan Hansen <ahØartemis*dk> | |
---|
16 | // +----------------------------------------------------------------------+ |
---|
17 | // | module.tag.apetag.php | |
---|
18 | // | module for analyzing APE tags | |
---|
19 | // | dependencies: NONE | |
---|
20 | // +----------------------------------------------------------------------+ |
---|
21 | // |
---|
22 | // $Id: module.tag.apetag.php 3318 2009-05-20 21:54:10Z vdigital $ |
---|
23 | |
---|
24 | |
---|
25 | |
---|
26 | class getid3_apetag extends getid3_handler |
---|
27 | { |
---|
28 | /* |
---|
29 | ID3v1_TAG_SIZE = 128; |
---|
30 | APETAG_HEADER_SIZE = 32; |
---|
31 | LYRICS3_TAG_SIZE = 10; |
---|
32 | */ |
---|
33 | |
---|
34 | public $option_override_end_offset = 0; |
---|
35 | |
---|
36 | |
---|
37 | |
---|
38 | public function Analyze() { |
---|
39 | |
---|
40 | $getid3 = $this->getid3; |
---|
41 | |
---|
42 | if ($this->option_override_end_offset == 0) { |
---|
43 | |
---|
44 | fseek($getid3->fp, 0 - 170, SEEK_END); // 170 = ID3v1_TAG_SIZE + APETAG_HEADER_SIZE + LYRICS3_TAG_SIZE |
---|
45 | $apetag_footer_id3v1 = fread($getid3->fp, 170); // 170 = ID3v1_TAG_SIZE + APETAG_HEADER_SIZE + LYRICS3_TAG_SIZE |
---|
46 | |
---|
47 | // APE tag found before ID3v1 |
---|
48 | if (substr($apetag_footer_id3v1, strlen($apetag_footer_id3v1) - 160, 8) == 'APETAGEX') { // 160 = ID3v1_TAG_SIZE + APETAG_HEADER_SIZE |
---|
49 | $getid3->info['ape']['tag_offset_end'] = filesize($getid3->filename) - 128; // 128 = ID3v1_TAG_SIZE |
---|
50 | } |
---|
51 | |
---|
52 | // APE tag found, no ID3v1 |
---|
53 | elseif (substr($apetag_footer_id3v1, strlen($apetag_footer_id3v1) - 32, 8) == 'APETAGEX') { // 32 = APETAG_HEADER_SIZE |
---|
54 | $getid3->info['ape']['tag_offset_end'] = filesize($getid3->filename); |
---|
55 | } |
---|
56 | |
---|
57 | } |
---|
58 | else { |
---|
59 | |
---|
60 | fseek($getid3->fp, $this->option_override_end_offset - 32, SEEK_SET); // 32 = APETAG_HEADER_SIZE |
---|
61 | if (fread($getid3->fp, 8) == 'APETAGEX') { |
---|
62 | $getid3->info['ape']['tag_offset_end'] = $this->option_override_end_offset; |
---|
63 | } |
---|
64 | |
---|
65 | } |
---|
66 | |
---|
67 | // APE tag not found |
---|
68 | if (!@$getid3->info['ape']['tag_offset_end']) { |
---|
69 | return false; |
---|
70 | } |
---|
71 | |
---|
72 | // Shortcut |
---|
73 | $info_ape = &$getid3->info['ape']; |
---|
74 | |
---|
75 | // Read and parse footer |
---|
76 | fseek($getid3->fp, $info_ape['tag_offset_end'] - 32, SEEK_SET); // 32 = APETAG_HEADER_SIZE |
---|
77 | $apetag_footer_data = fread($getid3->fp, 32); |
---|
78 | if (!($this->ParseAPEHeaderFooter($apetag_footer_data, $info_ape['footer']))) { |
---|
79 | throw new getid3_exception('Error parsing APE footer at offset '.$info_ape['tag_offset_end']); |
---|
80 | } |
---|
81 | |
---|
82 | if (isset($info_ape['footer']['flags']['header']) && $info_ape['footer']['flags']['header']) { |
---|
83 | fseek($getid3->fp, $info_ape['tag_offset_end'] - $info_ape['footer']['raw']['tagsize'] - 32, SEEK_SET); |
---|
84 | $info_ape['tag_offset_start'] = ftell($getid3->fp); |
---|
85 | $apetag_data = fread($getid3->fp, $info_ape['footer']['raw']['tagsize'] + 32); |
---|
86 | } |
---|
87 | else { |
---|
88 | $info_ape['tag_offset_start'] = $info_ape['tag_offset_end'] - $info_ape['footer']['raw']['tagsize']; |
---|
89 | fseek($getid3->fp, $info_ape['tag_offset_start'], SEEK_SET); |
---|
90 | $apetag_data = fread($getid3->fp, $info_ape['footer']['raw']['tagsize']); |
---|
91 | } |
---|
92 | $getid3->info['avdataend'] = $info_ape['tag_offset_start']; |
---|
93 | |
---|
94 | if (isset($getid3->info['id3v1']['tag_offset_start']) && ($getid3->info['id3v1']['tag_offset_start'] < $info_ape['tag_offset_end'])) { |
---|
95 | $getid3->warning('ID3v1 tag information ignored since it appears to be a false synch in APEtag data'); |
---|
96 | unset($getid3->info['id3v1']); |
---|
97 | } |
---|
98 | |
---|
99 | $offset = 0; |
---|
100 | if (isset($info_ape['footer']['flags']['header']) && $info_ape['footer']['flags']['header']) { |
---|
101 | if (!$this->ParseAPEHeaderFooter(substr($apetag_data, 0, 32), $info_ape['header'])) { |
---|
102 | throw new getid3_exception('Error parsing APE header at offset '.$info_ape['tag_offset_start']); |
---|
103 | } |
---|
104 | $offset = 32; |
---|
105 | } |
---|
106 | |
---|
107 | // Shortcut |
---|
108 | $getid3->info['replay_gain'] = array (); |
---|
109 | $info_replaygain = &$getid3->info['replay_gain']; |
---|
110 | |
---|
111 | for ($i = 0; $i < $info_ape['footer']['raw']['tag_items']; $i++) { |
---|
112 | $value_size = getid3_lib::LittleEndian2Int(substr($apetag_data, $offset, 4)); |
---|
113 | $item_flags = getid3_lib::LittleEndian2Int(substr($apetag_data, $offset + 4, 4)); |
---|
114 | $offset += 8; |
---|
115 | |
---|
116 | if (strstr(substr($apetag_data, $offset), "\x00") === false) { |
---|
117 | throw new getid3_exception('Cannot find null-byte (0x00) seperator between ItemKey #'.$i.' and value. ItemKey starts ' . $offset . ' bytes into the APE tag, at file offset '.($info_ape['tag_offset_start'] + $offset)); |
---|
118 | } |
---|
119 | |
---|
120 | $item_key_length = strpos($apetag_data, "\x00", $offset) - $offset; |
---|
121 | $item_key = strtolower(substr($apetag_data, $offset, $item_key_length)); |
---|
122 | |
---|
123 | // Shortcut |
---|
124 | $info_ape['items'][$item_key] = array (); |
---|
125 | $info_ape_items_current = &$info_ape['items'][$item_key]; |
---|
126 | |
---|
127 | $offset += $item_key_length + 1; // skip 0x00 terminator |
---|
128 | $info_ape_items_current['data'] = substr($apetag_data, $offset, $value_size); |
---|
129 | $offset += $value_size; |
---|
130 | |
---|
131 | |
---|
132 | $info_ape_items_current['flags'] = $this->ParseAPEtagFlags($item_flags); |
---|
133 | |
---|
134 | switch ($info_ape_items_current['flags']['item_contents_raw']) { |
---|
135 | case 0: // UTF-8 |
---|
136 | case 3: // Locator (URL, filename, etc), UTF-8 encoded |
---|
137 | $info_ape_items_current['data'] = explode("\x00", trim($info_ape_items_current['data'])); |
---|
138 | break; |
---|
139 | |
---|
140 | default: // binary data |
---|
141 | break; |
---|
142 | } |
---|
143 | |
---|
144 | switch (strtolower($item_key)) { |
---|
145 | case 'replaygain_track_gain': |
---|
146 | $info_replaygain['track']['adjustment'] = (float)str_replace(',', '.', $info_ape_items_current['data'][0]); // float casting will see "0,95" as zero! |
---|
147 | $info_replaygain['track']['originator'] = 'unspecified'; |
---|
148 | break; |
---|
149 | |
---|
150 | case 'replaygain_track_peak': |
---|
151 | $info_replaygain['track']['peak'] = (float)str_replace(',', '.', $info_ape_items_current['data'][0]); // float casting will see "0,95" as zero! |
---|
152 | $info_replaygain['track']['originator'] = 'unspecified'; |
---|
153 | if ($info_replaygain['track']['peak'] <= 0) { |
---|
154 | $getid3->warning('ReplayGain Track peak from APEtag appears invalid: '.$info_replaygain['track']['peak'].' (original value = "'.$info_ape_items_current['data'][0].'")'); |
---|
155 | } |
---|
156 | break; |
---|
157 | |
---|
158 | case 'replaygain_album_gain': |
---|
159 | $info_replaygain['album']['adjustment'] = (float)str_replace(',', '.', $info_ape_items_current['data'][0]); // float casting will see "0,95" as zero! |
---|
160 | $info_replaygain['album']['originator'] = 'unspecified'; |
---|
161 | break; |
---|
162 | |
---|
163 | case 'replaygain_album_peak': |
---|
164 | $info_replaygain['album']['peak'] = (float)str_replace(',', '.', $info_ape_items_current['data'][0]); // float casting will see "0,95" as zero! |
---|
165 | $info_replaygain['album']['originator'] = 'unspecified'; |
---|
166 | if ($info_replaygain['album']['peak'] <= 0) { |
---|
167 | $getid3->warning('ReplayGain Album peak from APEtag appears invalid: '.$info_replaygain['album']['peak'].' (original value = "'.$info_ape_items_current['data'][0].'")'); |
---|
168 | } |
---|
169 | break; |
---|
170 | |
---|
171 | case 'mp3gain_undo': |
---|
172 | list($mp3gain_undo_left, $mp3gain_undo_right, $mp3gain_undo_wrap) = explode(',', $info_ape_items_current['data'][0]); |
---|
173 | $info_replaygain['mp3gain']['undo_left'] = intval($mp3gain_undo_left); |
---|
174 | $info_replaygain['mp3gain']['undo_right'] = intval($mp3gain_undo_right); |
---|
175 | $info_replaygain['mp3gain']['undo_wrap'] = (($mp3gain_undo_wrap == 'Y') ? true : false); |
---|
176 | break; |
---|
177 | |
---|
178 | case 'mp3gain_minmax': |
---|
179 | list($mp3gain_globalgain_min, $mp3gain_globalgain_max) = explode(',', $info_ape_items_current['data'][0]); |
---|
180 | $info_replaygain['mp3gain']['globalgain_track_min'] = intval($mp3gain_globalgain_min); |
---|
181 | $info_replaygain['mp3gain']['globalgain_track_max'] = intval($mp3gain_globalgain_max); |
---|
182 | break; |
---|
183 | |
---|
184 | case 'mp3gain_album_minmax': |
---|
185 | list($mp3gain_globalgain_album_min, $mp3gain_globalgain_album_max) = explode(',', $info_ape_items_current['data'][0]); |
---|
186 | $info_replaygain['mp3gain']['globalgain_album_min'] = intval($mp3gain_globalgain_album_min); |
---|
187 | $info_replaygain['mp3gain']['globalgain_album_max'] = intval($mp3gain_globalgain_album_max); |
---|
188 | break; |
---|
189 | |
---|
190 | case 'tracknumber': |
---|
191 | foreach ($info_ape_items_current['data'] as $comment) { |
---|
192 | $info_ape['comments']['track'][] = $comment; |
---|
193 | } |
---|
194 | break; |
---|
195 | |
---|
196 | default: |
---|
197 | foreach ($info_ape_items_current['data'] as $comment) { |
---|
198 | $info_ape['comments'][strtolower($item_key)][] = $comment; |
---|
199 | } |
---|
200 | break; |
---|
201 | } |
---|
202 | |
---|
203 | } |
---|
204 | if (empty($info_replaygain)) { |
---|
205 | unset($getid3->info['replay_gain']); |
---|
206 | } |
---|
207 | |
---|
208 | return true; |
---|
209 | } |
---|
210 | |
---|
211 | |
---|
212 | |
---|
213 | protected function ParseAPEheaderFooter($data, &$target) { |
---|
214 | |
---|
215 | // http://www.uni-jena.de/~pfk/mpp/sv8/apeheader.html |
---|
216 | |
---|
217 | if (substr($data, 0, 8) != 'APETAGEX') { |
---|
218 | return false; |
---|
219 | } |
---|
220 | |
---|
221 | // shortcut |
---|
222 | $target['raw'] = array (); |
---|
223 | $target_raw = &$target['raw']; |
---|
224 | |
---|
225 | $target_raw['footer_tag'] = 'APETAGEX'; |
---|
226 | |
---|
227 | getid3_lib::ReadSequence("LittleEndian2Int", $target_raw, $data, 8, |
---|
228 | array ( |
---|
229 | 'version' => 4, |
---|
230 | 'tagsize' => 4, |
---|
231 | 'tag_items' => 4, |
---|
232 | 'global_flags' => 4 |
---|
233 | ) |
---|
234 | ); |
---|
235 | $target_raw['reserved'] = substr($data, 24, 8); |
---|
236 | |
---|
237 | $target['tag_version'] = $target_raw['version'] / 1000; |
---|
238 | if ($target['tag_version'] >= 2) { |
---|
239 | |
---|
240 | $target['flags'] = $this->ParseAPEtagFlags($target_raw['global_flags']); |
---|
241 | } |
---|
242 | |
---|
243 | return true; |
---|
244 | } |
---|
245 | |
---|
246 | |
---|
247 | |
---|
248 | protected function ParseAPEtagFlags($raw_flag_int) { |
---|
249 | |
---|
250 | // "Note: APE Tags 1.0 do not use any of the APE Tag flags. |
---|
251 | // All are set to zero on creation and ignored on reading." |
---|
252 | // http://www.uni-jena.de/~pfk/mpp/sv8/apetagflags.html |
---|
253 | |
---|
254 | $target['header'] = (bool) ($raw_flag_int & 0x80000000); |
---|
255 | $target['footer'] = (bool) ($raw_flag_int & 0x40000000); |
---|
256 | $target['this_is_header'] = (bool) ($raw_flag_int & 0x20000000); |
---|
257 | $target['item_contents_raw'] = ($raw_flag_int & 0x00000006) >> 1; |
---|
258 | $target['read_only'] = (bool) ($raw_flag_int & 0x00000001); |
---|
259 | |
---|
260 | $target['item_contents'] = getid3_apetag::APEcontentTypeFlagLookup($target['item_contents_raw']); |
---|
261 | |
---|
262 | return $target; |
---|
263 | } |
---|
264 | |
---|
265 | |
---|
266 | |
---|
267 | public static function APEcontentTypeFlagLookup($content_type_id) { |
---|
268 | |
---|
269 | static $lookup = array ( |
---|
270 | 0 => 'utf-8', |
---|
271 | 1 => 'binary', |
---|
272 | 2 => 'external', |
---|
273 | 3 => 'reserved' |
---|
274 | ); |
---|
275 | return (isset($lookup[$content_type_id]) ? $lookup[$content_type_id] : 'invalid'); |
---|
276 | } |
---|
277 | |
---|
278 | |
---|
279 | |
---|
280 | public static function APEtagItemIsUTF8Lookup($item_key) { |
---|
281 | |
---|
282 | static $lookup = array ( |
---|
283 | 'title', |
---|
284 | 'subtitle', |
---|
285 | 'artist', |
---|
286 | 'album', |
---|
287 | 'debut album', |
---|
288 | 'publisher', |
---|
289 | 'conductor', |
---|
290 | 'track', |
---|
291 | 'composer', |
---|
292 | 'comment', |
---|
293 | 'copyright', |
---|
294 | 'publicationright', |
---|
295 | 'file', |
---|
296 | 'year', |
---|
297 | 'record date', |
---|
298 | 'record location', |
---|
299 | 'genre', |
---|
300 | 'media', |
---|
301 | 'related', |
---|
302 | 'isrc', |
---|
303 | 'abstract', |
---|
304 | 'language', |
---|
305 | 'bibliography' |
---|
306 | ); |
---|
307 | return in_array(strtolower($item_key), $lookup); |
---|
308 | } |
---|
309 | |
---|
310 | } |
---|
311 | |
---|
312 | ?> |
---|