[ALPHA] Album Art Tile - Tasker, MusicBrainz and Cover Art Archive

I have released an early version of this for anyone who wants to play around with it, including messing with the code. It is available through HPM or in my GitHub repository. I will try and get around to posting more details on how I use it with Tasker sometime soon...

Release Notes
2021-11-26: 0.1.0 - Alpha Release

Following some discussion about Chromecast / Google Nest speakers this morning, I have been playing around with mine most of the day, setting up a profile on both my phone and tablet to send the artist, album and track name to a virtual device on HE via Maker API.

I moved on to looking at getting the album art. Although the Music Track Change event in Tasker can give you the content uri to the image, I can't see a way to easily get it somewhere easily used on HE.

That lead me on to looking at the MusicBrainz API to get an id for the album and then using this to look up cover art archive. These each return JSON, with Cover Art Archive include various URLs in the JSON pointing to different sized album covers and other images for the album.

I'm thinking of a small driver to make the API calls above as a start, triggered on a change in the song being played, maybe including an iframe attribute to display the album cover. Might even include a cache of the albums queryed previously, avoiding the API calls every time.

Could lead on to a more advanced app around music controls and dashboard utilities, but I've got plenty of projects already, so will try and start small... :slight_smile:

Simon

As an example, to get the album cover for the Aussie album Vulture Street by Powerfinger:

https://musicbrainz.org/ws/2/release-group?query=artistname:"Powderfinger"AND%20release:"Vulture%20Street"

