myDigitalLife Blogs

Blogs about Digital, Lifestyle, current news and opinions

Writing ID3 tags using python and mutagen for dummies

Posted by: mandibleclaw

Tagged in: Untagged 

mandibleclaw

When I started working on puddletag I had a bit of a hard time grokking the ID3 standard and how to use it with mutagen. Mostly it was because I expected it to work like Ogg tags. I now realize that this was due to my excessive laziness and average programming ability.

So this is for programmers like me. Average and lazy people who don't feel like reading the ID3 docs over at id3.org.

When you're done reading this you can expect a couple of things:

  • Knowing how to read and write ID3 tags using mutagen and python
  • That ID3 kinda sucks.

Let's get started, shall we.

Some preliminaries

There are a bunch of different ID3 versions. The first, and simplest is ID3v1. ID3v1 sucks, because it only allows about 30 characters per frame.

After ID3v1 came ID3v2, which got rid of the limit and brought with it suprising complexity. ID3v2.4 is the latest version and the only one mutagen supports(though reads the others), so you don't have to bother looking at the other specs.

So according to id3.org, ID3 tag data are stored in what they call frames. Each frame has an unique name, and different way of storing it's data. For instance, the artist frame (named TPE1) can store text, picture (APIC), picture data and the artist website frame (WOAF) a web address. So, you can have text frames, binary frames, url frames and others which I can't be bothered to look up.

