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