Topic: coverart update for 0.15b2 and local files

I have coded an update for the coverart.py plugin.  (Which downloads cover art for any releases that have a CoverArtLink or ASIN relation.)

The code shown below works with Picard 0.15.0beta2, and will also process a local file if one exists.

Credit for these changes has to be given to other members of the forum.  I started with the coverart.py update mentioned in this post http://forums.musicbrainz.org/viewtopic … 869#p13869, and which can be downloaded from this link http://users.musicbrainz.org/~luks/pica … overart.py.

I then stole the code from this post http://forums.musicbrainz.org/viewtopic … 724#p11724 by rockmaloins, and integrated it.

I made a few changes to rockmaloins code when processing local files - I look in each directory only once, and search for files starting with "cover", "folder" and the user defined coverart name saved in the Picard options.  Multiple filetypes are searched (something that was commented our in the original code).  The first file found is used.

Hope sombody (other than me) can make use of my modification.

Cheers,
Dave.

""" 
A small plugin to download cover art for any releseas that have a
CoverArtLink or ASIN relation.


Changelog:

    [2008-04-15] Refactored the code to be similar to the server code (hartzell, phw)
    
    [2008-03-10] Added CDBaby support (phw)
    
    [2007-09-06] Added Jamendo support (phw)

    [2007-04-24] Moved parsing code into here
                 Swapped to QUrl
                 Moved to a list of urls

    [2007-04-23] Moved it to use the bzr picard
                 Took the hack out
                 Added Amazon ASIN support
                 
    [2007-04-23] Initial plugin, uses a hack that relies on Python being
                 installed and musicbrainz2 for the query.

"""

PLUGIN_NAME = 'Cover Art Downloader'
PLUGIN_AUTHOR = 'Oliver Charles, Philipp Wolfer'
PLUGIN_DESCRIPTION = '''Downloads cover artwork for releases that have a
CoverArtLink or ASIN.'''
PLUGIN_VERSION = "0.6.3"
PLUGIN_API_VERSIONS = ["0.12", "0.15"]

from picard.metadata import register_album_metadata_processor
from picard.util import partial, mimetype
from PyQt4.QtCore import QUrl
import re
import traceback
import os

# data transliterated from the perl stuff used to find cover art for the
# musicbrainz server.
# See mb_server/cgi-bin/MusicBrainz/Server/CoverArt.pm
# hartzell --- Tue Apr 15 15:25:58 PDT 2008
COVERART_SITES = (
    # CD-Baby
    # tested with http://musicbrainz.org/release/1243cc17-b9f7-48bd-a536-b10d2013c938.html
    {
    'regexp': 'http://(www\.)?cdbaby.com/cd/(\w)(\w)(\w*)',
    'imguri': 'http://cdbaby.name/$2/$3/$2$3$4.jpg',
    },
    # Jamendo
    # tested with http://musicbrainz.org/release/2fe63977-bda9-45da-8184-25a4e7af8da7.html
    {
    'regexp': 'http:\/\/(?:www.)?jamendo.com\/(?:[a-z]+\/)?album\/([0-9]+)',
    'imguri': 'http://www.jamendo.com/get/album/id/album/artworkurl/redirect/$1/?artwork_size=0',
    },
    )

# amazon image file names are unique on all servers and constructed like
# <ASIN>.<ServerNumber>.[SML]ZZZZZZZ.jpg
# A release sold on amazon.de has always <ServerNumber> = 03, for example.
# Releases not sold on amazon.com, don't have a "01"-version of the image,
# so we need to make sure we grab an existing image.
AMAZON_SERVER = {
    "amazon.jp": {
        "server": "ec1.images-amazon.com",
        "id"    : "09",
    },
    "amazon.co.jp": {
        "server": "ec1.images-amazon.com",
        "id"    : "09",
    },
    "amazon.co.uk": {
        "server": "ec1.images-amazon.com",
        "id"    : "02",
    },
    "amazon.de": {
        "server": "ec2.images-amazon.com",
        "id"    : "03",
    },
    "amazon.com": {
        "server": "ec1.images-amazon.com",
        "id"    : "01",
    },
    "amazon.ca": {
        "server": "ec1.images-amazon.com",
        "id"    : "01",                   # .com and .ca are identical
    },
    "amazon.fr": {
        "server": "ec1.images-amazon.com",
        "id"    : "08"
    },
}

AMAZON_IMAGE_PATH = '/images/P/%s.%s.%sZZZZZZZ.jpg'
AMAZON_ASIN_URL_REGEX = re.compile(r'^http://(?:www.)?(.*?)(?:\:[0-9]+)?/.*/([0-9B][0-9A-Z]{9})(?:[^0-9A-Z]|$)')

def _coverart_downloaded(album, metadata, release, try_list, data, http, error):
    try:
        if error or len(data) < 1000:
            if error:
                album.log.error(str(http.errorString()))
            coverart(album, metadata, release, try_list)
        else:
            mime = mimetype.get_from_data(data, default="image/jpeg")
            metadata.add_image(mime, data)
            for track in album._new_tracks:
                track.metadata.add_image(mime, data)
    finally:
        album._requests -= 1
        album._finalize_loading(None)


