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