Part of the XML returned looks like this (it can return json, I just couldn't set it in the headers when testing in Chrome):

<release-list count="5">
<release id="07aa93bb-6503-4767-bfdf-865779cb3f81">
<title>Vulture Street</title>
<status id="4e304316-386d-3409-af2e-78857eec5cfe">Official</status>
</release>
<release id="27dfba4e-0c83-4d05-bf91-620bdc1ad63a">
<title>Vulture Street</title>
<status id="4e304316-386d-3409-af2e-78857eec5cfe">Official</status>
</release>
<release id="3236febb-8cfc-3905-b87a-41b3f02b16c2">
<title>Vulture Street</title>
<status id="4e304316-386d-3409-af2e-78857eec5cfe">Official</status>
</release>

Strangely for this example I needed to use the second release, so will need to include some error handling, taking the id value and using it to call Cover Art:

https://ia800900.us.archive.org/14/items/mbid-27dfba4e-0c83-4d05-bf91-620bdc1ad63a/index.json

Providing this JSON:

{"images":[{"types":["Front"],"front":true,"back":false,"edit":31307514,"image":"http://coverartarchive.org/release/27dfba4e-0c83-4d05-bf91-620bdc1ad63a/9499162864.jpg","comment":"","approved":true,"id":"9499162864","thumbnails":{"large":"http://coverartarchive.org/release/27dfba4e-0c83-4d05-bf91-620bdc1ad63a/9499162864-500.jpg","small":"http://coverartarchive.org/release/27dfba4e-0c83-4d05-bf91-620bdc1ad63a/9499162864-250.jpg"}}],"release":"http://musicbrainz.org/release/27dfba4e-0c83-4d05-bf91-620bdc1ad63a"}

Then just use one of the image URLs, say the small thumbnail:

Simon

I did make a bit of a start on the driver for this. It's certainly not useable yet, but if someone wants to play around, it can provide a leg-up to get started. The success rate for finding the right album cover (or even one at all) is not great. That could be a combination of the Cover Art Archive coverage and/or my querying of it. I may look at using another resource, depending on whether I can get a more reliable outcome.

/**
 *  Album Art Tile
 *
 *  Copyright 2021 Simon Burke
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 *  Change History:
 *
 *    Date        Who            What
 *    ----        ---            ----
 *    2021-10-02  Simon Burke    Original Creation
 * 
 */
metadata {
	definition (name: "Album Art Tile", namespace: "simnet", author: "Simon Burke") {
        capability "Refresh"
        
        attribute "artist"           , "string"
        attribute "album"            , "string"
        attribute "albumMBId"        , "string"
        attribute "coverArtURL"  , "string"
        
        attribute "coverArtTile"     , "string"
        
        command "setCurrentAlbum" , [[name:"partist", type: "STRING", description: "Enter the Artist" ], [name:"palbum", type: "STRING", description: "Enter the Album" ] ]
        command "correctAlbumMBId", [[name:"partist", type: "STRING", description: "Enter the Artist" ], [name:"palbum", type: "STRING", description: "Enter the Album" ], [name:"pmbId", type: "STRING", description: "Enter the Music Brainz ID" ] ]
	}

	preferences {
		
        input(name: "DebugLogging", type: "bool", title:"Enable Debug Logging", displayDuringSetup: true, defaultValue: false)
        input(name: "WarnLogging", type: "bool", title:"Enable Warning Logging", displayDuringSetup: true, defaultValue: true)
        input(name: "ErrorLogging", type: "bool", title:"Enable Error Logging", displayDuringSetup: true, defaultValue: true)
        input(name: "InfoLogging", type: "bool", title:"Enable Description Text (Info) Logging", displayDuringSetup: true, defaultValue: false)
    }
}

void installed() {
    debugLog("installed: device installed")
    state.albumList = [:];
}

void updated() { debugLog("updated: device updated") }


void refresh() { debugLog("refresh: device refreshed") }

void setCurrentAlbum(String partist, String palbum) {
    
    String mbId = '';
    String coverArtURL = '';
    
    String urlEncoding = 'UTF-8';
    String bodyJson = '';
    def getParams = [:];
    def headers = [:];
    String uri = '';
    
    if(!findAlbum(partist, palbum)) {
    
        uri = "https://musicbrainz.org/ws/2/release-group?query=artistname%58%22${URLEncoder.encode(partist,urlEncoding)}%22ANDrelease%58%22${URLEncoder.encode(palbum,urlEncoding)}%22";
        headers.put("accept", "application/json")
        getParams = [
            uri: uri,
            headers: headers,
            contentType: "application/json",
            requestContentType: "application/json",
	    	body : bodyJson
	    ]
               
	    try {
            httpGet(getParams)
            { resp -> 
                //log.debug("refresh: resp = ${resp.data}")
                mbId = "${resp.data.'release-groups'[0].releases[0].id.value}";
                debugLog("setCurrentAlbum: MB Id = ${mbId}")
                
            }
        }
        catch(Exception e)
        {
            errorLog("setCurrentAlbum: Error looking up MusicBrainz Id -  ${e}")   
        }
        
        uri = "https://ia600900.us.archive.org/14/items/mbid-${mbId}/index.json"
        getParams = [
            uri: uri,
            headers: headers,
            contentType: "application/json",
            requestContentType: "application/json",
	    	body : bodyJson
	    ]
        
        try {
            httpGet(getParams) { resp -> 
                //log.debug("setCurrentAlbum: resp = ${resp.data}")
                coverArtURL = "${resp.data.images[0].thumbnails.small}";
                debugLog("setCurrentAlbum: Cover Art URL = ${coverArtURL}")
            }
            debugLog("setCurrentAlbum: updating album list for Artist: ${partist}, Album: ${palbum}, MB Id: ${mbId}, Cover Art URL: ${coverArtURL}");
            updateAlbumList(partist, palbum, mbId, coverArtURL)
        }
        catch(Exception e)
        {
            errorLog("setCurrentAlbum: Error looking up Cover Art Archive - ${e}")   
        }
    }
    setArtist(partist);
    setAlbum(palbum);
    setAlbumMBId(mbId);
    
    setCoverArtTile(partist, palbum)
}

boolean findAlbum(String partist, String palbum) {

    boolean result = false;
    String[] albumDetail = null;
    
    if(state.albumList != null) {
        albumDetail = state.albumList.get(partist)?.get(palbum);
    }
    if (albumDetail != null) { result = true }
    return result
}

void updateAlbumList(String partist, String palbum, String pmbId, String pcoverArtURL) {
    debugLog("updateAlbumList: method called")
    if(!findAlbum(partist, palbum)) {
        debugLog("updateAlbumList: Album not found, about to add ${partist}, Album: ${palbum}, MB Id: ${pmbId}, Cover Art URL: ${pcoverArtURL}");
        if (state.albumList == null) { state.albumList = [:] }
        state.albumList.putAll([(partist): [(palbum): [(pmbId): (pcoverArtURL)]]])
    }
}

void setAlbum(String palbum) {
    debugLog("setAlbum: album provided = ${palbum}")
    device.sendEvent(name: "album", value: palbum);
}

void setArtist(String partist) {
    debugLog("setArtist: artist provided = ${partist}")
    device.sendEvent(name: "artist", value: partist);
}

void setAlbumMBId(String pmbId) {
    
    debugLog("setAlbumMBId: Album MB Id provided = ${pmbId}")
    device.sendEvent(name: "albumMBId", value: pmbId);
}

void setCoverArtURL(pcoverArtURL) {
    
    debugLog("setCoverArtURL: Cover Art URL provided = ${pcoverArtURL}")
    device.sendEvent(name: "coverArtURL", value: pcoverArtURL);
}

void setCoverArtTile(String partist, String palbum) {
 
    debugLog("setCoverArtTile: Artist provided = ${partist}, Album provided = ${palbum}")
    
    String coverArtURL = '';
    if(findAlbum(partist, palbum)) {
        
        coverArtURL = state.albumList.get(partist).get(palbum).entrySet().iterator().next().getValue();
        device.sendEvent(name: "coverArtTile", value: "<img src=\"${coverArtURL}\" />")
        setCoverArtURL(coverArtURL);
    }
}

void correctAlbumMBId(String partist, String palbum, String pmbId) {
    
    debugLog("correctAlbumMBId: Artist provided = ${partist}, Album provided = ${palbum}, MB Id = ${pmbId}")
    debugLog("correctAlbumMBId: Yet to be implemented")
}

//Utility methods
void debugLog(debugMessage) {
	if (DebugLogging == true) {log.debug(debugMessage)}	
}

void errorLog(errorMessage) {
    if (ErrorLogging == true) { log.error(errorMessage)}  
}

void infoLog(infoMessage) {
    if(InfoLogging == true) {log.info(infoMessage)}    
}

void warnLog(warnMessage) {
    if(WarnLogging == true) {log.warn(warnMessage)}    
}
1 Like

Getting closer....

/**
 *  Album Art Tile
 *
 *  Copyright 2021 Simon Burke
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 *  Change History:
 *
 *    Date        Who            What
 *    ----        ---            ----
 *    2021-10-02  Simon Burke    Original Creation
 * 
 */
metadata {
	definition (name: "Album Art Tile", namespace: "simnet", author: "Simon Burke") {
        capability "Refresh"
        
        attribute "artist"           , "string"
        attribute "album"            , "string"
        attribute "albumMBId"        , "string"
        attribute "coverArtURL"  , "string"
        
        attribute "coverArtTile"     , "string"
        
        command "setCurrentAlbum" , [[name:"partist", type: "STRING", description: "Enter the Artist" ], [name:"palbum", type: "STRING", description: "Enter the Album" ] ]
        command "correctAlbumMBId", [[name:"partist", type: "STRING", description: "Enter the Artist" ], [name:"palbum", type: "STRING", description: "Enter the Album" ], [name:"pmbId", type: "STRING", description: "Enter the Music Brainz ID" ] ]
        command "removeAlbum"     , [[name:"partist", type: "STRING", description: "Enter the Artist" ], [name:"palbum", type: "STRING", description: "Enter the Album" ] ]
        command "removeArtist"    , [[name:"partist", type: "STRING", description: "Enter the Artist" ] ]
        command "clearAlbumList"
	}

	preferences {
		
        input(name: "country", type: "string", title:"Preferred country for searches (2 characters)", displayDuringSetup: true, defaultValue: "US")
        input(name: "DebugLogging", type: "bool", title:"Enable Debug Logging", displayDuringSetup: true, defaultValue: false)
        input(name: "WarnLogging", type: "bool", title:"Enable Warning Logging", displayDuringSetup: true, defaultValue: true)
        input(name: "ErrorLogging", type: "bool", title:"Enable Error Logging", displayDuringSetup: true, defaultValue: true)
        input(name: "InfoLogging", type: "bool", title:"Enable Description Text (Info) Logging", displayDuringSetup: true, defaultValue: false)
    }
}

def removeAlbum(String partist, String palbum) {
    if(state.albumList?.get((partist)) != null) { 
        debugLog("removeAlbum: ${partist} found, removing ${palbum}")
        state.albumList.get((partist)).remove((palbum))
        if(state.albumList.get((partist)).isEmpty()) { state.albumList.remove((partist)) }
    }
    else { debugLog("removeAlbum: Unable to find ${partist}") }
}

def removeArtist(String partist) {
    if(state.albumList?.get((partist)) != null) { 
        debugLog("removeArtist: ${partist} found")
        state.albumList.remove((partist))
    }
    else { debugLog("removeArtist: Unable to find ${partist}") }
}


void clearAlbumList() {
     state.albumList = [:]
}

void installed() {
    debugLog("installed: device installed")
    state.albumList = [:];
}

void updated() { debugLog("updated: device updated") }


void refresh() { debugLog("refresh: device refreshed") }

void setCurrentAlbum(String partist, String palbum) {
    
    String[] mbIdList = new String[3];
    String mbId = null;
    String coverArtURL = null;
    
    String urlEncoding = 'UTF-8';
    String bodyJson = '';
    def getParams = [:];
    def headers = [:];
    String uri = '';
    
    if(!findAlbum(partist, palbum)) {
        String uriNoCountry = "https://musicbrainz.org/ws/2/release/?query=artistname:%22${URLEncoder.encode(partist,urlEncoding)}%22%20AND%20release:%22${URLEncoder.encode(palbum,urlEncoding)}%22%20AND%20primarytype:%22Album%22%20AND%20status:%22official%22";
        
        uri = "${uriNoCountry}%20AND%20country:%22${country}%22";
        debugLog("setCurrentAlbum: Testing - ${uri}")
        headers.put("accept", "application/json")
        getParams = [
            uri: uri,
            headers: headers,
            contentType: "application/json",
            requestContentType: "application/json",
	    	body : bodyJson
	    ]
               
	    try {
            httpGet(getParams)
            { resp -> 
                debugLog("setCurrentAlbum: resp = ${resp.data}")
                
                if(resp.data.releases[0]?.id?.value != null) { mbIdList[0] = "${resp.data.releases[0]?.id?.value}" }
                if(resp.data.releases[1]?.id?.value != null) { mbIdList[1] = "${resp.data.releases[1]?.id?.value}" }
                if(resp.data.releases[2]?.id?.value != null) { mbIdList[2] = "${resp.data.releases[2]?.id?.value}" }
                debugLog("setCurrentAlbum: mbIds = ${mbIdList[0]}, ${mbIdList[1]}, ${mbIdList[2]}")
            }
        }
        catch(Exception e)
        {
            errorLog("setCurrentAlbum: Error looking up MusicBrainz Id -  ${e}")   
        }
        if(mbIdList[0] == null) {
         
            getParams.uri = uriNoCountry;
            httpGet(getParams)
            { resp -> 
                debugLog("setCurrentAlbum: resp = ${resp.data}")
                
                if(resp.data.releases[0]?.id?.value != null) { mbIdList[0] = "${resp.data.releases[0]?.id?.value}" }
                if(resp.data.releases[1]?.id?.value != null) { mbIdList[1] = "${resp.data.releases[1]?.id?.value}" }
                if(resp.data.releases[2]?.id?.value != null) { mbIdList[2] = "${resp.data.releases[2]?.id?.value}" }
                debugLog("setCurrentAlbum: mbIds = ${mbIdList[0]}, ${mbIdList[1]}, ${mbIdList[2]}")
            }
        }
        boolean imageFound = false;
        for(int i = 0; i < 3 && coverArtURL == null && mbIdList[i] != null; i++) {
            mbId = mbIdList[i];
            uri = "https://ia600900.us.archive.org/14/items/mbid-${mbId}/index.json"
            getParams.uri = uri
            
            try {
                httpGet(getParams) { resp -> 
                    //log.debug("setCurrentAlbum: resp = ${resp.data.images}")
                    //log.debug("setCurrentAlbum: resp = ${resp.data.images?.get(0)}")
                    coverArtURL = "${resp.data.images?.get(0)?.thumbnails?.small}";
                    
                    debugLog("setCurrentAlbum: Cover Art URL = ${coverArtURL}")
                    
                }
            }
            catch(Exception e)
            {
                errorLog("setCurrentAlbum: Error looking up Cover Art Archive - ${e}")   
            }
        }
        if(coverArtURL != null) {
            debugLog("setCurrentAlbum: updating album list for Artist: ${partist}, Album: ${palbum}, MB Id: ${mbId}, Cover Art URL: ${coverArtURL}");
            updateAlbumList(partist, palbum, mbId, coverArtURL)
        }
    }
    
    setCoverArtTile(partist, palbum)
}

boolean findAlbum(String partist, String palbum) {

    boolean result = false;
    def albumDetail = [:];
    
    if(state.albumList != null) {
        albumDetail = state.albumList.get(partist)?.get(palbum);
    }
    if (albumDetail != null) { result = true }
    return result
}

def getArtistAlbums(String partist) {

    def artistAlbums;
    
    if(state.albumList != null) {
        artistAlbums = state.albumList.get(partist);
    }
    if(artistAlbums == null){ artistAlbums = [:] }
    
    debugLog("getArtistAlbums: Artist = ${partist}, albums = ${artistAlbums}")
    return artistAlbums
}


void updateAlbumList(String partist, String palbum, String pmbId, String pcoverArtURL) {

    def artistAlbums = [:];
    
    debugLog("updateAlbumList: method called")
    if(!findAlbum(partist, palbum)) {
        
        debugLog("updateAlbumList: Album not found, about to add ${partist}, Album: ${palbum}, MB Id: ${pmbId}, Cover Art URL: ${pcoverArtURL}");
        
        if (state.albumList == null) { state.albumList = [:] }
        artistAlbums = getArtistAlbums(partist)
        
        if(artistAlbums.isEmpty()) { artistAlbums.putAll([(palbum): [(pmbId): (pcoverArtURL)]]) }
        else { artistAlbums.putAll([(palbum): [(pmbId): (pcoverArtURL)]]) }
        
        state.albumList.putAll([(partist): artistAlbums])
    }
}

void setAlbum(String palbum) {
    debugLog("setAlbum: album provided = ${palbum}")
    device.sendEvent(name: "album", value: palbum);
}

void setArtist(String partist) {
    debugLog("setArtist: artist provided = ${partist}")
    device.sendEvent(name: "artist", value: partist);
}

void setAlbumMBId(String pmbId) {
    
    debugLog("setAlbumMBId: Album MB Id provided = ${pmbId}")
    device.sendEvent(name: "albumMBId", value: pmbId);
}

void setCoverArtURL(pcoverArtURL) {
    
    debugLog("setCoverArtURL: Cover Art URL provided = ${pcoverArtURL}")
    device.sendEvent(name: "coverArtURL", value: pcoverArtURL);
}

void setCoverArtTile(String partist, String palbum) {
 
    debugLog("setCoverArtTile: Artist provided = ${partist}, Album provided = ${palbum}")
    
    String coverArtURL = '';
    if(findAlbum(partist, palbum)) {
        
        setArtist(partist);
        setAlbum(palbum);
        setAlbumMBId(coverArtURL = state.albumList.get(partist).get(palbum).entrySet().iterator().next().getKey());
        
        coverArtURL = state.albumList.get(partist).get(palbum).entrySet().iterator().next().getValue();
        device.sendEvent(name: "coverArtTile", value: "<img src=\"${coverArtURL}\" />")
        setCoverArtURL(coverArtURL);
    }
}

void correctAlbumMBId(String partist, String palbum, String pmbId) {
    
    debugLog("correctAlbumMBId: Artist provided = ${partist}, Album provided = ${palbum}, MB Id = ${pmbId}")
    debugLog("correctAlbumMBId: Yet to be implemented")
}

//Utility methods
void debugLog(debugMessage) {
	if (DebugLogging == true) {log.debug(debugMessage)}	
}

void errorLog(errorMessage) {
    if (ErrorLogging == true) { log.error(errorMessage)}  
}

void infoLog(infoMessage) {
    if(InfoLogging == true) {log.info(infoMessage)}    
}

void warnLog(warnMessage) {
    if(WarnLogging == true) {log.warn(warnMessage)}    
}

Released an early version of this via HPM. Details on how to use it will come hopefully in the next few days.

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.