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/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)