Search code examples
pythonmp3mp4mutagen

Running into problems setting cover art for MP4 files using Python and Mutagen


Following multiple suggestions from other StackOverflow questions and the mutagen documentation, I was able to come up with code to get and set every ID3 tag in both MP3 and MP4 files. The issue I have is with setting the cover art for M4B files.

I have reproduced the code exactly like it is laid out in this answer:

Embedding album cover in MP4 file using Mutagen

But I am still receiving errors when I attempt to run the code. If I run the code with the 'albumart' value by itself I receive the error:

MP4file.tags['covr'] = albumart

Exception has occurred: TypeError can't concat int to bytes

However, if I surround the albumart variable with brackets like is shown in the aforementioned StackOverflow question I get this output:

MP4file.tags['covr'] = [albumart]

Exception has occurred: struct.error required argument is not an integer

Here is the function in it's entirety. The MP3 section works without any problems.

from mutagen.mp3 import MP3
from mutagen.mp4 import MP4, MP4Cover


def set_cover(filename, cover):
    r = requests.get(cover)
    with open('C:/temp/cover.jpg', 'wb') as q:
        q.write(r.content)
    if(filename.endswith(".mp3")):
        MP3file = MP3(filename, ID3=ID3)
        if cover.endswith('.jpg') or cover.endswith('.jpeg'):
            mime = 'image/jpg'
        else:
            mime = 'image/png'
        with open('C:/temp/cover.jpg', 'rb') as albumart: 
            MP3file.tags.add(APIC(encoding=3, mime=mime, type=3, desc=u'Cover', data=albumart.read()))
        MP3file.save(filename)
    else:
        MP4file = MP4(filename)
        if cover.endswith('.jpg') or cover.endswith('.jpeg'):
            cover_format = 'MP4Cover.FORMAT_JPEG'
        else:
            cover_format = 'MP4Cover.FORMAT_PNG'
        with open('C:/temp/cover.jpg', 'rb') as f:
            albumart = MP4Cover(f.read(), imageformat=cover_format)
        MP4file.tags['covr'] = [albumart]

I have been trying to figure out what I am doing wrong for two days now. If anyone can help me spot the problem I would be in your debt.

Thanks!


Solution

  • In the source code of mutagen at the location where the exception is being raised I've found the following lines:

     def __render_cover(self, key, value):
            ...
            for cover in value:
                try:
                    imageformat = cover.imageformat
                except AttributeError:
                    imageformat = MP4Cover.FORMAT_JPEG
                ...
                Atom.render(b"data", struct.pack(">2I", imageformat, 0) + cover))
            ...
    

    There key is the name for the cover tag and value is the data read from the image, wrapped into an MP4Cover object. Well, it turns out that if you iterates over an MP4Cover object, as the above code does, the iteration yields one byte of the image per iteration as int.

    Moreover, in Python version 3+, struct.pack returns an object of type bytes. I think the cover argument was intended to be the collection of bytes taken from the cover image.

    In the code you've given above the bytes of the cover image are wrapped inside an object of type MP4Cover that cannot be added to bytes as done in the second argument of Atom.render.

    To avoid having to edit or patch the mutagen library source code, the trick is converting the 'MP4Cover' object to bytes and wrapping the result inside a collection as shown below.

    import requests
    from mutagen.mp3 import MP3
    from mutagen.mp4 import MP4, MP4Cover
    
    
    def set_cover(filename, cover):
        r = requests.get(cover)
        with open('C:/temp/cover.jpg', 'wb') as q:
            q.write(r.content)
        if(filename.endswith(".mp3")):
            MP3file = MP3(filename, ID3=ID3)
            if cover.endswith('.jpg') or cover.endswith('.jpeg'):
                mime = 'image/jpg'
            else:
                mime = 'image/png'
            with open('C:/temp/cover.jpg', 'rb') as albumart: 
                MP3file.tags.add(APIC(encoding=3, mime=mime, type=3, desc=u'Cover', data=albumart.read()))
            MP3file.save(filename)
        else:
            MP4file = MP4(filename)
            if cover.endswith('.jpg') or cover.endswith('.jpeg'):
                cover_format = 'MP4Cover.FORMAT_JPEG'
            else:
                cover_format = 'MP4Cover.FORMAT_PNG'
            with open('C:/temp/cover.jpg', 'rb') as f:
                albumart = MP4Cover(f.read(), imageformat=cover_format)
            MP4file.tags['covr'] = [bytes(albumart)]
            MP4file.save(filename)
    

    I've also added MP4file.save(filename) as the last line of the code to persists the changes done to the file.