Ratings from .nfo files should no longer be converted to strings.
[pyTivo/wmcbrine.git] / mutagen / easyid3.py
blobb4983839d26dfde1e8df90a9d5a8c79b81cc8b6c
1 # Simpler (but far more limited) API for ID3 editing
2 # Copyright 2006 Joe Wreschnig
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of version 2 of the GNU General Public License as
6 # published by the Free Software Foundation.
8 # $Id: id3.py 3086 2006-04-04 02:13:21Z piman $
10 """Easier access to ID3 tags.
12 EasyID3 is a wrapper around mutagen.id3.ID3 to make ID3 tags appear
13 more like Vorbis or APEv2 tags.
14 """
16 from fnmatch import fnmatchcase
18 import mutagen.id3
20 from mutagen import Metadata
21 from mutagen._util import DictMixin, dict_match
22 from mutagen.id3 import ID3, error, delete, ID3FileType
24 __all__ = ['EasyID3', 'Open', 'delete']
26 class EasyID3KeyError(KeyError, ValueError, error):
27 """Raised when trying to get/set an invalid key.
29 Subclasses both KeyError and ValueError for API compatibility,
30 catching KeyError is preferred.
31 """
33 class EasyID3(DictMixin, Metadata):
34 """A file with an ID3 tag.
36 Like Vorbis comments, EasyID3 keys are case-insensitive ASCII
37 strings. Only a subset of ID3 frames are supported by default. Use
38 EasyID3.RegisterKey and its wrappers to support more.
40 You can also set the GetFallback, SetFallback, and DeleteFallback
41 to generic key getter/setter/deleter functions, which are called
42 if no specific handler is registered for a key. Additionally,
43 ListFallback can be used to supply an arbitrary list of extra
44 keys. These can be set on EasyID3 or on individual instances after
45 creation.
47 To use an EasyID3 class with mutagen.mp3.MP3:
48 from mutagen.mp3 import EasyMP3 as MP3
49 MP3(filename)
51 Because many of the attributes are constructed on the fly, things
52 like the following will not work:
53 ezid3["performer"].append("Joe")
54 Instead, you must do:
55 values = ezid3["performer"]
56 values.append("Joe")
57 ezid3["performer"] = values
60 """
62 Set = {}
63 Get = {}
64 Delete = {}
65 List = {}
67 # For compatibility.
68 valid_keys = Get
70 GetFallback = None
71 SetFallback = None
72 DeleteFallback = None
73 ListFallback = None
75 def RegisterKey(cls, key,
76 getter=None, setter=None, deleter=None, lister=None):
77 """Register a new key mapping.
79 A key mapping is four functions, a getter, setter, deleter,
80 and lister. The key may be either a string or a glob pattern.
82 The getter, deleted, and lister receive an ID3 instance and
83 the requested key name. The setter also receives the desired
84 value, which will be a list of strings.
86 The getter, setter, and deleter are used to implement __getitem__,
87 __setitem__, and __delitem__.
89 The lister is used to implement keys(). It should return a
90 list of keys that are actually in the ID3 instance, provided
91 by its associated getter.
92 """
93 key = key.lower()
94 if getter is not None:
95 cls.Get[key] = getter
96 if setter is not None:
97 cls.Set[key] = setter
98 if deleter is not None:
99 cls.Delete[key] = deleter
100 if lister is not None:
101 cls.List[key] = lister
102 RegisterKey = classmethod(RegisterKey)
104 def RegisterTextKey(cls, key, frameid):
105 """Register a text key.
107 If the key you need to register is a simple one-to-one mapping
108 of ID3 frame name to EasyID3 key, then you can use this
109 function:
110 EasyID3.RegisterTextKey("title", "TIT2")
112 def getter(id3, key):
113 return list(id3[frameid])
115 def setter(id3, key, value):
116 try:
117 frame = id3[frameid]
118 except KeyError:
119 id3.add(mutagen.id3.Frames[frameid](encoding=3, text=value))
120 else:
121 frame.encoding = 3
122 frame.text = value
124 def deleter(id3, key):
125 del(id3[frameid])
127 cls.RegisterKey(key, getter, setter, deleter)
128 RegisterTextKey = classmethod(RegisterTextKey)
130 def RegisterTXXXKey(cls, key, desc):
131 """Register a user-defined text frame key.
133 Some ID3 tags are stored in TXXX frames, which allow a
134 freeform 'description' which acts as a subkey,
135 e.g. TXXX:BARCODE.
136 EasyID3.RegisterTXXXKey('barcode', 'BARCODE').
138 frameid = "TXXX:" + desc
139 def getter(id3, key):
140 return list(id3[frameid])
142 def setter(id3, key, value):
143 try:
144 frame = id3[frameid]
145 except KeyError:
146 enc = 0
147 # Store 8859-1 if we can, per MusicBrainz spec.
148 for v in value:
149 if max(v) > u'\x7f':
150 enc = 3
151 id3.add(mutagen.id3.TXXX(encoding=enc, text=value, desc=desc))
152 else:
153 frame.text = value
155 def deleter(id3, key):
156 del(id3[frameid])
158 cls.RegisterKey(key, getter, setter, deleter)
159 RegisterTXXXKey = classmethod(RegisterTXXXKey)
161 def __init__(self, filename=None):
162 self.__id3 = ID3()
163 self.load = self.__id3.load
164 self.save = self.__id3.save
165 self.delete = self.__id3.delete
166 if filename is not None:
167 self.load(filename)
169 filename = property(lambda s: s.__id3.filename,
170 lambda s, fn: setattr(s.__id3, 'filename', fn))
172 _size = property(lambda s: s._id3.size,
173 lambda s, fn: setattr(s.__id3, '_size', fn))
175 def __getitem__(self, key):
176 key = key.lower()
177 func = dict_match(self.Get, key, self.GetFallback)
178 if func is not None:
179 return func(self.__id3, key)
180 else:
181 raise EasyID3KeyError("%r is not a valid key" % key)
183 def __setitem__(self, key, value):
184 key = key.lower()
185 if isinstance(value, basestring):
186 value = [value]
187 func = dict_match(self.Set, key, self.SetFallback)
188 if func is not None:
189 return func(self.__id3, key, value)
190 else:
191 raise EasyID3KeyError("%r is not a valid key" % key)
193 def __delitem__(self, key):
194 key = key.lower()
195 func = dict_match(self.Delete, key, self.DeleteFallback)
196 if func is not None:
197 return func(self.__id3, key)
198 else:
199 raise EasyID3KeyError("%r is not a valid key" % key)
201 def keys(self):
202 keys = []
203 for key in self.Get.keys():
204 if key in self.List:
205 keys.extend(self.List[key](self.__id3, key))
206 elif key in self:
207 keys.append(key)
208 if self.ListFallback is not None:
209 keys.extend(self.ListFallback(self.__id3, ""))
210 return keys
212 def pprint(self):
213 """Print tag key=value pairs."""
214 strings = []
215 for key in sorted(self.keys()):
216 values = self[key]
217 for value in values:
218 strings.append("%s=%s" % (key, value))
219 return "\n".join(strings)
221 Open = EasyID3
223 def genre_get(id3, key):
224 return id3["TCON"].genres
226 def genre_set(id3, key, value):
227 try:
228 frame = id3["TCON"]
229 except KeyError:
230 id3.add(mutagen.id3.TCON(encoding=3, text=value))
231 else:
232 frame.encoding = 3
233 frame.genres = value
235 def genre_delete(id3, key):
236 del(id3["TCON"])
238 def date_get(id3, key):
239 return [stamp.text for stamp in id3["TDRC"].text]
241 def date_set(id3, key, value):
242 id3.add(mutagen.id3.TDRC(encoding=3, text=value))
244 def date_delete(id3, key):
245 del(id3["TDRC"])
247 def performer_get(id3, key):
248 people = []
249 wanted_role = key.split(":", 1)[1]
250 try:
251 mcl = id3["TMCL"]
252 except KeyError:
253 raise KeyError(key)
254 for role, person in mcl.people:
255 if role == wanted_role:
256 people.append(person)
257 if people:
258 return people
259 else:
260 raise KeyError(key)
262 def performer_set(id3, key, value):
263 wanted_role = key.split(":", 1)[1]
264 try:
265 mcl = id3["TMCL"]
266 except KeyError:
267 mcl = mutagen.id3.TMCL(encoding=3, people=[])
268 id3.add(mcl)
269 mcl.encoding = 3
270 people = [p for p in mcl.people if p[0] != wanted_role]
271 for v in value:
272 people.append((wanted_role, v))
273 mcl.people = people
275 def performer_delete(id3, key):
276 wanted_role = key.split(":", 1)[1]
277 try:
278 mcl = id3["TMCL"]
279 except KeyError:
280 raise KeyError(key)
281 people = [p for p in mcl.people if p[0] != wanted_role]
282 if people == mcl.people:
283 raise KeyError(key)
284 elif people:
285 mcl.people = people
286 else:
287 del(id3["TMCL"])
289 def performer_list(id3, key):
290 try: mcl = id3["TMCL"]
291 except KeyError:
292 return []
293 else:
294 return list(set("performer:" + p[0] for p in mcl.people))
296 def musicbrainz_trackid_get(id3, key):
297 return [id3["UFID:http://musicbrainz.org"].data.decode('ascii')]
299 def musicbrainz_trackid_set(id3, key, value):
300 if len(value) != 1:
301 raise ValueError("only one track ID may be set per song")
302 value = value[0].encode('ascii')
303 try:
304 frame = id3["UFID:http://musicbrainz.org"]
305 except KeyError:
306 frame = mutagen.id3.UFID(owner="http://musicbrainz.org", data=value)
307 id3.add(frame)
308 else:
309 frame.data = value
311 def musicbrainz_trackid_delete(id3, key):
312 del(id3["UFID:http://musicbrainz.org"])
314 def website_get(id3, key):
315 urls = [frame.url for frame in id3.getall("WOAR")]
316 if urls:
317 return urls
318 else:
319 raise EasyID3KeyError(key)
321 def website_set(id3, key, value):
322 id3.delall("WOAR")
323 for v in value:
324 id3.add(mutagen.id3.WOAR(url=v))
326 def website_delete(id3, key):
327 id3.delall("WOAR")
329 def gain_get(id3, key):
330 try:
331 frame = id3["RVA2:" + key[11:-5]]
332 except KeyError:
333 raise EasyID3KeyError(key)
334 else:
335 return [u"%+f dB" % frame.gain]
337 def gain_set(id3, key, value):
338 if len(value) != 1:
339 raise ValueError("there must be exactly one gain value, not %r.", value)
340 gain = float(value[0].split()[0])
341 try:
342 frame = id3["RVA2:" + key[11:-5]]
343 except KeyError:
344 frame = mutagen.id3.RVA2(desc=key[11:-5], gain=0, peak=0, channel=1)
345 id3.add(frame)
346 frame.gain = gain
348 def gain_delete(id3, key):
349 try:
350 frame = id3["RVA2:" + key[11:-5]]
351 except KeyError:
352 pass
353 else:
354 if frame.peak:
355 frame.gain = 0.0
356 else:
357 del(id3["RVA2:" + key[11:-5]])
359 def peak_get(id3, key):
360 try:
361 frame = id3["RVA2:" + key[11:-5]]
362 except KeyError:
363 raise EasyID3KeyError(key)
364 else:
365 return [u"%f" % frame.peak]
367 def peak_set(id3, key, value):
368 if len(value) != 1:
369 raise ValueError("there must be exactly one peak value, not %r.", value)
370 peak = float(value[0])
371 if peak >= 2 or peak < 0:
372 raise ValueError("peak must be => 0 and < 2.")
373 try:
374 frame = id3["RVA2:" + key[11:-5]]
375 except KeyError:
376 frame = mutagen.id3.RVA2(desc=key[11:-5], gain=0, peak=0, channel=1)
377 id3.add(frame)
378 frame.peak = peak
380 def peak_delete(id3, key):
381 try:
382 frame = id3["RVA2:" + key[11:-5]]
383 except KeyError:
384 pass
385 else:
386 if frame.gain:
387 frame.peak = 0.0
388 else:
389 del(id3["RVA2:" + key[11:-5]])
391 def peakgain_list(id3, key):
392 keys = []
393 for frame in id3.getall("RVA2"):
394 keys.append("replaygain_%s_gain" % frame.desc)
395 keys.append("replaygain_%s_peak" % frame.desc)
396 return keys
398 for frameid, key in {
399 "TALB": "album",
400 "TBPM": "bpm",
401 "TCMP": "compilation", # iTunes extension
402 "TCOM": "composer",
403 "TCOP": "copyright",
404 "TENC": "encodedby",
405 "TEXT": "lyricist",
406 "TLEN": "length",
407 "TMED": "media",
408 "TMOO": "mood",
409 "TIT2": "title",
410 "TIT3": "version",
411 "TPE1": "artist",
412 "TPE2": "performer",
413 "TPE3": "conductor",
414 "TPE4": "arranger",
415 "TPOS": "discnumber",
416 "TPUB": "organization",
417 "TRCK": "tracknumber",
418 "TOLY": "author",
419 "TSO2": "albumartistsort", # iTunes extension
420 "TSOA": "albumsort",
421 "TSOC": "composersort", # iTunes extension
422 "TSOP": "artistsort",
423 "TSOT": "titlesort",
424 "TSRC": "isrc",
425 "TSST": "discsubtitle",
426 }.iteritems():
427 EasyID3.RegisterTextKey(key, frameid)
429 EasyID3.RegisterKey("genre", genre_get, genre_set, genre_delete)
430 EasyID3.RegisterKey("date", date_get, date_set, date_delete)
431 EasyID3.RegisterKey(
432 "performer:*", performer_get, performer_set, performer_delete,
433 performer_list)
434 EasyID3.RegisterKey("musicbrainz_trackid", musicbrainz_trackid_get,
435 musicbrainz_trackid_set, musicbrainz_trackid_delete)
436 EasyID3.RegisterKey("website", website_get, website_set, website_delete)
437 EasyID3.RegisterKey("website", website_get, website_set, website_delete)
438 EasyID3.RegisterKey(
439 "replaygain_*_gain", gain_get, gain_set, gain_delete, peakgain_list)
440 EasyID3.RegisterKey("replaygain_*_peak", peak_get, peak_set, peak_delete)
442 # At various times, information for this came from
443 # http://musicbrainz.org/docs/specs/metadata_tags.html
444 # http://bugs.musicbrainz.org/ticket/1383
445 # http://musicbrainz.org/doc/MusicBrainzTag
446 for desc, key in {
447 u"MusicBrainz Artist Id": "musicbrainz_artistid",
448 u"MusicBrainz Album Id": "musicbrainz_albumid",
449 u"MusicBrainz Album Artist Id": "musicbrainz_albumartistid",
450 u"MusicBrainz TRM Id": "musicbrainz_trmid",
451 u"MusicIP PUID": "musicip_puid",
452 u"MusicMagic Fingerprint": "musicip_fingerprint",
453 u"MusicBrainz Album Status": "musicbrainz_albumstatus",
454 u"MusicBrainz Album Type": "musicbrainz_albumtype",
455 u"MusicBrainz Album Release Country": "releasecountry",
456 u"MusicBrainz Disc Id": "musicbrainz_discid",
457 u"ASIN": "asin",
458 u"ALBUMARTISTSORT": "albumartistsort",
459 u"BARCODE": "barcode",
460 }.iteritems():
461 EasyID3.RegisterTXXXKey(key, desc)
463 class EasyID3FileType(ID3FileType):
464 """Like ID3FileType, but uses EasyID3 for tags."""
465 ID3 = EasyID3