1# -*- coding: utf-8 -*- 2# Copyright (C) 2006 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 9"""Easier access to ID3 tags. 10 11EasyID3 is a wrapper around mutagen.id3.ID3 to make ID3 tags appear 12more like Vorbis or APEv2 tags. 13""" 14 15import mutagen.id3 16 17from ._compat import iteritems, text_type, PY2 18from mutagen import Metadata 19from mutagen._util import DictMixin, dict_match, loadfile 20from mutagen.id3 import ID3, error, delete, ID3FileType 21 22 23__all__ = ['EasyID3', 'Open', 'delete'] 24 25 26class EasyID3KeyError(KeyError, ValueError, error): 27 """Raised when trying to get/set an invalid key. 28 29 Subclasses both KeyError and ValueError for API compatibility, 30 catching KeyError is preferred. 31 """ 32 33 34class EasyID3(DictMixin, Metadata): 35 """EasyID3(filething=None) 36 37 A file with an ID3 tag. 38 39 Like Vorbis comments, EasyID3 keys are case-insensitive ASCII 40 strings. Only a subset of ID3 frames are supported by default. Use 41 EasyID3.RegisterKey and its wrappers to support more. 42 43 You can also set the GetFallback, SetFallback, and DeleteFallback 44 to generic key getter/setter/deleter functions, which are called 45 if no specific handler is registered for a key. Additionally, 46 ListFallback can be used to supply an arbitrary list of extra 47 keys. These can be set on EasyID3 or on individual instances after 48 creation. 49 50 To use an EasyID3 class with mutagen.mp3.MP3:: 51 52 from mutagen.mp3 import EasyMP3 as MP3 53 MP3(filename) 54 55 Because many of the attributes are constructed on the fly, things 56 like the following will not work:: 57 58 ezid3["performer"].append("Joe") 59 60 Instead, you must do:: 61 62 values = ezid3["performer"] 63 values.append("Joe") 64 ezid3["performer"] = values 65 66 """ 67 68 Set = {} 69 Get = {} 70 Delete = {} 71 List = {} 72 73 # For compatibility. 74 valid_keys = Get 75 76 GetFallback = None 77 SetFallback = None 78 DeleteFallback = None 79 ListFallback = None 80 81 @classmethod 82 def RegisterKey(cls, key, 83 getter=None, setter=None, deleter=None, lister=None): 84 """Register a new key mapping. 85 86 A key mapping is four functions, a getter, setter, deleter, 87 and lister. The key may be either a string or a glob pattern. 88 89 The getter, deleted, and lister receive an ID3 instance and 90 the requested key name. The setter also receives the desired 91 value, which will be a list of strings. 92 93 The getter, setter, and deleter are used to implement __getitem__, 94 __setitem__, and __delitem__. 95 96 The lister is used to implement keys(). It should return a 97 list of keys that are actually in the ID3 instance, provided 98 by its associated getter. 99 """ 100 key = key.lower() 101 if getter is not None: 102 cls.Get[key] = getter 103 if setter is not None: 104 cls.Set[key] = setter 105 if deleter is not None: 106 cls.Delete[key] = deleter 107 if lister is not None: 108 cls.List[key] = lister 109 110 @classmethod 111 def RegisterTextKey(cls, key, frameid): 112 """Register a text key. 113 114 If the key you need to register is a simple one-to-one mapping 115 of ID3 frame name to EasyID3 key, then you can use this 116 function:: 117 118 EasyID3.RegisterTextKey("title", "TIT2") 119 """ 120 def getter(id3, key): 121 return list(id3[frameid]) 122 123 def setter(id3, key, value): 124 try: 125 frame = id3[frameid] 126 except KeyError: 127 id3.add(mutagen.id3.Frames[frameid](encoding=3, text=value)) 128 else: 129 frame.encoding = 3 130 frame.text = value 131 132 def deleter(id3, key): 133 del(id3[frameid]) 134 135 cls.RegisterKey(key, getter, setter, deleter) 136 137 @classmethod 138 def RegisterTXXXKey(cls, key, desc): 139 """Register a user-defined text frame key. 140 141 Some ID3 tags are stored in TXXX frames, which allow a 142 freeform 'description' which acts as a subkey, 143 e.g. TXXX:BARCODE.:: 144 145 EasyID3.RegisterTXXXKey('barcode', 'BARCODE'). 146 """ 147 frameid = "TXXX:" + desc 148 149 def getter(id3, key): 150 return list(id3[frameid]) 151 152 def setter(id3, key, value): 153 enc = 0 154 # Store 8859-1 if we can, per MusicBrainz spec. 155 for v in value: 156 if v and max(v) > u'\x7f': 157 enc = 3 158 break 159 160 id3.add(mutagen.id3.TXXX(encoding=enc, text=value, desc=desc)) 161 162 def deleter(id3, key): 163 del(id3[frameid]) 164 165 cls.RegisterKey(key, getter, setter, deleter) 166 167 def __init__(self, filename=None): 168 self.__id3 = ID3() 169 if filename is not None: 170 self.load(filename) 171 172 load = property(lambda s: s.__id3.load, 173 lambda s, v: setattr(s.__id3, 'load', v)) 174 175 @loadfile(writable=True, create=True) 176 def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', 177 padding=None): 178 """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None) 179 180 Save changes to a file. 181 See :meth:`mutagen.id3.ID3.save` for more info. 182 """ 183 184 if v2_version == 3: 185 # EasyID3 only works with v2.4 frames, so update_to_v23() would 186 # break things. We have to save a shallow copy of all tags 187 # and restore it after saving. Due to CHAP/CTOC copying has 188 # to be done recursively implemented in ID3Tags. 189 backup = self.__id3._copy() 190 try: 191 self.__id3.update_to_v23() 192 self.__id3.save( 193 filething, v1=v1, v2_version=v2_version, v23_sep=v23_sep, 194 padding=padding) 195 finally: 196 self.__id3._restore(backup) 197 else: 198 self.__id3.save(filething, v1=v1, v2_version=v2_version, 199 v23_sep=v23_sep, padding=padding) 200 201 delete = property(lambda s: s.__id3.delete, 202 lambda s, v: setattr(s.__id3, 'delete', v)) 203 204 filename = property(lambda s: s.__id3.filename, 205 lambda s, fn: setattr(s.__id3, 'filename', fn)) 206 207 @property 208 def size(self): 209 return self.__id3.size 210 211 def __getitem__(self, key): 212 func = dict_match(self.Get, key.lower(), self.GetFallback) 213 if func is not None: 214 return func(self.__id3, key) 215 else: 216 raise EasyID3KeyError("%r is not a valid key" % key) 217 218 def __setitem__(self, key, value): 219 if PY2: 220 if isinstance(value, basestring): 221 value = [value] 222 else: 223 if isinstance(value, text_type): 224 value = [value] 225 func = dict_match(self.Set, key.lower(), self.SetFallback) 226 if func is not None: 227 return func(self.__id3, key, value) 228 else: 229 raise EasyID3KeyError("%r is not a valid key" % key) 230 231 def __delitem__(self, key): 232 func = dict_match(self.Delete, key.lower(), self.DeleteFallback) 233 if func is not None: 234 return func(self.__id3, key) 235 else: 236 raise EasyID3KeyError("%r is not a valid key" % key) 237 238 def keys(self): 239 keys = [] 240 for key in self.Get.keys(): 241 if key in self.List: 242 keys.extend(self.List[key](self.__id3, key)) 243 elif key in self: 244 keys.append(key) 245 if self.ListFallback is not None: 246 keys.extend(self.ListFallback(self.__id3, "")) 247 return keys 248 249 def pprint(self): 250 """Print tag key=value pairs.""" 251 strings = [] 252 for key in sorted(self.keys()): 253 values = self[key] 254 for value in values: 255 strings.append("%s=%s" % (key, value)) 256 return "\n".join(strings) 257 258 259Open = EasyID3 260 261 262def genre_get(id3, key): 263 return id3["TCON"].genres 264 265 266def genre_set(id3, key, value): 267 try: 268 frame = id3["TCON"] 269 except KeyError: 270 id3.add(mutagen.id3.TCON(encoding=3, text=value)) 271 else: 272 frame.encoding = 3 273 frame.genres = value 274 275 276def genre_delete(id3, key): 277 del(id3["TCON"]) 278 279 280def date_get(id3, key): 281 return [stamp.text for stamp in id3["TDRC"].text] 282 283 284def date_set(id3, key, value): 285 id3.add(mutagen.id3.TDRC(encoding=3, text=value)) 286 287 288def date_delete(id3, key): 289 del(id3["TDRC"]) 290 291 292def original_date_get(id3, key): 293 return [stamp.text for stamp in id3["TDOR"].text] 294 295 296def original_date_set(id3, key, value): 297 id3.add(mutagen.id3.TDOR(encoding=3, text=value)) 298 299 300def original_date_delete(id3, key): 301 del(id3["TDOR"]) 302 303 304def performer_get(id3, key): 305 people = [] 306 wanted_role = key.split(":", 1)[1] 307 try: 308 mcl = id3["TMCL"] 309 except KeyError: 310 raise KeyError(key) 311 for role, person in mcl.people: 312 if role == wanted_role: 313 people.append(person) 314 if people: 315 return people 316 else: 317 raise KeyError(key) 318 319 320def performer_set(id3, key, value): 321 wanted_role = key.split(":", 1)[1] 322 try: 323 mcl = id3["TMCL"] 324 except KeyError: 325 mcl = mutagen.id3.TMCL(encoding=3, people=[]) 326 id3.add(mcl) 327 mcl.encoding = 3 328 people = [p for p in mcl.people if p[0] != wanted_role] 329 for v in value: 330 people.append((wanted_role, v)) 331 mcl.people = people 332 333 334def performer_delete(id3, key): 335 wanted_role = key.split(":", 1)[1] 336 try: 337 mcl = id3["TMCL"] 338 except KeyError: 339 raise KeyError(key) 340 people = [p for p in mcl.people if p[0] != wanted_role] 341 if people == mcl.people: 342 raise KeyError(key) 343 elif people: 344 mcl.people = people 345 else: 346 del(id3["TMCL"]) 347 348 349def performer_list(id3, key): 350 try: 351 mcl = id3["TMCL"] 352 except KeyError: 353 return [] 354 else: 355 return list(set("performer:" + p[0] for p in mcl.people)) 356 357 358def musicbrainz_trackid_get(id3, key): 359 return [id3["UFID:http://musicbrainz.org"].data.decode('ascii')] 360 361 362def musicbrainz_trackid_set(id3, key, value): 363 if len(value) != 1: 364 raise ValueError("only one track ID may be set per song") 365 value = value[0].encode('ascii') 366 try: 367 frame = id3["UFID:http://musicbrainz.org"] 368 except KeyError: 369 frame = mutagen.id3.UFID(owner="http://musicbrainz.org", data=value) 370 id3.add(frame) 371 else: 372 frame.data = value 373 374 375def musicbrainz_trackid_delete(id3, key): 376 del(id3["UFID:http://musicbrainz.org"]) 377 378 379def website_get(id3, key): 380 urls = [frame.url for frame in id3.getall("WOAR")] 381 if urls: 382 return urls 383 else: 384 raise EasyID3KeyError(key) 385 386 387def website_set(id3, key, value): 388 id3.delall("WOAR") 389 for v in value: 390 id3.add(mutagen.id3.WOAR(url=v)) 391 392 393def website_delete(id3, key): 394 id3.delall("WOAR") 395 396 397def gain_get(id3, key): 398 try: 399 frame = id3["RVA2:" + key[11:-5]] 400 except KeyError: 401 raise EasyID3KeyError(key) 402 else: 403 return [u"%+f dB" % frame.gain] 404 405 406def gain_set(id3, key, value): 407 if len(value) != 1: 408 raise ValueError( 409 "there must be exactly one gain value, not %r.", value) 410 gain = float(value[0].split()[0]) 411 try: 412 frame = id3["RVA2:" + key[11:-5]] 413 except KeyError: 414 frame = mutagen.id3.RVA2(desc=key[11:-5], gain=0, peak=0, channel=1) 415 id3.add(frame) 416 frame.gain = gain 417 418 419def gain_delete(id3, key): 420 try: 421 frame = id3["RVA2:" + key[11:-5]] 422 except KeyError: 423 pass 424 else: 425 if frame.peak: 426 frame.gain = 0.0 427 else: 428 del(id3["RVA2:" + key[11:-5]]) 429 430 431def peak_get(id3, key): 432 try: 433 frame = id3["RVA2:" + key[11:-5]] 434 except KeyError: 435 raise EasyID3KeyError(key) 436 else: 437 return [u"%f" % frame.peak] 438 439 440def peak_set(id3, key, value): 441 if len(value) != 1: 442 raise ValueError( 443 "there must be exactly one peak value, not %r.", value) 444 peak = float(value[0]) 445 if peak >= 2 or peak < 0: 446 raise ValueError("peak must be => 0 and < 2.") 447 try: 448 frame = id3["RVA2:" + key[11:-5]] 449 except KeyError: 450 frame = mutagen.id3.RVA2(desc=key[11:-5], gain=0, peak=0, channel=1) 451 id3.add(frame) 452 frame.peak = peak 453 454 455def peak_delete(id3, key): 456 try: 457 frame = id3["RVA2:" + key[11:-5]] 458 except KeyError: 459 pass 460 else: 461 if frame.gain: 462 frame.peak = 0.0 463 else: 464 del(id3["RVA2:" + key[11:-5]]) 465 466 467def peakgain_list(id3, key): 468 keys = [] 469 for frame in id3.getall("RVA2"): 470 keys.append("replaygain_%s_gain" % frame.desc) 471 keys.append("replaygain_%s_peak" % frame.desc) 472 return keys 473 474for frameid, key in iteritems({ 475 "TALB": "album", 476 "TBPM": "bpm", 477 "TCMP": "compilation", # iTunes extension 478 "TCOM": "composer", 479 "TCOP": "copyright", 480 "TENC": "encodedby", 481 "TEXT": "lyricist", 482 "TLEN": "length", 483 "TMED": "media", 484 "TMOO": "mood", 485 "TIT2": "title", 486 "TIT3": "version", 487 "TPE1": "artist", 488 "TPE2": "albumartist", 489 "TPE3": "conductor", 490 "TPE4": "arranger", 491 "TPOS": "discnumber", 492 "TPUB": "organization", 493 "TRCK": "tracknumber", 494 "TOLY": "author", 495 "TSO2": "albumartistsort", # iTunes extension 496 "TSOA": "albumsort", 497 "TSOC": "composersort", # iTunes extension 498 "TSOP": "artistsort", 499 "TSOT": "titlesort", 500 "TSRC": "isrc", 501 "TSST": "discsubtitle", 502 "TLAN": "language", 503}): 504 EasyID3.RegisterTextKey(key, frameid) 505 506EasyID3.RegisterKey("genre", genre_get, genre_set, genre_delete) 507EasyID3.RegisterKey("date", date_get, date_set, date_delete) 508EasyID3.RegisterKey("originaldate", original_date_get, original_date_set, 509 original_date_delete) 510EasyID3.RegisterKey( 511 "performer:*", performer_get, performer_set, performer_delete, 512 performer_list) 513EasyID3.RegisterKey("musicbrainz_trackid", musicbrainz_trackid_get, 514 musicbrainz_trackid_set, musicbrainz_trackid_delete) 515EasyID3.RegisterKey("website", website_get, website_set, website_delete) 516EasyID3.RegisterKey( 517 "replaygain_*_gain", gain_get, gain_set, gain_delete, peakgain_list) 518EasyID3.RegisterKey("replaygain_*_peak", peak_get, peak_set, peak_delete) 519 520# At various times, information for this came from 521# http://musicbrainz.org/docs/specs/metadata_tags.html 522# http://bugs.musicbrainz.org/ticket/1383 523# http://musicbrainz.org/doc/MusicBrainzTag 524for desc, key in iteritems({ 525 u"MusicBrainz Artist Id": "musicbrainz_artistid", 526 u"MusicBrainz Album Id": "musicbrainz_albumid", 527 u"MusicBrainz Album Artist Id": "musicbrainz_albumartistid", 528 u"MusicBrainz TRM Id": "musicbrainz_trmid", 529 u"MusicIP PUID": "musicip_puid", 530 u"MusicMagic Fingerprint": "musicip_fingerprint", 531 u"MusicBrainz Album Status": "musicbrainz_albumstatus", 532 u"MusicBrainz Album Type": "musicbrainz_albumtype", 533 u"MusicBrainz Album Release Country": "releasecountry", 534 u"MusicBrainz Disc Id": "musicbrainz_discid", 535 u"ASIN": "asin", 536 u"ALBUMARTISTSORT": "albumartistsort", 537 u"PERFORMER": "performer", 538 u"BARCODE": "barcode", 539 u"CATALOGNUMBER": "catalognumber", 540 u"MusicBrainz Release Track Id": "musicbrainz_releasetrackid", 541 u"MusicBrainz Release Group Id": "musicbrainz_releasegroupid", 542 u"MusicBrainz Work Id": "musicbrainz_workid", 543 u"Acoustid Fingerprint": "acoustid_fingerprint", 544 u"Acoustid Id": "acoustid_id", 545}): 546 EasyID3.RegisterTXXXKey(key, desc) 547 548 549class EasyID3FileType(ID3FileType): 550 """EasyID3FileType(filething=None) 551 552 Like ID3FileType, but uses EasyID3 for tags. 553 554 Arguments: 555 filething (filething) 556 557 Attributes: 558 tags (`EasyID3`) 559 """ 560 561 ID3 = EasyID3 562