def coverart(album, metadata, release, try_list=None):
    """ Gets all cover art URLs from the metadata and then attempts to
    download the album art. """

    # try_list will be None for the first call
    if try_list is None:
        try_list = []
        
        _process_local_files(try_list, album)

        try:
            if release.children.has_key('relation_list'):
                for relation_list in release.relation_list:
                    if relation_list.target_type == 'url':
                        for relation in relation_list.relation:
                            _process_url_relation(try_list, relation)
    
                            # Use the URL of a cover art link directly
                            if relation.type == 'cover art link' or relation.type == 'has_cover_art_at':
                                _try_list_append_image_url(try_list, QUrl(relation.target[0].text))
                            elif relation.type == 'amazon asin' or relation.type == 'has_Amazon_ASIN':
                                _process_asin_relation(try_list, relation)
        except AttributeError, e:
            album.log.error(traceback.format_exc())

    if len(try_list) > 0:
        # We still have some items to try!
        album._requests += 1
        if try_list[0]['host'] == 'localhost':
            f = open(try_list[0]['path'], 'rb')
            data = f.read()
            mime = mimetype.get_from_data(data)
            metadata.add_image(mime, data)
            for track in album._new_tracks:
                track.metadata.add_image(mime, data)
            f.close()
            album._requests -= 1
            album._finalize_loading(None)
        else:
            album.tagger.xmlws.download(
                try_list[0]['host'], try_list[0]['port'], try_list[0]['path'],
                partial(_coverart_downloaded, album, metadata, release, try_list[1:]),
                position=1)


def _process_url_relation(try_list, relation):
    # Search for cover art on special sites
    for site in COVERART_SITES:
        # this loop transliterated from the perl stuff used to find cover art for the
        # musicbrainz server.
        # See mb_server/cgi-bin/MusicBrainz/Server/CoverArt.pm
        # hartzell --- Tue Apr 15 15:25:58 PDT 2008
        match = re.match(site['regexp'], relation.target[0].text)
        if match != None:
            imgURI = site['imguri']
            for i in range(1, len(match.groups())+1 ):
                if match.group(i) != None:
                    imgURI = imgURI.replace('$' + str(i), match.group(i))
            _try_list_append_image_url(try_list, QUrl(imgURI))


def _process_asin_relation(try_list, relation):
    match = AMAZON_ASIN_URL_REGEX.match(relation.target[0].text)
    if match != None:
        asinHost = match.group(1)
        asin = match.group(2);
        if AMAZON_SERVER.has_key(asinHost):
            serverInfo = AMAZON_SERVER[asinHost]
        else:
            serverInfo = AMAZON_SERVER['amazon.com']
        try_list.append({'host': serverInfo['server'], 'port': 80,
            'path': AMAZON_IMAGE_PATH % (asin, serverInfo['id'], 'L')
        })
        try_list.append({'host': serverInfo['server'], 'port': 80,
            'path': AMAZON_IMAGE_PATH % (asin, serverInfo['id'], 'M')
        })


def _try_list_append_image_url(try_list, parsedUrl):
    path = str(parsedUrl.encodedPath())
    if parsedUrl.hasQuery():
        path += '?' + parsedUrl.encodedQuery()
    try_list.append({
        'host': str(parsedUrl.host()),
        'port': parsedUrl.port(80),
        'path': str(path)
    })

def _process_local_files(try_list, album):
    filenameList = []
    parsedDirectories = {}
    for file in album.iterfiles():
        currentDirectory = os.path.dirname(file.filename)
        if (currentDirectory in parsedDirectories):
            continue
        parsedDirectories[currentDirectory] = 1
        for root, dirs, files in os.walk(currentDirectory):
            for filename in files:
                lowercaseFilename = filename.lower()
                if (lowercaseFilename.endswith(".jpg") or
                    lowercaseFilename.endswith(".jpeg") or
                    lowercaseFilename.endswith(".png") or
                    lowercaseFilename.endswith(".gif") or
                    lowercaseFilename.endswith(".tif") or
                    lowercaseFilename.endswith(".tiff")):
                    if (lowercaseFilename.startswith("cover.") or
                        lowercaseFilename.startswith("folder.") or
                        lowercaseFilename.startswith(album.config.setting['cover_image_filename'].lower() + '.')):
                        album.log.debug("coverart _process_local_files found %r", filename) 
                        filenameList.append(os.path.join(currentDirectory, root, filename))

    imageFilename = ""
    if len(filenameList) > 0:
        imageFilename = filenameList[0]

    if os.path.exists(imageFilename):
        try_list.append({
            'host': 'localhost',
            'port': '0',
            'path': imageFilename
        })

register_album_metadata_processor(coverart)

Re: coverart update for 0.15b2 and local files

Thank you! :D

