# -*- coding: utf-8 -*- # Copyright (C) 2009 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. from mutagen import Metadata from mutagen._util import DictMixin, dict_match from mutagen.mp4 import MP4, MP4Tags, error, delete from ._compat import PY2, text_type, PY3 __all__ = ["EasyMP4Tags", "EasyMP4", "delete", "error"] class EasyMP4KeyError(error, KeyError, ValueError): pass class EasyMP4Tags(DictMixin, Metadata): """A file with MPEG-4 iTunes metadata. Like Vorbis comments, EasyMP4Tags keys are case-insensitive ASCII strings, and values are a list of Unicode strings (and these lists are always of length 0 or 1). If you need access to the full MP4 metadata feature set, you should use MP4, not EasyMP4. """ Set = {} Get = {} Delete = {} List = {} def __init__(self, *args, **kwargs): self.__mp4 = MP4Tags(*args, **kwargs) self.load = self.__mp4.load self.save = self.__mp4.save self.delete = self.__mp4.delete filename = property(lambda s: s.__mp4.filename, lambda s, fn: setattr(s.__mp4, 'filename', fn)) @classmethod def RegisterKey(cls, key, getter=None, setter=None, deleter=None, lister=None): """Register a new key mapping. A key mapping is four functions, a getter, setter, deleter, and lister. The key may be either a string or a glob pattern. The getter, deleted, and lister receive an MP4Tags instance and the requested key name. The setter also receives the desired value, which will be a list of strings. The getter, setter, and deleter are used to implement __getitem__, __setitem__, and __delitem__. The lister is used to implement keys(). It should return a list of keys that are actually in the MP4 instance, provided by its associated getter. """ key = key.lower() if getter is not None: cls.Get[key] = getter if setter is not None: cls.Set[key] = setter if deleter is not None: cls.Delete[key] = deleter if lister is not None: cls.List[key] = lister @classmethod def RegisterTextKey(cls, key, atomid): """Register a text key. If the key you need to register is a simple one-to-one mapping of MP4 atom name to EasyMP4Tags key, then you can use this function:: EasyMP4Tags.RegisterTextKey("artist", "\xa9ART") """ def getter(tags, key): return tags[atomid] def setter(tags, key, value): tags[atomid] = value def deleter(tags, key): del(tags[atomid]) cls.RegisterKey(key, getter, setter, deleter) @classmethod def RegisterIntKey(cls, key, atomid, min_value=0, max_value=(2 ** 16) - 1): """Register a scalar integer key. """ def getter(tags, key): return list(map(text_type, tags[atomid])) def setter(tags, key, value): clamp = lambda x: int(min(max(min_value, x), max_value)) tags[atomid] = [clamp(v) for v in map(int, value)] def deleter(tags, key): del(tags[atomid]) cls.RegisterKey(key, getter, setter, deleter) @classmethod def RegisterIntPairKey(cls, key, atomid, min_value=0, max_value=(2 ** 16) - 1): def getter(tags, key): ret = [] for (track, total) in tags[atomid]: if total: ret.append(u"%d/%d" % (track, total)) else: ret.append(text_type(track)) return ret def setter(tags, key, value): clamp = lambda x: int(min(max(min_value, x), max_value)) data = [] for v in value: try: tracks, total = v.split("/") tracks = clamp(int(tracks)) total = clamp(int(total)) except (ValueError, TypeError): tracks = clamp(int(v)) total = min_value data.append((tracks, total)) tags[atomid] = data def deleter(tags, key): del(tags[atomid]) cls.RegisterKey(key, getter, setter, deleter) @classmethod def RegisterFreeformKey(cls, key, name, mean="com.apple.iTunes"): """Register a text key. If the key you need to register is a simple one-to-one mapping of MP4 freeform atom (----) and name to EasyMP4Tags key, then you can use this function:: EasyMP4Tags.RegisterFreeformKey( "musicbrainz_artistid", "MusicBrainz Artist Id") """ atomid = "----:" + mean + ":" + name def getter(tags, key): return [s.decode("utf-8", "replace") for s in tags[atomid]] def setter(tags, key, value): encoded = [] for v in value: if not isinstance(v, text_type): if PY3: raise TypeError("%r not str" % v) v = v.decode("utf-8") encoded.append(v.encode("utf-8")) tags[atomid] = encoded def deleter(tags, key): del(tags[atomid]) cls.RegisterKey(key, getter, setter, deleter) def __getitem__(self, key): key = key.lower() func = dict_match(self.Get, key) if func is not None: return func(self.__mp4, key) else: raise EasyMP4KeyError("%r is not a valid key" % key) def __setitem__(self, key, value): key = key.lower() if PY2: if isinstance(value, basestring): value = [value] else: if isinstance(value, text_type): value = [value] func = dict_match(self.Set, key) if func is not None: return func(self.__mp4, key, value) else: raise EasyMP4KeyError("%r is not a valid key" % key) def __delitem__(self, key): key = key.lower() func = dict_match(self.Delete, key) if func is not None: return func(self.__mp4, key) else: raise EasyMP4KeyError("%r is not a valid key" % key) def keys(self): keys = [] for key in self.Get.keys(): if key in self.List: keys.extend(self.List[key](self.__mp4, key)) elif key in self: keys.append(key) return keys def pprint(self): """Print tag key=value pairs.""" strings = [] for key in sorted(self.keys()): values = self[key] for value in values: strings.append("%s=%s" % (key, value)) return "\n".join(strings) for atomid, key in { '\xa9nam': 'title', '\xa9alb': 'album', '\xa9ART': 'artist', 'aART': 'albumartist', '\xa9day': 'date', '\xa9cmt': 'comment', 'desc': 'description', '\xa9grp': 'grouping', '\xa9gen': 'genre', 'cprt': 'copyright', 'soal': 'albumsort', 'soaa': 'albumartistsort', 'soar': 'artistsort', 'sonm': 'titlesort', 'soco': 'composersort', }.items(): EasyMP4Tags.RegisterTextKey(key, atomid) for name, key in { 'MusicBrainz Artist Id': 'musicbrainz_artistid', 'MusicBrainz Track Id': 'musicbrainz_trackid', 'MusicBrainz Album Id': 'musicbrainz_albumid', 'MusicBrainz Album Artist Id': 'musicbrainz_albumartistid', 'MusicIP PUID': 'musicip_puid', 'MusicBrainz Album Status': 'musicbrainz_albumstatus', 'MusicBrainz Album Type': 'musicbrainz_albumtype', 'MusicBrainz Release Country': 'releasecountry', }.items(): EasyMP4Tags.RegisterFreeformKey(key, name) for name, key in { "tmpo": "bpm", }.items(): EasyMP4Tags.RegisterIntKey(key, name) for name, key in { "trkn": "tracknumber", "disk": "discnumber", }.items(): EasyMP4Tags.RegisterIntPairKey(key, name) class EasyMP4(MP4): """Like :class:`MP4 `, but uses :class:`EasyMP4Tags` for tags. :ivar info: :class:`MP4Info ` :ivar tags: :class:`EasyMP4Tags` """ MP4Tags = EasyMP4Tags Get = EasyMP4Tags.Get Set = EasyMP4Tags.Set Delete = EasyMP4Tags.Delete List = EasyMP4Tags.List RegisterTextKey = EasyMP4Tags.RegisterTextKey RegisterKey = EasyMP4Tags.RegisterKey