# Copyright 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, utf8 from mutagen.mp4 import MP4, MP4Tags, error, delete from ._compat import PY2, text_type __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] = list(map(clamp, 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=b"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 = b"----:" + mean + b":" + name def getter(tags, key): return [s.decode("utf-8", "replace") for s in tags[atomid]] def setter(tags, key, value): tags[atomid] = [utf8(v) for v in value] 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 { b'\xa9nam': 'title', b'\xa9alb': 'album', b'\xa9ART': 'artist', b'aART': 'albumartist', b'\xa9day': 'date', b'\xa9cmt': 'comment', b'desc': 'description', b'\xa9grp': 'grouping', b'\xa9gen': 'genre', b'cprt': 'copyright', b'soal': 'albumsort', b'soaa': 'albumartistsort', b'soar': 'artistsort', b'sonm': 'titlesort', b'soco': 'composersort', }.items(): EasyMP4Tags.RegisterTextKey(key, atomid) for name, key in { b'MusicBrainz Artist Id': 'musicbrainz_artistid', b'MusicBrainz Track Id': 'musicbrainz_trackid', b'MusicBrainz Album Id': 'musicbrainz_albumid', b'MusicBrainz Album Artist Id': 'musicbrainz_albumartistid', b'MusicIP PUID': 'musicip_puid', b'MusicBrainz Album Status': 'musicbrainz_albumstatus', b'MusicBrainz Album Type': 'musicbrainz_albumtype', b'MusicBrainz Release Country': 'releasecountry', }.items(): EasyMP4Tags.RegisterFreeformKey(key, name) for name, key in { b"tmpo": "bpm", }.items(): EasyMP4Tags.RegisterIntKey(key, name) for name, key in { b"trkn": "tracknumber", b"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