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