1# -*- coding: utf-8 -*- 2# Copyright (C) 2009 Joe Wreschnig 3# 4# This program is free software; you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation; either version 2 of the License, or 7# (at your option) any later version. 8 9from mutagen import Tags 10from mutagen._util import DictMixin, dict_match 11from mutagen.mp4 import MP4, MP4Tags, error, delete 12from ._compat import PY2, text_type, PY3 13 14 15__all__ = ["EasyMP4Tags", "EasyMP4", "delete", "error"] 16 17 18class EasyMP4KeyError(error, KeyError, ValueError): 19 pass 20 21 22class EasyMP4Tags(DictMixin, Tags): 23 """EasyMP4Tags() 24 25 A file with MPEG-4 iTunes metadata. 26 27 Like Vorbis comments, EasyMP4Tags keys are case-insensitive ASCII 28 strings, and values are a list of Unicode strings (and these lists 29 are always of length 0 or 1). 30 31 If you need access to the full MP4 metadata feature set, you should use 32 MP4, not EasyMP4. 33 """ 34 35 Set = {} 36 Get = {} 37 Delete = {} 38 List = {} 39 40 def __init__(self, *args, **kwargs): 41 self.__mp4 = MP4Tags(*args, **kwargs) 42 self.load = self.__mp4.load 43 self.save = self.__mp4.save 44 self.delete = self.__mp4.delete 45 46 filename = property(lambda s: s.__mp4.filename, 47 lambda s, fn: setattr(s.__mp4, 'filename', fn)) 48 49 @property 50 def _padding(self): 51 return self.__mp4._padding 52 53 @classmethod 54 def RegisterKey(cls, key, 55 getter=None, setter=None, deleter=None, lister=None): 56 """Register a new key mapping. 57 58 A key mapping is four functions, a getter, setter, deleter, 59 and lister. The key may be either a string or a glob pattern. 60 61 The getter, deleted, and lister receive an MP4Tags instance 62 and the requested key name. The setter also receives the 63 desired value, which will be a list of strings. 64 65 The getter, setter, and deleter are used to implement __getitem__, 66 __setitem__, and __delitem__. 67 68 The lister is used to implement keys(). It should return a 69 list of keys that are actually in the MP4 instance, provided 70 by its associated getter. 71 """ 72 key = key.lower() 73 if getter is not None: 74 cls.Get[key] = getter 75 if setter is not None: 76 cls.Set[key] = setter 77 if deleter is not None: 78 cls.Delete[key] = deleter 79 if lister is not None: 80 cls.List[key] = lister 81 82 @classmethod 83 def RegisterTextKey(cls, key, atomid): 84 """Register a text key. 85 86 If the key you need to register is a simple one-to-one mapping 87 of MP4 atom name to EasyMP4Tags key, then you can use this 88 function:: 89 90 EasyMP4Tags.RegisterTextKey("artist", "\xa9ART") 91 """ 92 def getter(tags, key): 93 return tags[atomid] 94 95 def setter(tags, key, value): 96 tags[atomid] = value 97 98 def deleter(tags, key): 99 del(tags[atomid]) 100 101 cls.RegisterKey(key, getter, setter, deleter) 102 103 @classmethod 104 def RegisterIntKey(cls, key, atomid, min_value=0, max_value=(2 ** 16) - 1): 105 """Register a scalar integer key. 106 """ 107 108 def getter(tags, key): 109 return list(map(text_type, tags[atomid])) 110 111 def setter(tags, key, value): 112 clamp = lambda x: int(min(max(min_value, x), max_value)) 113 tags[atomid] = [clamp(v) for v in map(int, value)] 114 115 def deleter(tags, key): 116 del(tags[atomid]) 117 118 cls.RegisterKey(key, getter, setter, deleter) 119 120 @classmethod 121 def RegisterIntPairKey(cls, key, atomid, min_value=0, 122 max_value=(2 ** 16) - 1): 123 def getter(tags, key): 124 ret = [] 125 for (track, total) in tags[atomid]: 126 if total: 127 ret.append(u"%d/%d" % (track, total)) 128 else: 129 ret.append(text_type(track)) 130 return ret 131 132 def setter(tags, key, value): 133 clamp = lambda x: int(min(max(min_value, x), max_value)) 134 data = [] 135 for v in value: 136 try: 137 tracks, total = v.split("/") 138 tracks = clamp(int(tracks)) 139 total = clamp(int(total)) 140 except (ValueError, TypeError): 141 tracks = clamp(int(v)) 142 total = min_value 143 data.append((tracks, total)) 144 tags[atomid] = data 145 146 def deleter(tags, key): 147 del(tags[atomid]) 148 149 cls.RegisterKey(key, getter, setter, deleter) 150 151 @classmethod 152 def RegisterFreeformKey(cls, key, name, mean="com.apple.iTunes"): 153 """Register a text key. 154 155 If the key you need to register is a simple one-to-one mapping 156 of MP4 freeform atom (----) and name to EasyMP4Tags key, then 157 you can use this function:: 158 159 EasyMP4Tags.RegisterFreeformKey( 160 "musicbrainz_artistid", "MusicBrainz Artist Id") 161 """ 162 atomid = "----:" + mean + ":" + name 163 164 def getter(tags, key): 165 return [s.decode("utf-8", "replace") for s in tags[atomid]] 166 167 def setter(tags, key, value): 168 encoded = [] 169 for v in value: 170 if not isinstance(v, text_type): 171 if PY3: 172 raise TypeError("%r not str" % v) 173 v = v.decode("utf-8") 174 encoded.append(v.encode("utf-8")) 175 tags[atomid] = encoded 176 177 def deleter(tags, key): 178 del(tags[atomid]) 179 180 cls.RegisterKey(key, getter, setter, deleter) 181 182 def __getitem__(self, key): 183 key = key.lower() 184 func = dict_match(self.Get, key) 185 if func is not None: 186 return func(self.__mp4, key) 187 else: 188 raise EasyMP4KeyError("%r is not a valid key" % key) 189 190 def __setitem__(self, key, value): 191 key = key.lower() 192 193 if PY2: 194 if isinstance(value, basestring): 195 value = [value] 196 else: 197 if isinstance(value, text_type): 198 value = [value] 199 200 func = dict_match(self.Set, key) 201 if func is not None: 202 return func(self.__mp4, key, value) 203 else: 204 raise EasyMP4KeyError("%r is not a valid key" % key) 205 206 def __delitem__(self, key): 207 key = key.lower() 208 func = dict_match(self.Delete, key) 209 if func is not None: 210 return func(self.__mp4, key) 211 else: 212 raise EasyMP4KeyError("%r is not a valid key" % key) 213 214 def keys(self): 215 keys = [] 216 for key in self.Get.keys(): 217 if key in self.List: 218 keys.extend(self.List[key](self.__mp4, key)) 219 elif key in self: 220 keys.append(key) 221 return keys 222 223 def pprint(self): 224 """Print tag key=value pairs.""" 225 strings = [] 226 for key in sorted(self.keys()): 227 values = self[key] 228 for value in values: 229 strings.append("%s=%s" % (key, value)) 230 return "\n".join(strings) 231 232for atomid, key in { 233 '\xa9nam': 'title', 234 '\xa9alb': 'album', 235 '\xa9ART': 'artist', 236 'aART': 'albumartist', 237 '\xa9day': 'date', 238 '\xa9cmt': 'comment', 239 'desc': 'description', 240 '\xa9grp': 'grouping', 241 '\xa9gen': 'genre', 242 'cprt': 'copyright', 243 'soal': 'albumsort', 244 'soaa': 'albumartistsort', 245 'soar': 'artistsort', 246 'sonm': 'titlesort', 247 'soco': 'composersort', 248}.items(): 249 EasyMP4Tags.RegisterTextKey(key, atomid) 250 251for name, key in { 252 'MusicBrainz Artist Id': 'musicbrainz_artistid', 253 'MusicBrainz Track Id': 'musicbrainz_trackid', 254 'MusicBrainz Album Id': 'musicbrainz_albumid', 255 'MusicBrainz Album Artist Id': 'musicbrainz_albumartistid', 256 'MusicIP PUID': 'musicip_puid', 257 'MusicBrainz Album Status': 'musicbrainz_albumstatus', 258 'MusicBrainz Album Type': 'musicbrainz_albumtype', 259 'MusicBrainz Release Country': 'releasecountry', 260}.items(): 261 EasyMP4Tags.RegisterFreeformKey(key, name) 262 263for name, key in { 264 "tmpo": "bpm", 265}.items(): 266 EasyMP4Tags.RegisterIntKey(key, name) 267 268for name, key in { 269 "trkn": "tracknumber", 270 "disk": "discnumber", 271}.items(): 272 EasyMP4Tags.RegisterIntPairKey(key, name) 273 274 275class EasyMP4(MP4): 276 """EasyMP4(filelike) 277 278 Like :class:`MP4 <mutagen.mp4.MP4>`, but uses :class:`EasyMP4Tags` for 279 tags. 280 281 Attributes: 282 info (`mutagen.mp4.MP4Info`) 283 tags (`EasyMP4Tags`) 284 """ 285 286 MP4Tags = EasyMP4Tags 287 288 Get = EasyMP4Tags.Get 289 Set = EasyMP4Tags.Set 290 Delete = EasyMP4Tags.Delete 291 List = EasyMP4Tags.List 292 RegisterTextKey = EasyMP4Tags.RegisterTextKey 293 RegisterKey = EasyMP4Tags.RegisterKey 294