1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Class ID3v22 |
4
|
|
|
* |
5
|
|
|
* @created 22.09.2018 |
6
|
|
|
* @author smiley <[email protected]> |
7
|
|
|
* @copyright 2018 smiley |
8
|
|
|
* @license MIT |
9
|
|
|
* |
10
|
|
|
* @noinspection PhpUnusedParameterInspection |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
namespace chillerlan\ID3Tag; |
14
|
|
|
|
15
|
|
|
use function bin2hex, ord, sha1, strlen, strpos, strtolower, substr, trim, unpack; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* @link http://id3.org/id3v2-00 |
19
|
|
|
*/ |
20
|
|
|
class ID3v22 extends ID3v2Abstract{ |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* 4. Declared ID3v2 frames |
24
|
|
|
*/ |
25
|
|
|
protected array $declaredFrames = [ |
26
|
|
|
'BUF' => 'Recommended buffer size', |
27
|
|
|
'CNT' => 'Play counter', |
28
|
|
|
'COM' => 'Comments', |
29
|
|
|
'CRA' => 'Audio encryption', |
30
|
|
|
'CRM' => 'Encrypted meta frame', |
31
|
|
|
'ETC' => 'Event timing codes', |
32
|
|
|
'EQU' => 'Equalization', |
33
|
|
|
'GEO' => 'General encapsulated object', |
34
|
|
|
'IPL' => 'Involved people list', |
35
|
|
|
'LNK' => 'Linked information', |
36
|
|
|
'MCI' => 'Music CD Identifier', |
37
|
|
|
'MLL' => 'MPEG location lookup table', |
38
|
|
|
'PIC' => 'Attached picture', |
39
|
|
|
'POP' => 'Popularimeter', |
40
|
|
|
'REV' => 'Reverb', |
41
|
|
|
'RVA' => 'Relative volume adjustment', |
42
|
|
|
'SLT' => 'Synchronized lyric/text', |
43
|
|
|
'STC' => 'Synced tempo codes', |
44
|
|
|
'TAL' => 'Album/Movie/Show title', |
45
|
|
|
'TBP' => 'BPM (Beats Per Minute)', |
46
|
|
|
'TCM' => 'Composer', |
47
|
|
|
'TCO' => 'Content type', |
48
|
|
|
'TCR' => 'Copyright message', |
49
|
|
|
'TDA' => 'Date', |
50
|
|
|
'TDY' => 'Playlist delay', |
51
|
|
|
'TEN' => 'Encoded by', |
52
|
|
|
'TFT' => 'File type', |
53
|
|
|
'TIM' => 'Time', |
54
|
|
|
'TKE' => 'Initial key', |
55
|
|
|
'TLA' => 'Language(s)', |
56
|
|
|
'TLE' => 'Length', |
57
|
|
|
'TMT' => 'Media type', |
58
|
|
|
'TOA' => 'Original artist(s)/performer(s)', |
59
|
|
|
'TOF' => 'Original filename', |
60
|
|
|
'TOL' => 'Original Lyricist(s)/text writer(s)', |
61
|
|
|
'TOR' => 'Original release year', |
62
|
|
|
'TOT' => 'Original album/Movie/Show title', |
63
|
|
|
'TP1' => 'Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group', |
64
|
|
|
'TP2' => 'Band/Orchestra/Accompaniment', |
65
|
|
|
'TP3' => 'Conductor/Performer refinement', |
66
|
|
|
'TP4' => 'Interpreted, remixed, or otherwise modified by', |
67
|
|
|
'TPA' => 'Part of a set', |
68
|
|
|
'TPB' => 'Publisher', |
69
|
|
|
'TRC' => 'ISRC (International Standard Recording Code)', |
70
|
|
|
'TRD' => 'Recording dates', |
71
|
|
|
'TRK' => 'Track number/Position in set', |
72
|
|
|
'TSI' => 'Size', |
73
|
|
|
'TSS' => 'Software/hardware and settings used for encoding', |
74
|
|
|
'TT1' => 'Content group description', |
75
|
|
|
'TT2' => 'Title/Songname/Content description', |
76
|
|
|
'TT3' => 'Subtitle/Description refinement', |
77
|
|
|
'TXT' => 'Lyricist/text writer', |
78
|
|
|
'TXX' => 'User defined text information frame', |
79
|
|
|
'TYE' => 'Year', |
80
|
|
|
'UFI' => 'Unique file identifier', |
81
|
|
|
'ULT' => 'Unsychronized lyric/text transcription', |
82
|
|
|
'WAF' => 'Official audio file webpage', |
83
|
|
|
'WAR' => 'Official artist/performer webpage', |
84
|
|
|
'WAS' => 'Official audio source webpage', |
85
|
|
|
'WCM' => 'Commercial information', |
86
|
|
|
'WCP' => 'Copyright/Legal information', |
87
|
|
|
'WPB' => 'Publishers official webpage', |
88
|
|
|
'WXX' => 'User defined URL link frame', |
89
|
|
|
|
90
|
|
|
'ITU' => 'iTunes?', |
91
|
|
|
'PCS' => 'Podcast?', |
92
|
|
|
'TDR' => 'Release date', |
93
|
|
|
'TDS' => '?', |
94
|
|
|
'TID' => '?', |
95
|
|
|
'WFD' => '?', |
96
|
|
|
'CM1' => '?', |
97
|
|
|
]; |
98
|
|
|
|
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* 3.2. ID3v2.2 frames overview (excerpt) |
102
|
|
|
* |
103
|
|
|
* @inheritDoc |
104
|
|
|
*/ |
105
|
|
|
public function parse(string $rawdata):array{ |
106
|
|
|
$this->parsedFrames = []; |
107
|
|
|
$index = 0; |
108
|
|
|
$rawlength = strlen($rawdata); |
109
|
|
|
|
110
|
|
|
while($index < $rawlength){ |
111
|
|
|
|
112
|
|
|
// frame name |
113
|
|
|
$name = substr($rawdata, $index, 3); |
114
|
|
|
$index += 3; |
115
|
|
|
|
116
|
|
|
// name is end tag or garbage |
117
|
|
|
if($name === "\x00\x00\x00" || strlen($name) !== 3){ |
118
|
|
|
break; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
// frame length bytes |
122
|
|
|
$length = substr($rawdata, $index, 3); |
123
|
|
|
|
124
|
|
|
// length data is garbage |
125
|
|
|
if(strlen($length) !== 3){ |
126
|
|
|
break; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
$length = unpack('N', "\x00".$length)[1] ?? 0; |
130
|
|
|
$index += 3; |
131
|
|
|
|
132
|
|
|
// frame length exceeds tag size |
133
|
|
|
if($length > $rawlength || $index >= $rawlength){ |
134
|
|
|
break; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
// frame is empty |
138
|
|
|
if($length < 1){ |
139
|
|
|
continue; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
// frame data |
143
|
|
|
$data = substr($rawdata, $index, $length); |
144
|
|
|
$index += $length; |
145
|
|
|
|
146
|
|
|
// frame is empty |
147
|
|
|
if(strlen($data) < 1){ |
148
|
|
|
continue; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
$this->setTermpos($data); |
152
|
|
|
|
153
|
|
|
$parsedFrame = $this->parseFrame([ |
154
|
|
|
'name' => $name, |
155
|
|
|
'data' => $data, |
156
|
|
|
'length' => $length, |
157
|
|
|
]); |
158
|
|
|
|
159
|
|
|
$this->addFrame($name, $parsedFrame); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
return $this->parsedFrames; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* 4.2. Text information frames |
167
|
|
|
* |
168
|
|
|
* Text information identifier "T00" - "TZZ" , excluding "TXX", |
169
|
|
|
* described in 4.2.2. |
170
|
|
|
* Frame size $xx xx xx |
171
|
|
|
* Text encoding $xx |
172
|
|
|
* Information <textstring> |
173
|
|
|
*/ |
174
|
|
|
protected function T(array $frame):array{ |
175
|
|
|
$content = $this->decodeString(substr($frame['data'], 1)); |
176
|
|
|
|
177
|
|
|
// lists with a slash-delimiter |
178
|
|
|
# if(in_array($frame['name'], ['TP1', 'TCM', 'TXT', 'TOA', 'TOL'], true)){ |
179
|
|
|
# $content = explode('/', $content); |
180
|
|
|
# } |
181
|
|
|
|
182
|
|
|
return ['content' => $content]; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* 4.2.2. User defined text information frame |
187
|
|
|
* |
188
|
|
|
* User defined... "TXX" |
189
|
|
|
* Frame size $xx xx xx |
190
|
|
|
* Text encoding $xx |
191
|
|
|
* Description <textstring> $00 (00) |
192
|
|
|
* Value <textstring> |
193
|
|
|
*/ |
194
|
|
|
protected function TXX(array $frame):array{ |
195
|
|
|
$content = $this->decodeString(substr($frame['data'], $this->termpos)); |
196
|
|
|
|
197
|
|
|
// multi value delimited by a null byte |
198
|
|
|
# if(strpos($content, "\x00") !== false){ |
199
|
|
|
# $content = explode("\x00", $content); |
200
|
|
|
# } |
201
|
|
|
|
202
|
|
|
return [ |
203
|
|
|
'desc' => $this->decodeString(substr($frame['data'], 1, $this->termpos)), |
204
|
|
|
'content' => $content, |
205
|
|
|
]; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* 4.3. URL link frames |
210
|
|
|
* |
211
|
|
|
* URL link frame "W00" - "WZZ" , excluding "WXX" |
212
|
|
|
* (described in 4.3.2.) |
213
|
|
|
* Frame size $xx xx xx |
214
|
|
|
* URL <textstring> |
215
|
|
|
*/ |
216
|
|
|
protected function W(array $frame):array{ |
217
|
|
|
return ['content' => trim($frame['data'])]; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* 4.3.2. User defined URL link frame |
222
|
|
|
* |
223
|
|
|
* User defined... "WXX" |
224
|
|
|
* Frame size $xx xx xx |
225
|
|
|
* Text encoding $xx |
226
|
|
|
* Description <textstring> $00 (00) |
227
|
|
|
* URL <textstring> |
228
|
|
|
*/ |
229
|
|
|
protected function WXX(array $frame):array{ |
230
|
|
|
return [ |
231
|
|
|
'desc' => $this->decodeString(substr($frame['data'], 1, $this->termpos)), |
232
|
|
|
'content' => trim(substr($frame['data'], $this->termpos)), |
233
|
|
|
]; |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* 4.4. Involved people list |
238
|
|
|
* |
239
|
|
|
* Involved people list "IPL" |
240
|
|
|
* Frame size $xx xx xx |
241
|
|
|
* Text encoding $xx |
242
|
|
|
* People list strings <textstrings> |
243
|
|
|
*/ |
244
|
|
|
protected function IPL(array $frame):array{ |
245
|
|
|
return $this->T($frame); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* 4.5. Music CD Identifier |
250
|
|
|
* |
251
|
|
|
* Music CD identifier "MCI" |
252
|
|
|
* Frame size $xx xx xx |
253
|
|
|
* CD TOC <binary data> |
254
|
|
|
*/ |
255
|
|
|
protected function MCI(array $frame):array{ |
256
|
|
|
return ['content' => $frame['data'],]; |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* 4.9. Unsychronised lyrics/text transcription |
261
|
|
|
* |
262
|
|
|
* Unsynced lyrics/text "ULT" |
263
|
|
|
* Frame size $xx xx xx |
264
|
|
|
* Text encoding $xx |
265
|
|
|
* Language $xx xx xx |
266
|
|
|
* Content descriptor <textstring> $00 (00) |
267
|
|
|
* Lyrics/text <textstring> |
268
|
|
|
*/ |
269
|
|
|
protected function ULT(array $frame):array{ |
270
|
|
|
return $this->COM($frame); |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
/** |
274
|
|
|
* 4.11. Comments |
275
|
|
|
* |
276
|
|
|
* Comment "COM" |
277
|
|
|
* Frame size $xx xx xx |
278
|
|
|
* Text encoding $xx |
279
|
|
|
* Language $xx xx xx |
280
|
|
|
* Short content description <textstring> $00 (00) |
281
|
|
|
* The actual text <textstring> |
282
|
|
|
*/ |
283
|
|
|
protected function COM(array $frame):array{ |
284
|
|
|
return [ |
285
|
|
|
'desc' => $this->decodeString(substr($frame['data'], 4, $this->termpos - 3)), |
286
|
|
|
'content' => $this->decodeString(substr($frame['data'], $this->termpos)), |
287
|
|
|
'lang' => substr($frame['data'], 1, 3), |
288
|
|
|
]; |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* 4.15. Attached picture |
293
|
|
|
* |
294
|
|
|
* Attached picture "PIC" |
295
|
|
|
* Frame size $xx xx xx |
296
|
|
|
* Text encoding $xx |
297
|
|
|
* Image format $xx xx xx |
298
|
|
|
* Picture type $xx |
299
|
|
|
* Description <textstring> $00 (00) |
300
|
|
|
* Picture data <binary data> |
301
|
|
|
*/ |
302
|
|
|
protected function PIC(array $frame):array{ |
303
|
|
|
$format = strtolower(substr($frame['data'], 1, 3)); |
304
|
|
|
$type = ord(substr($frame['data'], 4, 1)); |
305
|
|
|
|
306
|
|
|
if($format === 'jpeg'){ |
307
|
|
|
$format = 'jpg'; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
$magicbytes = $this::imageFormatMagicbytes[$format] ?? false; |
311
|
|
|
|
312
|
|
|
if(!$magicbytes){ |
313
|
|
|
return ['rawdata' => bin2hex($frame['data'])]; |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
$termpos = strpos($frame['data'], "\x00".$magicbytes); |
317
|
|
|
$image = substr($frame['data'], $termpos + 1); |
318
|
|
|
|
319
|
|
|
return [ |
320
|
|
|
'desc' => $this->decodeString(substr($frame['data'], 5, $termpos - 5)), |
321
|
|
|
'content' => $image, # 'data:image/'.$format.';base64,'.base64_encode($image), |
322
|
|
|
'format' => $format, |
323
|
|
|
'mime' => 'image/'.$format, |
324
|
|
|
'typeID' => $type, |
325
|
|
|
'typeInfo' => $this::PICTURE_TYPE[$type] ?? '', |
326
|
|
|
'hash' => sha1($image), |
327
|
|
|
]; |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
/** |
331
|
|
|
* 4.17. Play counter |
332
|
|
|
* |
333
|
|
|
* Play counter "CNT" |
334
|
|
|
* Frame size $xx xx xx |
335
|
|
|
* Counter $xx xx xx xx (xx ...) |
336
|
|
|
*/ |
337
|
|
|
protected function CNT(array $frame):array{ |
338
|
|
|
return ['count' => ID3Helpers::bigEndian2Int($frame['data']) ?? 0]; |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
/** |
342
|
|
|
* 4.18. Popularimeter |
343
|
|
|
* |
344
|
|
|
* Popularimeter "POP" |
345
|
|
|
* Frame size $xx xx xx |
346
|
|
|
* Email to user <textstring> $00 |
347
|
|
|
* Rating $xx |
348
|
|
|
* Counter $xx xx xx xx (xx ...) |
349
|
|
|
* |
350
|
|
|
* |
351
|
|
|
* The following list details how Windows Explorer reads and writes the POPM frame: |
352
|
|
|
* |
353
|
|
|
* 224-255 = 5 stars when READ with Windows Explorer, writes 255 |
354
|
|
|
* 160-223 = 4 stars, writes 196 |
355
|
|
|
* 096-159 = 3 stars, writes 128 |
356
|
|
|
* 032-095 = 2 stars, writes 64 |
357
|
|
|
* 001-031 = 1 star, writes 1 |
358
|
|
|
*/ |
359
|
|
|
protected function POP(array $frame):array{ |
360
|
|
|
$t = strpos($frame['data'], "\x00", 1); |
361
|
|
|
|
362
|
|
|
return [ |
363
|
|
|
'desc' => substr($frame['data'], 0, $t), |
364
|
|
|
'rating' => ord(substr($frame['data'], $t + 1, 1)), |
365
|
|
|
# 'count' => ID3Helpers::bigEndian2Int(substr($frame['data'], $t + 2)) ?? 0, |
366
|
|
|
]; |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
} |
370
|
|
|
|