eyeD3 0.9.8__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/frames.py ADDED
@@ -0,0 +1,2270 @@
1
+ import dataclasses
2
+ from io import BytesIO
3
+ from collections import namedtuple
4
+
5
+ from .. import core
6
+ from ..utils import requireUnicode, requireBytes
7
+ from ..utils.binfuncs import (
8
+ bin2bytes, bin2dec, bytes2bin, dec2bin, bytes2dec, dec2bytes,
9
+ signedInt162bytes, bytes2signedInt16,
10
+ )
11
+ from .. import Error
12
+ from . import ID3_V2, ID3_V2_2, ID3_V2_3, ID3_V2_4
13
+ from . import (LATIN1_ENCODING, UTF_8_ENCODING, UTF_16BE_ENCODING,
14
+ UTF_16_ENCODING, DEFAULT_LANG)
15
+ from .headers import FrameHeader
16
+ from ..utils import b
17
+ from ..utils.log import getLogger
18
+
19
+ log = getLogger(__name__)
20
+ ISO_8859_1 = "iso-8859-1"
21
+
22
+
23
+ class FrameException(Error):
24
+ pass
25
+
26
+
27
+ TITLE_FID = b"TIT2" # noqa
28
+ SUBTITLE_FID = b"TIT3" # noqa
29
+ ARTIST_FID = b"TPE1" # noqa
30
+ ALBUM_ARTIST_FID = b"TPE2" # noqa
31
+ ORIG_ARTIST_FID = b"TOPE" # noqa
32
+ COMPOSER_FID = b"TCOM" # noqa
33
+ ALBUM_FID = b"TALB" # noqa
34
+ TRACKNUM_FID = b"TRCK" # noqa
35
+ GENRE_FID = b"TCON" # noqa
36
+ COMMENT_FID = b"COMM" # noqa
37
+ USERTEXT_FID = b"TXXX" # noqa
38
+ OBJECT_FID = b"GEOB" # noqa
39
+ UNIQUE_FILE_ID_FID = b"UFID" # noqa
40
+ LYRICS_FID = b"USLT" # noqa
41
+ DISCNUM_FID = b"TPOS" # noqa
42
+ IMAGE_FID = b"APIC" # noqa
43
+ USERURL_FID = b"WXXX" # noqa
44
+ PLAYCOUNT_FID = b"PCNT" # noqa
45
+ BPM_FID = b"TBPM" # noqa
46
+ PUBLISHER_FID = b"TPUB" # noqa
47
+ CDID_FID = b"MCDI" # noqa
48
+ PRIVATE_FID = b"PRIV" # noqa
49
+ TOS_FID = b"USER" # noqa
50
+ POPULARITY_FID = b"POPM" # noqa
51
+ ENCODED_BY_FID = b"TENC" # noqa
52
+ COPYRIGHT_FID = b"TCOP" # noqa
53
+
54
+ URL_COMMERCIAL_FID = b"WCOM" # noqa
55
+ URL_COPYRIGHT_FID = b"WCOP" # noqa
56
+ URL_AUDIOFILE_FID = b"WOAF" # noqa
57
+ URL_ARTIST_FID = b"WOAR" # noqa
58
+ URL_AUDIOSRC_FID = b"WOAS" # noqa
59
+ URL_INET_RADIO_FID = b"WORS" # noqa
60
+ URL_PAYMENT_FID = b"WPAY" # noqa
61
+ URL_PUBLISHER_FID = b"WPUB" # noqa
62
+ URL_FIDS = [URL_COMMERCIAL_FID, URL_COPYRIGHT_FID, # noqa
63
+ URL_AUDIOFILE_FID, URL_ARTIST_FID, URL_AUDIOSRC_FID,
64
+ URL_INET_RADIO_FID, URL_PAYMENT_FID,
65
+ URL_PUBLISHER_FID]
66
+
67
+ TOC_FID = b"CTOC" # noqa
68
+ CHAPTER_FID = b"CHAP" # noqa
69
+
70
+ DEPRECATED_DATE_FIDS = [b"TDAT", b"TYER", b"TIME", b"TORY", b"TRDA",
71
+ # Nonstandard v2.3 only
72
+ b"XDOR",
73
+ ]
74
+ DATE_FIDS = [b"TDEN", b"TDOR", b"TDRC", b"TDRL", b"TDTG"]
75
+
76
+
77
+ class Frame(object):
78
+ _render_strict = True
79
+
80
+ @requireBytes(1)
81
+ def __init__(self, id):
82
+ self.id = id
83
+ self.header = None
84
+
85
+ self.decompressed_size = 0
86
+ self.group_id = None
87
+ self.encrypt_method = None
88
+ self.data = None
89
+ self.data_len = 0
90
+ self._encoding = None
91
+ self._unknown = False
92
+
93
+ @property
94
+ def header(self):
95
+ return self._header
96
+
97
+ @header.setter
98
+ def header(self, h):
99
+ self._header = h
100
+
101
+ @requireBytes(1)
102
+ def parse(self, data, frame_header):
103
+ self.id = frame_header.id
104
+ self.header = frame_header
105
+ self.data = self._disassembleFrame(data)
106
+
107
+ def render(self):
108
+ return self._assembleFrame(self.data)
109
+
110
+ def __lt__(self, other):
111
+ return self.id < other.id
112
+
113
+ @staticmethod
114
+ def decompress(data):
115
+ import zlib
116
+ log.debug("before decompression: %d bytes" % len(data))
117
+ data = zlib.decompress(data, 15)
118
+ log.debug("after decompression: %d bytes" % len(data))
119
+ return data
120
+
121
+ @staticmethod
122
+ def compress(data):
123
+ import zlib
124
+ log.debug("before compression: %d bytes" % len(data))
125
+ data = zlib.compress(data)
126
+ log.debug("after compression: %d bytes" % len(data))
127
+ return data
128
+
129
+ @staticmethod
130
+ def decrypt(data):
131
+ log.warning("Frame decryption not yet supported, leaving data as is.")
132
+ return data
133
+
134
+ @staticmethod
135
+ def encrypt(data):
136
+ log.warning("Frame encryption not yet supported, leaving data as is.")
137
+ return data
138
+
139
+ @requireBytes(1)
140
+ def _disassembleFrame(self, data):
141
+ header = self.header
142
+ # Format flags in the frame header may add extra data to the
143
+ # beginning of this data.
144
+ if header.minor_version <= 3:
145
+ # 2.3: compression(4), encryption(1), group(1)
146
+ if header.compressed:
147
+ self.decompressed_size = bin2dec(bytes2bin(data[:4]))
148
+ data = data[4:]
149
+ log.debug("Decompressed Size: %d" % self.decompressed_size)
150
+ if header.encrypted:
151
+ self.encrypt_method = bin2dec(bytes2bin(data[0:1]))
152
+ data = data[1:]
153
+ log.debug("Encryption Method: %d" % self.encrypt_method)
154
+ if header.grouped:
155
+ self.group_id = bin2dec(bytes2bin(data[0:1]))
156
+ data = data[1:]
157
+ log.debug("Group ID: %d" % self.group_id)
158
+ else:
159
+ # 2.4: group(1), encrypted(1), data_length_indicator (4,7)
160
+ if header.grouped:
161
+ self.group_id = bin2dec(bytes2bin(data[0:1]))
162
+ log.debug("Group ID: %d" % self.group_id)
163
+ data = data[1:]
164
+ if header.encrypted:
165
+ self.encrypt_method = bin2dec(bytes2bin(data[0:1]))
166
+ data = data[1:]
167
+ log.debug("Encryption Method: %d" % self.encrypt_method)
168
+ if header.data_length_indicator:
169
+ self.data_len = bin2dec(bytes2bin(data[:4], 7))
170
+ data = data[4:]
171
+ log.debug("Data Length: %d" % self.data_len)
172
+ if header.compressed:
173
+ self.decompressed_size = self.data_len
174
+ log.debug("Decompressed Size: %d" % self.decompressed_size)
175
+
176
+ if header.minor_version == 4 and header.unsync:
177
+ data = deunsyncData(data)
178
+ if header.encrypted:
179
+ data = self.decrypt(data)
180
+ if header.compressed:
181
+ data = self.decompress(data)
182
+
183
+ return data
184
+
185
+ @requireBytes(1)
186
+ def _assembleFrame(self, data):
187
+ header = self.header
188
+
189
+ # eyeD3 never writes unsync'd frames
190
+ header.unsync = False
191
+
192
+ format_data = b""
193
+ if header.minor_version == 3:
194
+ if header.compressed:
195
+ format_data += bin2bytes(dec2bin(len(data), 32))
196
+ if header.encrypted:
197
+ format_data += bin2bytes(dec2bin(self.encrypt_method, 8))
198
+ if header.grouped:
199
+ format_data += bin2bytes(dec2bin(self.group_id, 8))
200
+ else:
201
+ if header.grouped:
202
+ format_data += bin2bytes(dec2bin(self.group_id, 8))
203
+ if header.encrypted:
204
+ format_data += bin2bytes(dec2bin(self.encrypt_method, 8))
205
+ if header.compressed or header.data_length_indicator:
206
+ header.data_length_indicator = 1
207
+ format_data += bin2bytes(dec2bin(len(data), 32))
208
+
209
+ if header.compressed:
210
+ data = self.compress(data)
211
+
212
+ if header.encrypted:
213
+ data = self.encrypt(data)
214
+
215
+ self.data = format_data + data
216
+ return header.render(len(self.data)) + self.data
217
+
218
+ @property
219
+ def text_delim(self):
220
+ if self.encoding is None:
221
+ raise ValueError("Encoding required")
222
+ return b"\x00\x00" if self.encoding in (UTF_16_ENCODING,
223
+ UTF_16BE_ENCODING) else b"\x00"
224
+
225
+ def _initEncoding(self):
226
+ if not self.header.version or len(self.header.version) != 3:
227
+ raise ValueError("Invalid header version")
228
+ curr_enc = self.encoding
229
+
230
+ if self.encoding is not None:
231
+ # Make sure the encoding is valid for this version
232
+ if self.header.version[:2] < (2, 4):
233
+ if self.header.version[0] == 1:
234
+ self.encoding = LATIN1_ENCODING
235
+ else:
236
+ if self.encoding > UTF_16_ENCODING:
237
+ # v2.3 cannot do utf16 BE or utf8
238
+ self.encoding = UTF_16_ENCODING
239
+ else:
240
+ if self.header.version[:2] < (2, 4):
241
+ if self.header.version[0] == 2:
242
+ self.encoding = UTF_16_ENCODING
243
+ else:
244
+ self.encoding = LATIN1_ENCODING
245
+ else:
246
+ self.encoding = UTF_8_ENCODING
247
+
248
+ log.debug(f"_initEncoding: was={curr_enc} now={self.encoding}")
249
+
250
+ @property
251
+ def encoding(self):
252
+ return self._encoding
253
+
254
+ @encoding.setter
255
+ def encoding(self, enc):
256
+ if not isinstance(enc, bytes):
257
+ raise TypeError("encoding argument must be a byte string.")
258
+ elif not LATIN1_ENCODING <= enc <= UTF_8_ENCODING:
259
+ log.warning("Unknown encoding value {}".format(enc))
260
+ enc = LATIN1_ENCODING
261
+ self._encoding = enc
262
+
263
+ @property
264
+ def strict_rendering(self):
265
+ return self._render_strict
266
+
267
+ @property
268
+ def unknown(self):
269
+ return self._unknown
270
+
271
+
272
+ class TextFrame(Frame):
273
+ """Text frames.
274
+ Data string format: encoding (one byte) + text
275
+ """
276
+ @requireUnicode("text")
277
+ def __init__(self, id, text=None):
278
+ super(TextFrame, self).__init__(id)
279
+ if not TextFrame.isValidFrameId(id):
280
+ raise ValueError("Invalid frame ID for TextFrame")
281
+ self.text = text or ""
282
+
283
+ @property
284
+ def text(self):
285
+ return self._text
286
+
287
+ @text.setter
288
+ @requireUnicode(1)
289
+ def text(self, txt):
290
+ self._text = txt
291
+
292
+ def parse(self, data, frame_header):
293
+ super().parse(data, frame_header)
294
+
295
+ try:
296
+ self.encoding = self.data[0:1]
297
+ text_data = self.data[1:]
298
+ except ValueError as err:
299
+ log.warning("TextFrame[{fid}] - {err}; using latin1"
300
+ .format(err=err, fid=self.id))
301
+ self.encoding = LATIN1_ENCODING
302
+ text_data = self.data[:]
303
+
304
+ try:
305
+ self.text = decodeUnicode(text_data, self.encoding)
306
+ except UnicodeDecodeError as err:
307
+ log.warning(f"Error decoding text frame {self.id}: {err}")
308
+ self.text = ""
309
+ log.debug("TextFrame text: %s" % self.text)
310
+
311
+ def render(self):
312
+ self._initEncoding()
313
+ self.data = (self.encoding +
314
+ self.text.encode(id3EncodingToString(self.encoding)))
315
+ if type(self.data) is not bytes:
316
+ raise ValueError("Frame date not bytes type")
317
+ return super().render()
318
+
319
+ @staticmethod
320
+ def isValidFrameId(fid: bytes) -> bool:
321
+ return (fid[0:1] == b'T' or
322
+ fid in [b"XSOA", b"XSOP", b"XSOT", b"XDOR", b"WFED", b"GRP1",
323
+ b"MVNM", b"MVIN"])
324
+
325
+
326
+ class UserTextFrame(TextFrame):
327
+ @requireUnicode("description", "text")
328
+ def __init__(self, id=USERTEXT_FID, description="", text=""):
329
+ super(UserTextFrame, self).__init__(id, text=text)
330
+ self.description = description
331
+
332
+ @property
333
+ def description(self):
334
+ return self._description
335
+
336
+ @description.setter
337
+ @requireUnicode(1)
338
+ def description(self, txt):
339
+ self._description = txt
340
+
341
+ def parse(self, data, frame_header):
342
+ """Data string format:
343
+ encoding (one byte) + description + b"\x00" + text """
344
+ # Calling Frame, not TextFrame implementation here since TextFrame
345
+ # does not know about description
346
+ Frame.parse(self, data, frame_header)
347
+
348
+ try:
349
+ self.encoding = self.data[0:1]
350
+ (d, t) = splitUnicode(self.data[1:], self.encoding)
351
+ except ValueError as err:
352
+ log.warning("UserTextFrame[{fid}] - {err}; using latin1"
353
+ .format(err=err, fid=self.id))
354
+ self.encoding = LATIN1_ENCODING
355
+ (d, t) = splitUnicode(self.data[:], self.encoding)
356
+
357
+ self.description = decodeUnicode(d, self.encoding)
358
+ log.debug("UserTextFrame description: %s" % self.description)
359
+ self.text = decodeUnicode(t, self.encoding)
360
+ log.debug("UserTextFrame text: %s" % self.text)
361
+
362
+ def render(self):
363
+ self._initEncoding()
364
+ data = (self.encoding +
365
+ self.description.encode(id3EncodingToString(self.encoding)) +
366
+ self.text_delim +
367
+ self.text.encode(id3EncodingToString(self.encoding)))
368
+ self.data = data
369
+ # Calling Frame, not the base
370
+ return Frame.render(self)
371
+
372
+
373
+ class DateFrame(TextFrame):
374
+ def __init__(self, id, date=""):
375
+ if id not in DATE_FIDS and id not in DEPRECATED_DATE_FIDS:
376
+ raise ValueError(f"Invalid date frame ID: {id}")
377
+ super().__init__(id, text=str(date))
378
+ self.date = self.text
379
+ self.encoding = LATIN1_ENCODING
380
+
381
+ def parse(self, data, frame_header):
382
+ super().parse(data, frame_header)
383
+ try:
384
+ if self.text:
385
+ _ = core.Date.parse(self.text) # noqa
386
+ except ValueError:
387
+ # Date is invalid, log it and reset.
388
+ core.parseError(FrameException(f"Invalid date: {self.text}"))
389
+ self.text = ""
390
+
391
+ @property
392
+ def date(self):
393
+ return core.Date.parse(self.text.encode("latin1")) if self.text else None
394
+
395
+ @date.setter
396
+ def date(self, date):
397
+ """Set value with a either an ISO 8601 date string or a eyed3.core.Date object."""
398
+ if not date:
399
+ self.text = ""
400
+ return
401
+
402
+ try:
403
+ if type(date) is str:
404
+ date = core.Date.parse(date)
405
+ elif type(date) is int:
406
+ # Date is year
407
+ date = core.Date(date)
408
+ elif not isinstance(date, core.Date):
409
+ raise TypeError("str, int, or eyed3.core.Date type expected")
410
+ except ValueError:
411
+ log.warning(f"Invalid date text: {date}")
412
+ self.text = ""
413
+ return
414
+
415
+ str_date = str(date)
416
+ if ((DorT := str_date[0]) in ("D", "T")) and (len(str_date) <= 6): # maxlength 6
417
+ str_date = str_date.replace(DorT, "", 1)
418
+ str_date = str_date.replace("-" if DorT == "D" else ":", "")
419
+ self.text = str_date
420
+
421
+ def _initEncoding(self):
422
+ # Dates are always latin1 since they are always represented in ISO 8601
423
+ self.encoding = LATIN1_ENCODING
424
+
425
+
426
+ class UrlFrame(Frame):
427
+
428
+ def __init__(self, id, url=""):
429
+ if id not in URL_FIDS and id != USERURL_FID:
430
+ raise ValueError(f"Invalid URL frame ID: {id}")
431
+ super(UrlFrame, self).__init__(id)
432
+
433
+ self.encoding = LATIN1_ENCODING # Per the specs
434
+ self.url = url
435
+
436
+ @property
437
+ def url(self):
438
+ return self._url
439
+
440
+ @url.setter
441
+ def url(self, url):
442
+ if isinstance(url, bytes):
443
+ url = str(url, ISO_8859_1)
444
+ else:
445
+ url.encode(ISO_8859_1) # Likewise, it must encode
446
+
447
+ self._url = url
448
+
449
+ def parse(self, data, frame_header):
450
+ super().parse(data, frame_header)
451
+
452
+ try:
453
+ self.url = self.data
454
+ except UnicodeDecodeError:
455
+ log.warning("Non ascii url, clearing.")
456
+ self.url = ""
457
+
458
+ def render(self):
459
+ self.data = self.url.encode(ISO_8859_1)
460
+ return super(UrlFrame, self).render()
461
+
462
+
463
+ class UserUrlFrame(UrlFrame):
464
+ """
465
+ Data string format:
466
+ encoding (one byte) + description + b"\x00" + url (iso-8859-1)
467
+ """
468
+ @requireUnicode("description")
469
+ def __init__(self, id=USERURL_FID, description="", url=""):
470
+ if id != USERURL_FID:
471
+ raise ValueError(f"Unexpected frame ID: {id}")
472
+ UrlFrame.__init__(self, id, url=url)
473
+
474
+ self.description = description
475
+
476
+ @property
477
+ def description(self):
478
+ return self._description
479
+
480
+ @description.setter
481
+ @requireUnicode(1)
482
+ def description(self, desc):
483
+ self._description = desc
484
+
485
+ def parse(self, data, frame_header):
486
+ # Calling Frame and NOT UrlFrame to get the basic disassemble behavior
487
+ # UrlFrame would be confused by the encoding, desc, etc.
488
+ super().parse(data, frame_header)
489
+ self.encoding = encoding = self.data[0:1]
490
+
491
+ (d, u) = splitUnicode(self.data[1:], encoding)
492
+ self.description = decodeUnicode(d, encoding)
493
+ log.debug("UserUrlFrame description: %s" % self.description)
494
+ # The URL is ascii, ensure
495
+ try:
496
+ self.url = str(u, "ascii").encode("ascii")
497
+ except UnicodeDecodeError:
498
+ log.warning("Non ascii url, clearing.")
499
+ self.url = ""
500
+ log.debug("UserUrlFrame text: %s" % self.url)
501
+
502
+ def render(self):
503
+ self._initEncoding()
504
+ data = (self.encoding +
505
+ self.description.encode(id3EncodingToString(self.encoding)) +
506
+ self.text_delim + self.url.encode(ISO_8859_1))
507
+ self.data = data
508
+ # Calling Frame, not the base.
509
+ return Frame.render(self)
510
+
511
+
512
+ class UnknownFrame(Frame):
513
+ """Unknown Frame."""
514
+ def __init__(self, id):
515
+ super().__init__(id)
516
+ if self.id in ID3_FRAMES or self.id in NONSTANDARD_ID3_FRAMES:
517
+ raise ValueError(f"Unknown constructed with known frame ID: {self.id}")
518
+ self._unknown = True
519
+
520
+
521
+ ##
522
+ # Data string format:
523
+ # <Header for 'Attached picture', ID: "APIC">
524
+ # Text encoding $xx
525
+ # MIME type <text string> $00
526
+ # Picture type $xx
527
+ # Description <text string according to encoding> $00 (00)
528
+ # Picture data <binary data>
529
+ class ImageFrame(Frame):
530
+ OTHER = 0x00 # noqa
531
+ ICON = 0x01 # 32x32 png only. # noqa
532
+ OTHER_ICON = 0x02 # noqa
533
+ FRONT_COVER = 0x03 # noqa
534
+ BACK_COVER = 0x04 # noqa
535
+ LEAFLET = 0x05 # noqa
536
+ MEDIA = 0x06 # label side of cd, vinyl, etc. # noqa
537
+ LEAD_ARTIST = 0x07 # noqa
538
+ ARTIST = 0x08 # noqa
539
+ CONDUCTOR = 0x09 # noqa
540
+ BAND = 0x0A # noqa
541
+ COMPOSER = 0x0B # noqa
542
+ LYRICIST = 0x0C # noqa
543
+ RECORDING_LOCATION = 0x0D # noqa
544
+ DURING_RECORDING = 0x0E # noqa
545
+ DURING_PERFORMANCE = 0x0F # noqa
546
+ VIDEO = 0x10 # noqa
547
+ BRIGHT_COLORED_FISH = 0x11 # There's always room for porno. # noqa
548
+ ILLUSTRATION = 0x12 # noqa
549
+ BAND_LOGO = 0x13 # noqa
550
+ PUBLISHER_LOGO = 0x14 # noqa
551
+ MIN_TYPE = OTHER # noqa
552
+ MAX_TYPE = PUBLISHER_LOGO # noqa
553
+
554
+ URL_MIME_TYPE = b"-->" # noqa
555
+ URL_MIME_TYPE_STR = "-->" # noqa
556
+ URL_MIME_TYPE_VALUES = (URL_MIME_TYPE, URL_MIME_TYPE_STR)
557
+
558
+ @requireUnicode("description")
559
+ def __init__(self, id=IMAGE_FID, description="",
560
+ image_data=None, image_url=None,
561
+ picture_type=None, mime_type=None):
562
+ if id != IMAGE_FID:
563
+ raise ValueError(f"Unexpected frame ID: {id}")
564
+
565
+ super(ImageFrame, self).__init__(id)
566
+ self.description = description
567
+ self.image_data = image_data
568
+ self.image_url = image_url
569
+
570
+ # XXX: Add this member as `type` and deprecate picture_type??
571
+ self.picture_type = picture_type
572
+ self.mime_type = mime_type
573
+
574
+ @property
575
+ def description(self):
576
+ return self._description
577
+
578
+ @description.setter
579
+ @requireUnicode(1)
580
+ def description(self, d):
581
+ self._description = d
582
+
583
+ @property
584
+ def mime_type(self):
585
+ return str(self._mime_type, "ascii")
586
+
587
+ @mime_type.setter
588
+ def mime_type(self, m):
589
+ m = m or b''
590
+ self._mime_type = m if isinstance(m, bytes) else m.encode('ascii')
591
+
592
+ @property
593
+ def picture_type(self):
594
+ return self._pic_type
595
+
596
+ @picture_type.setter
597
+ def picture_type(self, t):
598
+ if t is not None and (t < ImageFrame.MIN_TYPE or
599
+ t > ImageFrame.MAX_TYPE):
600
+ raise ValueError("Invalid picture_type: %d" % t)
601
+ self._pic_type = t
602
+
603
+ def parse(self, data, frame_header):
604
+ super().parse(data, frame_header)
605
+
606
+ input = BytesIO(self.data)
607
+ log.debug("APIC frame data size: %d" % len(self.data))
608
+ self.encoding = encoding = input.read(1)
609
+
610
+ # Mime type
611
+ self._mime_type = b""
612
+ if frame_header.minor_version != 2:
613
+ ch = input.read(1)
614
+ while ch and ch != b"\x00":
615
+ self._mime_type += ch
616
+ ch = input.read(1)
617
+ else:
618
+ # v2.2 (OBSOLETE) special case
619
+ self._mime_type = input.read(3)
620
+ log.debug("APIC mime type: %s" % self._mime_type)
621
+ if not self._mime_type:
622
+ core.parseError(FrameException("APIC frame does not contain a mime "
623
+ "type"))
624
+ if (self._mime_type != self.URL_MIME_TYPE and
625
+ self._mime_type.find(b"/") == -1):
626
+ self._mime_type = b"image/" + self._mime_type
627
+
628
+ pt = ord(input.read(1))
629
+ log.debug("Initial APIC picture type: %d" % pt)
630
+ if pt < self.MIN_TYPE or pt > self.MAX_TYPE:
631
+ core.parseError(FrameException("Invalid APIC picture type: %d" %
632
+ pt))
633
+ self.picture_type = self.OTHER
634
+ else:
635
+ self.picture_type = pt
636
+ log.debug("APIC picture type: %d" % self.picture_type)
637
+
638
+ self.desciption = ""
639
+
640
+ # Remaining data is a NULL separated description and image data
641
+ buffer = input.read()
642
+ input.close()
643
+
644
+ (desc, img) = splitUnicode(buffer, encoding)
645
+ log.debug("description len: %d" % len(desc))
646
+ log.debug("image len: %d" % len(img))
647
+ self.description = decodeUnicode(desc, encoding)
648
+ log.debug("APIC description: %s" % self.description)
649
+
650
+ if self._mime_type.find(self.URL_MIME_TYPE) != -1:
651
+ self.image_data = None
652
+ self.image_url = img
653
+ log.debug("APIC image URL: %s" %
654
+ len(self.image_url.decode("ascii")))
655
+ else:
656
+ self.image_data = img
657
+ self.image_url = None
658
+ log.debug("APIC image data: %d bytes" % len(self.image_data))
659
+ if not self.image_data and not self.image_url:
660
+ core.parseError(FrameException("APIC frame does not contain image "
661
+ "data/url"))
662
+
663
+ def render(self):
664
+ # some code has problems with image descriptions encoded <> latin1
665
+ # namely mp3diags: work around the problem by forcing latin1 encoding
666
+ # for empty descriptions, which is by far the most common case anyway
667
+ self._initEncoding()
668
+
669
+ if not self.image_data and self.image_url:
670
+ self._mime_type = self.URL_MIME_TYPE
671
+
672
+ data = (self.encoding + self._mime_type + b"\x00" +
673
+ bin2bytes(dec2bin(self.picture_type, 8)) +
674
+ self.description.encode(id3EncodingToString(self.encoding)) +
675
+ self.text_delim)
676
+
677
+ if self.image_data:
678
+ data += self.image_data
679
+ elif self.image_url:
680
+ data += self.image_url
681
+
682
+ self.data = data
683
+ return super(ImageFrame, self).render()
684
+
685
+ @staticmethod
686
+ def picTypeToString(t):
687
+ if t == ImageFrame.OTHER:
688
+ return "OTHER"
689
+ elif t == ImageFrame.ICON:
690
+ return "ICON"
691
+ elif t == ImageFrame.OTHER_ICON:
692
+ return "OTHER_ICON"
693
+ elif t == ImageFrame.FRONT_COVER:
694
+ return "FRONT_COVER"
695
+ elif t == ImageFrame.BACK_COVER:
696
+ return "BACK_COVER"
697
+ elif t == ImageFrame.LEAFLET:
698
+ return "LEAFLET"
699
+ elif t == ImageFrame.MEDIA:
700
+ return "MEDIA"
701
+ elif t == ImageFrame.LEAD_ARTIST:
702
+ return "LEAD_ARTIST"
703
+ elif t == ImageFrame.ARTIST:
704
+ return "ARTIST"
705
+ elif t == ImageFrame.CONDUCTOR:
706
+ return "CONDUCTOR"
707
+ elif t == ImageFrame.BAND:
708
+ return "BAND"
709
+ elif t == ImageFrame.COMPOSER:
710
+ return "COMPOSER"
711
+ elif t == ImageFrame.LYRICIST:
712
+ return "LYRICIST"
713
+ elif t == ImageFrame.RECORDING_LOCATION:
714
+ return "RECORDING_LOCATION"
715
+ elif t == ImageFrame.DURING_RECORDING:
716
+ return "DURING_RECORDING"
717
+ elif t == ImageFrame.DURING_PERFORMANCE:
718
+ return "DURING_PERFORMANCE"
719
+ elif t == ImageFrame.VIDEO:
720
+ return "VIDEO"
721
+ elif t == ImageFrame.BRIGHT_COLORED_FISH:
722
+ return "BRIGHT_COLORED_FISH"
723
+ elif t == ImageFrame.ILLUSTRATION:
724
+ return "ILLUSTRATION"
725
+ elif t == ImageFrame.BAND_LOGO:
726
+ return "BAND_LOGO"
727
+ elif t == ImageFrame.PUBLISHER_LOGO:
728
+ return "PUBLISHER_LOGO"
729
+ else:
730
+ raise ValueError("Invalid APIC picture type: %d" % t)
731
+
732
+ @staticmethod
733
+ def stringToPicType(s):
734
+ if s == "OTHER":
735
+ return ImageFrame.OTHER
736
+ elif s == "ICON":
737
+ return ImageFrame.ICON
738
+ elif s == "OTHER_ICON":
739
+ return ImageFrame.OTHER_ICON
740
+ elif s == "FRONT_COVER":
741
+ return ImageFrame.FRONT_COVER
742
+ elif s == "BACK_COVER":
743
+ return ImageFrame.BACK_COVER
744
+ elif s == "LEAFLET":
745
+ return ImageFrame.LEAFLET
746
+ elif s == "MEDIA":
747
+ return ImageFrame.MEDIA
748
+ elif s == "LEAD_ARTIST":
749
+ return ImageFrame.LEAD_ARTIST
750
+ elif s == "ARTIST":
751
+ return ImageFrame.ARTIST
752
+ elif s == "CONDUCTOR":
753
+ return ImageFrame.CONDUCTOR
754
+ elif s == "BAND":
755
+ return ImageFrame.BAND
756
+ elif s == "COMPOSER":
757
+ return ImageFrame.COMPOSER
758
+ elif s == "LYRICIST":
759
+ return ImageFrame.LYRICIST
760
+ elif s == "RECORDING_LOCATION":
761
+ return ImageFrame.RECORDING_LOCATION
762
+ elif s == "DURING_RECORDING":
763
+ return ImageFrame.DURING_RECORDING
764
+ elif s == "DURING_PERFORMANCE":
765
+ return ImageFrame.DURING_PERFORMANCE
766
+ elif s == "VIDEO":
767
+ return ImageFrame.VIDEO
768
+ elif s == "BRIGHT_COLORED_FISH":
769
+ return ImageFrame.BRIGHT_COLORED_FISH
770
+ elif s == "ILLUSTRATION":
771
+ return ImageFrame.ILLUSTRATION
772
+ elif s == "BAND_LOGO":
773
+ return ImageFrame.BAND_LOGO
774
+ elif s == "PUBLISHER_LOGO":
775
+ return ImageFrame.PUBLISHER_LOGO
776
+ else:
777
+ raise ValueError("Invalid APIC picture type: %s" % s)
778
+
779
+ def makeFileName(self, name=None):
780
+ name = ImageFrame.picTypeToString(self.picture_type) if not name \
781
+ else name
782
+ ext = self.mime_type.split("/")[1]
783
+ if ext == "jpeg":
784
+ ext = "jpg"
785
+ return ".".join([name, ext])
786
+
787
+
788
+ class ObjectFrame(Frame):
789
+ @requireUnicode("description", "filename")
790
+ def __init__(self, fid=OBJECT_FID, description="", filename="",
791
+ object_data=None, mime_type=None):
792
+ super().__init__(fid)
793
+ self.description = description
794
+ self.filename = filename
795
+ self.mime_type = mime_type
796
+ self.object_data = object_data
797
+
798
+ @property
799
+ def description(self):
800
+ return self._description
801
+
802
+ @description.setter
803
+ @requireUnicode(1)
804
+ def description(self, txt):
805
+ self._description = txt
806
+
807
+ @property
808
+ def mime_type(self):
809
+ return str(self._mime_type, "ascii")
810
+
811
+ @mime_type.setter
812
+ def mime_type(self, m):
813
+ m = m or b''
814
+ self._mime_type = m if isinstance(m, bytes) else m.encode('ascii')
815
+
816
+ @property
817
+ def filename(self):
818
+ return self._filename
819
+
820
+ @filename.setter
821
+ @requireUnicode(1)
822
+ def filename(self, txt):
823
+ self._filename = txt
824
+
825
+ def parse(self, data, frame_header):
826
+ """Parse the frame from ``data`` bytes using details from
827
+ ``frame_header``.
828
+
829
+ Data string format:
830
+ <Header for 'General encapsulated object', ID: "GEOB">
831
+ Text encoding $xx
832
+ MIME type <text string> $00
833
+ Filename <text string according to encoding> $00 (00)
834
+ Content description <text string according to encoding> $00 (00)
835
+ Encapsulated object <binary data>
836
+ """
837
+ super().parse(data, frame_header)
838
+
839
+ input = BytesIO(self.data)
840
+ log.debug("GEOB frame data size: " + str(len(self.data)))
841
+ self.encoding = encoding = input.read(1)
842
+
843
+ # Mime type
844
+ self._mime_type = b""
845
+ if self.header.minor_version != 2:
846
+ ch = input.read(1)
847
+ while ch not in (b'', b'\0'):
848
+ self._mime_type += ch
849
+ ch = input.read(1)
850
+ else:
851
+ # v2.2 (OBSOLETE) special case
852
+ self._mime_type = input.read(3)
853
+ log.debug("GEOB mime type: %s" % self._mime_type)
854
+ if not self._mime_type:
855
+ core.parseError(FrameException("GEOB frame does not contain a "
856
+ "mime type"))
857
+ if self._mime_type.find(b"/") == -1:
858
+ core.parseError(FrameException("GEOB frame does not contain a "
859
+ "valid mime type"))
860
+
861
+ self.filename = ""
862
+ self.description = ""
863
+
864
+ # Remaining data is a NULL separated filename, description and object
865
+ # data
866
+ buffer = input.read()
867
+ input.close()
868
+
869
+ (filename, buffer) = splitUnicode(buffer, encoding)
870
+ (desc, obj) = splitUnicode(buffer, encoding)
871
+ self.filename = decodeUnicode(filename, encoding)
872
+ log.debug("GEOB filename: " + self.filename)
873
+ self.description = decodeUnicode(desc, encoding)
874
+ log.debug("GEOB description: " + self.description)
875
+
876
+ self.object_data = obj
877
+ log.debug("GEOB data: %d bytes " % len(self.object_data))
878
+ if not self.object_data:
879
+ core.parseError(FrameException("GEOB frame does not contain any "
880
+ "data"))
881
+
882
+ def render(self):
883
+ self._initEncoding()
884
+ data = (self.encoding + self._mime_type + b"\x00" +
885
+ self.filename.encode(id3EncodingToString(self.encoding)) +
886
+ self.text_delim +
887
+ self.description.encode(id3EncodingToString(self.encoding)) +
888
+ self.text_delim +
889
+ (self.object_data or b""))
890
+ self.data = data
891
+ return super(ObjectFrame, self).render()
892
+
893
+
894
+ class PrivateFrame(Frame):
895
+ """PRIV"""
896
+ owner_id: bytes
897
+ owner_data: bytes
898
+
899
+ def __init__(self, id=PRIVATE_FID, owner_id=b"", owner_data=b""):
900
+ super().__init__(id)
901
+ if id != PRIVATE_FID:
902
+ raise ValueError(f"Unexpected frame ID: {id}")
903
+ for arg in (owner_id, owner_data):
904
+ if type(arg) is not bytes:
905
+ raise ValueError("PRIV owner fields require bytes type")
906
+
907
+ self.owner_id = owner_id
908
+ self.owner_data = owner_data
909
+
910
+ def parse(self, data, frame_header):
911
+ super().parse(data, frame_header)
912
+ try:
913
+ self.owner_id, self.owner_data = self.data.split(b"\x00", 1)
914
+ except ValueError:
915
+ # If data doesn't contain required \x00
916
+ # all data is taken to be owner_id
917
+ self.owner_id = self.data
918
+
919
+ def render(self):
920
+ self.data = self.owner_id + b"\x00" + self.owner_data
921
+ return super(PrivateFrame, self).render()
922
+
923
+
924
+ class MusicCDIdFrame(Frame):
925
+
926
+ def __init__(self, id=CDID_FID, toc=b""):
927
+ super(MusicCDIdFrame, self).__init__(id)
928
+ if id != CDID_FID:
929
+ raise ValueError(f"Unexpected frame ID: {id}")
930
+ self.toc = toc
931
+
932
+ @property
933
+ def toc(self):
934
+ return self.data
935
+
936
+ @toc.setter
937
+ def toc(self, toc):
938
+ self.data = toc
939
+
940
+ def parse(self, data, frame_header):
941
+ super().parse(data, frame_header)
942
+ self.toc = self.data
943
+
944
+
945
+ class PlayCountFrame(Frame):
946
+ def __init__(self, id=PLAYCOUNT_FID, count=0):
947
+ super(PlayCountFrame, self).__init__(id)
948
+ if self.id != PLAYCOUNT_FID:
949
+ raise ValueError(f"Unexpected frame ID: {self.id}")
950
+
951
+ if count is None or count < 0:
952
+ raise ValueError("Invalid count value: %s" % str(count))
953
+ self.count = count
954
+
955
+ def parse(self, data, frame_header):
956
+ super().parse(data, frame_header)
957
+ # data of less then 4 bytes is handled with with 'sz' arg
958
+ if len(self.data) < 4:
959
+ log.warning("Fixing invalid PCNT frame: less than 32 bits")
960
+
961
+ self.count = bytes2dec(self.data)
962
+
963
+ def render(self):
964
+ self.data = dec2bytes(self.count, 32)
965
+ return super(PlayCountFrame, self).render()
966
+
967
+
968
+ class PopularityFrame(Frame):
969
+ """Frame type for 'POPM' frames; popularity.
970
+ Frame format:
971
+ <Header for 'Popularimeter', ID: "POPM">
972
+ Email to user <text string> $00
973
+ Rating $xx
974
+ Counter $xx xx xx xx (xx ...)
975
+ """
976
+ def __init__(self, id=POPULARITY_FID, email=b"", rating=0, count=0):
977
+ super(PopularityFrame, self).__init__(id)
978
+ if self.id != POPULARITY_FID:
979
+ raise ValueError(f"Unexpected frame ID: {self.id}")
980
+
981
+ self.email = email
982
+ self.rating = rating
983
+ if count is None or count < 0:
984
+ raise ValueError("Invalid count value: %s" % str(count))
985
+ self.count = count
986
+
987
+ @property
988
+ def rating(self):
989
+ return self._rating
990
+
991
+ @rating.setter
992
+ def rating(self, rating):
993
+ if rating < 0 or rating > 255:
994
+ raise ValueError("Popularity rating must be >= 0 and <=255")
995
+ self._rating = rating
996
+
997
+ @property
998
+ def email(self):
999
+ return self._email
1000
+
1001
+ @email.setter
1002
+ def email(self, email):
1003
+ # XXX: becoming a pattern?
1004
+ if isinstance(email, str):
1005
+ self._email = email.encode("ascii")
1006
+ elif isinstance(email, bytes):
1007
+ _ = email.decode("ascii") # noqa
1008
+ self._email = email
1009
+ else:
1010
+ raise TypeError("bytes, str, unicode email required")
1011
+
1012
+ @property
1013
+ def count(self):
1014
+ return self._count
1015
+
1016
+ @count.setter
1017
+ def count(self, count):
1018
+ if count < 0:
1019
+ raise ValueError("Popularity count must be > 0")
1020
+ self._count = count
1021
+
1022
+ def parse(self, data, frame_header):
1023
+ super().parse(data, frame_header)
1024
+ data = self.data
1025
+
1026
+ null_byte = data.find(b'\x00')
1027
+ try:
1028
+ self.email = data[:null_byte]
1029
+ except UnicodeDecodeError:
1030
+ core.parseError(FrameException("Invalid (non-ascii) POPM email "
1031
+ "address. Setting to 'BOGUS'"))
1032
+ self.email = b"BOGUS"
1033
+ data = data[null_byte + 1:]
1034
+
1035
+ self.rating = bytes2dec(data[0:1])
1036
+
1037
+ data = data[1:]
1038
+ if len(self.data) < 4:
1039
+ core.parseError(FrameException(
1040
+ "Invalid POPM play count: less than 32 bits."))
1041
+ self.count = bytes2dec(data)
1042
+
1043
+ def render(self):
1044
+ data = (self.email or b"") + b'\x00'
1045
+ data += dec2bytes(self.rating)
1046
+ data += dec2bytes(self.count, 32)
1047
+
1048
+ self.data = data
1049
+ return super(PopularityFrame, self).render()
1050
+
1051
+
1052
+ class UniqueFileIDFrame(Frame):
1053
+ def __init__(self, id=UNIQUE_FILE_ID_FID, owner_id=b"", uniq_id=b""):
1054
+ super().__init__(id)
1055
+ if self.id != UNIQUE_FILE_ID_FID:
1056
+ raise ValueError(f"Unexpected frame ID: {self.id}")
1057
+ self.owner_id = owner_id
1058
+ self.uniq_id = uniq_id
1059
+
1060
+ @property
1061
+ def owner_id(self):
1062
+ return self._owner_id
1063
+
1064
+ @owner_id.setter
1065
+ def owner_id(self, oid):
1066
+ self._owner_id = b(oid) if oid else b""
1067
+
1068
+ @property
1069
+ def uniq_id(self):
1070
+ return self._uniq_id
1071
+
1072
+ @uniq_id.setter
1073
+ def uniq_id(self, uid):
1074
+ self._uniq_id = b(uid) if uid else b""
1075
+
1076
+ def parse(self, data, frame_header):
1077
+ """
1078
+ Data format
1079
+ Owner identifier <text string> $00
1080
+ Identifier up to 64 bytes binary data>
1081
+ """
1082
+ super().parse(data, frame_header)
1083
+ split_data = self.data.split(b'\x00', 1)
1084
+ if len(split_data) == 2:
1085
+ (self.owner_id, self.uniq_id) = split_data
1086
+ else:
1087
+ self.owner_id, self.uniq_id = b"", b"".join(split_data[0:1])
1088
+ log.debug("UFID owner_id: %s" % self.owner_id)
1089
+ log.debug("UFID id: %s" % self.uniq_id)
1090
+ if not self.owner_id:
1091
+ dummy_owner_id = "http://www.id3.org/dummy/ufid.html"
1092
+ self.owner_id = dummy_owner_id
1093
+ core.parseError(FrameException("Invalid UFID, owner_id is empty. "
1094
+ "Setting to '%s'" % dummy_owner_id))
1095
+ elif 0 <= len(self.uniq_id) > 64:
1096
+ core.parseError(FrameException("Invalid UFID, ID is empty or too "
1097
+ "long: %s" % self.uniq_id))
1098
+
1099
+ def render(self):
1100
+ self.data = self.owner_id + b"\x00" + self.uniq_id
1101
+ return super().render()
1102
+
1103
+
1104
+ class LanguageCodeMixin(object):
1105
+ @property
1106
+ def lang(self):
1107
+ if self._lang is None:
1108
+ raise ValueError("lang must be set")
1109
+ return self._lang
1110
+
1111
+ @lang.setter
1112
+ @requireBytes(1)
1113
+ def lang(self, lang):
1114
+ if not lang:
1115
+ self._lang = b""
1116
+ return
1117
+
1118
+ lang = lang.strip(b"\00")
1119
+ lang = lang[:3] if lang else DEFAULT_LANG
1120
+ try:
1121
+ if lang != DEFAULT_LANG:
1122
+ lang.decode("ascii")
1123
+ except UnicodeDecodeError:
1124
+ lang = DEFAULT_LANG
1125
+ if len(lang) > 3:
1126
+ raise ValueError(f"invalid lang: {lang}")
1127
+ self._lang = lang
1128
+
1129
+ def _renderLang(self):
1130
+ lang = self.lang
1131
+ if len(lang) < 3:
1132
+ lang = lang + (b"\x00" * (3 - len(lang)))
1133
+ return lang
1134
+
1135
+
1136
+ class DescriptionLangTextFrame(Frame, LanguageCodeMixin):
1137
+ @requireBytes(1, 3)
1138
+ @requireUnicode(2, 4)
1139
+ def __init__(self, id, description, lang, text):
1140
+ super().__init__(id)
1141
+ self.lang = lang
1142
+ self.description = description
1143
+ self.text = text
1144
+
1145
+ @property
1146
+ def description(self):
1147
+ return self._description
1148
+
1149
+ @description.setter
1150
+ @requireUnicode(1)
1151
+ def description(self, description):
1152
+ self._description = description
1153
+
1154
+ @property
1155
+ def text(self):
1156
+ return self._text
1157
+
1158
+ @text.setter
1159
+ @requireUnicode(1)
1160
+ def text(self, text):
1161
+ self._text = text
1162
+
1163
+ def parse(self, data, frame_header):
1164
+ super().parse(data, frame_header)
1165
+
1166
+ self.encoding = self.data[0:1]
1167
+ self.lang = self.data[1:4]
1168
+ log.debug("%s lang: %s" % (self.id, self.lang))
1169
+
1170
+ try:
1171
+ (d, t) = splitUnicode(self.data[4:], self.encoding)
1172
+ self.description = decodeUnicode(d, self.encoding)
1173
+ log.debug("%s description: %s" % (self.id, self.description))
1174
+ self.text = decodeUnicode(t, self.encoding)
1175
+ log.debug("%s text: %s" % (self.id, self.text))
1176
+ except ValueError:
1177
+ log.warning("Invalid %s frame; no description/text" % self.id)
1178
+ self.description = ""
1179
+ self.text = ""
1180
+
1181
+ def render(self):
1182
+ lang = self._renderLang()
1183
+
1184
+ self._initEncoding()
1185
+ data = (self.encoding + lang +
1186
+ self.description.encode(id3EncodingToString(self.encoding)) +
1187
+ self.text_delim +
1188
+ self.text.encode(id3EncodingToString(self.encoding)))
1189
+ self.data = data
1190
+ return super(DescriptionLangTextFrame, self).render()
1191
+
1192
+
1193
+ class CommentFrame(DescriptionLangTextFrame):
1194
+ def __init__(self, id=COMMENT_FID, description="", lang=DEFAULT_LANG,
1195
+ text=""):
1196
+ super(CommentFrame, self).__init__(id, description, lang, text)
1197
+ if id != COMMENT_FID:
1198
+ raise ValueError(f"Unexpected frame ID: {id}")
1199
+
1200
+
1201
+ class LyricsFrame(DescriptionLangTextFrame):
1202
+ def __init__(self, id=LYRICS_FID, description="", lang=DEFAULT_LANG,
1203
+ text=""):
1204
+ super(LyricsFrame, self).__init__(id, description, lang, text)
1205
+ if self.id != LYRICS_FID:
1206
+ raise ValueError(f"Unexpected frame ID: {self.id}")
1207
+
1208
+
1209
+ class TermsOfUseFrame(Frame, LanguageCodeMixin):
1210
+ @requireUnicode("text")
1211
+ def __init__(self, id=b"USER", text="", lang=DEFAULT_LANG):
1212
+ super(TermsOfUseFrame, self).__init__(id)
1213
+ self.lang = lang
1214
+ self.text = text
1215
+
1216
+ @property
1217
+ def text(self):
1218
+ return self._text
1219
+
1220
+ @text.setter
1221
+ @requireUnicode(1)
1222
+ def text(self, text):
1223
+ self._text = text
1224
+
1225
+ def parse(self, data, frame_header):
1226
+ super().parse(data, frame_header)
1227
+
1228
+ self.encoding = encoding = self.data[0:1]
1229
+ self.lang = self.data[1:4]
1230
+ log.debug("%s lang: %s" % (self.id, self.lang))
1231
+ self.text = decodeUnicode(self.data[4:], encoding)
1232
+ log.debug("%s text: %s" % (self.id, self.text))
1233
+
1234
+ def render(self):
1235
+ lang = self._renderLang()
1236
+ self._initEncoding()
1237
+ self.data = (self.encoding + lang +
1238
+ self.text.encode(id3EncodingToString(self.encoding)))
1239
+ return super(TermsOfUseFrame, self).render()
1240
+
1241
+
1242
+ class TocFrame(Frame):
1243
+ """Table of content frame. There may be more than one, but only one may
1244
+ have the top-level flag set.
1245
+
1246
+ Data format:
1247
+ Element ID: <string>\x00
1248
+ TOC flags: %000000ab
1249
+ Entry count: %xx
1250
+ Child elem IDs: <string>\x00 (... num entry count)
1251
+ Description: TIT2 frame (optional)
1252
+ """
1253
+ TOP_LEVEL_FLAG_BIT = 6
1254
+ ORDERED_FLAG_BIT = 7
1255
+
1256
+ @requireBytes(1, 2)
1257
+ def __init__(self, id=TOC_FID, element_id=None, toplevel=True, ordered=True,
1258
+ child_ids=None, description=None):
1259
+ if id != TOC_FID:
1260
+ raise ValueError(f"Unexpected frame ID: {id}")
1261
+ super().__init__(id)
1262
+
1263
+ self.element_id = element_id
1264
+ self.toplevel = toplevel
1265
+ self.ordered = ordered
1266
+ self.child_ids = child_ids or []
1267
+ self.description = description
1268
+
1269
+ def parse(self, data, frame_header):
1270
+ super().parse(data, frame_header)
1271
+
1272
+ data = self.data
1273
+ log.debug("CTOC frame data size: %d" % len(data))
1274
+
1275
+ null_byte = data.find(b'\x00')
1276
+ self.element_id = data[0:null_byte]
1277
+ data = data[null_byte + 1:]
1278
+
1279
+ flag_bits = bytes2bin(data[0:1])
1280
+ self.toplevel = bool(flag_bits[self.TOP_LEVEL_FLAG_BIT])
1281
+ self.ordered = bool(flag_bits[self.ORDERED_FLAG_BIT])
1282
+ entry_count = bytes2dec(data[1:2])
1283
+ data = data[2:]
1284
+
1285
+ self.child_ids = []
1286
+ for _ in range(entry_count):
1287
+ null_byte = data.find(b'\x00')
1288
+ self.child_ids.append(data[:null_byte])
1289
+ data = data[null_byte + 1:]
1290
+
1291
+ # Any data remaining must be a TIT2 frame
1292
+ self.description = None
1293
+ if data and data[:4] != b"TIT2":
1294
+ log.warning("Invalid toc data, TIT2 frame expected")
1295
+ return
1296
+ elif data:
1297
+ data = BytesIO(data)
1298
+ frame_header = FrameHeader.parse(data, self.header.version)
1299
+ data = data.read()
1300
+ description_frame = TextFrame(TITLE_FID)
1301
+ description_frame.parse(data, frame_header)
1302
+
1303
+ self.description = description_frame.text
1304
+
1305
+ def render(self):
1306
+ flags = [0] * 8
1307
+ if self.toplevel:
1308
+ flags[self.TOP_LEVEL_FLAG_BIT] = 1
1309
+ if self.ordered:
1310
+ flags[self.ORDERED_FLAG_BIT] = 1
1311
+
1312
+ data = (self.element_id + b'\x00' +
1313
+ bin2bytes(flags) + dec2bytes(len(self.child_ids)))
1314
+
1315
+ for cid in self.child_ids:
1316
+ data += cid + b'\x00'
1317
+
1318
+ if self.description is not None:
1319
+ desc_frame = TextFrame(TITLE_FID, self.description)
1320
+ desc_frame.header = FrameHeader(TITLE_FID, self.header.version)
1321
+ data += desc_frame.render()
1322
+
1323
+ self.data = data
1324
+ return super().render()
1325
+
1326
+
1327
+ class RelVolAdjFrameV24(Frame):
1328
+ CHANNEL_TYPE_OTHER = 0
1329
+ CHANNEL_TYPE_MASTER = 1
1330
+ CHANNEL_TYPE_FRONT_RIGHT = 2
1331
+ CHANNEL_TYPE_FRONT_LEFT = 3
1332
+ CHANNEL_TYPE_BACK_RIGHT = 4
1333
+ CHANNEL_TYPE_BACK_LEFT = 5
1334
+ CHANNEL_TYPE_FRONT_CENTER = 6
1335
+ CHANNEL_TYPE_BACK_CENTER = 7
1336
+ CHANNEL_TYPE_BASS = 8
1337
+ _render_strict = False
1338
+
1339
+ @property
1340
+ def identifier(self):
1341
+ return str(self._identifier, "latin1")
1342
+
1343
+ @identifier.setter
1344
+ def identifier(self, ident):
1345
+ if type(ident) is not bytes:
1346
+ ident = ident.encode("latin1")
1347
+ self._identifier = ident
1348
+
1349
+ @property
1350
+ def channel_type(self):
1351
+ return self._channel_type
1352
+
1353
+ @channel_type.setter
1354
+ def channel_type(self, t):
1355
+ if 0 <= t <= 8:
1356
+ self._channel_type = t
1357
+ else:
1358
+ raise ValueError(f"Invalid type {t}")
1359
+
1360
+ @property
1361
+ def adjustment(self):
1362
+ return (self._adjustment or 0) / 512
1363
+
1364
+ @adjustment.setter
1365
+ def adjustment(self, adj):
1366
+ self._adjustment = adj * 512
1367
+
1368
+ @property
1369
+ def peak(self):
1370
+ return self._peak
1371
+
1372
+ @peak.setter
1373
+ def peak(self, v):
1374
+ self._peak = v
1375
+
1376
+ def __init__(self, fid=b"RVA2", identifier=None, channel_type=None, adjustment=None, peak=None):
1377
+ if fid != b"RVA2":
1378
+ raise ValueError(f"Unexpected frame ID: {fid}")
1379
+ super().__init__(fid)
1380
+
1381
+ self.identifier = identifier or ""
1382
+ self.channel_type = channel_type or self.CHANNEL_TYPE_OTHER
1383
+ self.adjustment = adjustment or 0
1384
+ self.peak = peak or 0
1385
+
1386
+ def parse(self, data, frame_header):
1387
+ super().parse(data, frame_header)
1388
+ if self.header.version != ID3_V2_4:
1389
+ raise FrameException(f"Invalid frame version: {self.header.version}")
1390
+ elif not data:
1391
+ raise FrameException("Invalid frame data: empty")
1392
+
1393
+ data = self.data
1394
+
1395
+ self.identifier, data = data.split(b"\x00", maxsplit=1)
1396
+ if not data:
1397
+ raise FrameException("Invalid frame data: no channel type")
1398
+ self.channel_type = data[0]
1399
+ self._adjustment = bytes2signedInt16(data[1:3])
1400
+ if len(data) > 3:
1401
+ bits_per_peak = data[3]
1402
+ if bits_per_peak:
1403
+ self._peak = bytes2dec(data[4:4 + (bits_per_peak // 8)])
1404
+
1405
+ log.debug(f"Parsed RVA2: identifier={self.identifier} channel_type={self.channel_type} "
1406
+ f"adjustment={self.adjustment} _adjustment={self._adjustment} peak={self.peak}")
1407
+
1408
+ def render(self):
1409
+ if self._channel_type is None:
1410
+ raise ValueError("No channel type")
1411
+ if self.header is None:
1412
+ self.header = FrameHeader(self.id, ID3_V2_4)
1413
+ if self.header.version != ID3_V2_4:
1414
+ raise ValueError("Value is not 2.4")
1415
+
1416
+ self.data =\
1417
+ self._identifier + b"\x00" +\
1418
+ dec2bytes(self._channel_type) +\
1419
+ signedInt162bytes(self._adjustment or 0)
1420
+
1421
+ if self._peak:
1422
+ peak_data = b""
1423
+ num_pk_bits = len(dec2bin(self._peak))
1424
+ for sz in (8, 16, 32):
1425
+ if num_pk_bits > sz:
1426
+ continue
1427
+ peak_data += dec2bytes(sz, 8) + dec2bytes(self._peak, sz)
1428
+ break
1429
+
1430
+ if not peak_data:
1431
+ raise ValueError(f"Peak value out of range: {self._peak}")
1432
+ self.data += peak_data
1433
+
1434
+ return super().render()
1435
+
1436
+
1437
+ class RelVolAdjFrameV23(Frame):
1438
+ FRONT_CHANNEL_RIGHT_BIT = 0
1439
+ FRONT_CHANNEL_LEFT_BIT = 1
1440
+ BACK_CHANNEL_RIGHT_BIT = 2
1441
+ BACK_CHANNEL_LEFT_BIT = 3
1442
+ FRONT_CENTER_CHANNEL_BIT = 4
1443
+ BASS_CHANNEL_BIT = 5
1444
+
1445
+ CHANNEL_DEFN = [("front_right", FRONT_CHANNEL_RIGHT_BIT),
1446
+ ("front_left", FRONT_CHANNEL_LEFT_BIT),
1447
+ ("front_right_peak", None),
1448
+ ("front_left_peak", None),
1449
+ ("back_right", BACK_CHANNEL_RIGHT_BIT),
1450
+ ("back_left", BACK_CHANNEL_LEFT_BIT),
1451
+ ("back_right_peak", None),
1452
+ ("back_left_peak", None),
1453
+ ("front_center", FRONT_CENTER_CHANNEL_BIT),
1454
+ ("front_center_peak", None),
1455
+ ("bass", BASS_CHANNEL_BIT),
1456
+ ("bass_peak", None),
1457
+ ]
1458
+
1459
+ @dataclasses.dataclass
1460
+ class VolumeAdjustments:
1461
+ master: int = 0
1462
+ master_peak: int = 0
1463
+
1464
+ front_right: int = 0
1465
+ front_left: int = 0
1466
+ front_right_peak: int = 0
1467
+ front_left_peak: int = 0
1468
+
1469
+ back_right: int = 0
1470
+ back_left: int = 0
1471
+ back_right_peak: int = 0
1472
+ back_left_peak: int = 0
1473
+
1474
+ front_center: int = 0
1475
+ front_center_peak: int = 0
1476
+
1477
+ back_center: int = 0
1478
+ back_center_peak: int = 0
1479
+
1480
+ bass: int = 0
1481
+ bass_peak: int = 0
1482
+
1483
+ other: int = 0
1484
+ other_peak: int = 0
1485
+
1486
+ _channel_map = {
1487
+ RelVolAdjFrameV24.CHANNEL_TYPE_MASTER: "master",
1488
+ RelVolAdjFrameV24.CHANNEL_TYPE_OTHER: "other",
1489
+ RelVolAdjFrameV24.CHANNEL_TYPE_FRONT_RIGHT: "front_right",
1490
+ RelVolAdjFrameV24.CHANNEL_TYPE_FRONT_LEFT: "front_left",
1491
+ RelVolAdjFrameV24.CHANNEL_TYPE_BACK_RIGHT: "back_right",
1492
+ RelVolAdjFrameV24.CHANNEL_TYPE_BACK_LEFT: "back_left",
1493
+ RelVolAdjFrameV24.CHANNEL_TYPE_FRONT_CENTER: "front_center",
1494
+ RelVolAdjFrameV24.CHANNEL_TYPE_BACK_CENTER: "back_center",
1495
+ RelVolAdjFrameV24.CHANNEL_TYPE_BASS: "bass",
1496
+ }
1497
+
1498
+ @property
1499
+ def has_master_channel(self) -> bool:
1500
+ return bool(self.master or self.master_peak)
1501
+
1502
+ @property
1503
+ def has_front_channel(self) -> bool:
1504
+ return bool(
1505
+ self.front_right or self.front_left or self.front_right_peak or self.front_left_peak
1506
+ )
1507
+
1508
+ @property
1509
+ def has_back_channel(self) -> bool:
1510
+ return bool(
1511
+ self.back_right or self.back_left or self.back_right_peak or self.back_left_peak
1512
+ )
1513
+
1514
+ @property
1515
+ def has_front_center_channel(self) -> bool:
1516
+ return bool(self.front_center or self.front_center_peak)
1517
+
1518
+ @property
1519
+ def has_back_center_channel(self) -> bool:
1520
+ return bool(self.back_center or self.back_center_peak)
1521
+
1522
+ @property
1523
+ def has_bass_channel(self) -> bool:
1524
+ return bool(self.bass or self.bass_peak)
1525
+
1526
+ @property
1527
+ def has_other_channel(self) -> bool:
1528
+ return bool(self.other or self.other_peak)
1529
+
1530
+ def boundsCheck(self):
1531
+ invalids = []
1532
+ for name, value in dataclasses.asdict(self).items():
1533
+
1534
+ if value > 65536 or value < -65536:
1535
+ invalids.append(name)
1536
+ if invalids:
1537
+ raise ValueError(f"Invalid RVAD channel values: {','.join(invalids)}")
1538
+
1539
+ def setChannelAdj(self, chan_type, value):
1540
+ setattr(self, self._channel_map[chan_type], value)
1541
+
1542
+ def setChannelPeak(self, chan_type, value):
1543
+ setattr(self, f"{self._channel_map[chan_type]}_peak", value)
1544
+
1545
+ def __init__(self, fid=b"RVAD"):
1546
+ if fid != b"RVAD":
1547
+ raise ValueError(f"Unexpected frame ID: {fid}")
1548
+ super().__init__(fid)
1549
+ self.adjustments = None
1550
+
1551
+ def toV24(self) -> list:
1552
+ """Return a list of RVA2 frames"""
1553
+ converted = []
1554
+
1555
+ def append(ch_type, ch_adj, ch_peak):
1556
+ if not ch_adj and not ch_peak:
1557
+ return
1558
+ converted.append(
1559
+ RelVolAdjFrameV24(channel_type=ch_type, adjustment=ch_adj / 512, peak=ch_peak)
1560
+ )
1561
+
1562
+ for channel in ["front_right", "front_left", "back_right", "back_left",
1563
+ "front_center", "bass"]:
1564
+ chtype = getattr(RelVolAdjFrameV24, f"CHANNEL_TYPE_{channel.upper()}")
1565
+ adj = getattr(self.adjustments, channel)
1566
+ pk = getattr(self.adjustments, f"{channel}_peak")
1567
+
1568
+ append(chtype, adj, pk)
1569
+
1570
+ return converted
1571
+
1572
+ def parse(self, data, frame_header):
1573
+ super().parse(data, frame_header)
1574
+ if self.header.version not in (ID3_V2_3, ID3_V2_2):
1575
+ raise FrameException("Invalid v2.4 frame: RVAD")
1576
+ data = self.data
1577
+
1578
+ inc_dec_bit_list = bytes2bin(bytes([data[0]]))
1579
+ inc_dec_bit_list.reverse()
1580
+ bytes_per_vol = data[1] // 8
1581
+ if bytes_per_vol > 2:
1582
+ raise FrameException("RVAD volume adj out of bounds")
1583
+
1584
+ self.adjustments = self.VolumeAdjustments()
1585
+ offset = 2
1586
+ for adj_name, inc_dec_bit in self.CHANNEL_DEFN:
1587
+ if offset >= len(data):
1588
+ break
1589
+
1590
+ adj_val = bytes2dec(data[offset:offset + bytes_per_vol])
1591
+ offset += bytes_per_vol
1592
+
1593
+ if (inc_dec_bit is not None
1594
+ and adj_val
1595
+ and inc_dec_bit_list[inc_dec_bit] == 0):
1596
+ # Decrement
1597
+ adj_val = -adj_val
1598
+
1599
+ setattr(self.adjustments, adj_name, adj_val)
1600
+
1601
+ try:
1602
+ log.debug(f"Parsed RVAD frames adjustments: {self.adjustments}")
1603
+ self.adjustments.boundsCheck()
1604
+ except ValueError: # pragma: nocover
1605
+ self.adjustments = None
1606
+ raise
1607
+
1608
+ def render(self):
1609
+ data = b""
1610
+ inc_dec_bits = [0] * 8
1611
+
1612
+ if self.header is None:
1613
+ self.header = FrameHeader(self.id, ID3_V2_3)
1614
+ if self.header.version != ID3_V2_3:
1615
+ raise ValueError("Version is not 2.3")
1616
+
1617
+ self.adjustments.boundsCheck() # May raise ValueError
1618
+
1619
+ # Only the front channel is required
1620
+ inc_dec_bits[self.FRONT_CHANNEL_RIGHT_BIT] = 1 if self.adjustments.front_right > 0 else 0
1621
+ inc_dec_bits[self.FRONT_CHANNEL_LEFT_BIT] = 1 if self.adjustments.front_left > 0 else 0
1622
+ data += dec2bytes(abs(self.adjustments.front_right), p=16)
1623
+ data += dec2bytes(abs(self.adjustments.front_left), p=16)
1624
+ data += dec2bytes(abs(self.adjustments.front_right_peak), p=16)
1625
+ data += dec2bytes(abs(self.adjustments.front_left_peak), p=16)
1626
+
1627
+ # Back channel
1628
+ if True in (self.adjustments.has_bass_channel, self.adjustments.has_front_center_channel,
1629
+ self.adjustments.has_back_channel):
1630
+ inc_dec_bits[self.BACK_CHANNEL_RIGHT_BIT] = 1 if self.adjustments.back_right > 0 else 0
1631
+ inc_dec_bits[self.BACK_CHANNEL_LEFT_BIT] = 1 if self.adjustments.back_left > 0 else 0
1632
+ data += dec2bytes(abs(self.adjustments.back_right), p=16)
1633
+ data += dec2bytes(abs(self.adjustments.back_left), p=16)
1634
+ data += dec2bytes(abs(self.adjustments.back_right_peak), p=16)
1635
+ data += dec2bytes(abs(self.adjustments.back_left_peak), p=16)
1636
+
1637
+ # Center (front) channel
1638
+ if True in (self.adjustments.has_bass_channel, self.adjustments.has_front_center_channel):
1639
+ inc_dec_bits[self.FRONT_CENTER_CHANNEL_BIT] = 1 if self.adjustments.front_center > 0 \
1640
+ else 0
1641
+ data += dec2bytes(abs(self.adjustments.front_center), p=16)
1642
+ data += dec2bytes(abs(self.adjustments.front_center_peak), p=16)
1643
+
1644
+ # Bass channel
1645
+ if self.adjustments.has_bass_channel:
1646
+ inc_dec_bits[self.BASS_CHANNEL_BIT] = 1 if self.adjustments.bass > 0 else 0
1647
+ data += dec2bytes(abs(self.adjustments.bass), p=16)
1648
+ data += dec2bytes(abs(self.adjustments.bass_peak), p=16)
1649
+
1650
+ self.data = bin2bytes(reversed(inc_dec_bits)) + b"\x10" + data
1651
+ return super().render()
1652
+
1653
+
1654
+ StartEndTuple = namedtuple("StartEndTuple", ["start", "end"])
1655
+ """A 2-tuple, with names 'start' and 'end'."""
1656
+
1657
+
1658
+ class ChapterFrame(Frame):
1659
+ """Frame type for chapter/section of the audio file.
1660
+ <ID3v2.3 or ID3v2.4 frame header, ID: "CHAP"> (10 bytes)
1661
+ Element ID <text string> $00
1662
+ Start time $xx xx xx xx
1663
+ End time $xx xx xx xx
1664
+ Start offset $xx xx xx xx
1665
+ End offset $xx xx xx xx
1666
+ <Optional embedded sub-frames>
1667
+ """
1668
+
1669
+ NO_OFFSET = 4294967295
1670
+ """No offset value, aka '0xff0xff0xff0xff'"""
1671
+
1672
+ def __init__(self, id=CHAPTER_FID, element_id=None, times=None,
1673
+ offsets=None, sub_frames=None):
1674
+ if id != CHAPTER_FID:
1675
+ raise ValueError(f"Unexpected frame ID: {id}")
1676
+ super(ChapterFrame, self).__init__(id)
1677
+ self.element_id = element_id
1678
+ self.times = times or StartEndTuple(None, None)
1679
+ self.offsets = offsets or StartEndTuple(None, None)
1680
+ self.sub_frames = sub_frames or FrameSet()
1681
+
1682
+ def parse(self, data, frame_header):
1683
+ from .headers import TagHeader, ExtendedTagHeader
1684
+
1685
+ super().parse(data, frame_header)
1686
+
1687
+ data = self.data
1688
+ log.debug("CTOC frame data size: %d" % len(data))
1689
+
1690
+ null_byte = data.find(b'\x00')
1691
+ self.element_id = data[0:null_byte]
1692
+ data = data[null_byte + 1:]
1693
+
1694
+ start = bytes2dec(data[:4])
1695
+ data = data[4:]
1696
+ end = bytes2dec(data[:4])
1697
+ data = data[4:]
1698
+ self.times = StartEndTuple(start, end)
1699
+
1700
+ start = bytes2dec(data[:4])
1701
+ data = data[4:]
1702
+ end = bytes2dec(data[:4])
1703
+ data = data[4:]
1704
+ self.offsets = StartEndTuple(start if start != self.NO_OFFSET else None,
1705
+ end if end != self.NO_OFFSET else None)
1706
+
1707
+ if data:
1708
+ dummy_tag_header = TagHeader(self.header.version)
1709
+ dummy_tag_header.tag_size = len(data)
1710
+ _ = self.sub_frames.parse(BytesIO(data), dummy_tag_header, # noqa
1711
+ ExtendedTagHeader())
1712
+ else:
1713
+ self.sub_frames = FrameSet()
1714
+
1715
+ def render(self):
1716
+ data = self.element_id + b'\x00'
1717
+
1718
+ for n in self.times + self.offsets:
1719
+ if n is not None:
1720
+ data += dec2bytes(n, 32)
1721
+ else:
1722
+ data += b'\xff\xff\xff\xff'
1723
+
1724
+ for f in self.sub_frames.getAllFrames():
1725
+ f.header = FrameHeader(f.id, self.header.version)
1726
+ data += f.render()
1727
+
1728
+ self.data = data
1729
+ return super(ChapterFrame, self).render()
1730
+
1731
+ @property
1732
+ def title(self):
1733
+ if TITLE_FID in self.sub_frames:
1734
+ return self.sub_frames[TITLE_FID][0].text
1735
+ return None
1736
+
1737
+ @title.setter
1738
+ def title(self, title):
1739
+ self.sub_frames.setTextFrame(TITLE_FID, title)
1740
+
1741
+ @property
1742
+ def subtitle(self):
1743
+ if SUBTITLE_FID in self.sub_frames:
1744
+ return self.sub_frames[SUBTITLE_FID][0].text
1745
+ return None
1746
+
1747
+ @subtitle.setter
1748
+ def subtitle(self, subtitle):
1749
+ self.sub_frames.setTextFrame(SUBTITLE_FID, subtitle)
1750
+
1751
+ @property
1752
+ def user_url(self):
1753
+ if USERURL_FID in self.sub_frames:
1754
+ frame = self.sub_frames[USERURL_FID][0]
1755
+ # Not returning frame description, it is always the same since it
1756
+ # allows only 1 URL.
1757
+ return frame.url
1758
+ return None
1759
+
1760
+ @user_url.setter
1761
+ def user_url(self, url):
1762
+ DESCRIPTION = "chapter url"
1763
+
1764
+ if url is None:
1765
+ del self.sub_frames[USERURL_FID]
1766
+ else:
1767
+ if USERURL_FID in self.sub_frames:
1768
+ for frame in self.sub_frames[USERURL_FID]:
1769
+ if frame.description == DESCRIPTION:
1770
+ frame.url = url
1771
+ return
1772
+
1773
+ self.sub_frames[USERURL_FID] = UserUrlFrame(USERURL_FID,
1774
+ DESCRIPTION, url)
1775
+
1776
+
1777
+ # XXX: This data structure pretty much sucks, or it is beautiful anarchy
1778
+ class FrameSet(dict):
1779
+ def __init__(self):
1780
+ dict.__init__(self)
1781
+ self._unknown_frame_ids = set()
1782
+
1783
+ def parse(self, f, tag_header, extended_header):
1784
+ """Read frames starting from the current read position of the file
1785
+ object. Returns the amount of padding which occurs after the tag, but
1786
+ before the audio content. A return value of 0 does not mean error."""
1787
+ self.clear()
1788
+ self._unknown_frame_ids.clear()
1789
+
1790
+ padding_size = 0
1791
+ size_left = tag_header.tag_size - extended_header.size
1792
+ consumed_size = 0
1793
+
1794
+ # Handle a tag-level unsync. Some frames may have their own unsync bit
1795
+ # set instead.
1796
+ tag_data = f.read(size_left)
1797
+
1798
+ # If the tag is 2.3 and the tag header unsync bit is set then all the
1799
+ # frame data is deunsync'd at once, otherwise it will happen on a per-frame basis.
1800
+ if tag_header.unsync and tag_header.version <= ID3_V2_3:
1801
+ log.debug("De-unsynching %d bytes at once (<= 2.3 tag)" %
1802
+ len(tag_data))
1803
+ og_size = len(tag_data)
1804
+ tag_data = deunsyncData(tag_data)
1805
+ size_left = len(tag_data)
1806
+ log.debug("De-unsynch'd %d bytes at once (<= 2.3 tag) to %d bytes" %
1807
+ (og_size, size_left))
1808
+
1809
+ # Adding bytes to simulate the tag header(s) in the buffer. This keeps
1810
+ # f.tell() values matching the file offsets for logging.
1811
+ prepadding = b'\x00' * 10 # Tag header
1812
+ prepadding += b'\x00' * extended_header.size
1813
+ tag_buffer = BytesIO(prepadding + tag_data)
1814
+ tag_buffer.seek(len(prepadding))
1815
+
1816
+ frame_count = 0
1817
+ while size_left > 0:
1818
+ log.debug("size_left: " + str(size_left))
1819
+ if size_left < (10 + 1): # The size of the smallest frame.
1820
+ log.debug("FrameSet: Implied padding (size_left<minFrameSize)")
1821
+ padding_size = size_left
1822
+ break
1823
+
1824
+ log.debug("+++++++++++++++++++++++++++++++++++++++++++++++++")
1825
+ log.debug("FrameSet: Reading Frame #" + str(frame_count + 1))
1826
+ frame_header = FrameHeader.parse(tag_buffer, tag_header.version)
1827
+ if not frame_header:
1828
+ log.debug("No frame found, implied padding of %d bytes" %
1829
+ size_left)
1830
+ padding_size = size_left
1831
+ break
1832
+
1833
+ # Frame data.
1834
+ if frame_header.data_size:
1835
+ log.debug("FrameSet: Reading %d (0x%X) bytes of data from byte "
1836
+ "pos %d (0x%X)" % (frame_header.data_size,
1837
+ frame_header.data_size,
1838
+ tag_buffer.tell(),
1839
+ tag_buffer.tell()))
1840
+ data = tag_buffer.read(frame_header.data_size)
1841
+
1842
+ log.debug("FrameSet: %d bytes of data read" % len(data))
1843
+ consumed_size += (frame_header.size +
1844
+ frame_header.data_size)
1845
+ try:
1846
+ frame = createFrame(tag_header, frame_header, data)
1847
+ except FrameException as frame_ex:
1848
+ log.warning(f"Frame error: {frame_ex}")
1849
+ else:
1850
+ self[frame.id] = frame
1851
+ frame_count += 1
1852
+ if frame.unknown:
1853
+ self._unknown_frame_ids.add(frame.id)
1854
+
1855
+ # Each frame contains data_size + headerSize bytes.
1856
+ size_left -= (frame_header.size +
1857
+ frame_header.data_size)
1858
+
1859
+ return padding_size
1860
+
1861
+ @requireBytes(1)
1862
+ def __getitem__(self, fid):
1863
+ if fid in self:
1864
+ return dict.__getitem__(self, fid)
1865
+ else:
1866
+ return None
1867
+
1868
+ @requireBytes(1)
1869
+ def __setitem__(self, fid, frame):
1870
+ if fid != frame.id:
1871
+ raise ValueError(f"Frame ID mismatch: {fid} != {frame.id}")
1872
+
1873
+ if fid in self:
1874
+ self[fid].append(frame)
1875
+ else:
1876
+ dict.__setitem__(self, fid, [frame])
1877
+
1878
+ @property
1879
+ def unknown_frame_ids(self):
1880
+ return self._unknown_frame_ids
1881
+
1882
+ def getAllFrames(self):
1883
+ """Return all the frames in the set as a list. The list is sorted
1884
+ in an arbitrary but consistent order."""
1885
+ frames = []
1886
+ for flist in list(self.values()):
1887
+ frames += flist
1888
+ frames.sort()
1889
+ return frames
1890
+
1891
+ @requireBytes(1)
1892
+ @requireUnicode(2)
1893
+ def setTextFrame(self, fid, text):
1894
+ """Set a text frame value.
1895
+ Text frame IDs must be unique. If a frame with
1896
+ the same Id is already in the list it's value is changed, otherwise
1897
+ the frame is added.
1898
+ """
1899
+ if fid not in ID3_FRAMES and fid not in NONSTANDARD_ID3_FRAMES:
1900
+ raise ValueError(f"Invalid frame ID: {fid}")
1901
+
1902
+ if fid in self:
1903
+ self[fid][0].text = text
1904
+ else:
1905
+ if fid in (DATE_FIDS + DEPRECATED_DATE_FIDS):
1906
+ self[fid] = DateFrame(fid, date=text)
1907
+ else:
1908
+ self[fid] = TextFrame(fid, text=text)
1909
+
1910
+ @requireBytes(1)
1911
+ def __contains__(self, fid):
1912
+ return dict.__contains__(self, fid)
1913
+
1914
+
1915
+ def deunsyncData(data):
1916
+ output = []
1917
+ safe = True
1918
+ for val in [bytes([b]) for b in data]:
1919
+ if safe:
1920
+ output.append(val)
1921
+ safe = (val != b'\xff')
1922
+ else:
1923
+ if val != b'\x00':
1924
+ output.append(val)
1925
+ safe = True
1926
+ return b''.join(output)
1927
+
1928
+
1929
+ # Create and return the appropriate frame.
1930
+ def createFrame(tag_header, frame_header, data):
1931
+ fid = frame_header.id
1932
+ if fid in ID3_FRAMES:
1933
+ (desc, ver, FrameClass) = ID3_FRAMES[fid]
1934
+ elif fid in NONSTANDARD_ID3_FRAMES:
1935
+ log.verbose("Non standard frame '%s' encountered" % fid)
1936
+ (desc, ver, FrameClass) = NONSTANDARD_ID3_FRAMES[fid]
1937
+ else:
1938
+ log.warning(f"Unknown ID3 frame ID: {fid}")
1939
+ (desc, ver, FrameClass) = ("Unknown", None, UnknownFrame)
1940
+ log.debug(f"createFrame (desc:{desc}) - {ver} - {FrameClass}")
1941
+
1942
+ # FrameClass may still be None if the frame is standard but does not
1943
+ # yet have a concrete type.
1944
+ if not FrameClass:
1945
+ log.warning(f"Frame '{fid.decode('ascii')}' is not yet supported, using raw Frame to parse")
1946
+ FrameClass = Frame
1947
+
1948
+ log.debug(f"createFrame '{fid}' with class '{FrameClass}'")
1949
+ if tag_header.version[:2] == ID3_V2_4 and tag_header.unsync:
1950
+ frame_header.unsync = True
1951
+
1952
+ frame = FrameClass(fid)
1953
+ frame.parse(data, frame_header)
1954
+ return frame
1955
+
1956
+
1957
+ def decodeUnicode(bites, encoding):
1958
+ for obj, obj_name in ((bites, "bites"), (encoding, "encoding")):
1959
+ if not isinstance(obj, bytes):
1960
+ raise TypeError("%s argument must be a byte string." % obj_name)
1961
+
1962
+ codec = id3EncodingToString(encoding)
1963
+ log.debug("Unicode encoding: %s" % codec)
1964
+ if (codec.startswith("utf_16") and
1965
+ len(bites) % 2 != 0 and bites[-1:] == b"\x00"):
1966
+ # Catch and fix bad utf16 data, it is everywhere.
1967
+ log.warning("Fixing utf16 data with extra zero bytes")
1968
+ bites = bites[:-1]
1969
+ return str(bites, codec).rstrip("\x00")
1970
+
1971
+
1972
+ def splitUnicode(data, encoding):
1973
+ try:
1974
+ if encoding == LATIN1_ENCODING or encoding == UTF_8_ENCODING:
1975
+ (d, t) = data.split(b"\x00", 1)
1976
+ elif encoding == UTF_16_ENCODING or encoding == UTF_16BE_ENCODING:
1977
+ # Two null bytes split, but since each utf16 char is also two
1978
+ # bytes we need to ensure we found a proper boundary.
1979
+ (d, t) = data.split(b"\x00\x00", 1)
1980
+ if (len(d) % 2) != 0:
1981
+ (d, t) = data.split(b"\x00\x00\x00", 1)
1982
+ d += b"\x00"
1983
+ else:
1984
+ raise NotImplementedError(f"Unknown ID3 encoding: {encoding}")
1985
+ except ValueError as ex:
1986
+ log.warning(f"Invalid 2-tuple ID3 frame data: {ex}")
1987
+ d, t = data, b""
1988
+
1989
+ return d, t
1990
+
1991
+
1992
+ def id3EncodingToString(encoding):
1993
+ if not isinstance(encoding, bytes):
1994
+ raise TypeError("encoding argument must be a byte string.")
1995
+
1996
+ if encoding == LATIN1_ENCODING:
1997
+ return "latin_1"
1998
+ elif encoding == UTF_8_ENCODING:
1999
+ return "utf_8"
2000
+ elif encoding == UTF_16_ENCODING:
2001
+ return "utf_16"
2002
+ elif encoding == UTF_16BE_ENCODING:
2003
+ return "utf_16_be"
2004
+ else:
2005
+ raise ValueError("Encoding unknown: %s" % encoding)
2006
+
2007
+
2008
+ def stringToEncoding(s):
2009
+ s = s.replace('-', '_')
2010
+ if s in ("latin_1", "latin1"):
2011
+ return LATIN1_ENCODING
2012
+ elif s in ("utf_8", "utf8"):
2013
+ return UTF_8_ENCODING
2014
+ elif s in ("utf_16", "utf16"):
2015
+ return UTF_16_ENCODING
2016
+ elif s in ("utf_16_be", "utf16_be"):
2017
+ return UTF_16BE_ENCODING
2018
+ else:
2019
+ raise ValueError("Encoding unknown: %s" % s)
2020
+
2021
+
2022
+ # { frame-id : (frame-description, valid-id3-version, frame-class) }
2023
+ ID3_FRAMES = {b"AENC": ("Audio encryption",
2024
+ ID3_V2,
2025
+ None),
2026
+ b"APIC": ("Attached picture",
2027
+ ID3_V2,
2028
+ ImageFrame),
2029
+ b"ASPI": ("Audio seek point index",
2030
+ ID3_V2_4,
2031
+ None),
2032
+
2033
+ b"COMM": ("Comments", ID3_V2, CommentFrame),
2034
+ b"COMR": ("Commercial frame", ID3_V2, None),
2035
+
2036
+ b"CTOC": ("Table of contents", ID3_V2, TocFrame),
2037
+ b"CHAP": ("Chapter", ID3_V2, ChapterFrame),
2038
+
2039
+ b"ENCR": ("Encryption method registration", ID3_V2, None),
2040
+ b"EQUA": ("Equalisation", ID3_V2_3, None),
2041
+ b"EQU2": ("Equalisation (2)", ID3_V2_4, None),
2042
+ b"ETCO": ("Event timing codes", ID3_V2, None),
2043
+
2044
+ b"GEOB": ("General encapsulated object", ID3_V2, ObjectFrame),
2045
+ b"GRID": ("Group identification registration", ID3_V2, None),
2046
+
2047
+ b"IPLS": ("Involved people list", ID3_V2_3, None),
2048
+
2049
+ b"LINK": ("Linked information", ID3_V2, None),
2050
+
2051
+ b"MCDI": ("Music CD identifier", ID3_V2, MusicCDIdFrame),
2052
+ b"MLLT": ("MPEG location lookup table", ID3_V2, None),
2053
+
2054
+ b"OWNE": ("Ownership frame", ID3_V2, None),
2055
+
2056
+ b"PRIV": ("Private frame", ID3_V2, PrivateFrame),
2057
+ b"PCNT": ("Play counter", ID3_V2, PlayCountFrame),
2058
+ b"POPM": ("Popularimeter", ID3_V2, PopularityFrame),
2059
+ b"POSS": ("Position synchronisation frame", ID3_V2, None),
2060
+
2061
+ b"RBUF": ("Recommended buffer size", ID3_V2, None),
2062
+ b"RVAD": ("Relative volume adjustment", ID3_V2_3, RelVolAdjFrameV23),
2063
+ b"RVA2": ("Relative volume adjustment (2)", ID3_V2_4, RelVolAdjFrameV24),
2064
+ b"RVRB": ("Reverb", ID3_V2, None),
2065
+
2066
+ b"SEEK": ("Seek frame", ID3_V2_4, None),
2067
+ b"SIGN": ("Signature frame", ID3_V2_4, None),
2068
+ b"SYLT": ("Synchronised lyric/text", ID3_V2, None),
2069
+ b"SYTC": ("Synchronised tempo codes", ID3_V2, None),
2070
+
2071
+ b"TALB": ("Album/Movie/Show title", ID3_V2, TextFrame),
2072
+ b"TBPM": ("BPM (beats per minute)", ID3_V2, TextFrame),
2073
+ b"TCOM": ("Composer", ID3_V2, TextFrame),
2074
+ b"TCON": ("Content type", ID3_V2, TextFrame),
2075
+ b"TCOP": ("Copyright message", ID3_V2, TextFrame),
2076
+ b"TDAT": ("Date", ID3_V2_3, DateFrame),
2077
+ b"TDEN": ("Encoding time", ID3_V2_4, DateFrame),
2078
+ b"TDLY": ("Playlist delay", ID3_V2, TextFrame),
2079
+ b"TDOR": ("Original release time", ID3_V2_4, DateFrame),
2080
+ b"TDRC": ("Recording time", ID3_V2_4, DateFrame),
2081
+ b"TDRL": ("Release time", ID3_V2_4, DateFrame),
2082
+ b"TDTG": ("Tagging time", ID3_V2_4, DateFrame),
2083
+ b"TENC": ("Encoded by", ID3_V2, TextFrame),
2084
+ b"TEXT": ("Lyricist/Text writer", ID3_V2, TextFrame),
2085
+ b"TFLT": ("File type", ID3_V2, TextFrame),
2086
+ b"TIME": ("Time", ID3_V2_3, DateFrame),
2087
+ b"TIPL": ("Involved people list", ID3_V2_4, TextFrame),
2088
+ b"TIT1": ("Content group description", ID3_V2, TextFrame),
2089
+ b"TIT2": ("Title/songname/content description", ID3_V2,
2090
+ TextFrame),
2091
+ b"TIT3": ("Subtitle/Description refinement", ID3_V2, TextFrame),
2092
+ b"TKEY": ("Initial key", ID3_V2, TextFrame),
2093
+ b"TLAN": ("Language(s)", ID3_V2, TextFrame),
2094
+ b"TLEN": ("Length", ID3_V2, TextFrame),
2095
+ b"TMCL": ("Musician credits list", ID3_V2_4, TextFrame),
2096
+ b"TMED": ("Media type", ID3_V2, TextFrame),
2097
+ b"TMOO": ("Mood", ID3_V2_4, TextFrame),
2098
+ b"TOAL": ("Original album/movie/show title", ID3_V2, TextFrame),
2099
+ b"TOFN": ("Original filename", ID3_V2, TextFrame),
2100
+ b"TOLY": ("Original lyricist(s)/text writer(s)", ID3_V2,
2101
+ TextFrame),
2102
+ b"TOPE": ("Original artist(s)/performer(s)", ID3_V2, TextFrame),
2103
+ b"TORY": ("Original release year", ID3_V2_3, DateFrame),
2104
+ b"TOWN": ("File owner/licensee", ID3_V2, TextFrame),
2105
+ b"TPE1": ("Lead performer(s)/Soloist(s)", ID3_V2, TextFrame),
2106
+ b"TPE2": ("Band/orchestra/accompaniment", ID3_V2, TextFrame),
2107
+ b"TPE3": ("Conductor/performer refinement", ID3_V2, TextFrame),
2108
+ b"TPE4": ("Interpreted, remixed, or otherwise modified by",
2109
+ ID3_V2, TextFrame),
2110
+ b"TPOS": ("Part of a set", ID3_V2, TextFrame),
2111
+ b"TPRO": ("Produced notice", ID3_V2_4, TextFrame),
2112
+ b"TPUB": ("Publisher", ID3_V2, TextFrame),
2113
+ b"TRCK": ("Track number/Position in set", ID3_V2, TextFrame),
2114
+ b"TRDA": ("Recording dates", ID3_V2_3, DateFrame),
2115
+ b"TRSN": ("Internet radio station name", ID3_V2, TextFrame),
2116
+ b"TRSO": ("Internet radio station owner", ID3_V2, TextFrame),
2117
+ b"TSOA": ("Album sort order", ID3_V2_4, TextFrame),
2118
+ b"TSOP": ("Performer sort order", ID3_V2_4, TextFrame),
2119
+ b"TSOT": ("Title sort order", ID3_V2_4, TextFrame),
2120
+ b"TSIZ": ("Size", ID3_V2_3, TextFrame),
2121
+ b"TSRC": ("ISRC (international standard recording code)", ID3_V2,
2122
+ TextFrame),
2123
+ b"TSSE": ("Software/Hardware and settings used for encoding",
2124
+ ID3_V2, TextFrame),
2125
+ b"TSST": ("Set subtitle", ID3_V2_4, TextFrame),
2126
+ b"TYER": ("Year", ID3_V2_3, DateFrame),
2127
+ b"TXXX": ("User defined text information frame", ID3_V2,
2128
+ UserTextFrame),
2129
+
2130
+ b"UFID": ("Unique file identifier", ID3_V2, UniqueFileIDFrame),
2131
+ b"USER": ("Terms of use", ID3_V2, TermsOfUseFrame),
2132
+ b"USLT": ("Unsynchronised lyric/text transcription", ID3_V2,
2133
+ LyricsFrame),
2134
+
2135
+ b"WCOM": ("Commercial information", ID3_V2, UrlFrame),
2136
+ b"WCOP": ("Copyright/Legal information", ID3_V2, UrlFrame),
2137
+ b"WOAF": ("Official audio file webpage", ID3_V2, UrlFrame),
2138
+ b"WOAR": ("Official artist/performer webpage", ID3_V2, UrlFrame),
2139
+ b"WOAS": ("Official audio source webpage", ID3_V2, UrlFrame),
2140
+ b"WORS": ("Official Internet radio station homepage", ID3_V2,
2141
+ UrlFrame),
2142
+ b"WPAY": ("Payment", ID3_V2, UrlFrame),
2143
+ b"WPUB": ("Publishers official webpage", ID3_V2, UrlFrame),
2144
+ b"WXXX": ("User defined URL link frame", ID3_V2, UserUrlFrame),
2145
+ }
2146
+
2147
+
2148
+ def map2_2FrameId(orig_id):
2149
+ if orig_id not in TAGS2_2_TO_TAGS_2_3_AND_4:
2150
+ return orig_id
2151
+ return TAGS2_2_TO_TAGS_2_3_AND_4[orig_id]
2152
+
2153
+
2154
+ # mapping of 2.2 frames to 2.3/2.4
2155
+ TAGS2_2_TO_TAGS_2_3_AND_4 = {
2156
+ b"TT1": b"TIT1", # CONTENTGROUP content group description
2157
+ b"TT2": b"TIT2", # TITLE title/songname/content description
2158
+ b"TT3": b"TIT3", # SUBTITLE subtitle/description refinement
2159
+ b"TP1": b"TPE1", # ARTIST lead performer(s)/soloist(s)
2160
+ b"TP2": b"TPE2", # BAND band/orchestra/accompaniment
2161
+ b"TP3": b"TPE3", # CONDUCTOR conductor/performer refinement
2162
+ b"TP4": b"TPE4", # MIXARTIST interpreted, remixed, modified by
2163
+ b"TCM": b"TCOM", # COMPOSER composer
2164
+ b"TXT": b"TEXT", # LYRICIST lyricist/text writer
2165
+ b"TLA": b"TLAN", # LANGUAGE language(s)
2166
+ b"TCO": b"TCON", # CONTENTTYPE content type
2167
+ b"TAL": b"TALB", # ALBUM album/movie/show title
2168
+ b"TRK": b"TRCK", # TRACKNUM track number/position in set
2169
+ b"TPA": b"TPOS", # PARTINSET part of set
2170
+ b"TRC": b"TSRC", # ISRC international standard recording code
2171
+ b"TDA": b"TDAT", # DATE date
2172
+ b"TYE": b"TYER", # YEAR year
2173
+ b"TIM": b"TIME", # TIME time
2174
+ b"TRD": b"TRDA", # RECORDINGDATES recording dates
2175
+ b"TOR": b"TORY", # ORIGYEAR original release year
2176
+ b"TBP": b"TBPM", # BPM beats per minute
2177
+ b"TMT": b"TMED", # MEDIATYPE media type
2178
+ b"TFT": b"TFLT", # FILETYPE file type
2179
+ b"TCR": b"TCOP", # COPYRIGHT copyright message
2180
+ b"TPB": b"TPUB", # PUBLISHER publisher
2181
+ b"TEN": b"TENC", # ENCODEDBY encoded by
2182
+ b"TSS": b"TSSE", # ENCODERSETTINGS software/hardware+settings for encoding
2183
+ b"TLE": b"TLEN", # SONGLEN length (ms)
2184
+ b"TSI": b"TSIZ", # SIZE size (bytes)
2185
+ b"TDY": b"TDLY", # PLAYLISTDELAY playlist delay
2186
+ b"TKE": b"TKEY", # INITIALKEY initial key
2187
+ b"TOT": b"TOAL", # ORIGALBUM original album/movie/show title
2188
+ b"TOF": b"TOFN", # ORIGFILENAME original filename
2189
+ b"TOA": b"TOPE", # ORIGARTIST original artist(s)/performer(s)
2190
+ b"TOL": b"TOLY", # ORIGLYRICIST original lyricist(s)/text writer(s)
2191
+ b"TXX": b"TXXX", # USERTEXT user defined text information frame
2192
+ b"WAF": b"WOAF", # WWWAUDIOFILE official audio file webpage
2193
+ b"WAR": b"WOAR", # WWWARTIST official artist/performer webpage
2194
+ b"WAS": b"WOAS", # WWWAUDIOSOURCE official audion source webpage
2195
+ b"WCM": b"WCOM", # WWWCOMMERCIALINFO commercial information
2196
+ b"WCP": b"WCOP", # WWWCOPYRIGHT copyright/legal information
2197
+ b"WPB": b"WPUB", # WWWPUBLISHER publishers official webpage
2198
+ b"WXX": b"WXXX", # WWWUSER user defined URL link frame
2199
+ b"IPL": b"IPLS", # INVOLVEDPEOPLE involved people list
2200
+ b"ULT": b"USLT", # UNSYNCEDLYRICS unsynchronised lyrics/text transcription
2201
+ b"COM": b"COMM", # COMMENT comments
2202
+ b"UFI": b"UFID", # UNIQUEFILEID unique file identifier
2203
+ b"MCI": b"MCDI", # CDID music CD identifier
2204
+ b"ETC": b"ETCO", # EVENTTIMING event timing codes
2205
+ b"MLL": b"MLLT", # MPEGLOOKUP MPEG location lookup table
2206
+ b"STC": b"SYTC", # SYNCEDTEMPO synchronised tempo codes
2207
+ b"SLT": b"SYLT", # SYNCEDLYRICS synchronised lyrics/text
2208
+ b"RVA": b"RVAD", # VOLUMEADJ relative volume adjustment
2209
+ b"EQU": b"EQUA", # EQUALIZATION equalization
2210
+ b"REV": b"RVRB", # REVERB reverb
2211
+ b"PIC": b"APIC", # PICTURE attached picture
2212
+ b"GEO": b"GEOB", # GENERALOBJECT general encapsulated object
2213
+ b"CNT": b"PCNT", # PLAYCOUNTER play counter
2214
+ b"POP": b"POPM", # POPULARIMETER popularimeter
2215
+ b"BUF": b"RBUF", # BUFFERSIZE recommended buffer size
2216
+ b"CRA": b"AENC", # AUDIOCRYPTO audio encryption
2217
+ b"LNK": b"LINK", # LINKEDINFO linked information
2218
+ # Extension workarounds i.e., ignore them
2219
+ b"TCP": b"TCMP", # iTunes "extension" for compilation marking
2220
+ b"TST": b"TSOT", # iTunes "extension" for title sort
2221
+ b"TSP": b"TSOP", # iTunes "extension" for artist sort
2222
+ b"TSA": b"TSOA", # iTunes "extension" for album sort
2223
+ b"TS2": b"TSO2", # iTunes "extension" for album artist sort
2224
+ b"TSC": b"TSOC", # iTunes "extension" for composer sort
2225
+ b"TDR": b"TDRL", # iTunes "extension" for release date
2226
+ b"TDS": b"TDES", # iTunes "extension" for podcast description
2227
+ b"TID": b"TGID", # iTunes "extension" for podcast identifier
2228
+ b"WFD": b"WFED", # iTunes "extension" for podcast feed URL
2229
+ b"CM1": b"CM1 ", # Seems to be some script kiddie tagging the tag.
2230
+ # For example, [rH] join #rH on efnet [rH]
2231
+ b"PCS": b"PCST", # iTunes extension for podcast marking.
2232
+ }
2233
+
2234
+ from . import apple # noqa
2235
+ NONSTANDARD_ID3_FRAMES = {
2236
+ b"NCON": ("Undefined MusicMatch extension", ID3_V2, Frame),
2237
+ b"TCMP": ("iTunes compilation flag extension", ID3_V2, TextFrame),
2238
+ b"XSOA": ("Album sort-order string extension for v2.3",
2239
+ ID3_V2_3, TextFrame),
2240
+ b"XSOP": ("Performer sort-order string extension for v2.3",
2241
+ ID3_V2_3, TextFrame),
2242
+ b"XSOT": ("Title sort-order string extension for v2.3",
2243
+ ID3_V2_3, TextFrame),
2244
+ b"XDOR": ("MusicBrainz release date (full) extension for v2.3",
2245
+ ID3_V2_3, DateFrame),
2246
+
2247
+ b"TSO2": ("Album artist sort-order used in iTunes and Picard",
2248
+ ID3_V2, TextFrame),
2249
+ b"TSOC": ("Composer sort-order used in iTunes and Picard",
2250
+ ID3_V2, TextFrame),
2251
+
2252
+ b"PCST": ("iTunes extension; marks the file as a podcast",
2253
+ ID3_V2, apple.PCST),
2254
+ b"TKWD": ("iTunes extension; podcast keywords?",
2255
+ ID3_V2, apple.TKWD),
2256
+ b"TDES": ("iTunes extension; podcast description?",
2257
+ ID3_V2, apple.TDES),
2258
+ b"TGID": ("iTunes extension; podcast ?????",
2259
+ ID3_V2, apple.TGID),
2260
+ b"WFED": ("iTunes extension; podcast feed URL?",
2261
+ ID3_V2, apple.WFED),
2262
+ b"TCAT": ("iTunes extension; podcast category.",
2263
+ ID3_V2, TextFrame),
2264
+ b"GRP1": ("iTunes extension; grouping.",
2265
+ ID3_V2, apple.GRP1),
2266
+ b"MVNM": ("iTunes extension; movement name.",
2267
+ ID3_V2, apple.MVNM),
2268
+ b"MVIN": ("iTunes extension; movement index.",
2269
+ ID3_V2, apple.MVIN)
2270
+ }