# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. """Handles low-level interfacing for files' tags. Wraps Mutagen to automatically detect file types and provide a unified interface for a useful subset of music files' tags. Usage: >>> f = MediaFile('Lucy.mp3') >>> f.title u'Lucy in the Sky with Diamonds' >>> f.artist = 'The Beatles' >>> f.save() A field will always return a reasonable value of the correct type, even if no tag is present. If no value is available, the value will be false (e.g., zero or the empty string). """ import mutagen import mutagen.mp3 import mutagen.oggvorbis import mutagen.mp4 import mutagen.flac import mutagen.monkeysaudio import mutagen.asf import datetime import re import base64 import struct import imghdr import os import logging import traceback from beets.util.enumeration import enum __all__ = ['UnreadableFileError', 'FileTypeError', 'MediaFile'] # Logger. log = logging.getLogger('beets') # Exceptions. # Raised for any file MediaFile can't read. class UnreadableFileError(IOError): pass # Raised for files that don't seem to have a type MediaFile supports. class FileTypeError(UnreadableFileError): pass # Constants. # Human-readable type names. TYPES = { 'mp3': 'MP3', 'mp4': 'AAC', 'ogg': 'OGG', 'flac': 'FLAC', 'ape': 'APE', 'wv': 'WavPack', 'mpc': 'Musepack', 'asf': 'Windows Media', } # Utility. def _safe_cast(out_type, val): """Tries to covert val to out_type but will never raise an exception. If the value can't be converted, then a sensible default value is returned. out_type should be bool, int, or unicode; otherwise, the value is just passed through. """ if out_type == int: if val is None: return 0 elif isinstance(val, int) or isinstance(val, float): # Just a number. return int(val) else: # Process any other type as a string. if not isinstance(val, basestring): val = unicode(val) # Get a number from the front of the string. val = re.match(r'[0-9]*', val.strip()).group(0) if not val: return 0 else: return int(val) elif out_type == bool: if val is None: return False else: try: if isinstance(val, mutagen.asf.ASFBoolAttribute): return val.value else: # Should work for strings, bools, ints: return bool(int(val)) except ValueError: return False elif out_type == unicode: if val is None: return u'' else: if isinstance(val, str): return val.decode('utf8', 'ignore') elif isinstance(val, unicode): return val else: return unicode(val) elif out_type == float: if val is None: return 0.0 elif isinstance(val, int) or isinstance(val, float): return float(val) else: if not isinstance(val, basestring): val = unicode(val) val = re.match(r'[\+-]?[0-9\.]*', val.strip()).group(0) if not val: return 0.0 else: return float(val) else: return val # Image coding for ASF/WMA. def _unpack_asf_image(data): """Unpack image data from a WM/Picture tag. Return a tuple containing the MIME type, the raw image data, a type indicator, and the image's description. This function is treated as "untrusted" and could throw all manner of exceptions (out-of-bounds, etc.). We should clean this up sometime so that the failure modes are well-defined. """ type, size = struct.unpack_from("