1# -*- coding: utf-8 -*- 2# This file is part of beets. 3# Copyright 2016, Adrian Sampson. 4# 5# Permission is hereby granted, free of charge, to any person obtaining 6# a copy of this software and associated documentation files (the 7# "Software"), to deal in the Software without restriction, including 8# without limitation the rights to use, copy, modify, merge, publish, 9# distribute, sublicense, and/or sell copies of the Software, and to 10# permit persons to whom the Software is furnished to do so, subject to 11# the following conditions: 12# 13# The above copyright notice and this permission notice shall be 14# included in all copies or substantial portions of the Software. 15 16"""Handles low-level interfacing for files' tags. Wraps Mutagen to 17automatically detect file types and provide a unified interface for a 18useful subset of music files' tags. 19 20Usage: 21 22 >>> f = MediaFile('Lucy.mp3') 23 >>> f.title 24 u'Lucy in the Sky with Diamonds' 25 >>> f.artist = 'The Beatles' 26 >>> f.save() 27 28A field will always return a reasonable value of the correct type, even 29if no tag is present. If no value is available, the value will be false 30(e.g., zero or the empty string). 31 32Internally ``MediaFile`` uses ``MediaField`` descriptors to access the 33data from the tags. In turn ``MediaField`` uses a number of 34``StorageStyle`` strategies to handle format specific logic. 35""" 36from __future__ import division, absolute_import, print_function 37 38import mutagen 39import mutagen.id3 40import mutagen.mp4 41import mutagen.flac 42import mutagen.asf 43 44import codecs 45import datetime 46import re 47import base64 48import binascii 49import math 50import struct 51import imghdr 52import os 53import traceback 54import enum 55import logging 56import six 57 58 59__all__ = ['UnreadableFileError', 'FileTypeError', 'MediaFile'] 60 61log = logging.getLogger(__name__) 62 63# Human-readable type names. 64TYPES = { 65 'mp3': 'MP3', 66 'aac': 'AAC', 67 'alac': 'ALAC', 68 'ogg': 'OGG', 69 'opus': 'Opus', 70 'flac': 'FLAC', 71 'ape': 'APE', 72 'wv': 'WavPack', 73 'mpc': 'Musepack', 74 'asf': 'Windows Media', 75 'aiff': 'AIFF', 76 'dsf': 'DSD Stream File', 77} 78 79PREFERRED_IMAGE_EXTENSIONS = {'jpeg': 'jpg'} 80 81 82# Exceptions. 83 84class UnreadableFileError(Exception): 85 """Mutagen is not able to extract information from the file. 86 """ 87 def __init__(self, path, msg): 88 Exception.__init__(self, msg if msg else repr(path)) 89 90 91class FileTypeError(UnreadableFileError): 92 """Reading this type of file is not supported. 93 94 If passed the `mutagen_type` argument this indicates that the 95 mutagen type is not supported by `Mediafile`. 96 """ 97 def __init__(self, path, mutagen_type=None): 98 if mutagen_type is None: 99 msg = u'{0!r}: not in a recognized format'.format(path) 100 else: 101 msg = u'{0}: of mutagen type {1}'.format(repr(path), mutagen_type) 102 Exception.__init__(self, msg) 103 104 105class MutagenError(UnreadableFileError): 106 """Raised when Mutagen fails unexpectedly---probably due to a bug. 107 """ 108 def __init__(self, path, mutagen_exc): 109 msg = u'{0}: {1}'.format(repr(path), mutagen_exc) 110 Exception.__init__(self, msg) 111 112 113# Interacting with Mutagen. 114 115def mutagen_call(action, path, func, *args, **kwargs): 116 """Call a Mutagen function with appropriate error handling. 117 118 `action` is a string describing what the function is trying to do, 119 and `path` is the relevant filename. The rest of the arguments 120 describe the callable to invoke. 121 122 We require at least Mutagen 1.33, where `IOError` is *never* used, 123 neither for internal parsing errors *nor* for ordinary IO error 124 conditions such as a bad filename. Mutagen-specific parsing errors and IO 125 errors are reraised as `UnreadableFileError`. Other exceptions 126 raised inside Mutagen---i.e., bugs---are reraised as `MutagenError`. 127 """ 128 try: 129 return func(*args, **kwargs) 130 except mutagen.MutagenError as exc: 131 log.debug(u'%s failed: %s', action, six.text_type(exc)) 132 raise UnreadableFileError(path, six.text_type(exc)) 133 except Exception as exc: 134 # Isolate bugs in Mutagen. 135 log.debug(u'%s', traceback.format_exc()) 136 log.error(u'uncaught Mutagen exception in %s: %s', action, exc) 137 raise MutagenError(path, exc) 138 139 140# Utility. 141 142def _safe_cast(out_type, val): 143 """Try to covert val to out_type but never raise an exception. If 144 the value can't be converted, then a sensible default value is 145 returned. out_type should be bool, int, or unicode; otherwise, the 146 value is just passed through. 147 """ 148 if val is None: 149 return None 150 151 if out_type == int: 152 if isinstance(val, int) or isinstance(val, float): 153 # Just a number. 154 return int(val) 155 else: 156 # Process any other type as a string. 157 if isinstance(val, bytes): 158 val = val.decode('utf-8', 'ignore') 159 elif not isinstance(val, six.string_types): 160 val = six.text_type(val) 161 # Get a number from the front of the string. 162 match = re.match(r'[\+-]?[0-9]+', val.strip()) 163 return int(match.group(0)) if match else 0 164 165 elif out_type == bool: 166 try: 167 # Should work for strings, bools, ints: 168 return bool(int(val)) 169 except ValueError: 170 return False 171 172 elif out_type == six.text_type: 173 if isinstance(val, bytes): 174 return val.decode('utf-8', 'ignore') 175 elif isinstance(val, six.text_type): 176 return val 177 else: 178 return six.text_type(val) 179 180 elif out_type == float: 181 if isinstance(val, int) or isinstance(val, float): 182 return float(val) 183 else: 184 if isinstance(val, bytes): 185 val = val.decode('utf-8', 'ignore') 186 else: 187 val = six.text_type(val) 188 match = re.match(r'[\+-]?([0-9]+\.?[0-9]*|[0-9]*\.[0-9]+)', 189 val.strip()) 190 if match: 191 val = match.group(0) 192 if val: 193 return float(val) 194 return 0.0 195 196 else: 197 return val 198 199 200# Image coding for ASF/WMA. 201 202def _unpack_asf_image(data): 203 """Unpack image data from a WM/Picture tag. Return a tuple 204 containing the MIME type, the raw image data, a type indicator, and 205 the image's description. 206 207 This function is treated as "untrusted" and could throw all manner 208 of exceptions (out-of-bounds, etc.). We should clean this up 209 sometime so that the failure modes are well-defined. 210 """ 211 type, size = struct.unpack_from('<bi', data) 212 pos = 5 213 mime = b'' 214 while data[pos:pos + 2] != b'\x00\x00': 215 mime += data[pos:pos + 2] 216 pos += 2 217 pos += 2 218 description = b'' 219 while data[pos:pos + 2] != b'\x00\x00': 220 description += data[pos:pos + 2] 221 pos += 2 222 pos += 2 223 image_data = data[pos:pos + size] 224 return (mime.decode("utf-16-le"), image_data, type, 225 description.decode("utf-16-le")) 226 227 228def _pack_asf_image(mime, data, type=3, description=""): 229 """Pack image data for a WM/Picture tag. 230 """ 231 tag_data = struct.pack('<bi', type, len(data)) 232 tag_data += mime.encode("utf-16-le") + b'\x00\x00' 233 tag_data += description.encode("utf-16-le") + b'\x00\x00' 234 tag_data += data 235 return tag_data 236 237 238# iTunes Sound Check encoding. 239 240def _sc_decode(soundcheck): 241 """Convert a Sound Check bytestring value to a (gain, peak) tuple as 242 used by ReplayGain. 243 """ 244 # We decode binary data. If one of the formats gives us a text 245 # string, interpret it as UTF-8. 246 if isinstance(soundcheck, six.text_type): 247 soundcheck = soundcheck.encode('utf-8') 248 249 # SoundCheck tags consist of 10 numbers, each represented by 8 250 # characters of ASCII hex preceded by a space. 251 try: 252 soundcheck = codecs.decode(soundcheck.replace(b' ', b''), 'hex') 253 soundcheck = struct.unpack('!iiiiiiiiii', soundcheck) 254 except (struct.error, TypeError, binascii.Error): 255 # SoundCheck isn't in the format we expect, so return default 256 # values. 257 return 0.0, 0.0 258 259 # SoundCheck stores absolute calculated/measured RMS value in an 260 # unknown unit. We need to find the ratio of this measurement 261 # compared to a reference value of 1000 to get our gain in dB. We 262 # play it safe by using the larger of the two values (i.e., the most 263 # attenuation). 264 maxgain = max(soundcheck[:2]) 265 if maxgain > 0: 266 gain = math.log10(maxgain / 1000.0) * -10 267 else: 268 # Invalid gain value found. 269 gain = 0.0 270 271 # SoundCheck stores peak values as the actual value of the sample, 272 # and again separately for the left and right channels. We need to 273 # convert this to a percentage of full scale, which is 32768 for a 274 # 16 bit sample. Once again, we play it safe by using the larger of 275 # the two values. 276 peak = max(soundcheck[6:8]) / 32768.0 277 278 return round(gain, 2), round(peak, 6) 279 280 281def _sc_encode(gain, peak): 282 """Encode ReplayGain gain/peak values as a Sound Check string. 283 """ 284 # SoundCheck stores the peak value as the actual value of the 285 # sample, rather than the percentage of full scale that RG uses, so 286 # we do a simple conversion assuming 16 bit samples. 287 peak *= 32768.0 288 289 # SoundCheck stores absolute RMS values in some unknown units rather 290 # than the dB values RG uses. We can calculate these absolute values 291 # from the gain ratio using a reference value of 1000 units. We also 292 # enforce the maximum value here, which is equivalent to about 293 # -18.2dB. 294 g1 = int(min(round((10 ** (gain / -10)) * 1000), 65534)) 295 # Same as above, except our reference level is 2500 units. 296 g2 = int(min(round((10 ** (gain / -10)) * 2500), 65534)) 297 298 # The purpose of these values are unknown, but they also seem to be 299 # unused so we just use zero. 300 uk = 0 301 values = (g1, g1, g2, g2, uk, uk, int(peak), int(peak), uk, uk) 302 return (u' %08X' * 10) % values 303 304 305# Cover art and other images. 306def _imghdr_what_wrapper(data): 307 """A wrapper around imghdr.what to account for jpeg files that can only be 308 identified as such using their magic bytes 309 See #1545 310 See https://github.com/file/file/blob/master/magic/Magdir/jpeg#L12 311 """ 312 # imghdr.what returns none for jpegs with only the magic bytes, so 313 # _wider_test_jpeg is run in that case. It still returns None if it didn't 314 # match such a jpeg file. 315 return imghdr.what(None, h=data) or _wider_test_jpeg(data) 316 317 318def _wider_test_jpeg(data): 319 """Test for a jpeg file following the UNIX file implementation which 320 uses the magic bytes rather than just looking for the bytes that 321 represent 'JFIF' or 'EXIF' at a fixed position. 322 """ 323 if data[:2] == b'\xff\xd8': 324 return 'jpeg' 325 326 327def image_mime_type(data): 328 """Return the MIME type of the image data (a bytestring). 329 """ 330 # This checks for a jpeg file with only the magic bytes (unrecognized by 331 # imghdr.what). imghdr.what returns none for that type of file, so 332 # _wider_test_jpeg is run in that case. It still returns None if it didn't 333 # match such a jpeg file. 334 kind = _imghdr_what_wrapper(data) 335 if kind in ['gif', 'jpeg', 'png', 'tiff', 'bmp']: 336 return 'image/{0}'.format(kind) 337 elif kind == 'pgm': 338 return 'image/x-portable-graymap' 339 elif kind == 'pbm': 340 return 'image/x-portable-bitmap' 341 elif kind == 'ppm': 342 return 'image/x-portable-pixmap' 343 elif kind == 'xbm': 344 return 'image/x-xbitmap' 345 else: 346 return 'image/x-{0}'.format(kind) 347 348 349def image_extension(data): 350 ext = _imghdr_what_wrapper(data) 351 return PREFERRED_IMAGE_EXTENSIONS.get(ext, ext) 352 353 354class ImageType(enum.Enum): 355 """Indicates the kind of an `Image` stored in a file's tag. 356 """ 357 other = 0 358 icon = 1 359 other_icon = 2 360 front = 3 361 back = 4 362 leaflet = 5 363 media = 6 364 lead_artist = 7 365 artist = 8 366 conductor = 9 367 group = 10 368 composer = 11 369 lyricist = 12 370 recording_location = 13 371 recording_session = 14 372 performance = 15 373 screen_capture = 16 374 fish = 17 375 illustration = 18 376 artist_logo = 19 377 publisher_logo = 20 378 379 380class Image(object): 381 """Structure representing image data and metadata that can be 382 stored and retrieved from tags. 383 384 The structure has four properties. 385 * ``data`` The binary data of the image 386 * ``desc`` An optional description of the image 387 * ``type`` An instance of `ImageType` indicating the kind of image 388 * ``mime_type`` Read-only property that contains the mime type of 389 the binary data 390 """ 391 def __init__(self, data, desc=None, type=None): 392 assert isinstance(data, bytes) 393 if desc is not None: 394 assert isinstance(desc, six.text_type) 395 self.data = data 396 self.desc = desc 397 if isinstance(type, int): 398 try: 399 type = list(ImageType)[type] 400 except IndexError: 401 log.debug(u"ignoring unknown image type index %s", type) 402 type = ImageType.other 403 self.type = type 404 405 @property 406 def mime_type(self): 407 if self.data: 408 return image_mime_type(self.data) 409 410 @property 411 def type_index(self): 412 if self.type is None: 413 # This method is used when a tag format requires the type 414 # index to be set, so we return "other" as the default value. 415 return 0 416 return self.type.value 417 418 419# StorageStyle classes describe strategies for accessing values in 420# Mutagen file objects. 421 422class StorageStyle(object): 423 """A strategy for storing a value for a certain tag format (or set 424 of tag formats). This basic StorageStyle describes simple 1:1 425 mapping from raw values to keys in a Mutagen file object; subclasses 426 describe more sophisticated translations or format-specific access 427 strategies. 428 429 MediaFile uses a StorageStyle via three methods: ``get()``, 430 ``set()``, and ``delete()``. It passes a Mutagen file object to 431 each. 432 433 Internally, the StorageStyle implements ``get()`` and ``set()`` 434 using two steps that may be overridden by subtypes. To get a value, 435 the StorageStyle first calls ``fetch()`` to retrieve the value 436 corresponding to a key and then ``deserialize()`` to convert the raw 437 Mutagen value to a consumable Python value. Similarly, to set a 438 field, we call ``serialize()`` to encode the value and then 439 ``store()`` to assign the result into the Mutagen object. 440 441 Each StorageStyle type has a class-level `formats` attribute that is 442 a list of strings indicating the formats that the style applies to. 443 MediaFile only uses StorageStyles that apply to the correct type for 444 a given audio file. 445 """ 446 447 formats = ['FLAC', 'OggOpus', 'OggTheora', 'OggSpeex', 'OggVorbis', 448 'OggFlac', 'APEv2File', 'WavPack', 'Musepack', 'MonkeysAudio'] 449 """List of mutagen classes the StorageStyle can handle. 450 """ 451 452 def __init__(self, key, as_type=six.text_type, suffix=None, 453 float_places=2): 454 """Create a basic storage strategy. Parameters: 455 456 - `key`: The key on the Mutagen file object used to access the 457 field's data. 458 - `as_type`: The Python type that the value is stored as 459 internally (`unicode`, `int`, `bool`, or `bytes`). 460 - `suffix`: When `as_type` is a string type, append this before 461 storing the value. 462 - `float_places`: When the value is a floating-point number and 463 encoded as a string, the number of digits to store after the 464 decimal point. 465 """ 466 self.key = key 467 self.as_type = as_type 468 self.suffix = suffix 469 self.float_places = float_places 470 471 # Convert suffix to correct string type. 472 if self.suffix and self.as_type is six.text_type \ 473 and not isinstance(self.suffix, six.text_type): 474 self.suffix = self.suffix.decode('utf-8') 475 476 # Getter. 477 478 def get(self, mutagen_file): 479 """Get the value for the field using this style. 480 """ 481 return self.deserialize(self.fetch(mutagen_file)) 482 483 def fetch(self, mutagen_file): 484 """Retrieve the raw value of for this tag from the Mutagen file 485 object. 486 """ 487 try: 488 return mutagen_file[self.key][0] 489 except (KeyError, IndexError): 490 return None 491 492 def deserialize(self, mutagen_value): 493 """Given a raw value stored on a Mutagen object, decode and 494 return the represented value. 495 """ 496 if self.suffix and isinstance(mutagen_value, six.text_type) \ 497 and mutagen_value.endswith(self.suffix): 498 return mutagen_value[:-len(self.suffix)] 499 else: 500 return mutagen_value 501 502 # Setter. 503 504 def set(self, mutagen_file, value): 505 """Assign the value for the field using this style. 506 """ 507 self.store(mutagen_file, self.serialize(value)) 508 509 def store(self, mutagen_file, value): 510 """Store a serialized value in the Mutagen file object. 511 """ 512 mutagen_file[self.key] = [value] 513 514 def serialize(self, value): 515 """Convert the external Python value to a type that is suitable for 516 storing in a Mutagen file object. 517 """ 518 if isinstance(value, float) and self.as_type is six.text_type: 519 value = u'{0:.{1}f}'.format(value, self.float_places) 520 value = self.as_type(value) 521 elif self.as_type is six.text_type: 522 if isinstance(value, bool): 523 # Store bools as 1/0 instead of True/False. 524 value = six.text_type(int(bool(value))) 525 elif isinstance(value, bytes): 526 value = value.decode('utf-8', 'ignore') 527 else: 528 value = six.text_type(value) 529 else: 530 value = self.as_type(value) 531 532 if self.suffix: 533 value += self.suffix 534 535 return value 536 537 def delete(self, mutagen_file): 538 """Remove the tag from the file. 539 """ 540 if self.key in mutagen_file: 541 del mutagen_file[self.key] 542 543 544class ListStorageStyle(StorageStyle): 545 """Abstract storage style that provides access to lists. 546 547 The ListMediaField descriptor uses a ListStorageStyle via two 548 methods: ``get_list()`` and ``set_list()``. It passes a Mutagen file 549 object to each. 550 551 Subclasses may overwrite ``fetch`` and ``store``. ``fetch`` must 552 return a (possibly empty) list and ``store`` receives a serialized 553 list of values as the second argument. 554 555 The `serialize` and `deserialize` methods (from the base 556 `StorageStyle`) are still called with individual values. This class 557 handles packing and unpacking the values into lists. 558 """ 559 def get(self, mutagen_file): 560 """Get the first value in the field's value list. 561 """ 562 try: 563 return self.get_list(mutagen_file)[0] 564 except IndexError: 565 return None 566 567 def get_list(self, mutagen_file): 568 """Get a list of all values for the field using this style. 569 """ 570 return [self.deserialize(item) for item in self.fetch(mutagen_file)] 571 572 def fetch(self, mutagen_file): 573 """Get the list of raw (serialized) values. 574 """ 575 try: 576 return mutagen_file[self.key] 577 except KeyError: 578 return [] 579 580 def set(self, mutagen_file, value): 581 """Set an individual value as the only value for the field using 582 this style. 583 """ 584 self.set_list(mutagen_file, [value]) 585 586 def set_list(self, mutagen_file, values): 587 """Set all values for the field using this style. `values` 588 should be an iterable. 589 """ 590 self.store(mutagen_file, [self.serialize(value) for value in values]) 591 592 def store(self, mutagen_file, values): 593 """Set the list of all raw (serialized) values for this field. 594 """ 595 mutagen_file[self.key] = values 596 597 598class SoundCheckStorageStyleMixin(object): 599 """A mixin for storage styles that read and write iTunes SoundCheck 600 analysis values. The object must have an `index` field that 601 indicates which half of the gain/peak pair---0 or 1---the field 602 represents. 603 """ 604 def get(self, mutagen_file): 605 data = self.fetch(mutagen_file) 606 if data is not None: 607 return _sc_decode(data)[self.index] 608 609 def set(self, mutagen_file, value): 610 data = self.fetch(mutagen_file) 611 if data is None: 612 gain_peak = [0, 0] 613 else: 614 gain_peak = list(_sc_decode(data)) 615 gain_peak[self.index] = value or 0 616 data = self.serialize(_sc_encode(*gain_peak)) 617 self.store(mutagen_file, data) 618 619 620class ASFStorageStyle(ListStorageStyle): 621 """A general storage style for Windows Media/ASF files. 622 """ 623 formats = ['ASF'] 624 625 def deserialize(self, data): 626 if isinstance(data, mutagen.asf.ASFBaseAttribute): 627 data = data.value 628 return data 629 630 631class MP4StorageStyle(StorageStyle): 632 """A general storage style for MPEG-4 tags. 633 """ 634 formats = ['MP4'] 635 636 def serialize(self, value): 637 value = super(MP4StorageStyle, self).serialize(value) 638 if self.key.startswith('----:') and isinstance(value, six.text_type): 639 value = value.encode('utf-8') 640 return value 641 642 643class MP4TupleStorageStyle(MP4StorageStyle): 644 """A style for storing values as part of a pair of numbers in an 645 MPEG-4 file. 646 """ 647 def __init__(self, key, index=0, **kwargs): 648 super(MP4TupleStorageStyle, self).__init__(key, **kwargs) 649 self.index = index 650 651 def deserialize(self, mutagen_value): 652 items = mutagen_value or [] 653 packing_length = 2 654 return list(items) + [0] * (packing_length - len(items)) 655 656 def get(self, mutagen_file): 657 value = super(MP4TupleStorageStyle, self).get(mutagen_file)[self.index] 658 if value == 0: 659 # The values are always present and saved as integers. So we 660 # assume that "0" indicates it is not set. 661 return None 662 else: 663 return value 664 665 def set(self, mutagen_file, value): 666 if value is None: 667 value = 0 668 items = self.deserialize(self.fetch(mutagen_file)) 669 items[self.index] = int(value) 670 self.store(mutagen_file, items) 671 672 def delete(self, mutagen_file): 673 if self.index == 0: 674 super(MP4TupleStorageStyle, self).delete(mutagen_file) 675 else: 676 self.set(mutagen_file, None) 677 678 679class MP4ListStorageStyle(ListStorageStyle, MP4StorageStyle): 680 pass 681 682 683class MP4SoundCheckStorageStyle(SoundCheckStorageStyleMixin, MP4StorageStyle): 684 def __init__(self, key, index=0, **kwargs): 685 super(MP4SoundCheckStorageStyle, self).__init__(key, **kwargs) 686 self.index = index 687 688 689class MP4BoolStorageStyle(MP4StorageStyle): 690 """A style for booleans in MPEG-4 files. (MPEG-4 has an atom type 691 specifically for representing booleans.) 692 """ 693 def get(self, mutagen_file): 694 try: 695 return mutagen_file[self.key] 696 except KeyError: 697 return None 698 699 def get_list(self, mutagen_file): 700 raise NotImplementedError(u'MP4 bool storage does not support lists') 701 702 def set(self, mutagen_file, value): 703 mutagen_file[self.key] = value 704 705 def set_list(self, mutagen_file, values): 706 raise NotImplementedError(u'MP4 bool storage does not support lists') 707 708 709class MP4ImageStorageStyle(MP4ListStorageStyle): 710 """Store images as MPEG-4 image atoms. Values are `Image` objects. 711 """ 712 def __init__(self, **kwargs): 713 super(MP4ImageStorageStyle, self).__init__(key='covr', **kwargs) 714 715 def deserialize(self, data): 716 return Image(data) 717 718 def serialize(self, image): 719 if image.mime_type == 'image/png': 720 kind = mutagen.mp4.MP4Cover.FORMAT_PNG 721 elif image.mime_type == 'image/jpeg': 722 kind = mutagen.mp4.MP4Cover.FORMAT_JPEG 723 else: 724 raise ValueError(u'MP4 files only supports PNG and JPEG images') 725 return mutagen.mp4.MP4Cover(image.data, kind) 726 727 728class MP3StorageStyle(StorageStyle): 729 """Store data in ID3 frames. 730 """ 731 formats = ['MP3', 'AIFF', 'DSF'] 732 733 def __init__(self, key, id3_lang=None, **kwargs): 734 """Create a new ID3 storage style. `id3_lang` is the value for 735 the language field of newly created frames. 736 """ 737 self.id3_lang = id3_lang 738 super(MP3StorageStyle, self).__init__(key, **kwargs) 739 740 def fetch(self, mutagen_file): 741 try: 742 return mutagen_file[self.key].text[0] 743 except (KeyError, IndexError): 744 return None 745 746 def store(self, mutagen_file, value): 747 frame = mutagen.id3.Frames[self.key](encoding=3, text=[value]) 748 mutagen_file.tags.setall(self.key, [frame]) 749 750 751class MP3PeopleStorageStyle(MP3StorageStyle): 752 """Store list of people in ID3 frames. 753 """ 754 def __init__(self, key, involvement='', **kwargs): 755 self.involvement = involvement 756 super(MP3PeopleStorageStyle, self).__init__(key, **kwargs) 757 758 def store(self, mutagen_file, value): 759 frames = mutagen_file.tags.getall(self.key) 760 761 # Try modifying in place. 762 found = False 763 for frame in frames: 764 if frame.encoding == mutagen.id3.Encoding.UTF8: 765 for pair in frame.people: 766 if pair[0].lower() == self.involvement.lower(): 767 pair[1] = value 768 found = True 769 770 # Try creating a new frame. 771 if not found: 772 frame = mutagen.id3.Frames[self.key]( 773 encoding=mutagen.id3.Encoding.UTF8, 774 people=[[self.involvement, value]] 775 ) 776 mutagen_file.tags.add(frame) 777 778 def fetch(self, mutagen_file): 779 for frame in mutagen_file.tags.getall(self.key): 780 for pair in frame.people: 781 if pair[0].lower() == self.involvement.lower(): 782 try: 783 return pair[1] 784 except IndexError: 785 return None 786 787 788class MP3ListStorageStyle(ListStorageStyle, MP3StorageStyle): 789 """Store lists of data in multiple ID3 frames. 790 """ 791 def fetch(self, mutagen_file): 792 try: 793 return mutagen_file[self.key].text 794 except KeyError: 795 return [] 796 797 def store(self, mutagen_file, values): 798 frame = mutagen.id3.Frames[self.key](encoding=3, text=values) 799 mutagen_file.tags.setall(self.key, [frame]) 800 801 802class MP3UFIDStorageStyle(MP3StorageStyle): 803 """Store string data in a UFID ID3 frame with a particular owner. 804 """ 805 def __init__(self, owner, **kwargs): 806 self.owner = owner 807 super(MP3UFIDStorageStyle, self).__init__('UFID:' + owner, **kwargs) 808 809 def fetch(self, mutagen_file): 810 try: 811 return mutagen_file[self.key].data 812 except KeyError: 813 return None 814 815 def store(self, mutagen_file, value): 816 # This field type stores text data as encoded data. 817 assert isinstance(value, six.text_type) 818 value = value.encode('utf-8') 819 820 frames = mutagen_file.tags.getall(self.key) 821 for frame in frames: 822 # Replace existing frame data. 823 if frame.owner == self.owner: 824 frame.data = value 825 else: 826 # New frame. 827 frame = mutagen.id3.UFID(owner=self.owner, data=value) 828 mutagen_file.tags.setall(self.key, [frame]) 829 830 831class MP3DescStorageStyle(MP3StorageStyle): 832 """Store data in a TXXX (or similar) ID3 frame. The frame is 833 selected based its ``desc`` field. 834 """ 835 def __init__(self, desc=u'', key='TXXX', **kwargs): 836 assert isinstance(desc, six.text_type) 837 self.description = desc 838 super(MP3DescStorageStyle, self).__init__(key=key, **kwargs) 839 840 def store(self, mutagen_file, value): 841 frames = mutagen_file.tags.getall(self.key) 842 if self.key != 'USLT': 843 value = [value] 844 845 # Try modifying in place. 846 found = False 847 for frame in frames: 848 if frame.desc.lower() == self.description.lower(): 849 frame.text = value 850 frame.encoding = mutagen.id3.Encoding.UTF8 851 found = True 852 853 # Try creating a new frame. 854 if not found: 855 frame = mutagen.id3.Frames[self.key]( 856 desc=self.description, 857 text=value, 858 encoding=mutagen.id3.Encoding.UTF8, 859 ) 860 if self.id3_lang: 861 frame.lang = self.id3_lang 862 mutagen_file.tags.add(frame) 863 864 def fetch(self, mutagen_file): 865 for frame in mutagen_file.tags.getall(self.key): 866 if frame.desc.lower() == self.description.lower(): 867 if self.key == 'USLT': 868 return frame.text 869 try: 870 return frame.text[0] 871 except IndexError: 872 return None 873 874 def delete(self, mutagen_file): 875 found_frame = None 876 for frame in mutagen_file.tags.getall(self.key): 877 if frame.desc.lower() == self.description.lower(): 878 found_frame = frame 879 break 880 if found_frame is not None: 881 del mutagen_file[frame.HashKey] 882 883 884class MP3SlashPackStorageStyle(MP3StorageStyle): 885 """Store value as part of pair that is serialized as a slash- 886 separated string. 887 """ 888 def __init__(self, key, pack_pos=0, **kwargs): 889 super(MP3SlashPackStorageStyle, self).__init__(key, **kwargs) 890 self.pack_pos = pack_pos 891 892 def _fetch_unpacked(self, mutagen_file): 893 data = self.fetch(mutagen_file) 894 if data: 895 items = six.text_type(data).split('/') 896 else: 897 items = [] 898 packing_length = 2 899 return list(items) + [None] * (packing_length - len(items)) 900 901 def get(self, mutagen_file): 902 return self._fetch_unpacked(mutagen_file)[self.pack_pos] 903 904 def set(self, mutagen_file, value): 905 items = self._fetch_unpacked(mutagen_file) 906 items[self.pack_pos] = value 907 if items[0] is None: 908 items[0] = '' 909 if items[1] is None: 910 items.pop() # Do not store last value 911 self.store(mutagen_file, '/'.join(map(six.text_type, items))) 912 913 def delete(self, mutagen_file): 914 if self.pack_pos == 0: 915 super(MP3SlashPackStorageStyle, self).delete(mutagen_file) 916 else: 917 self.set(mutagen_file, None) 918 919 920class MP3ImageStorageStyle(ListStorageStyle, MP3StorageStyle): 921 """Converts between APIC frames and ``Image`` instances. 922 923 The `get_list` method inherited from ``ListStorageStyle`` returns a 924 list of ``Image``s. Similarly, the `set_list` method accepts a 925 list of ``Image``s as its ``values`` argument. 926 """ 927 def __init__(self): 928 super(MP3ImageStorageStyle, self).__init__(key='APIC') 929 self.as_type = bytes 930 931 def deserialize(self, apic_frame): 932 """Convert APIC frame into Image.""" 933 return Image(data=apic_frame.data, desc=apic_frame.desc, 934 type=apic_frame.type) 935 936 def fetch(self, mutagen_file): 937 return mutagen_file.tags.getall(self.key) 938 939 def store(self, mutagen_file, frames): 940 mutagen_file.tags.setall(self.key, frames) 941 942 def delete(self, mutagen_file): 943 mutagen_file.tags.delall(self.key) 944 945 def serialize(self, image): 946 """Return an APIC frame populated with data from ``image``. 947 """ 948 assert isinstance(image, Image) 949 frame = mutagen.id3.Frames[self.key]() 950 frame.data = image.data 951 frame.mime = image.mime_type 952 frame.desc = image.desc or u'' 953 954 # For compatibility with OS X/iTunes prefer latin-1 if possible. 955 # See issue #899 956 try: 957 frame.desc.encode("latin-1") 958 except UnicodeEncodeError: 959 frame.encoding = mutagen.id3.Encoding.UTF16 960 else: 961 frame.encoding = mutagen.id3.Encoding.LATIN1 962 963 frame.type = image.type_index 964 return frame 965 966 967class MP3SoundCheckStorageStyle(SoundCheckStorageStyleMixin, 968 MP3DescStorageStyle): 969 def __init__(self, index=0, **kwargs): 970 super(MP3SoundCheckStorageStyle, self).__init__(**kwargs) 971 self.index = index 972 973 974class ASFImageStorageStyle(ListStorageStyle): 975 """Store images packed into Windows Media/ASF byte array attributes. 976 Values are `Image` objects. 977 """ 978 formats = ['ASF'] 979 980 def __init__(self): 981 super(ASFImageStorageStyle, self).__init__(key='WM/Picture') 982 983 def deserialize(self, asf_picture): 984 mime, data, type, desc = _unpack_asf_image(asf_picture.value) 985 return Image(data, desc=desc, type=type) 986 987 def serialize(self, image): 988 pic = mutagen.asf.ASFByteArrayAttribute() 989 pic.value = _pack_asf_image(image.mime_type, image.data, 990 type=image.type_index, 991 description=image.desc or u'') 992 return pic 993 994 995class VorbisImageStorageStyle(ListStorageStyle): 996 """Store images in Vorbis comments. Both legacy COVERART fields and 997 modern METADATA_BLOCK_PICTURE tags are supported. Data is 998 base64-encoded. Values are `Image` objects. 999 """ 1000 formats = ['OggOpus', 'OggTheora', 'OggSpeex', 'OggVorbis', 1001 'OggFlac'] 1002 1003 def __init__(self): 1004 super(VorbisImageStorageStyle, self).__init__( 1005 key='metadata_block_picture' 1006 ) 1007 self.as_type = bytes 1008 1009 def fetch(self, mutagen_file): 1010 images = [] 1011 if 'metadata_block_picture' not in mutagen_file: 1012 # Try legacy COVERART tags. 1013 if 'coverart' in mutagen_file: 1014 for data in mutagen_file['coverart']: 1015 images.append(Image(base64.b64decode(data))) 1016 return images 1017 for data in mutagen_file["metadata_block_picture"]: 1018 try: 1019 pic = mutagen.flac.Picture(base64.b64decode(data)) 1020 except (TypeError, AttributeError): 1021 continue 1022 images.append(Image(data=pic.data, desc=pic.desc, 1023 type=pic.type)) 1024 return images 1025 1026 def store(self, mutagen_file, image_data): 1027 # Strip all art, including legacy COVERART. 1028 if 'coverart' in mutagen_file: 1029 del mutagen_file['coverart'] 1030 if 'coverartmime' in mutagen_file: 1031 del mutagen_file['coverartmime'] 1032 super(VorbisImageStorageStyle, self).store(mutagen_file, image_data) 1033 1034 def serialize(self, image): 1035 """Turn a Image into a base64 encoded FLAC picture block. 1036 """ 1037 pic = mutagen.flac.Picture() 1038 pic.data = image.data 1039 pic.type = image.type_index 1040 pic.mime = image.mime_type 1041 pic.desc = image.desc or u'' 1042 1043 # Encoding with base64 returns bytes on both Python 2 and 3. 1044 # Mutagen requires the data to be a Unicode string, so we decode 1045 # it before passing it along. 1046 return base64.b64encode(pic.write()).decode('ascii') 1047 1048 1049class FlacImageStorageStyle(ListStorageStyle): 1050 """Converts between ``mutagen.flac.Picture`` and ``Image`` instances. 1051 """ 1052 formats = ['FLAC'] 1053 1054 def __init__(self): 1055 super(FlacImageStorageStyle, self).__init__(key='') 1056 1057 def fetch(self, mutagen_file): 1058 return mutagen_file.pictures 1059 1060 def deserialize(self, flac_picture): 1061 return Image(data=flac_picture.data, desc=flac_picture.desc, 1062 type=flac_picture.type) 1063 1064 def store(self, mutagen_file, pictures): 1065 """``pictures`` is a list of mutagen.flac.Picture instances. 1066 """ 1067 mutagen_file.clear_pictures() 1068 for pic in pictures: 1069 mutagen_file.add_picture(pic) 1070 1071 def serialize(self, image): 1072 """Turn a Image into a mutagen.flac.Picture. 1073 """ 1074 pic = mutagen.flac.Picture() 1075 pic.data = image.data 1076 pic.type = image.type_index 1077 pic.mime = image.mime_type 1078 pic.desc = image.desc or u'' 1079 return pic 1080 1081 def delete(self, mutagen_file): 1082 """Remove all images from the file. 1083 """ 1084 mutagen_file.clear_pictures() 1085 1086 1087class APEv2ImageStorageStyle(ListStorageStyle): 1088 """Store images in APEv2 tags. Values are `Image` objects. 1089 """ 1090 formats = ['APEv2File', 'WavPack', 'Musepack', 'MonkeysAudio', 'OptimFROG'] 1091 1092 TAG_NAMES = { 1093 ImageType.other: 'Cover Art (other)', 1094 ImageType.icon: 'Cover Art (icon)', 1095 ImageType.other_icon: 'Cover Art (other icon)', 1096 ImageType.front: 'Cover Art (front)', 1097 ImageType.back: 'Cover Art (back)', 1098 ImageType.leaflet: 'Cover Art (leaflet)', 1099 ImageType.media: 'Cover Art (media)', 1100 ImageType.lead_artist: 'Cover Art (lead)', 1101 ImageType.artist: 'Cover Art (artist)', 1102 ImageType.conductor: 'Cover Art (conductor)', 1103 ImageType.group: 'Cover Art (band)', 1104 ImageType.composer: 'Cover Art (composer)', 1105 ImageType.lyricist: 'Cover Art (lyricist)', 1106 ImageType.recording_location: 'Cover Art (studio)', 1107 ImageType.recording_session: 'Cover Art (recording)', 1108 ImageType.performance: 'Cover Art (performance)', 1109 ImageType.screen_capture: 'Cover Art (movie scene)', 1110 ImageType.fish: 'Cover Art (colored fish)', 1111 ImageType.illustration: 'Cover Art (illustration)', 1112 ImageType.artist_logo: 'Cover Art (band logo)', 1113 ImageType.publisher_logo: 'Cover Art (publisher logo)', 1114 } 1115 1116 def __init__(self): 1117 super(APEv2ImageStorageStyle, self).__init__(key='') 1118 1119 def fetch(self, mutagen_file): 1120 images = [] 1121 for cover_type, cover_tag in self.TAG_NAMES.items(): 1122 try: 1123 frame = mutagen_file[cover_tag] 1124 text_delimiter_index = frame.value.find(b'\x00') 1125 if text_delimiter_index > 0: 1126 comment = frame.value[0:text_delimiter_index] 1127 comment = comment.decode('utf-8', 'replace') 1128 else: 1129 comment = None 1130 image_data = frame.value[text_delimiter_index + 1:] 1131 images.append(Image(data=image_data, type=cover_type, 1132 desc=comment)) 1133 except KeyError: 1134 pass 1135 1136 return images 1137 1138 def set_list(self, mutagen_file, values): 1139 self.delete(mutagen_file) 1140 1141 for image in values: 1142 image_type = image.type or ImageType.other 1143 comment = image.desc or '' 1144 image_data = comment.encode('utf-8') + b'\x00' + image.data 1145 cover_tag = self.TAG_NAMES[image_type] 1146 mutagen_file[cover_tag] = image_data 1147 1148 def delete(self, mutagen_file): 1149 """Remove all images from the file. 1150 """ 1151 for cover_tag in self.TAG_NAMES.values(): 1152 try: 1153 del mutagen_file[cover_tag] 1154 except KeyError: 1155 pass 1156 1157 1158# MediaField is a descriptor that represents a single logical field. It 1159# aggregates several StorageStyles describing how to access the data for 1160# each file type. 1161 1162class MediaField(object): 1163 """A descriptor providing access to a particular (abstract) metadata 1164 field. 1165 """ 1166 def __init__(self, *styles, **kwargs): 1167 """Creates a new MediaField. 1168 1169 :param styles: `StorageStyle` instances that describe the strategy 1170 for reading and writing the field in particular 1171 formats. There must be at least one style for 1172 each possible file format. 1173 1174 :param out_type: the type of the value that should be returned when 1175 getting this property. 1176 1177 """ 1178 self.out_type = kwargs.get('out_type', six.text_type) 1179 self._styles = styles 1180 1181 def styles(self, mutagen_file): 1182 """Yields the list of storage styles of this field that can 1183 handle the MediaFile's format. 1184 """ 1185 for style in self._styles: 1186 if mutagen_file.__class__.__name__ in style.formats: 1187 yield style 1188 1189 def __get__(self, mediafile, owner=None): 1190 out = None 1191 for style in self.styles(mediafile.mgfile): 1192 out = style.get(mediafile.mgfile) 1193 if out: 1194 break 1195 return _safe_cast(self.out_type, out) 1196 1197 def __set__(self, mediafile, value): 1198 if value is None: 1199 value = self._none_value() 1200 for style in self.styles(mediafile.mgfile): 1201 style.set(mediafile.mgfile, value) 1202 1203 def __delete__(self, mediafile): 1204 for style in self.styles(mediafile.mgfile): 1205 style.delete(mediafile.mgfile) 1206 1207 def _none_value(self): 1208 """Get an appropriate "null" value for this field's type. This 1209 is used internally when setting the field to None. 1210 """ 1211 if self.out_type == int: 1212 return 0 1213 elif self.out_type == float: 1214 return 0.0 1215 elif self.out_type == bool: 1216 return False 1217 elif self.out_type == six.text_type: 1218 return u'' 1219 1220 1221class ListMediaField(MediaField): 1222 """Property descriptor that retrieves a list of multiple values from 1223 a tag. 1224 1225 Uses ``get_list`` and set_list`` methods of its ``StorageStyle`` 1226 strategies to do the actual work. 1227 """ 1228 def __get__(self, mediafile, _): 1229 values = [] 1230 for style in self.styles(mediafile.mgfile): 1231 values.extend(style.get_list(mediafile.mgfile)) 1232 return [_safe_cast(self.out_type, value) for value in values] 1233 1234 def __set__(self, mediafile, values): 1235 for style in self.styles(mediafile.mgfile): 1236 style.set_list(mediafile.mgfile, values) 1237 1238 def single_field(self): 1239 """Returns a ``MediaField`` descriptor that gets and sets the 1240 first item. 1241 """ 1242 options = {'out_type': self.out_type} 1243 return MediaField(*self._styles, **options) 1244 1245 1246class DateField(MediaField): 1247 """Descriptor that handles serializing and deserializing dates 1248 1249 The getter parses value from tags into a ``datetime.date`` instance 1250 and setter serializes such an instance into a string. 1251 1252 For granular access to year, month, and day, use the ``*_field`` 1253 methods to create corresponding `DateItemField`s. 1254 """ 1255 def __init__(self, *date_styles, **kwargs): 1256 """``date_styles`` is a list of ``StorageStyle``s to store and 1257 retrieve the whole date from. The ``year`` option is an 1258 additional list of fallback styles for the year. The year is 1259 always set on this style, but is only retrieved if the main 1260 storage styles do not return a value. 1261 """ 1262 super(DateField, self).__init__(*date_styles) 1263 year_style = kwargs.get('year', None) 1264 if year_style: 1265 self._year_field = MediaField(*year_style) 1266 1267 def __get__(self, mediafile, owner=None): 1268 year, month, day = self._get_date_tuple(mediafile) 1269 if not year: 1270 return None 1271 try: 1272 return datetime.date( 1273 year, 1274 month or 1, 1275 day or 1 1276 ) 1277 except ValueError: # Out of range values. 1278 return None 1279 1280 def __set__(self, mediafile, date): 1281 if date is None: 1282 self._set_date_tuple(mediafile, None, None, None) 1283 else: 1284 self._set_date_tuple(mediafile, date.year, date.month, date.day) 1285 1286 def __delete__(self, mediafile): 1287 super(DateField, self).__delete__(mediafile) 1288 if hasattr(self, '_year_field'): 1289 self._year_field.__delete__(mediafile) 1290 1291 def _get_date_tuple(self, mediafile): 1292 """Get a 3-item sequence representing the date consisting of a 1293 year, month, and day number. Each number is either an integer or 1294 None. 1295 """ 1296 # Get the underlying data and split on hyphens and slashes. 1297 datestring = super(DateField, self).__get__(mediafile, None) 1298 if isinstance(datestring, six.string_types): 1299 datestring = re.sub(r'[Tt ].*$', '', six.text_type(datestring)) 1300 items = re.split('[-/]', six.text_type(datestring)) 1301 else: 1302 items = [] 1303 1304 # Ensure that we have exactly 3 components, possibly by 1305 # truncating or padding. 1306 items = items[:3] 1307 if len(items) < 3: 1308 items += [None] * (3 - len(items)) 1309 1310 # Use year field if year is missing. 1311 if not items[0] and hasattr(self, '_year_field'): 1312 items[0] = self._year_field.__get__(mediafile) 1313 1314 # Convert each component to an integer if possible. 1315 items_ = [] 1316 for item in items: 1317 try: 1318 items_.append(int(item)) 1319 except (TypeError, ValueError): 1320 items_.append(None) 1321 return items_ 1322 1323 def _set_date_tuple(self, mediafile, year, month=None, day=None): 1324 """Set the value of the field given a year, month, and day 1325 number. Each number can be an integer or None to indicate an 1326 unset component. 1327 """ 1328 if year is None: 1329 self.__delete__(mediafile) 1330 return 1331 1332 date = [u'{0:04d}'.format(int(year))] 1333 if month: 1334 date.append(u'{0:02d}'.format(int(month))) 1335 if month and day: 1336 date.append(u'{0:02d}'.format(int(day))) 1337 date = map(six.text_type, date) 1338 super(DateField, self).__set__(mediafile, u'-'.join(date)) 1339 1340 if hasattr(self, '_year_field'): 1341 self._year_field.__set__(mediafile, year) 1342 1343 def year_field(self): 1344 return DateItemField(self, 0) 1345 1346 def month_field(self): 1347 return DateItemField(self, 1) 1348 1349 def day_field(self): 1350 return DateItemField(self, 2) 1351 1352 1353class DateItemField(MediaField): 1354 """Descriptor that gets and sets constituent parts of a `DateField`: 1355 the month, day, or year. 1356 """ 1357 def __init__(self, date_field, item_pos): 1358 self.date_field = date_field 1359 self.item_pos = item_pos 1360 1361 def __get__(self, mediafile, _): 1362 return self.date_field._get_date_tuple(mediafile)[self.item_pos] 1363 1364 def __set__(self, mediafile, value): 1365 items = self.date_field._get_date_tuple(mediafile) 1366 items[self.item_pos] = value 1367 self.date_field._set_date_tuple(mediafile, *items) 1368 1369 def __delete__(self, mediafile): 1370 self.__set__(mediafile, None) 1371 1372 1373class CoverArtField(MediaField): 1374 """A descriptor that provides access to the *raw image data* for the 1375 cover image on a file. This is used for backwards compatibility: the 1376 full `ImageListField` provides richer `Image` objects. 1377 1378 When there are multiple images we try to pick the most likely to be a front 1379 cover. 1380 """ 1381 def __init__(self): 1382 pass 1383 1384 def __get__(self, mediafile, _): 1385 candidates = mediafile.images 1386 if candidates: 1387 return self.guess_cover_image(candidates).data 1388 else: 1389 return None 1390 1391 @staticmethod 1392 def guess_cover_image(candidates): 1393 if len(candidates) == 1: 1394 return candidates[0] 1395 try: 1396 return next(c for c in candidates if c.type == ImageType.front) 1397 except StopIteration: 1398 return candidates[0] 1399 1400 def __set__(self, mediafile, data): 1401 if data: 1402 mediafile.images = [Image(data=data)] 1403 else: 1404 mediafile.images = [] 1405 1406 def __delete__(self, mediafile): 1407 delattr(mediafile, 'images') 1408 1409 1410class ImageListField(ListMediaField): 1411 """Descriptor to access the list of images embedded in tags. 1412 1413 The getter returns a list of `Image` instances obtained from 1414 the tags. The setter accepts a list of `Image` instances to be 1415 written to the tags. 1416 """ 1417 def __init__(self): 1418 # The storage styles used here must implement the 1419 # `ListStorageStyle` interface and get and set lists of 1420 # `Image`s. 1421 super(ImageListField, self).__init__( 1422 MP3ImageStorageStyle(), 1423 MP4ImageStorageStyle(), 1424 ASFImageStorageStyle(), 1425 VorbisImageStorageStyle(), 1426 FlacImageStorageStyle(), 1427 APEv2ImageStorageStyle(), 1428 out_type=Image, 1429 ) 1430 1431 1432# MediaFile is a collection of fields. 1433 1434class MediaFile(object): 1435 """Represents a multimedia file on disk and provides access to its 1436 metadata. 1437 """ 1438 def __init__(self, path, id3v23=False): 1439 """Constructs a new `MediaFile` reflecting the file at path. May 1440 throw `UnreadableFileError`. 1441 1442 By default, MP3 files are saved with ID3v2.4 tags. You can use 1443 the older ID3v2.3 standard by specifying the `id3v23` option. 1444 """ 1445 self.path = path 1446 1447 self.mgfile = mutagen_call('open', path, mutagen.File, path) 1448 1449 if self.mgfile is None: 1450 # Mutagen couldn't guess the type 1451 raise FileTypeError(path) 1452 elif (type(self.mgfile).__name__ == 'M4A' or 1453 type(self.mgfile).__name__ == 'MP4'): 1454 info = self.mgfile.info 1455 if info.codec and info.codec.startswith('alac'): 1456 self.type = 'alac' 1457 else: 1458 self.type = 'aac' 1459 elif (type(self.mgfile).__name__ == 'ID3' or 1460 type(self.mgfile).__name__ == 'MP3'): 1461 self.type = 'mp3' 1462 elif type(self.mgfile).__name__ == 'FLAC': 1463 self.type = 'flac' 1464 elif type(self.mgfile).__name__ == 'OggOpus': 1465 self.type = 'opus' 1466 elif type(self.mgfile).__name__ == 'OggVorbis': 1467 self.type = 'ogg' 1468 elif type(self.mgfile).__name__ == 'MonkeysAudio': 1469 self.type = 'ape' 1470 elif type(self.mgfile).__name__ == 'WavPack': 1471 self.type = 'wv' 1472 elif type(self.mgfile).__name__ == 'Musepack': 1473 self.type = 'mpc' 1474 elif type(self.mgfile).__name__ == 'ASF': 1475 self.type = 'asf' 1476 elif type(self.mgfile).__name__ == 'AIFF': 1477 self.type = 'aiff' 1478 elif type(self.mgfile).__name__ == 'DSF': 1479 self.type = 'dsf' 1480 else: 1481 raise FileTypeError(path, type(self.mgfile).__name__) 1482 1483 # Add a set of tags if it's missing. 1484 if self.mgfile.tags is None: 1485 self.mgfile.add_tags() 1486 1487 # Set the ID3v2.3 flag only for MP3s. 1488 self.id3v23 = id3v23 and self.type == 'mp3' 1489 1490 def save(self): 1491 """Write the object's tags back to the file. May 1492 throw `UnreadableFileError`. 1493 """ 1494 # Possibly save the tags to ID3v2.3. 1495 kwargs = {} 1496 if self.id3v23: 1497 id3 = self.mgfile 1498 if hasattr(id3, 'tags'): 1499 # In case this is an MP3 object, not an ID3 object. 1500 id3 = id3.tags 1501 id3.update_to_v23() 1502 kwargs['v2_version'] = 3 1503 1504 mutagen_call('save', self.path, self.mgfile.save, **kwargs) 1505 1506 def delete(self): 1507 """Remove the current metadata tag from the file. May 1508 throw `UnreadableFileError`. 1509 """ 1510 mutagen_call('delete', self.path, self.mgfile.delete) 1511 1512 # Convenient access to the set of available fields. 1513 1514 @classmethod 1515 def fields(cls): 1516 """Get the names of all writable properties that reflect 1517 metadata tags (i.e., those that are instances of 1518 :class:`MediaField`). 1519 """ 1520 for property, descriptor in cls.__dict__.items(): 1521 if isinstance(descriptor, MediaField): 1522 if isinstance(property, bytes): 1523 # On Python 2, class field names are bytes. This method 1524 # produces text strings. 1525 yield property.decode('utf8', 'ignore') 1526 else: 1527 yield property 1528 1529 @classmethod 1530 def _field_sort_name(cls, name): 1531 """Get a sort key for a field name that determines the order 1532 fields should be written in. 1533 1534 Fields names are kept unchanged, unless they are instances of 1535 :class:`DateItemField`, in which case `year`, `month`, and `day` 1536 are replaced by `date0`, `date1`, and `date2`, respectively, to 1537 make them appear in that order. 1538 """ 1539 if isinstance(cls.__dict__[name], DateItemField): 1540 name = re.sub('year', 'date0', name) 1541 name = re.sub('month', 'date1', name) 1542 name = re.sub('day', 'date2', name) 1543 return name 1544 1545 @classmethod 1546 def sorted_fields(cls): 1547 """Get the names of all writable metadata fields, sorted in the 1548 order that they should be written. 1549 1550 This is a lexicographic order, except for instances of 1551 :class:`DateItemField`, which are sorted in year-month-day 1552 order. 1553 """ 1554 for property in sorted(cls.fields(), key=cls._field_sort_name): 1555 yield property 1556 1557 @classmethod 1558 def readable_fields(cls): 1559 """Get all metadata fields: the writable ones from 1560 :meth:`fields` and also other audio properties. 1561 """ 1562 for property in cls.fields(): 1563 yield property 1564 for property in ('length', 'samplerate', 'bitdepth', 'bitrate', 1565 'channels', 'format'): 1566 yield property 1567 1568 @classmethod 1569 def add_field(cls, name, descriptor): 1570 """Add a field to store custom tags. 1571 1572 :param name: the name of the property the field is accessed 1573 through. It must not already exist on this class. 1574 1575 :param descriptor: an instance of :class:`MediaField`. 1576 """ 1577 if not isinstance(descriptor, MediaField): 1578 raise ValueError( 1579 u'{0} must be an instance of MediaField'.format(descriptor)) 1580 if name in cls.__dict__: 1581 raise ValueError( 1582 u'property "{0}" already exists on MediaField'.format(name)) 1583 setattr(cls, name, descriptor) 1584 1585 def update(self, dict): 1586 """Set all field values from a dictionary. 1587 1588 For any key in `dict` that is also a field to store tags the 1589 method retrieves the corresponding value from `dict` and updates 1590 the `MediaFile`. If a key has the value `None`, the 1591 corresponding property is deleted from the `MediaFile`. 1592 """ 1593 for field in self.sorted_fields(): 1594 if field in dict: 1595 if dict[field] is None: 1596 delattr(self, field) 1597 else: 1598 setattr(self, field, dict[field]) 1599 1600 # Field definitions. 1601 1602 title = MediaField( 1603 MP3StorageStyle('TIT2'), 1604 MP4StorageStyle('\xa9nam'), 1605 StorageStyle('TITLE'), 1606 ASFStorageStyle('Title'), 1607 ) 1608 artist = MediaField( 1609 MP3StorageStyle('TPE1'), 1610 MP4StorageStyle('\xa9ART'), 1611 StorageStyle('ARTIST'), 1612 ASFStorageStyle('Author'), 1613 ) 1614 album = MediaField( 1615 MP3StorageStyle('TALB'), 1616 MP4StorageStyle('\xa9alb'), 1617 StorageStyle('ALBUM'), 1618 ASFStorageStyle('WM/AlbumTitle'), 1619 ) 1620 genres = ListMediaField( 1621 MP3ListStorageStyle('TCON'), 1622 MP4ListStorageStyle('\xa9gen'), 1623 ListStorageStyle('GENRE'), 1624 ASFStorageStyle('WM/Genre'), 1625 ) 1626 genre = genres.single_field() 1627 1628 lyricist = MediaField( 1629 MP3StorageStyle('TEXT'), 1630 MP4StorageStyle('----:com.apple.iTunes:LYRICIST'), 1631 StorageStyle('LYRICIST'), 1632 ASFStorageStyle('WM/Writer'), 1633 ) 1634 composer = MediaField( 1635 MP3StorageStyle('TCOM'), 1636 MP4StorageStyle('\xa9wrt'), 1637 StorageStyle('COMPOSER'), 1638 ASFStorageStyle('WM/Composer'), 1639 ) 1640 composer_sort = MediaField( 1641 MP3StorageStyle('TSOC'), 1642 MP4StorageStyle('soco'), 1643 StorageStyle('COMPOSERSORT'), 1644 ASFStorageStyle('WM/Composersortorder'), 1645 ) 1646 arranger = MediaField( 1647 MP3PeopleStorageStyle('TIPL', involvement='arranger'), 1648 MP4StorageStyle('----:com.apple.iTunes:Arranger'), 1649 StorageStyle('ARRANGER'), 1650 ASFStorageStyle('beets/Arranger'), 1651 ) 1652 1653 grouping = MediaField( 1654 MP3StorageStyle('TIT1'), 1655 MP4StorageStyle('\xa9grp'), 1656 StorageStyle('GROUPING'), 1657 ASFStorageStyle('WM/ContentGroupDescription'), 1658 ) 1659 track = MediaField( 1660 MP3SlashPackStorageStyle('TRCK', pack_pos=0), 1661 MP4TupleStorageStyle('trkn', index=0), 1662 StorageStyle('TRACK'), 1663 StorageStyle('TRACKNUMBER'), 1664 ASFStorageStyle('WM/TrackNumber'), 1665 out_type=int, 1666 ) 1667 tracktotal = MediaField( 1668 MP3SlashPackStorageStyle('TRCK', pack_pos=1), 1669 MP4TupleStorageStyle('trkn', index=1), 1670 StorageStyle('TRACKTOTAL'), 1671 StorageStyle('TRACKC'), 1672 StorageStyle('TOTALTRACKS'), 1673 ASFStorageStyle('TotalTracks'), 1674 out_type=int, 1675 ) 1676 disc = MediaField( 1677 MP3SlashPackStorageStyle('TPOS', pack_pos=0), 1678 MP4TupleStorageStyle('disk', index=0), 1679 StorageStyle('DISC'), 1680 StorageStyle('DISCNUMBER'), 1681 ASFStorageStyle('WM/PartOfSet'), 1682 out_type=int, 1683 ) 1684 disctotal = MediaField( 1685 MP3SlashPackStorageStyle('TPOS', pack_pos=1), 1686 MP4TupleStorageStyle('disk', index=1), 1687 StorageStyle('DISCTOTAL'), 1688 StorageStyle('DISCC'), 1689 StorageStyle('TOTALDISCS'), 1690 ASFStorageStyle('TotalDiscs'), 1691 out_type=int, 1692 ) 1693 lyrics = MediaField( 1694 MP3DescStorageStyle(key='USLT'), 1695 MP4StorageStyle('\xa9lyr'), 1696 StorageStyle('LYRICS'), 1697 ASFStorageStyle('WM/Lyrics'), 1698 ) 1699 comments = MediaField( 1700 MP3DescStorageStyle(key='COMM'), 1701 MP4StorageStyle('\xa9cmt'), 1702 StorageStyle('DESCRIPTION'), 1703 StorageStyle('COMMENT'), 1704 ASFStorageStyle('WM/Comments'), 1705 ASFStorageStyle('Description') 1706 ) 1707 bpm = MediaField( 1708 MP3StorageStyle('TBPM'), 1709 MP4StorageStyle('tmpo', as_type=int), 1710 StorageStyle('BPM'), 1711 ASFStorageStyle('WM/BeatsPerMinute'), 1712 out_type=int, 1713 ) 1714 comp = MediaField( 1715 MP3StorageStyle('TCMP'), 1716 MP4BoolStorageStyle('cpil'), 1717 StorageStyle('COMPILATION'), 1718 ASFStorageStyle('WM/IsCompilation', as_type=bool), 1719 out_type=bool, 1720 ) 1721 albumartist = MediaField( 1722 MP3StorageStyle('TPE2'), 1723 MP4StorageStyle('aART'), 1724 StorageStyle('ALBUM ARTIST'), 1725 StorageStyle('ALBUMARTIST'), 1726 ASFStorageStyle('WM/AlbumArtist'), 1727 ) 1728 albumtype = MediaField( 1729 MP3DescStorageStyle(u'MusicBrainz Album Type'), 1730 MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Type'), 1731 StorageStyle('MUSICBRAINZ_ALBUMTYPE'), 1732 ASFStorageStyle('MusicBrainz/Album Type'), 1733 ) 1734 label = MediaField( 1735 MP3StorageStyle('TPUB'), 1736 MP4StorageStyle('----:com.apple.iTunes:Label'), 1737 MP4StorageStyle('----:com.apple.iTunes:publisher'), 1738 StorageStyle('LABEL'), 1739 StorageStyle('PUBLISHER'), # Traktor 1740 ASFStorageStyle('WM/Publisher'), 1741 ) 1742 artist_sort = MediaField( 1743 MP3StorageStyle('TSOP'), 1744 MP4StorageStyle('soar'), 1745 StorageStyle('ARTISTSORT'), 1746 ASFStorageStyle('WM/ArtistSortOrder'), 1747 ) 1748 albumartist_sort = MediaField( 1749 MP3DescStorageStyle(u'ALBUMARTISTSORT'), 1750 MP4StorageStyle('soaa'), 1751 StorageStyle('ALBUMARTISTSORT'), 1752 ASFStorageStyle('WM/AlbumArtistSortOrder'), 1753 ) 1754 asin = MediaField( 1755 MP3DescStorageStyle(u'ASIN'), 1756 MP4StorageStyle('----:com.apple.iTunes:ASIN'), 1757 StorageStyle('ASIN'), 1758 ASFStorageStyle('MusicBrainz/ASIN'), 1759 ) 1760 catalognum = MediaField( 1761 MP3DescStorageStyle(u'CATALOGNUMBER'), 1762 MP4StorageStyle('----:com.apple.iTunes:CATALOGNUMBER'), 1763 StorageStyle('CATALOGNUMBER'), 1764 ASFStorageStyle('WM/CatalogNo'), 1765 ) 1766 disctitle = MediaField( 1767 MP3StorageStyle('TSST'), 1768 MP4StorageStyle('----:com.apple.iTunes:DISCSUBTITLE'), 1769 StorageStyle('DISCSUBTITLE'), 1770 ASFStorageStyle('WM/SetSubTitle'), 1771 ) 1772 encoder = MediaField( 1773 MP3StorageStyle('TENC'), 1774 MP4StorageStyle('\xa9too'), 1775 StorageStyle('ENCODEDBY'), 1776 StorageStyle('ENCODER'), 1777 ASFStorageStyle('WM/EncodedBy'), 1778 ) 1779 script = MediaField( 1780 MP3DescStorageStyle(u'Script'), 1781 MP4StorageStyle('----:com.apple.iTunes:SCRIPT'), 1782 StorageStyle('SCRIPT'), 1783 ASFStorageStyle('WM/Script'), 1784 ) 1785 language = MediaField( 1786 MP3StorageStyle('TLAN'), 1787 MP4StorageStyle('----:com.apple.iTunes:LANGUAGE'), 1788 StorageStyle('LANGUAGE'), 1789 ASFStorageStyle('WM/Language'), 1790 ) 1791 country = MediaField( 1792 MP3DescStorageStyle(u'MusicBrainz Album Release Country'), 1793 MP4StorageStyle('----:com.apple.iTunes:MusicBrainz ' 1794 'Album Release Country'), 1795 StorageStyle('RELEASECOUNTRY'), 1796 ASFStorageStyle('MusicBrainz/Album Release Country'), 1797 ) 1798 albumstatus = MediaField( 1799 MP3DescStorageStyle(u'MusicBrainz Album Status'), 1800 MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Status'), 1801 StorageStyle('MUSICBRAINZ_ALBUMSTATUS'), 1802 ASFStorageStyle('MusicBrainz/Album Status'), 1803 ) 1804 media = MediaField( 1805 MP3StorageStyle('TMED'), 1806 MP4StorageStyle('----:com.apple.iTunes:MEDIA'), 1807 StorageStyle('MEDIA'), 1808 ASFStorageStyle('WM/Media'), 1809 ) 1810 albumdisambig = MediaField( 1811 # This tag mapping was invented for beets (not used by Picard, etc). 1812 MP3DescStorageStyle(u'MusicBrainz Album Comment'), 1813 MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Comment'), 1814 StorageStyle('MUSICBRAINZ_ALBUMCOMMENT'), 1815 ASFStorageStyle('MusicBrainz/Album Comment'), 1816 ) 1817 1818 # Release date. 1819 date = DateField( 1820 MP3StorageStyle('TDRC'), 1821 MP4StorageStyle('\xa9day'), 1822 StorageStyle('DATE'), 1823 ASFStorageStyle('WM/Year'), 1824 year=(StorageStyle('YEAR'),)) 1825 1826 year = date.year_field() 1827 month = date.month_field() 1828 day = date.day_field() 1829 1830 # *Original* release date. 1831 original_date = DateField( 1832 MP3StorageStyle('TDOR'), 1833 MP4StorageStyle('----:com.apple.iTunes:ORIGINAL YEAR'), 1834 StorageStyle('ORIGINALDATE'), 1835 ASFStorageStyle('WM/OriginalReleaseYear')) 1836 1837 original_year = original_date.year_field() 1838 original_month = original_date.month_field() 1839 original_day = original_date.day_field() 1840 1841 # Nonstandard metadata. 1842 artist_credit = MediaField( 1843 MP3DescStorageStyle(u'Artist Credit'), 1844 MP4StorageStyle('----:com.apple.iTunes:Artist Credit'), 1845 StorageStyle('ARTIST_CREDIT'), 1846 ASFStorageStyle('beets/Artist Credit'), 1847 ) 1848 albumartist_credit = MediaField( 1849 MP3DescStorageStyle(u'Album Artist Credit'), 1850 MP4StorageStyle('----:com.apple.iTunes:Album Artist Credit'), 1851 StorageStyle('ALBUMARTIST_CREDIT'), 1852 ASFStorageStyle('beets/Album Artist Credit'), 1853 ) 1854 1855 # Legacy album art field 1856 art = CoverArtField() 1857 1858 # Image list 1859 images = ImageListField() 1860 1861 # MusicBrainz IDs. 1862 mb_trackid = MediaField( 1863 MP3UFIDStorageStyle(owner='http://musicbrainz.org'), 1864 MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Track Id'), 1865 StorageStyle('MUSICBRAINZ_TRACKID'), 1866 ASFStorageStyle('MusicBrainz/Track Id'), 1867 ) 1868 mb_releasetrackid = MediaField( 1869 MP3DescStorageStyle(u'MusicBrainz Release Track Id'), 1870 MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Release Track Id'), 1871 StorageStyle('MUSICBRAINZ_RELEASETRACKID'), 1872 ASFStorageStyle('MusicBrainz/Release Track Id'), 1873 ) 1874 mb_albumid = MediaField( 1875 MP3DescStorageStyle(u'MusicBrainz Album Id'), 1876 MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Id'), 1877 StorageStyle('MUSICBRAINZ_ALBUMID'), 1878 ASFStorageStyle('MusicBrainz/Album Id'), 1879 ) 1880 mb_artistid = MediaField( 1881 MP3DescStorageStyle(u'MusicBrainz Artist Id'), 1882 MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Artist Id'), 1883 StorageStyle('MUSICBRAINZ_ARTISTID'), 1884 ASFStorageStyle('MusicBrainz/Artist Id'), 1885 ) 1886 mb_albumartistid = MediaField( 1887 MP3DescStorageStyle(u'MusicBrainz Album Artist Id'), 1888 MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Artist Id'), 1889 StorageStyle('MUSICBRAINZ_ALBUMARTISTID'), 1890 ASFStorageStyle('MusicBrainz/Album Artist Id'), 1891 ) 1892 mb_releasegroupid = MediaField( 1893 MP3DescStorageStyle(u'MusicBrainz Release Group Id'), 1894 MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Release Group Id'), 1895 StorageStyle('MUSICBRAINZ_RELEASEGROUPID'), 1896 ASFStorageStyle('MusicBrainz/Release Group Id'), 1897 ) 1898 1899 # Acoustid fields. 1900 acoustid_fingerprint = MediaField( 1901 MP3DescStorageStyle(u'Acoustid Fingerprint'), 1902 MP4StorageStyle('----:com.apple.iTunes:Acoustid Fingerprint'), 1903 StorageStyle('ACOUSTID_FINGERPRINT'), 1904 ASFStorageStyle('Acoustid/Fingerprint'), 1905 ) 1906 acoustid_id = MediaField( 1907 MP3DescStorageStyle(u'Acoustid Id'), 1908 MP4StorageStyle('----:com.apple.iTunes:Acoustid Id'), 1909 StorageStyle('ACOUSTID_ID'), 1910 ASFStorageStyle('Acoustid/Id'), 1911 ) 1912 1913 # ReplayGain fields. 1914 rg_track_gain = MediaField( 1915 MP3DescStorageStyle( 1916 u'REPLAYGAIN_TRACK_GAIN', 1917 float_places=2, suffix=u' dB' 1918 ), 1919 MP3DescStorageStyle( 1920 u'replaygain_track_gain', 1921 float_places=2, suffix=u' dB' 1922 ), 1923 MP3SoundCheckStorageStyle( 1924 key='COMM', 1925 index=0, desc=u'iTunNORM', 1926 id3_lang='eng' 1927 ), 1928 MP4StorageStyle( 1929 '----:com.apple.iTunes:replaygain_track_gain', 1930 float_places=2, suffix=' dB' 1931 ), 1932 MP4SoundCheckStorageStyle( 1933 '----:com.apple.iTunes:iTunNORM', 1934 index=0 1935 ), 1936 StorageStyle( 1937 u'REPLAYGAIN_TRACK_GAIN', 1938 float_places=2, suffix=u' dB' 1939 ), 1940 ASFStorageStyle( 1941 u'replaygain_track_gain', 1942 float_places=2, suffix=u' dB' 1943 ), 1944 out_type=float 1945 ) 1946 rg_album_gain = MediaField( 1947 MP3DescStorageStyle( 1948 u'REPLAYGAIN_ALBUM_GAIN', 1949 float_places=2, suffix=u' dB' 1950 ), 1951 MP3DescStorageStyle( 1952 u'replaygain_album_gain', 1953 float_places=2, suffix=u' dB' 1954 ), 1955 MP4StorageStyle( 1956 '----:com.apple.iTunes:replaygain_album_gain', 1957 float_places=2, suffix=' dB' 1958 ), 1959 StorageStyle( 1960 u'REPLAYGAIN_ALBUM_GAIN', 1961 float_places=2, suffix=u' dB' 1962 ), 1963 ASFStorageStyle( 1964 u'replaygain_album_gain', 1965 float_places=2, suffix=u' dB' 1966 ), 1967 out_type=float 1968 ) 1969 rg_track_peak = MediaField( 1970 MP3DescStorageStyle( 1971 u'REPLAYGAIN_TRACK_PEAK', 1972 float_places=6 1973 ), 1974 MP3DescStorageStyle( 1975 u'replaygain_track_peak', 1976 float_places=6 1977 ), 1978 MP3SoundCheckStorageStyle( 1979 key=u'COMM', 1980 index=1, desc=u'iTunNORM', 1981 id3_lang='eng' 1982 ), 1983 MP4StorageStyle( 1984 '----:com.apple.iTunes:replaygain_track_peak', 1985 float_places=6 1986 ), 1987 MP4SoundCheckStorageStyle( 1988 '----:com.apple.iTunes:iTunNORM', 1989 index=1 1990 ), 1991 StorageStyle(u'REPLAYGAIN_TRACK_PEAK', float_places=6), 1992 ASFStorageStyle(u'replaygain_track_peak', float_places=6), 1993 out_type=float, 1994 ) 1995 rg_album_peak = MediaField( 1996 MP3DescStorageStyle( 1997 u'REPLAYGAIN_ALBUM_PEAK', 1998 float_places=6 1999 ), 2000 MP3DescStorageStyle( 2001 u'replaygain_album_peak', 2002 float_places=6 2003 ), 2004 MP4StorageStyle( 2005 '----:com.apple.iTunes:replaygain_album_peak', 2006 float_places=6 2007 ), 2008 StorageStyle(u'REPLAYGAIN_ALBUM_PEAK', float_places=6), 2009 ASFStorageStyle(u'replaygain_album_peak', float_places=6), 2010 out_type=float, 2011 ) 2012 2013 # EBU R128 fields. 2014 r128_track_gain = MediaField( 2015 MP3DescStorageStyle( 2016 u'R128_TRACK_GAIN' 2017 ), 2018 MP4StorageStyle( 2019 '----:com.apple.iTunes:R128_TRACK_GAIN' 2020 ), 2021 StorageStyle( 2022 u'R128_TRACK_GAIN' 2023 ), 2024 ASFStorageStyle( 2025 u'R128_TRACK_GAIN' 2026 ), 2027 out_type=int, 2028 ) 2029 r128_album_gain = MediaField( 2030 MP3DescStorageStyle( 2031 u'R128_ALBUM_GAIN' 2032 ), 2033 MP4StorageStyle( 2034 '----:com.apple.iTunes:R128_ALBUM_GAIN' 2035 ), 2036 StorageStyle( 2037 u'R128_ALBUM_GAIN' 2038 ), 2039 ASFStorageStyle( 2040 u'R128_ALBUM_GAIN' 2041 ), 2042 out_type=int, 2043 ) 2044 2045 initial_key = MediaField( 2046 MP3StorageStyle('TKEY'), 2047 MP4StorageStyle('----:com.apple.iTunes:initialkey'), 2048 StorageStyle('INITIALKEY'), 2049 ASFStorageStyle('INITIALKEY'), 2050 ) 2051 2052 @property 2053 def length(self): 2054 """The duration of the audio in seconds (a float).""" 2055 return self.mgfile.info.length 2056 2057 @property 2058 def samplerate(self): 2059 """The audio's sample rate (an int).""" 2060 if hasattr(self.mgfile.info, 'sample_rate'): 2061 return self.mgfile.info.sample_rate 2062 elif self.type == 'opus': 2063 # Opus is always 48kHz internally. 2064 return 48000 2065 return 0 2066 2067 @property 2068 def bitdepth(self): 2069 """The number of bits per sample in the audio encoding (an int). 2070 Only available for certain file formats (zero where 2071 unavailable). 2072 """ 2073 if hasattr(self.mgfile.info, 'bits_per_sample'): 2074 return self.mgfile.info.bits_per_sample 2075 return 0 2076 2077 @property 2078 def channels(self): 2079 """The number of channels in the audio (an int).""" 2080 if hasattr(self.mgfile.info, 'channels'): 2081 return self.mgfile.info.channels 2082 return 0 2083 2084 @property 2085 def bitrate(self): 2086 """The number of bits per seconds used in the audio coding (an 2087 int). If this is provided explicitly by the compressed file 2088 format, this is a precise reflection of the encoding. Otherwise, 2089 it is estimated from the on-disk file size. In this case, some 2090 imprecision is possible because the file header is incorporated 2091 in the file size. 2092 """ 2093 if hasattr(self.mgfile.info, 'bitrate') and self.mgfile.info.bitrate: 2094 # Many formats provide it explicitly. 2095 return self.mgfile.info.bitrate 2096 else: 2097 # Otherwise, we calculate bitrate from the file size. (This 2098 # is the case for all of the lossless formats.) 2099 if not self.length: 2100 # Avoid division by zero if length is not available. 2101 return 0 2102 size = os.path.getsize(self.path) 2103 return int(size * 8 / self.length) 2104 2105 @property 2106 def format(self): 2107 """A string describing the file format/codec.""" 2108 return TYPES[self.type] 2109