Mutagen allows Text frames to have only one of four encodings, ISO-8859, UTF-16, UTF-16BE, UTF-8. If you're unsure, go with UTF-8 as it'll cause the least amount of headaches (your mp3 player won't choke on reading the tag).


Reading Files

So, that's it for the preliminaries. Here's some code to get you started.

1   >>>import mutagen
2   >>>audio = mutagen.File('smiley.mp3')
3   >>>audio
4   {'TPOS': TPOS(encoding=3, text=[u'1/1']), 'TDRC': TDRC(encoding=3, text=[u'2006']), 'TALB': TALB(encoding=3, text=[u'St. Elsewhere']), u"COMM:0:'eng'": COMM(encoding=3, lang='eng', desc=u'0', text=[u"One of the happiest songs you'll come across."]), 'TRCK': TRCK(encoding=3, text=[u'5']), 'TPE2': TPE2(encoding=3, text=[u'Gnarls Barkley']), 'TPE1': TPE1(encoding=3, text=[u'Gnarls Barkley']), 'TIT2': TIT2(encoding=3, text=[u'Smiley faces']), 'TCON': TCON(encoding=3, text=[u'Pop']), u'TXXX:owner': TXXX(encoding=3, desc=u'owner', text=[u'mandibleclaw'])}
5   >>> audio.keys()
6   ['TPE1', 'TDRC', 'TIT2', u"COMM:0:'eng'", 'TRCK', 'TPE2', 'TPOS', 'TALB', 'TCON', u'TXXX:owner']


Let's go over it line-by-line.
  1. Imports the mutagen module. Nothing to see here. Moving on.
  2. Use mutagen's File function to read a file. You can use this on any file and it'll load the tags provided the format is supported. However, this function is pretty fucking slow, because it validates the file against all supported formats and chooses the best match. The difference is not negligible if you're reading a large number of files.       If you already know what type of file it is (like by extension) use the files object directly. For id3 this happens to be ID3 (for FLAC its FLAC, Ogg=OggVorbis etc.). Like so...

    >>>from mutagen.id3 import ID3
    >>>audio = ID3('smiley.mp3')
    >>>audio
    {'TPOS': TPOS(encoding=3, text=[u'1/1']), 'TDRC': TDRC(encoding=3, text=[u'2006']), 'TALB': TALB(encoding=3, text=[u'St. Elsewhere']), u"COMM:0:'eng'": COMM(encoding=3, lang='eng', desc=u'0', text=[u"One of the happiest songs you'll come across."]), 'TRCK': TRCK(encoding=3, text=[u'5']), 'TPE2': TPE2(encoding=3, text=[u'Gnarls Barkley']), 'TPE1': TPE1(encoding=3, text=[u'Gnarls Barkley']), 'TIT2': TIT2(encoding=3, text=[u'Smiley faces']), 'TCON': TCON(encoding=3, text=[u'Pop']), u'TXXX:owner': TXXX(encoding=3, desc=u'owner', text=[u'mandibleclaw'])}
    >>> audio.keys()
    ['TPE1', 'TDRC', 'TIT2', u"COMM:0:'eng'", 'TRCK', 'TPE2', 'TPOS', 'TALB', 'TCON', u'TXXX:owner']

    You see? Same result, except it's hella-faster.
  3. A printout of the id3 tag to illustrate what it looks like. Mutagen uses dictionaries (or dictionary-like objects) to represent tags.
  4. Mutagen let's you access a frame by it's name as defined in the ID3v2.4 standard. All frames are read as ID3v2.4, even if another version is present in the file. Therefore, you don't have to mess with ID3v< 2.4 at all. Everything works the same.
  5. From id3.org. TPE1 corresponds to the artist tag, TIT2, title, COMM to comment, TXXX to a user defined tag. For the rest you'll have to check out id3.org or browse the id3.py file for its frame docstrings a la 
      for f in mutagen.id3.Frames.itervalues(): print f.__name__, f.__doc__

Let's take a quick detour into text frames.
 

Text Frames

Text frames are frames such TPE1 (Lead composer, but used as artist everywhere else), TIT2 (title) and TALB (album) are all derived from mutagen.id3.TextFrame. Frames of this type need just and encoding and the text to be created. Here's an example of them in action.   

>>>from mutagen.id3 import TPE1
>>>TPE1(3, 'just some text')
TPE1(encoding=3, text=[u'just some text'])


The first argument is the encoding corresponding to 0 for ISO-8859 1, 1 for UTF-16, 2 for UTF-16BE, and 3 for UTF-8. Notice that mutagen converted the string to unicode and stored it as a list. mutagen.id3 stores all text as unicode strings and any keys that support multiple values are stored as lists.

I'd just wanna point out that the text and encoding become properties of the TPE1 object, which you can use if you've already created the object. Like so...
   
>>>tp = TPE1(3, 'just some text')
>>>tp
TPE1(encoding=3, text=[u'just some text'])
>>>tp.text = ['text 1', u'text 2']
>>>tp
TPE1(encoding=3, text=['text 1', u'text 2'])


Time stamp frames

Dates are stored as Time stamp frames (mutagen.id3.TimeStampTextFrame). Although they seem like normal timestamp frames (and can work that way too), they take ID3TimeStamp objects instead of text. Probably the most popular frame you'll see derived from this class is the TDRC frame for specifying when the audio was recorded. This is used as the 'year' tag by many tag editors out there.

Anyway, here's an example:

>>>from mutagen.id3 import TDRC, ID3TimeStamp
>>> ts = ID3TimeStamp('1999-11-13 23:55:16')
>>> TDRC(3, [ts]) #Mutagen really wants a list of ID3TimeStamp objects.
TDRC(encoding=3, text=[u'1999-11-13 23:55:16'])
>>>ID3TimeStamp('1999and a year') #Bad dates don't get parsed, so you can test for them.
u''


Text works well too, but you won't notice when you've added a bad date until you've written the data and reloaded it, so don't go this route unless you're sure the date is valid.

>>>TDRC(3, '2009')
TDRC(encoding=3, text=[u'2009'])

URL frames


The last textual frame of some import is the URL frames (mutagen.id3.UrlFrame). These usually contain links boring info such as copyright info(WCOP), payment info (WPAY), and artist info (WOAR).

URL frames are just text tags with the added restriction being that they are encoded as Latin-1. As such, there's no need to specify an encoding when using these frames.


TXXX and WXXX

TXXX frames are user defined text frames, while WXXX frames are user defined urls. The only difference being that WXXX must be encoded using Latin-1.

Anyway, TXXX frames are normal text frames, but they require an extra description parameter to differentiate themselves. For instance:

>>>from mutagen.id3 import TXXX
>>>tx = TXXX(3, 'name', 'mandibleclaw')
TXXX(encoding=3, desc=u'name', text=[u'mandibleclaw'])
>>>tx.text = ['manibleclaw', 'is spelt wrong...']
>>>tx
TXXX(encoding=3, desc=u'name', text=['manibleclaw', 'is spelt wrong...'])
>>>tx.HashKey
u'TXXX:name'


See? Not complicated at all. The HashKey (the key mutagen uses in the dictionary) goes like TXXX:desc. Hence the hashkey for tx = 'TXXX:name'.


Comment frames

Comment frames, specified by COMM, are like TXXX frames with an extra lang attribute, corresponding to the three-letter iso language codes.

>>>comment = COMM(3, 'eng', 'mandibleclaw', 'One ugly-ass sock puppet.')
>>>comment
COMM(encoding=3, lang='eng', desc=u'mandibleclaw', text=[u'One ugly-ass sock puppet.'])
>>>comment.HashKey
u"COMM:mandibleclaw:'eng'"

Writing tags

I think this is one of those an-example-would-be-better type of situations. So here goes.

>>>from mutagen.mp3 import MP3 #MP3 works as ID3 does, but the info property has more pertinent data.
>>>audio = MP3('smiley.mp3')
>>>audio['TPE2'] #There are...
TPE2(encoding=3, text=[u'Gnarls Barkley'])
>>>audio['TPE2'] = TPE2(3, 'not mandibleclaw')
>>>audio['TPE2'] #...different...
TPE2(encoding=3, text=[u'not mandibleclaw'])
>>>audio.tags.add(TPE2(3, 'or just Cee-Lo either'))
>>>audio['TPE2'] #...ways of writing data. Use whichever you prefer.
TPE2(encoding=3, text=[u'or just Cee-Lo either'])
>>>audio.save()  #You can now save the file this way.
>>>audio.tags.save() #However, saving this way is loads faster.
>>>audio.tags.save(v1=2) #Use v1=2 to save both the ID3v1 and ID3v2 tag. See the docstring for more details.


To remove frames, just delete the key and save. That's it for now and forever. What follows are some other frames you'll probably run into. I haven't covered even half of the available frames, so check out id3.org and docstrings for more info.

Genres

Genres (TCON) are Text frames with some specially defined numeric values. In the numeric case each value corresponds to the value in mutagen.id3.GENRES, which just as in the id3 spec.

>>>from mutagen.id3 import TCON
>>>TCON(3, [1,2,3]).genres
[u'Classic Rock', u'Country', u'Dance']

Pictures

ID3 stores picture info in the APIC (attached picture) frame. Four are needed to the object. The image's string data, mimetype, encoding, description and picture type.

Encodings are as with text frames. The mimetype can be either 'image/jpeg' for JPEG images or 'image/png' for PNG images. Nothing else. If you try to save another format you'll just fuck yourself, so don't.

The picture type is an integer which corresponds to the list found at id3.org. 0 is for other, 2 for cover (front), 3 for cover (back), etc.
String data can be obtained just by opening the file as a binary and reading it. Like so:

>>>imagedata = open('image.jpg', 'rb').read()
>>>pic = APIC(3, 'image/jpeg', 3, 'Front cover', imagedata)
>>>pic.HashKey
u'APIC:Front Cover'


Like the TXXX frames APIC frames have HashKey like APIC:desc

And we are done. I think I'll get some coffee now.



Comments (0)Add Comment

Add your 2Cents
You must be logged in to post a comment. Please register if you do not have an account yet.

busy