Re: coverart update for 0.15b2 and local files

Just saw this post. Anyone knows whether this is working with Picard 0.16? If so, would I just copy&paste the code below into my current coverart plugin?

This would be fantastic as it would finally allow me to integrate local fanart? Two related questions:

1) Does anyone know the "order"? Existing - ASIN - local?

2) How does the local art need to be named?

Re: coverart update for 0.15b2 and local files

steve1977 wrote:

Just saw this post. Anyone knows whether this is working with Picard 0.16?

It needs some modifications for compatibility, so I don't think so, no. I do have it on my TODO list though: http://tickets.musicbrainz.org/browse/PICARD-137

steve1977 wrote:

If so, would I just copy&paste the code below into my current coverart plugin?

Were it working, you could do this, yes. :)

steve1977 wrote:

Does anyone know the "order"? Existing - ASIN - local?

Looks like first preference is local, 2nd is ASIN, then fallback to existing (as long as you don't have 'clear existing tags' selected)

steve1977 wrote:

How does the local art need to be named?

cover.EXT or folder.EXT or picardCoverArtConfigSetting.EXT

where EXT in {jpg, jpeg, gif, png, tif, tiff}

Re: coverart update for 0.15b2 and local files

voiceinsideyou wrote:

It needs some modifications for compatibility, so I don't think so, no. I do have it on my TODO list though: http://tickets.musicbrainz.org/browse/PICARD-137

Got it, will wait the good news. Thanks in advance!!!

voiceinsideyou wrote:

Looks like first preference is local, 2nd is ASIN, then fallback to existing (as long as you don't have 'clear existing tags' selected)

What's your thought of what the best "order" would be once you "fix" it? I could imagine having ASIN before existing may create problems for those cases, where you have an ASIN, but still won't get a fanart (digi-pack, etc)? Also, any chance to keep the "existing" one even if you have 'clear existing tags' enabled (for cases without ASIN and local)?

Re: coverart update for 0.15b2 and local files

steve1977 wrote:

What's your thought of what the best "order" would be once you "fix" it? I could imagine having ASIN before existing may create problems for those cases, where you have an ASIN, but still won't get a fanart (digi-pack, etc)?

Not quite sure what you mean here. By "existing" do you mean local? Or existing in tag already?

At the end of the day, one size fits all probably isn't going to work. I think it probably needs to be configurable as to whether you'd prefer local art (cover.jpg etc) to anything from the Internet (ASIN etc); or vice versa.

Consider that if you have Picard also saving the art as folder.jpg, the first time you get art from ASIN for an album it would save it locally. If local art is preferred and you later re-tag and pick up a new ASIN changed in MB it wouldnt't download the new art. So probably need to allow both options.

The existing plugin doesn't seek to address the "different types of art" problem for users who do want to embed or save/manage different types of artwork. It's only designed to update/manage your "Front cover" type of art. This is in part a Picard limitation - for ID3 at least, Picard always writes a single image as type=3, i.e. front cover. While it can write multiple images to tags, the existing cover art plugin writes the first one it finds. Extension would need to be done to handle different types of artwork. This might happen by virtue of the new "Cover Art Archive" initiative, which I believe is intended at some point to handle and categorise different types of artwork along with metadata.

steve1977 wrote:

Also, any chance to keep the "existing" one even if you have 'clear existing tags' enabled (for cases without ASIN and local)?

Not currently. In a more general sense, this requires http://tickets.musicbrainz.org/browse/PICARD-19 to be addressed.

Re: coverart update for 0.15b2 and local files

Thanks, indeed looking forward to see a solution one day!

voiceinsideyou wrote:

Not quite sure what you mean here. By "existing" do you mean local? Or existing in tag already?

With "existing", I was referring to the embedded one in the tag already.

voiceinsideyou wrote:

At the end of the day, one size fits all probably isn't going to work. I think it probably needs to be configurable as to whether you'd prefer local art (cover.jpg etc) to anything from the Internet (ASIN etc); or vice versa.

That would probably be ideal. Having said this, I assume this would take much longer to code and delay the release of a plugin, which allows to include coverart.

I could live with either way. My major concern is how albums are treated with an ASIN, which does not have coverart (digi pack or so). I am worried that for these cases, the plugin would see the ASIN, but does not "know" that there is no coverart pullable through Picard.

voiceinsideyou wrote:

The existing plugin doesn't seek to address the "different types of art" problem for users who do want to embed or save/manage different types of artwork. It's only designed to update/manage your "Front cover" type of art. This is in part a Picard limitation - for ID3 at least, Picard always writes a single image as type=3, i.e. front cover. While it can write multiple images to tags, the existing cover art plugin writes the first one it finds. Extension would need to be done to handle different types of artwork. This might happen by virtue of the new "Cover Art Archive" initiative, which I believe is intended at some point to handle and categorise different types of artwork along with metadata.

Agree that this may be nice, but indeed more a horizon-2 or horizon-3. Wouldn't see the need for more than one coverart as part of this plugin.