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.
16 from fnmatch
import fnmatchcase
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.
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
47 To use an EasyID3 class with mutagen.mp3.MP3:
48 from mutagen.mp3 import EasyMP3 as MP3
51 Because many of the attributes are constructed on the fly, things
52 like the following will not work:
53 ezid3["performer"].append("Joe")
55 values = ezid3["performer"]
57 ezid3["performer"] = values
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.
94 if getter
is not None:
96 if setter
is not None:
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
110 EasyID3.RegisterTextKey("title", "TIT2")
112 def getter(id3
, key
):
113 return list(id3
[frameid
])
115 def setter(id3
, key
, value
):
119 id3
.add(mutagen
.id3
.Frames
[frameid
](encoding
=3, text
=value
))
124 def deleter(id3
, key
):
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,
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
):
147 # Store 8859-1 if we can, per MusicBrainz spec.
151 id3
.add(mutagen
.id3
.TXXX(encoding
=enc
, text
=value
, desc
=desc
))
155 def deleter(id3
, key
):
158 cls
.RegisterKey(key
, getter
, setter
, deleter
)
159 RegisterTXXXKey
= classmethod(RegisterTXXXKey
)
161 def __init__(self
, filename
=None):
163 self
.load
= self
.__id
3.load
164 self
.save
= self
.__id
3.save
165 self
.delete
= self
.__id
3.delete
166 if filename
is not None:
169 filename
= property(lambda s
: s
.__id
3.filename
,
170 lambda s
, fn
: setattr(s
.__id
3, 'filename', fn
))
172 _size
= property(lambda s
: s
._id
3.size
,
173 lambda s
, fn
: setattr(s
.__id
3, '_size', fn
))
175 def __getitem__(self
, key
):
177 func
= dict_match(self
.Get
, key
, self
.GetFallback
)
179 return func(self
.__id
3, key
)
181 raise EasyID3KeyError("%r is not a valid key" % key
)
183 def __setitem__(self
, key
, value
):
185 if isinstance(value
, basestring
):
187 func
= dict_match(self
.Set
, key
, self
.SetFallback
)
189 return func(self
.__id
3, key
, value
)
191 raise EasyID3KeyError("%r is not a valid key" % key
)
193 def __delitem__(self
, key
):
195 func
= dict_match(self
.Delete
, key
, self
.DeleteFallback
)
197 return func(self
.__id
3, key
)
199 raise EasyID3KeyError("%r is not a valid key" % key
)
203 for key
in self
.Get
.keys():
205 keys
.extend(self
.List
[key
](self
.__id
3, key
))
208 if self
.ListFallback
is not None:
209 keys
.extend(self
.ListFallback(self
.__id
3, ""))
213 """Print tag key=value pairs."""
215 for key
in sorted(self
.keys()):
218 strings
.append("%s=%s" % (key
, value
))
219 return "\n".join(strings
)
223 def genre_get(id3
, key
):
224 return id3
["TCON"].genres
226 def genre_set(id3
, key
, value
):
230 id3
.add(mutagen
.id3
.TCON(encoding
=3, text
=value
))
235 def genre_delete(id3
, key
):
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
):
247 def performer_get(id3
, key
):
249 wanted_role
= key
.split(":", 1)[1]
254 for role
, person
in mcl
.people
:
255 if role
== wanted_role
:
256 people
.append(person
)
262 def performer_set(id3
, key
, value
):
263 wanted_role
= key
.split(":", 1)[1]
267 mcl
= mutagen
.id3
.TMCL(encoding
=3, people
=[])
270 people
= [p
for p
in mcl
.people
if p
[0] != wanted_role
]
272 people
.append((wanted_role
, v
))
275 def performer_delete(id3
, key
):
276 wanted_role
= key
.split(":", 1)[1]
281 people
= [p
for p
in mcl
.people
if p
[0] != wanted_role
]
282 if people
== mcl
.people
:
289 def performer_list(id3
, key
):
290 try: mcl
= id3
["TMCL"]
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
):
301 raise ValueError("only one track ID may be set per song")
302 value
= value
[0].encode('ascii')
304 frame
= id3
["UFID:http://musicbrainz.org"]
306 frame
= mutagen
.id3
.UFID(owner
="http://musicbrainz.org", 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")]
319 raise EasyID3KeyError(key
)
321 def website_set(id3
, key
, value
):
324 id3
.add(mutagen
.id3
.WOAR(url
=v
))
326 def website_delete(id3
, key
):
329 def gain_get(id3
, key
):
331 frame
= id3
["RVA2:" + key
[11:-5]]
333 raise EasyID3KeyError(key
)
335 return [u
"%+f dB" % frame
.gain
]
337 def gain_set(id3
, key
, value
):
339 raise ValueError("there must be exactly one gain value, not %r.", value
)
340 gain
= float(value
[0].split()[0])
342 frame
= id3
["RVA2:" + key
[11:-5]]
344 frame
= mutagen
.id3
.RVA2(desc
=key
[11:-5], gain
=0, peak
=0, channel
=1)
348 def gain_delete(id3
, key
):
350 frame
= id3
["RVA2:" + key
[11:-5]]
357 del(id3
["RVA2:" + key
[11:-5]])
359 def peak_get(id3
, key
):
361 frame
= id3
["RVA2:" + key
[11:-5]]
363 raise EasyID3KeyError(key
)
365 return [u
"%f" % frame
.peak
]
367 def peak_set(id3
, key
, value
):
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.")
374 frame
= id3
["RVA2:" + key
[11:-5]]
376 frame
= mutagen
.id3
.RVA2(desc
=key
[11:-5], gain
=0, peak
=0, channel
=1)
380 def peak_delete(id3
, key
):
382 frame
= id3
["RVA2:" + key
[11:-5]]
389 del(id3
["RVA2:" + key
[11:-5]])
391 def peakgain_list(id3
, key
):
393 for frame
in id3
.getall("RVA2"):
394 keys
.append("replaygain_%s_gain" % frame
.desc
)
395 keys
.append("replaygain_%s_peak" % frame
.desc
)
398 for frameid
, key
in {
401 "TCMP": "compilation", # iTunes extension
415 "TPOS": "discnumber",
416 "TPUB": "organization",
417 "TRCK": "tracknumber",
419 "TSO2": "albumartistsort", # iTunes extension
421 "TSOC": "composersort", # iTunes extension
422 "TSOP": "artistsort",
425 "TSST": "discsubtitle",
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
)
432 "performer:*", performer_get
, performer_set
, performer_delete
,
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
)
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
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",
458 u
"ALBUMARTISTSORT": "albumartistsort",
459 u
"BARCODE": "barcode",
461 EasyID3
.RegisterTXXXKey(key
, desc
)
463 class EasyID3FileType(ID3FileType
):
464 """Like ID3FileType, but uses EasyID3 for tags."""