eyeD3 0.9.8a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eyed3/__about__.py +27 -0
- eyed3/__init__.py +38 -0
- eyed3/__regarding__.py +48 -0
- eyed3/core.py +457 -0
- eyed3/id3/__init__.py +544 -0
- eyed3/id3/apple.py +58 -0
- eyed3/id3/frames.py +2261 -0
- eyed3/id3/headers.py +696 -0
- eyed3/id3/tag.py +2047 -0
- eyed3/main.py +305 -0
- eyed3/mimetype.py +107 -0
- eyed3/mp3/__init__.py +188 -0
- eyed3/mp3/headers.py +866 -0
- eyed3/plugins/__init__.py +200 -0
- eyed3/plugins/art.py +266 -0
- eyed3/plugins/classic.py +1173 -0
- eyed3/plugins/extract.py +61 -0
- eyed3/plugins/fixup.py +631 -0
- eyed3/plugins/genres.py +48 -0
- eyed3/plugins/itunes.py +64 -0
- eyed3/plugins/jsontag.py +133 -0
- eyed3/plugins/lameinfo.py +86 -0
- eyed3/plugins/lastfm.py +50 -0
- eyed3/plugins/mimetype.py +93 -0
- eyed3/plugins/nfo.py +123 -0
- eyed3/plugins/pymod.py +72 -0
- eyed3/plugins/stats.py +479 -0
- eyed3/plugins/xep_118.py +45 -0
- eyed3/plugins/yamltag.py +25 -0
- eyed3/utils/__init__.py +443 -0
- eyed3/utils/art.py +79 -0
- eyed3/utils/binfuncs.py +153 -0
- eyed3/utils/console.py +553 -0
- eyed3/utils/log.py +59 -0
- eyed3/utils/prompt.py +90 -0
- eyed3-0.9.8a1.dist-info/METADATA +163 -0
- eyed3-0.9.8a1.dist-info/RECORD +42 -0
- eyed3-0.9.8a1.dist-info/WHEEL +5 -0
- eyed3-0.9.8a1.dist-info/entry_points.txt +2 -0
- eyed3-0.9.8a1.dist-info/licenses/AUTHORS.rst +39 -0
- eyed3-0.9.8a1.dist-info/licenses/LICENSE +675 -0
- eyed3-0.9.8a1.dist-info/top_level.txt +1 -0
eyed3/id3/headers.py
ADDED
@@ -0,0 +1,696 @@
|
|
1
|
+
import math
|
2
|
+
import logging
|
3
|
+
import binascii
|
4
|
+
from ..utils import requireBytes
|
5
|
+
from ..utils.binfuncs import (bin2dec, bytes2bin, bin2bytes,
|
6
|
+
bin2synchsafe, dec2bin)
|
7
|
+
from .. import core
|
8
|
+
from . import ID3_DEFAULT_VERSION, isValidVersion, normalizeVersion
|
9
|
+
|
10
|
+
from ..utils.log import getLogger
|
11
|
+
log = getLogger(__name__)
|
12
|
+
|
13
|
+
NULL_FRAME_FLAGS = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
14
|
+
|
15
|
+
|
16
|
+
class TagHeader(object):
|
17
|
+
SIZE = 10
|
18
|
+
|
19
|
+
def __init__(self, version=ID3_DEFAULT_VERSION):
|
20
|
+
self.clear()
|
21
|
+
self.version = version
|
22
|
+
|
23
|
+
def clear(self):
|
24
|
+
self.tag_size = 0
|
25
|
+
# Flag bits
|
26
|
+
self.unsync = False
|
27
|
+
self.extended = False
|
28
|
+
self.experimental = False
|
29
|
+
# v2.4 addition
|
30
|
+
self.footer = False
|
31
|
+
|
32
|
+
@property
|
33
|
+
def version(self):
|
34
|
+
return tuple([v for v in self._version])
|
35
|
+
|
36
|
+
@version.setter
|
37
|
+
def version(self, v):
|
38
|
+
v = normalizeVersion(v)
|
39
|
+
if not isValidVersion(v, fully_qualified=True):
|
40
|
+
raise ValueError("Invalid version: %s" % str(v))
|
41
|
+
self._version = v
|
42
|
+
|
43
|
+
@property
|
44
|
+
def major_version(self):
|
45
|
+
return self._version[0]
|
46
|
+
|
47
|
+
@property
|
48
|
+
def minor_version(self):
|
49
|
+
return self._version[1]
|
50
|
+
|
51
|
+
@property
|
52
|
+
def rev_version(self):
|
53
|
+
return self._version[2]
|
54
|
+
|
55
|
+
def parse(self, f):
|
56
|
+
"""Parse an ID3 v2 header starting at the current position of `f`.
|
57
|
+
|
58
|
+
If a header is parsed `True` is returned, otherwise `False`. If
|
59
|
+
a header is found but malformed an `eyed3.id3.tag.TagException` is
|
60
|
+
thrown.
|
61
|
+
"""
|
62
|
+
from .tag import TagException
|
63
|
+
|
64
|
+
self.clear()
|
65
|
+
|
66
|
+
# 3 bytes: v2 header is "ID3".
|
67
|
+
if f.read(3) != b"ID3":
|
68
|
+
return False
|
69
|
+
log.debug("Located ID3 v2 tag")
|
70
|
+
|
71
|
+
# 2 bytes: the minor and revision versions.
|
72
|
+
version = f.read(2)
|
73
|
+
if len(version) != 2:
|
74
|
+
return False
|
75
|
+
major = 2
|
76
|
+
minor = version[0]
|
77
|
+
rev = version[1]
|
78
|
+
log.debug("TagHeader [major]: %d " % major)
|
79
|
+
log.debug("TagHeader [minor]: %d " % minor)
|
80
|
+
log.debug("TagHeader [rev]: %d " % rev)
|
81
|
+
if not (major == 2 and (minor >= 2 and minor <= 4)):
|
82
|
+
raise TagException("ID3 v%d.%d is not supported" % (major, minor))
|
83
|
+
self.version = (major, minor, rev)
|
84
|
+
|
85
|
+
# 1 byte (first 4 bits): flags
|
86
|
+
data = f.read(1)
|
87
|
+
if not data:
|
88
|
+
return False
|
89
|
+
(self.unsync,
|
90
|
+
self.extended,
|
91
|
+
self.experimental,
|
92
|
+
self.footer) = (bool(b) for b in bytes2bin(data)[0:4])
|
93
|
+
log.debug("TagHeader [flags]: unsync(%d) extended(%d) "
|
94
|
+
"experimental(%d) footer(%d)" % (self.unsync, self.extended,
|
95
|
+
self.experimental,
|
96
|
+
self.footer))
|
97
|
+
|
98
|
+
# 4 bytes: The size of the extended header (if any), frames, and padding
|
99
|
+
# afer unsynchronization. This is a sync safe integer, so only the
|
100
|
+
# bottom 7 bits of each byte are used.
|
101
|
+
tag_size_bytes = f.read(4)
|
102
|
+
if len(tag_size_bytes) != 4:
|
103
|
+
return False
|
104
|
+
log.debug("TagHeader [size string]: 0x%02x%02x%02x%02x" %
|
105
|
+
(tag_size_bytes[0], tag_size_bytes[1],
|
106
|
+
tag_size_bytes[2], tag_size_bytes[3]))
|
107
|
+
self.tag_size = bin2dec(bytes2bin(tag_size_bytes, 7))
|
108
|
+
log.debug("TagHeader [size]: %d (0x%x)" % (self.tag_size,
|
109
|
+
self.tag_size))
|
110
|
+
|
111
|
+
return True
|
112
|
+
|
113
|
+
def render(self, tag_len=None):
|
114
|
+
if tag_len is not None:
|
115
|
+
self.tag_size = tag_len
|
116
|
+
|
117
|
+
if self.unsync:
|
118
|
+
raise NotImplementedError("eyeD3 does not write (only reads) "
|
119
|
+
"unsync'd data")
|
120
|
+
|
121
|
+
data = b"ID3"
|
122
|
+
data += bytes([self.minor_version]) + bytes([self.rev_version])
|
123
|
+
data += bin2bytes([int(self.unsync),
|
124
|
+
int(self.extended),
|
125
|
+
int(self.experimental),
|
126
|
+
int(self.footer),
|
127
|
+
0, 0, 0, 0])
|
128
|
+
log.debug("Setting tag size to %d" % self.tag_size)
|
129
|
+
data += bin2bytes(bin2synchsafe(dec2bin(self.tag_size, 32)))
|
130
|
+
log.debug("TagHeader rendered %d bytes" % len(data))
|
131
|
+
return data
|
132
|
+
|
133
|
+
|
134
|
+
class ExtendedTagHeader(object):
|
135
|
+
RESTRICT_TAG_SZ_LARGE = 0x00
|
136
|
+
RESTRICT_TAG_SZ_MED = 0x01
|
137
|
+
RESTRICT_TAG_SZ_SMALL = 0x02
|
138
|
+
RESTRICT_TAG_SZ_TINY = 0x03
|
139
|
+
|
140
|
+
RESTRICT_TEXT_ENC_NONE = 0x00
|
141
|
+
RESTRICT_TEXT_ENC_UTF8 = 0x01
|
142
|
+
|
143
|
+
RESTRICT_TEXT_LEN_NONE = 0x00
|
144
|
+
RESTRICT_TEXT_LEN_1024 = 0x01
|
145
|
+
RESTRICT_TEXT_LEN_128 = 0x02
|
146
|
+
RESTRICT_TEXT_LEN_30 = 0x03
|
147
|
+
|
148
|
+
RESTRICT_IMG_ENC_NONE = 0x00
|
149
|
+
RESTRICT_IMG_ENC_PNG_JPG = 0x01
|
150
|
+
|
151
|
+
RESTRICT_IMG_SZ_NONE = 0x00
|
152
|
+
RESTRICT_IMG_SZ_256 = 0x01
|
153
|
+
RESTRICT_IMG_SZ_64 = 0x02
|
154
|
+
RESTRICT_IMG_SZ_64_EXACT = 0x03
|
155
|
+
|
156
|
+
def __init__(self):
|
157
|
+
self.size = 0
|
158
|
+
self._flags = 0
|
159
|
+
self.crc = None
|
160
|
+
self._restrictions = 0
|
161
|
+
|
162
|
+
@property
|
163
|
+
def update_bit(self):
|
164
|
+
return bool(self._flags & 0x40)
|
165
|
+
|
166
|
+
@update_bit.setter
|
167
|
+
def update_bit(self, v):
|
168
|
+
if v:
|
169
|
+
self._flags |= 0x40
|
170
|
+
else:
|
171
|
+
self._flags &= ~0x40
|
172
|
+
|
173
|
+
@property
|
174
|
+
def crc_bit(self):
|
175
|
+
return bool(self._flags & 0x20)
|
176
|
+
|
177
|
+
@crc_bit.setter
|
178
|
+
def crc_bit(self, v):
|
179
|
+
if v:
|
180
|
+
self._flags |= 0x20
|
181
|
+
else:
|
182
|
+
self._flags &= ~0x20
|
183
|
+
|
184
|
+
@property
|
185
|
+
def crc(self):
|
186
|
+
return self._crc
|
187
|
+
|
188
|
+
@crc.setter
|
189
|
+
def crc(self, v):
|
190
|
+
self.crc_bit = 1 if v else 0
|
191
|
+
self._crc = v
|
192
|
+
|
193
|
+
@property
|
194
|
+
def restrictions_bit(self):
|
195
|
+
return bool(self._flags & 0x10)
|
196
|
+
|
197
|
+
@restrictions_bit.setter
|
198
|
+
def restrictions_bit(self, v):
|
199
|
+
if v:
|
200
|
+
self._flags |= 0x10
|
201
|
+
else:
|
202
|
+
self._flags &= ~0x10
|
203
|
+
|
204
|
+
@property
|
205
|
+
def tag_size_restriction(self):
|
206
|
+
return self._restrictions >> 6
|
207
|
+
|
208
|
+
@tag_size_restriction.setter
|
209
|
+
def tag_size_restriction(self, v):
|
210
|
+
assert v >= 0 and v <= 3
|
211
|
+
self.restrictions_bit = 1
|
212
|
+
self._restrictions = (v << 6) | (self._restrictions & 0x3f)
|
213
|
+
|
214
|
+
@property
|
215
|
+
def tag_size_restriction_description(self):
|
216
|
+
val = self.tag_size_restriction
|
217
|
+
if val == ExtendedTagHeader.RESTRICT_TAG_SZ_LARGE:
|
218
|
+
return "No more than 128 frames and 1 MB total tag size"
|
219
|
+
elif val == ExtendedTagHeader.RESTRICT_TAG_SZ_MED:
|
220
|
+
return "No more than 64 frames and 128 KB total tag size"
|
221
|
+
elif val == ExtendedTagHeader.RESTRICT_TAG_SZ_SMALL:
|
222
|
+
return "No more than 32 frames and 40 KB total tag size"
|
223
|
+
elif val == ExtendedTagHeader.RESTRICT_TAG_SZ_TINY:
|
224
|
+
return "No more than 32 frames and 4 KB total tag size"
|
225
|
+
|
226
|
+
@property
|
227
|
+
def text_enc_restriction(self):
|
228
|
+
return (self._restrictions & 0x20) >> 5
|
229
|
+
|
230
|
+
@text_enc_restriction.setter
|
231
|
+
def text_enc_restriction(self, v):
|
232
|
+
assert v == 0 or v == 1
|
233
|
+
self.restrictions_bit = 1
|
234
|
+
self._restrictions ^= 0x20
|
235
|
+
|
236
|
+
@property
|
237
|
+
def text_enc_restriction_description(self):
|
238
|
+
if self.text_enc_restriction:
|
239
|
+
return "Strings are only encoded with ISO-8859-1 or UTF-8"
|
240
|
+
else:
|
241
|
+
return "None"
|
242
|
+
|
243
|
+
@property
|
244
|
+
def text_length_restriction(self):
|
245
|
+
return (self._restrictions >> 3) & 0x03
|
246
|
+
|
247
|
+
@text_length_restriction.setter
|
248
|
+
def text_length_restriction(self, v):
|
249
|
+
assert v >= 0 and v <= 3
|
250
|
+
self.restrictions_bit = 1
|
251
|
+
self._restrictions = (v << 3) | (self._restrictions & 0xe7)
|
252
|
+
|
253
|
+
@property
|
254
|
+
def text_length_restriction_description(self):
|
255
|
+
val = self.text_length_restriction
|
256
|
+
if val == ExtendedTagHeader.RESTRICT_TEXT_LEN_NONE:
|
257
|
+
return "None"
|
258
|
+
elif val == ExtendedTagHeader.RESTRICT_TEXT_LEN_1024:
|
259
|
+
return "No string is longer than 1024 characters."
|
260
|
+
elif val == ExtendedTagHeader.RESTRICT_TEXT_LEN_128:
|
261
|
+
return "No string is longer than 128 characters."
|
262
|
+
elif val == ExtendedTagHeader.RESTRICT_TEXT_LEN_30:
|
263
|
+
return "No string is longer than 30 characters."
|
264
|
+
|
265
|
+
@property
|
266
|
+
def image_enc_restriction(self):
|
267
|
+
return (self._restrictions & 0x04) >> 2
|
268
|
+
|
269
|
+
@image_enc_restriction.setter
|
270
|
+
def image_enc_restriction(self, v):
|
271
|
+
assert v == 0 or v == 1
|
272
|
+
self.restrictions_bit = 1
|
273
|
+
self._restrictions ^= 0x04
|
274
|
+
|
275
|
+
@property
|
276
|
+
def image_enc_restriction_description(self):
|
277
|
+
if self.image_enc_restriction:
|
278
|
+
return "Images are encoded only with PNG [PNG] or JPEG [JFIF]."
|
279
|
+
else:
|
280
|
+
return "None"
|
281
|
+
|
282
|
+
@property
|
283
|
+
def image_size_restriction(self):
|
284
|
+
return self._restrictions & 0x03
|
285
|
+
|
286
|
+
@image_size_restriction.setter
|
287
|
+
def image_size_restriction(self, v):
|
288
|
+
assert v >= 0 and v <= 3
|
289
|
+
self.restrictions_bit = 1
|
290
|
+
self._restrictions = v | (self._restrictions & 0xfc)
|
291
|
+
|
292
|
+
@property
|
293
|
+
def image_size_restriction_description(self):
|
294
|
+
val = self.image_size_restriction
|
295
|
+
if val == ExtendedTagHeader.RESTRICT_IMG_SZ_NONE:
|
296
|
+
return "None"
|
297
|
+
elif val == ExtendedTagHeader.RESTRICT_IMG_SZ_256:
|
298
|
+
return "All images are 256x256 pixels or smaller."
|
299
|
+
elif val == ExtendedTagHeader.RESTRICT_IMG_SZ_64:
|
300
|
+
return "All images are 64x64 pixels or smaller."
|
301
|
+
elif val == ExtendedTagHeader.RESTRICT_IMG_SZ_64_EXACT:
|
302
|
+
return "All images are exactly 64x64 pixels, unless required "\
|
303
|
+
"otherwise."
|
304
|
+
|
305
|
+
def _syncsafeCRC(self):
|
306
|
+
return bytes([
|
307
|
+
(self.crc >> 28) & 0x7f,
|
308
|
+
(self.crc >> 21) & 0x7f,
|
309
|
+
(self.crc >> 14) & 0x7f,
|
310
|
+
(self.crc >> 7) & 0x7f,
|
311
|
+
(self.crc >> 0) & 0x7f,
|
312
|
+
])
|
313
|
+
|
314
|
+
def render(self, version, frame_data, padding=0):
|
315
|
+
if version[0] != 2:
|
316
|
+
raise ValueError(f"Invalid version: {version} != 2 (expected)")
|
317
|
+
|
318
|
+
data = b""
|
319
|
+
if version[1] == 4:
|
320
|
+
# Version 2.4
|
321
|
+
size = 6
|
322
|
+
# Extended flags.
|
323
|
+
if self.update_bit:
|
324
|
+
data += b"\x00"
|
325
|
+
if self.crc_bit:
|
326
|
+
data += b"\x05"
|
327
|
+
# XXX: Using the absolute value of the CRC. The spec is unclear
|
328
|
+
# about the type of this data.
|
329
|
+
self.crc = int(math.fabs(binascii.crc32(frame_data +
|
330
|
+
(b"\x00" * padding))))
|
331
|
+
crc_data = self._syncsafeCRC()
|
332
|
+
if len(crc_data) < 5:
|
333
|
+
# pad if necessary
|
334
|
+
crc_data = (b"\x00" * (5 - len(crc_data))) + crc_data
|
335
|
+
assert len(crc_data) == 5
|
336
|
+
data += crc_data
|
337
|
+
if self.restrictions_bit:
|
338
|
+
data += b"\x01"
|
339
|
+
data += bytes([self._restrictions])
|
340
|
+
log.debug("Rendered extended header data (%d bytes)" % len(data))
|
341
|
+
|
342
|
+
# Extended header size.
|
343
|
+
size = bin2bytes(bin2synchsafe(dec2bin(len(data) + 6, 32)))
|
344
|
+
assert len(size) == 4
|
345
|
+
|
346
|
+
data = size + b"\x01" + bin2bytes(dec2bin(self._flags)) + data
|
347
|
+
log.debug("Rendered extended header of size %d" % len(data))
|
348
|
+
else:
|
349
|
+
# Version 2.3
|
350
|
+
size = 6 # Note, the 4 size bytes are not included in the size
|
351
|
+
# Extended flags.
|
352
|
+
f = [0] * 16
|
353
|
+
crc = None
|
354
|
+
if self.crc_bit:
|
355
|
+
f[0] = 1
|
356
|
+
# XXX: Using the absolute value of the CRC. The spec is unclear
|
357
|
+
# about the type of this value.
|
358
|
+
self.crc = int(math.fabs(binascii.crc32(frame_data +
|
359
|
+
(b"\x00" * padding))))
|
360
|
+
crc = bin2bytes(dec2bin(self.crc))
|
361
|
+
assert len(crc) == 4
|
362
|
+
size += 4
|
363
|
+
flags = bin2bytes(f)
|
364
|
+
assert len(flags) == 2
|
365
|
+
# Extended header size.
|
366
|
+
size = bin2bytes(dec2bin(size, 32))
|
367
|
+
assert len(size) == 4
|
368
|
+
# Padding size
|
369
|
+
padding_size = bin2bytes(dec2bin(padding, 32))
|
370
|
+
|
371
|
+
data = size + flags + padding_size
|
372
|
+
if crc:
|
373
|
+
data += crc
|
374
|
+
|
375
|
+
return data
|
376
|
+
|
377
|
+
# Only call this when you *know* there is an extened header.
|
378
|
+
def parse(self, fp, version):
|
379
|
+
'''Parse an ID3 v2 extended header starting at the current position
|
380
|
+
of ``fp`` and per the format defined by ``version``. This method
|
381
|
+
should only be called when the presence of an extended header is known
|
382
|
+
since it moves the file position. If a header is found but malformed
|
383
|
+
an ``eyed3.id3.tag.TagException`` is thrown. The return value is
|
384
|
+
``None``.
|
385
|
+
'''
|
386
|
+
from .tag import TagException
|
387
|
+
assert version[0] == 2
|
388
|
+
|
389
|
+
log.debug("Parsing extended header @ 0x%x" % fp.tell())
|
390
|
+
# First 4 bytes is the size of the extended header.
|
391
|
+
data = fp.read(4)
|
392
|
+
if version[1] == 4:
|
393
|
+
# sync-safe
|
394
|
+
sz = bin2dec(bytes2bin(data, 7))
|
395
|
+
self.size = sz
|
396
|
+
log.debug("Extended header size (includes the 4 size bytes): %d" % sz)
|
397
|
+
data = fp.read(sz - 4)
|
398
|
+
|
399
|
+
# Number of flag bytes
|
400
|
+
if data[0] != 1 or (data[1] & 0x8f):
|
401
|
+
# As of 2.4 the first byte is 1 and the second can only have
|
402
|
+
# bits 6, 5, and 4 set.
|
403
|
+
raise TagException("Invalid Extended Header")
|
404
|
+
|
405
|
+
self._flags = data[1]
|
406
|
+
log.debug("Extended header flags: %x" % self._flags)
|
407
|
+
|
408
|
+
offset = 2
|
409
|
+
if self.update_bit:
|
410
|
+
log.debug("Extended header has update bit set")
|
411
|
+
assert data[offset] == 0
|
412
|
+
offset += 1
|
413
|
+
if self.crc_bit:
|
414
|
+
log.debug("Extended header has CRC bit set")
|
415
|
+
assert data[offset] == 5
|
416
|
+
offset += 1
|
417
|
+
crc_data = data[offset:offset + 5]
|
418
|
+
# This is sync-safe.
|
419
|
+
self.crc = bin2dec(bytes2bin(crc_data, 7))
|
420
|
+
log.debug("Extended header CRC: %d" % self.crc)
|
421
|
+
offset += 5
|
422
|
+
if self.restrictions_bit:
|
423
|
+
log.debug("Extended header has restrictions bit set")
|
424
|
+
assert data[offset] == 1
|
425
|
+
offset += 1
|
426
|
+
self._restrictions = data[offset]
|
427
|
+
offset += 1
|
428
|
+
else:
|
429
|
+
# v2.3 is totally different... *sigh*
|
430
|
+
sz = bin2dec(bytes2bin(data))
|
431
|
+
self.size = sz
|
432
|
+
log.debug("Extended header size (not including 4 size bytes): %d" %
|
433
|
+
sz)
|
434
|
+
tmpFlags = fp.read(2)
|
435
|
+
# Read the padding size, but it'll be computed during the parse.
|
436
|
+
ps = fp.read(4)
|
437
|
+
log.debug("Extended header says there is %d bytes of padding" %
|
438
|
+
bin2dec(bytes2bin(ps)))
|
439
|
+
# Make this look like a v2.4 mask.
|
440
|
+
self._flags = tmpFlags[0] >> 2
|
441
|
+
if self.crc_bit:
|
442
|
+
log.debug("Extended header has CRC bit set")
|
443
|
+
crc_data = fp.read(4)
|
444
|
+
self.crc = bin2dec(bytes2bin(crc_data))
|
445
|
+
log.debug("Extended header CRC: %d" % self.crc)
|
446
|
+
|
447
|
+
|
448
|
+
class FrameHeader:
|
449
|
+
"""A header for each and every ID3 frame in a tag."""
|
450
|
+
|
451
|
+
# 2.4 not only added flag bits, but also reordered the previously defined
|
452
|
+
# flags. So these are mapped once the ID3 version is known. Access through
|
453
|
+
# 'self', always
|
454
|
+
TAG_ALTER = None
|
455
|
+
FILE_ALTER = None
|
456
|
+
READ_ONLY = None
|
457
|
+
COMPRESSED = None
|
458
|
+
ENCRYPTED = None
|
459
|
+
GROUPED = None
|
460
|
+
UNSYNC = None
|
461
|
+
DATA_LEN = None
|
462
|
+
|
463
|
+
# Constructor.
|
464
|
+
@requireBytes(1)
|
465
|
+
def __init__(self, fid, version):
|
466
|
+
self._version = version
|
467
|
+
self._setBitMask()
|
468
|
+
# _setBitMask will throw if the version is no good
|
469
|
+
|
470
|
+
# Correctly set size of header (v2.2 is smaller)
|
471
|
+
self.size = 10 if self.minor_version != 2 else 6
|
472
|
+
|
473
|
+
# The frame header itself...
|
474
|
+
self.id = fid # First 4 bytes, frame ID
|
475
|
+
self._flags = [0] * 16 # 16 bits, represented here as a list
|
476
|
+
self.data_size = 0 # 4 bytes, size of frame data
|
477
|
+
|
478
|
+
def copyFlags(self, rhs):
|
479
|
+
self.tag_alter = rhs._flags[rhs.TAG_ALTER]
|
480
|
+
self.file_alter = rhs._flags[rhs.FILE_ALTER]
|
481
|
+
self.read_only = rhs._flags[rhs.READ_ONLY]
|
482
|
+
self.compressed = rhs._flags[rhs.COMPRESSED]
|
483
|
+
self.encrypted = rhs._flags[rhs.ENCRYPTED]
|
484
|
+
self.grouped = rhs._flags[rhs.GROUPED]
|
485
|
+
self.unsync = rhs._flags[rhs.UNSYNC]
|
486
|
+
self.data_length_indicator = rhs._flags[rhs.DATA_LEN]
|
487
|
+
|
488
|
+
@property
|
489
|
+
def major_version(self):
|
490
|
+
return self._version[0]
|
491
|
+
|
492
|
+
@property
|
493
|
+
def minor_version(self):
|
494
|
+
return self._version[1]
|
495
|
+
|
496
|
+
@property
|
497
|
+
def version(self):
|
498
|
+
return self._version
|
499
|
+
|
500
|
+
@property
|
501
|
+
def tag_alter(self):
|
502
|
+
return self._flags[self.TAG_ALTER]
|
503
|
+
|
504
|
+
@tag_alter.setter
|
505
|
+
def tag_alter(self, b):
|
506
|
+
self._flags[self.TAG_ALTER] = int(bool(b))
|
507
|
+
|
508
|
+
@property
|
509
|
+
def file_alter(self):
|
510
|
+
return self._flags[self.FILE_ALTER]
|
511
|
+
|
512
|
+
@file_alter.setter
|
513
|
+
def file_alter(self, b):
|
514
|
+
self._flags[self.FILE_ALTER] = int(bool(b))
|
515
|
+
|
516
|
+
@property
|
517
|
+
def read_only(self):
|
518
|
+
return self._flags[self.READ_ONLY]
|
519
|
+
|
520
|
+
@read_only.setter
|
521
|
+
def read_only(self, b):
|
522
|
+
self._flags[self.READ_ONLY] = int(bool(b))
|
523
|
+
|
524
|
+
@property
|
525
|
+
def compressed(self):
|
526
|
+
return self._flags[self.COMPRESSED]
|
527
|
+
|
528
|
+
@compressed.setter
|
529
|
+
def compressed(self, b):
|
530
|
+
self._flags[self.COMPRESSED] = int(bool(b))
|
531
|
+
|
532
|
+
@property
|
533
|
+
def encrypted(self):
|
534
|
+
return self._flags[self.ENCRYPTED]
|
535
|
+
|
536
|
+
@encrypted.setter
|
537
|
+
def encrypted(self, b):
|
538
|
+
self._flags[self.ENCRYPTED] = int(bool(b))
|
539
|
+
|
540
|
+
@property
|
541
|
+
def grouped(self):
|
542
|
+
return self._flags[self.GROUPED]
|
543
|
+
|
544
|
+
@grouped.setter
|
545
|
+
def grouped(self, b):
|
546
|
+
self._flags[self.GROUPED] = int(bool(b))
|
547
|
+
|
548
|
+
@property
|
549
|
+
def unsync(self):
|
550
|
+
return self._flags[self.UNSYNC]
|
551
|
+
|
552
|
+
@unsync.setter
|
553
|
+
def unsync(self, b):
|
554
|
+
self._flags[self.UNSYNC] = int(bool(b))
|
555
|
+
|
556
|
+
@property
|
557
|
+
def data_length_indicator(self):
|
558
|
+
return self._flags[self.DATA_LEN]
|
559
|
+
|
560
|
+
@data_length_indicator.setter
|
561
|
+
def data_length_indicator(self, b):
|
562
|
+
self._flags[self.DATA_LEN] = int(bool(b))
|
563
|
+
|
564
|
+
def _setBitMask(self):
|
565
|
+
major = self.major_version
|
566
|
+
minor = self.minor_version
|
567
|
+
|
568
|
+
# 1.x tags are converted to 2.4 frames internally. These frames are
|
569
|
+
# created with frame flags \x00.
|
570
|
+
|
571
|
+
if (major == 2 and minor in (3, 2)):
|
572
|
+
# v2.2 does not contain flags, but set anyway, as long as the
|
573
|
+
# values remain 0 all is good
|
574
|
+
self.TAG_ALTER = 0
|
575
|
+
self.FILE_ALTER = 1
|
576
|
+
self.READ_ONLY = 2
|
577
|
+
self.COMPRESSED = 8
|
578
|
+
self.ENCRYPTED = 9
|
579
|
+
self.GROUPED = 10
|
580
|
+
# This is not in 2.3 frame header flags, map to unused
|
581
|
+
self.UNSYNC = 14
|
582
|
+
# This is not in 2.3 frame header flags, map to unused
|
583
|
+
self.DATA_LEN = 4
|
584
|
+
elif ((major == 2 and minor == 4) or (major == 1 and minor in (0, 1))):
|
585
|
+
self.TAG_ALTER = 1
|
586
|
+
self.FILE_ALTER = 2
|
587
|
+
self.READ_ONLY = 3
|
588
|
+
self.COMPRESSED = 12
|
589
|
+
self.ENCRYPTED = 13
|
590
|
+
self.GROUPED = 9
|
591
|
+
self.UNSYNC = 14
|
592
|
+
self.DATA_LEN = 15
|
593
|
+
else:
|
594
|
+
raise ValueError("ID3 v" + str(major) + "." + str(minor) +
|
595
|
+
" is not supported.")
|
596
|
+
|
597
|
+
def render(self, data_size):
|
598
|
+
data = b''
|
599
|
+
|
600
|
+
assert type(self.id) is bytes
|
601
|
+
data += self.id
|
602
|
+
|
603
|
+
self.data_size = data_size
|
604
|
+
|
605
|
+
if self.minor_version == 3:
|
606
|
+
data += bin2bytes(dec2bin(data_size, 32))
|
607
|
+
else:
|
608
|
+
data += bin2bytes(bin2synchsafe(dec2bin(data_size, 32)))
|
609
|
+
|
610
|
+
if self.unsync:
|
611
|
+
raise NotImplementedError("eyeD3 does not write (only reads) "
|
612
|
+
"unsync'd data")
|
613
|
+
data += bin2bytes(self._flags)
|
614
|
+
|
615
|
+
return data
|
616
|
+
|
617
|
+
@staticmethod
|
618
|
+
def _parse2_2(f, version):
|
619
|
+
from .frames import map2_2FrameId
|
620
|
+
from .frames import FrameException
|
621
|
+
frame_id_22 = f.read(3)
|
622
|
+
frame_id = map2_2FrameId(frame_id_22)
|
623
|
+
if FrameHeader._isValidFrameId(frame_id):
|
624
|
+
log.debug("FrameHeader [id]: %s (0x%x%x%x)" %
|
625
|
+
(frame_id_22, frame_id_22[0], frame_id_22[1], frame_id_22[2]))
|
626
|
+
frame_header = FrameHeader(frame_id, version)
|
627
|
+
# data_size corresponds to the size of the data segment after
|
628
|
+
# encryption, compression, and unsynchronization.
|
629
|
+
sz = f.read(3)
|
630
|
+
frame_header.data_size = bin2dec(bytes2bin(sz, 8))
|
631
|
+
log.debug("FrameHeader [data size]: %d (0x%X)" %
|
632
|
+
(frame_header.data_size, frame_header.data_size))
|
633
|
+
return frame_header
|
634
|
+
elif frame_id == b'\x00\x00\x00':
|
635
|
+
log.debug("FrameHeader: Null frame id found at byte %d" % f.tell())
|
636
|
+
else:
|
637
|
+
core.parseError(FrameException("FrameHeader: Illegal Frame ID: %s" %
|
638
|
+
frame_id))
|
639
|
+
|
640
|
+
return None
|
641
|
+
|
642
|
+
@staticmethod
|
643
|
+
def parse(f, version):
|
644
|
+
from .frames import FrameException
|
645
|
+
log.debug("FrameHeader [start byte]: %d (0x%X)" % (f.tell(),
|
646
|
+
f.tell()))
|
647
|
+
major_version, minor_version = version[:2]
|
648
|
+
if minor_version == 2:
|
649
|
+
return FrameHeader._parse2_2(f, version)
|
650
|
+
|
651
|
+
frame_id = f.read(4)
|
652
|
+
if FrameHeader._isValidFrameId(frame_id):
|
653
|
+
log.debug("FrameHeader [id]: %s (0x%x%x%x%x)" %
|
654
|
+
(frame_id, frame_id[0], frame_id[1], frame_id[2], frame_id[3]))
|
655
|
+
frame_header = FrameHeader(frame_id, version)
|
656
|
+
# data_size corresponds to the size of the data segment after
|
657
|
+
# encryption, compression, and unsynchronization.
|
658
|
+
sz = f.read(4)
|
659
|
+
# In ID3 v2.4 this value became a synch-safe integer, meaning only
|
660
|
+
# the low 7 bits are used per byte.
|
661
|
+
if minor_version == 3:
|
662
|
+
frame_header.data_size = bin2dec(bytes2bin(sz, 8))
|
663
|
+
else:
|
664
|
+
frame_header.data_size = bin2dec(bytes2bin(sz, 7))
|
665
|
+
log.debug("FrameHeader [data size]: %d (0x%X)" %
|
666
|
+
(frame_header.data_size, frame_header.data_size))
|
667
|
+
|
668
|
+
# Frame flags.
|
669
|
+
flags = f.read(2)
|
670
|
+
frame_header._flags = bytes2bin(flags)
|
671
|
+
if log.getEffectiveLevel() <= logging.DEBUG:
|
672
|
+
log.debug("FrameHeader [flags]: ta(%d) fa(%d) ro(%d) co(%d) "
|
673
|
+
"en(%d) gr(%d) un(%d) dl(%d)" %
|
674
|
+
(frame_header.tag_alter,
|
675
|
+
frame_header.file_alter, frame_header.read_only,
|
676
|
+
frame_header.compressed, frame_header.encrypted,
|
677
|
+
frame_header.grouped, frame_header.unsync,
|
678
|
+
frame_header.data_length_indicator))
|
679
|
+
if (frame_header.minor_version >= 4 and frame_header.compressed and
|
680
|
+
not frame_header.data_length_indicator):
|
681
|
+
core.parseError(FrameException("Invalid frame; compressed with "
|
682
|
+
"no data length indicator"))
|
683
|
+
|
684
|
+
return frame_header
|
685
|
+
elif frame_id == b'\x00' * 4:
|
686
|
+
log.debug("FrameHeader: Null frame id found at byte %d" % f.tell())
|
687
|
+
else:
|
688
|
+
core.parseError(FrameException("FrameHeader: Illegal Frame ID: %s" %
|
689
|
+
frame_id))
|
690
|
+
|
691
|
+
return None
|
692
|
+
|
693
|
+
@staticmethod
|
694
|
+
def _isValidFrameId(id):
|
695
|
+
import re
|
696
|
+
return re.compile(b"^[A-Z0-9][A-Z0-9][A-Z0-9][A-Z0-9]$").match(id)
|