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