1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Class ID3v1 |
4
|
|
|
* |
5
|
|
|
* @created 26.09.2018 |
6
|
|
|
* @author smiley <[email protected]> |
7
|
|
|
* @copyright 2018 smiley |
8
|
|
|
* @license MIT |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
namespace chillerlan\ID3Tag; |
12
|
|
|
|
13
|
|
|
use function array_keys, array_map, array_values, call_user_func_array, date, intval, method_exists, strlen, substr, trim, unpack; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* @link http://id3.org/ID3v1 |
17
|
|
|
* @link http://www.birdcagesoft.com/ID3v12.txt |
18
|
|
|
*/ |
19
|
|
|
final class ID3v1 implements ParserInterface{ |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Genre definitions 0-79 follow the ID3 tag specification of 1999 |
23
|
|
|
* and the first set of Winamp extensions (80-125) |
24
|
|
|
*/ |
25
|
|
|
public const GENRES = [ |
26
|
|
|
0 => 'Blues', // starting at 0 was the stupidest thing to do... |
27
|
|
|
1 => 'Classic Rock', |
28
|
|
|
2 => 'Country', |
29
|
|
|
3 => 'Dance', |
30
|
|
|
4 => 'Disco', |
31
|
|
|
5 => 'Funk', |
32
|
|
|
6 => 'Grunge', |
33
|
|
|
7 => 'Hip-Hop', |
34
|
|
|
8 => 'Jazz', |
35
|
|
|
9 => 'Metal', |
36
|
|
|
10 => 'New Age', |
37
|
|
|
11 => 'Oldies', |
38
|
|
|
12 => 'Other', |
39
|
|
|
13 => 'Pop', |
40
|
|
|
14 => 'R&B', |
41
|
|
|
15 => 'Rap', |
42
|
|
|
16 => 'Reggae', |
43
|
|
|
17 => 'Rock', |
44
|
|
|
18 => 'Techno', |
45
|
|
|
19 => 'Industrial', |
46
|
|
|
20 => 'Alternative', |
47
|
|
|
21 => 'Ska', |
48
|
|
|
22 => 'Death Metal', |
49
|
|
|
23 => 'Pranks', |
50
|
|
|
24 => 'Soundtrack', |
51
|
|
|
25 => 'Euro-Techno', |
52
|
|
|
26 => 'Ambient', |
53
|
|
|
27 => 'Trip-Hop', |
54
|
|
|
28 => 'Vocal', |
55
|
|
|
29 => 'Jazz+Funk', |
56
|
|
|
30 => 'Fusion', |
57
|
|
|
31 => 'Trance', |
58
|
|
|
32 => 'Classical', |
59
|
|
|
33 => 'Instrumental', |
60
|
|
|
34 => 'Acid', |
61
|
|
|
35 => 'House', |
62
|
|
|
36 => 'Game', |
63
|
|
|
37 => 'Sound Clip', |
64
|
|
|
38 => 'Gospel', |
65
|
|
|
39 => 'Noise', |
66
|
|
|
40 => 'Alternative Rock', |
67
|
|
|
41 => 'Bass', |
68
|
|
|
42 => 'Soul', |
69
|
|
|
43 => 'Punk', |
70
|
|
|
44 => 'Space', |
71
|
|
|
45 => 'Meditative', |
72
|
|
|
46 => 'Instrumental Pop', |
73
|
|
|
47 => 'Instrumental Rock', |
74
|
|
|
48 => 'Ethnic', |
75
|
|
|
49 => 'Gothic', |
76
|
|
|
50 => 'Darkwave', |
77
|
|
|
51 => 'Techno-Industrial', |
78
|
|
|
52 => 'Electronic', |
79
|
|
|
53 => 'Pop-Folk', |
80
|
|
|
54 => 'Eurodance', |
81
|
|
|
55 => 'Dream', |
82
|
|
|
56 => 'Southern Rock', |
83
|
|
|
57 => 'Comedy', |
84
|
|
|
58 => 'Cult', |
85
|
|
|
59 => 'Gangsta', |
86
|
|
|
60 => 'Top 40', |
87
|
|
|
61 => 'Christian Rap', |
88
|
|
|
62 => 'Pop/Funk', |
89
|
|
|
63 => 'Jungle', |
90
|
|
|
64 => 'Native US', |
91
|
|
|
65 => 'Cabaret', |
92
|
|
|
66 => 'New Wave', |
93
|
|
|
67 => 'Psychadelic', |
94
|
|
|
68 => 'Rave', |
95
|
|
|
69 => 'Showtunes', |
96
|
|
|
70 => 'Trailer', |
97
|
|
|
71 => 'Lo-Fi', |
98
|
|
|
72 => 'Tribal', |
99
|
|
|
73 => 'Acid Punk', |
100
|
|
|
74 => 'Acid Jazz', |
101
|
|
|
75 => 'Polka', |
102
|
|
|
76 => 'Retro', |
103
|
|
|
77 => 'Musical', |
104
|
|
|
78 => 'Rock & Roll', |
105
|
|
|
79 => 'Hard Rock', |
106
|
|
|
// Winamp extensions |
107
|
|
|
80 => 'Folk', |
108
|
|
|
81 => 'Folk-Rock', |
109
|
|
|
82 => 'National Folk', |
110
|
|
|
83 => 'Swing', |
111
|
|
|
84 => 'Fast Fusion', |
112
|
|
|
85 => 'Bebob', |
113
|
|
|
86 => 'Latin', |
114
|
|
|
87 => 'Revival', |
115
|
|
|
88 => 'Celtic', |
116
|
|
|
89 => 'Bluegrass', |
117
|
|
|
90 => 'Avantgarde', |
118
|
|
|
91 => 'Gothic Rock', |
119
|
|
|
92 => 'Progressive Rock', |
120
|
|
|
93 => 'Psychedelic Rock', |
121
|
|
|
94 => 'Symphonic Rock', |
122
|
|
|
95 => 'Slow Rock', |
123
|
|
|
96 => 'Big Band', |
124
|
|
|
97 => 'Chorus', |
125
|
|
|
98 => 'Easy Listening', |
126
|
|
|
99 => 'Acoustic', |
127
|
|
|
100 => 'Humour', |
128
|
|
|
101 => 'Speech', |
129
|
|
|
102 => 'Chanson', |
130
|
|
|
103 => 'Opera', |
131
|
|
|
104 => 'Chamber Music', |
132
|
|
|
105 => 'Sonata', |
133
|
|
|
106 => 'Symphony', |
134
|
|
|
107 => 'Booty Bass', |
135
|
|
|
108 => 'Primus', |
136
|
|
|
109 => 'Porn Groove', |
137
|
|
|
110 => 'Satire', |
138
|
|
|
111 => 'Slow Jam', |
139
|
|
|
112 => 'Club', |
140
|
|
|
113 => 'Tango', |
141
|
|
|
114 => 'Samba', |
142
|
|
|
115 => 'Folklore', |
143
|
|
|
116 => 'Ballad', |
144
|
|
|
117 => 'Power Ballad', |
145
|
|
|
118 => 'Rhythmic Soul', |
146
|
|
|
119 => 'Freestyle', |
147
|
|
|
120 => 'Duet', |
148
|
|
|
121 => 'Punk Rock', |
149
|
|
|
122 => 'Drum Solo', |
150
|
|
|
123 => 'Acapella', |
151
|
|
|
124 => 'Euro-House', |
152
|
|
|
125 => 'Dance Hall', |
153
|
|
|
]; |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* @inheritDoc |
157
|
|
|
*/ |
158
|
|
|
public function parse(string $rawdata):?array{ |
159
|
|
|
|
160
|
|
|
if(strlen($rawdata) !== 256){ |
161
|
|
|
# throw new ID3Exception('invalid id3v1 tag size'); |
162
|
|
|
return null; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
$tagdata = substr($rawdata, 128); |
166
|
|
|
# $extdata = substr($rawdata, 0, 128); // @todo v1.2 |
167
|
|
|
|
168
|
|
|
$format = $tagdata[125] === "\x00" && $tagdata[126] !== "\x00" |
169
|
|
|
? 'a28comment/a1null/c1track/c1genre' // v1.1 |
170
|
|
|
: 'a30comment/c1genre'; // v1.0 |
171
|
|
|
|
172
|
|
|
$data = unpack('a3identifier/a30title/a30artist/a30album/a4year/'.$format, $tagdata); |
173
|
|
|
|
174
|
|
|
if($data['identifier'] !== 'TAG'){ |
175
|
|
|
# throw new ID3Exception('invalid id3v1 identifier'); |
176
|
|
|
return null; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
unset($data['identifier'], $data['null']); |
180
|
|
|
|
181
|
|
|
return array_map([$this, 'parseFrame'], array_keys($data), array_values($data)); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* @param string $tag |
186
|
|
|
* @param mixed $content |
187
|
|
|
* |
188
|
|
|
* @return array |
189
|
|
|
*/ |
190
|
|
|
protected function parseFrame(string $tag, $content):array{ |
191
|
|
|
|
192
|
|
|
if(method_exists($this, $tag)){ |
193
|
|
|
return call_user_func_array([$this, $tag], [$tag, $content]); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
// Detecting UTF-8 before ISO-8859-1 will cause ASCII strings being tagged as UTF-8, which is fine. |
197
|
|
|
// However, it will prevent UTF-8 encoded strings from being wrongly decoded twice. |
198
|
|
|
$encoding = mb_detect_encoding($content, ['Windows-1251', 'Windows-1252', 'KOI8-R', 'UTF-8', 'ISO-8859-1']); |
199
|
|
|
|
200
|
|
|
if($encoding !== 'UTF-8'){ |
201
|
|
|
$content = mb_convert_encoding($content, 'UTF-8', $encoding); |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
return [ |
205
|
|
|
'tag' => $tag, |
206
|
|
|
'content' => trim($content), |
|
|
|
|
207
|
|
|
'encoding' => $encoding, |
208
|
|
|
]; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* @param string $tag |
213
|
|
|
* @param mixed $content |
214
|
|
|
* |
215
|
|
|
* @return array |
216
|
|
|
*/ |
217
|
|
|
protected function year(string $tag, $content):array{ |
218
|
|
|
$content = intval(trim($content)); |
219
|
|
|
|
220
|
|
|
return [ |
221
|
|
|
'tag' => $tag, |
222
|
|
|
'content' => $content <= date('Y') && $content > 0 ? $content : null, |
223
|
|
|
]; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* |
228
|
|
|
*/ |
229
|
|
|
protected function genre(string $tag, int $content):array{ |
230
|
|
|
return [ |
231
|
|
|
'tag' => $tag, |
232
|
|
|
'content' => $content, // there's no sane way to tell whether a genre was actually set |
233
|
|
|
'desc' => $this::GENRES[$content] ?? null, |
234
|
|
|
]; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* |
239
|
|
|
*/ |
240
|
|
|
protected function track(string $tag, int $content):array{ |
241
|
|
|
return [ |
242
|
|
|
'tag' => $tag, |
243
|
|
|
'content' => $content, |
244
|
|
|
]; |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
} |
248
|
|
|
|