diff --git a/addon.xml b/addon.xml index 6b9dcfa..de58565 100644 --- a/addon.xml +++ b/addon.xml @@ -1,7 +1,7 @@ diff --git a/lib/helpers/mbrainz.py b/lib/helpers/mbrainz.py index 4d6a50e..babb78c 100644 --- a/lib/helpers/mbrainz.py +++ b/lib/helpers/mbrainz.py @@ -11,6 +11,7 @@ from simplecache import use_cache import xbmcvfs import xbmcaddon +import xbmc class MusicBrainz(object): @@ -36,53 +37,28 @@ def __init__(self, simplecache=None): del addon self.mbrainz = mbrainz - @use_cache(60) + #@use_cache(60) def search(self, artist, album, track): '''get musicbrainz id by query of artist, album and/or track''' - mb_albums = [] albumid = "" artistid = "" - if artist and album: - mb_albums = self.mbrainz.search_release_groups(query=album, - limit=3, offset=None, strict=False, artist=artist) - elif not mb_albums and artist and track: - mb_albums = self.mbrainz.search_recordings(query=track, - limit=3, offset=None, strict=False, artist=artist) - elif not mb_albums and artist and album: - # use albumname as track - track = album - mb_albums = self.mbrainz.search_recordings(query=track, - limit=3, offset=None, strict=False, artist=artist) + try: - if mb_albums and mb_albums.get("release-group-list"): - mb_albums = mb_albums.get("release-group-list") - elif mb_albums and mb_albums.get("recording-list"): - mb_albums = mb_albums.get("recording-list") + # lookup with artist and album (preferred method) + if artist and album: + artistid, albumid = self.search_release_group_match(artist, album) + + # lookup with artist and track (if no album provided) + if not (artistid and albumid) and artist and track: + artistid, albumid = self.search_recording_match(artist, track) + + # last resort: lookup with trackname as album + if not (artistid and albumid) and artist and track: + artistid, albumid = self.search_release_group_match(artist, track) + + except Exception as exc: + log_msg("Error in musicbrainz.search: %s" % str(exc), xbmc.LOGWARNING) - for mb_album in mb_albums: - if artistid: - break - if mb_album and isinstance(mb_album, dict): - albumid = mb_album.get("id", "") - if mb_album.get("artist-credit"): - for mb_artist in mb_album.get("artist-credit"): - if isinstance(mb_artist, dict) and mb_artist.get("artist", ""): - # safety check - only allow exact artist match - foundartist = mb_artist["artist"].get("name") - foundartist = foundartist.encode("utf-8").decode("utf-8") - if foundartist and get_compare_string(foundartist) == get_compare_string(artist): - artistid = mb_artist.get("artist").get("id") - break - if not artistid and mb_artist["artist"].get("alias-list"): - alias_list = [get_compare_string(item["alias"]) - for item in mb_artist["artist"]["alias-list"]] - if get_compare_string(artist) in alias_list: - artistid = mb_artist.get("artist").get("id") - break - else: - log_msg("mb_album not a dict! -- %s" % mb_album) - if not artistid: - albumid = "" return (artistid, albumid) def get_artist_id(self, artist, album, track): @@ -128,7 +104,7 @@ def get_albuminfo(self, mbalbumid): if not item in result["genre"] and int(tag["count"]) > 4: result["genre"].append(item) except Exception as exc: - log_msg("Error in musicbrainz - get album details: %s" % str(exc)) + log_msg("Error in musicbrainz - get album details: %s" % str(exc), xbmc.LOGWARNING) return result @staticmethod @@ -139,3 +115,86 @@ def get_albumthumb(albumid): if xbmcvfs.exists(url): thumb = url return thumb + + def search_release_group_match(self, artist, album): + '''try to get a match on releasegroup for given artist/album combi''' + artistid = "" + albumid = "" + mb_albums = self.mbrainz.search_release_groups(query=album, + limit=3, offset=None, strict=False, artist=artist) + if mb_albums and mb_albums.get("release-group-list"): + for mb_album in mb_albums["release-group-list"]: + if artistid and albumid: + break + if mb_album and isinstance(mb_album, dict): + if mb_album.get("artist-credit"): + artistid = self.match_artistcredit(mb_album["artist-credit"], artist) + if artistid: + albumid = mb_album.get("id", "") + break + return (artistid, albumid) + + @staticmethod + def match_artistcredit(artist_credit, artist): + '''find match for artist in artist-credits''' + artistid = "" + for mb_artist in artist_credit: + if artistid: + break + if isinstance(mb_artist, dict) and mb_artist.get("artist", ""): + # safety check - only allow exact artist match + foundartist = mb_artist["artist"].get("name") + foundartist = foundartist.encode("utf-8").decode("utf-8") + if foundartist and get_compare_string(foundartist) == get_compare_string(artist): + artistid = mb_artist.get("artist").get("id") + break + if not artistid and mb_artist["artist"].get("alias-list"): + alias_list = [get_compare_string(item["alias"]) + for item in mb_artist["artist"]["alias-list"]] + if get_compare_string(artist) in alias_list: + artistid = mb_artist.get("artist").get("id") + break + for item in artist.split("&"): + item = get_compare_string(item) + if item in alias_list or item in get_compare_string(foundartist): + artistid = mb_artist.get("artist").get("id") + break + return artistid + + + def search_recording_match(self, artist, track): + '''try to get the releasegroup (album) for the given artist/track combi, various-artists compilations are ignored''' + artistid = "" + albumid = "" + mb_albums = self.mbrainz.search_recordings(query=track, + limit=20, offset=None, strict=False, artist=artist) + if mb_albums and mb_albums.get("recording-list"): + for mb_recording in mb_albums["recording-list"]: + if albumid and artistid: + break + if mb_recording and isinstance(mb_recording, dict): + # look for match on artist + if mb_recording.get("artist-credit"): + artistid = self.match_artistcredit(mb_recording["artist-credit"], artist) + # if we have a match on artist, look for match in release list + if artistid: + if mb_recording.get("release-list"): + for mb_release in mb_recording["release-list"]: + if mb_release.get("artist-credit"): + if mb_release["artist-credit"][0].get("id","") == artistid: + albumid = mb_release["release-group"]["id"] + break + else: + continue + if mb_release.get("artist-credit-phrase","") == 'Various Artists': + continue + # grab release group details to make sure we're not looking at some various artists compilation + mb_album = self.mbrainz.get_release_group_by_id( + mb_release["release-group"]["id"], includes=["artist-credits"]) + mb_album = mb_album["release-group"] + if mb_album.get("artist-credit"): + artistid = self.match_artistcredit(mb_album["artist-credit"], artist) + if artistid: + albumid = mb_release["release-group"]["id"] + break + return (artistid, albumid) diff --git a/lib/helpers/musicartwork.py b/lib/helpers/musicartwork.py index f2b273b..c6bcb93 100644 --- a/lib/helpers/musicartwork.py +++ b/lib/helpers/musicartwork.py @@ -39,14 +39,15 @@ def get_music_artwork(self, artist, album, track, disc, ignore_cache=False, appe '''get music metadata by providing artist and/or track''' if artist == track or album == track: track = "" - artist = artist.split(" / ")[0] - artist = artist.split("/")[0] + artist = self.get_clean_title(artist) + album = self.get_clean_title(album) + track = self.get_clean_title(track) details = self.get_artist_metadata(artist, album, track, ignore_cache=ignore_cache) if album or track: album_details = self.get_album_metadata(artist, album, track, disc, ignore_cache=ignore_cache) if appendplot and details.get("plot") and album_details.get("plot"): - details["plot"] = "%s -- %s" % (album_details["plot"], details["plot"]) - details = extend_dict(details, album_details, ["thumb", "style", "mood"]) + album_details["plot"] = "%s -- %s" % (album_details["plot"], details["plot"]) + details = extend_dict(details, album_details, ["thumb", "style", "mood", "plot"]) if track: details["title"] = track return details @@ -55,20 +56,22 @@ def manual_set_music_artwork(self, artist, album, track, disc): '''manual override artwork options''' if album: - artwork = self.get_album_metadata(artist, album, track, disc) + details = self.get_album_metadata(artist, album, track, disc) art_types = ["thumb", "discart"] else: - artwork = self.get_artist_metadata(artist, album, track) + details = self.get_artist_metadata(artist, album, track) art_types = ["thumb", "poster", "fanart", "banner", "clearart", "clearlogo", "landscape"] - cache_str = artwork["cachestr"] + cache_str = details["cachestr"] + + changemade = False # show dialogselect with all artwork options abort = False while not abort: listitems = [] for arttype in art_types: - listitem = xbmcgui.ListItem(label=arttype, iconImage=artwork["art"].get(arttype, "")) - listitem.setProperty("icon", artwork["art"].get(arttype, "")) + listitem = xbmcgui.ListItem(label=arttype, iconImage=details["art"].get(arttype, "")) + listitem.setProperty("icon", details["art"].get(arttype, "")) listitems.append(listitem) dialog = DialogSelect("DialogSelect.xml", "", listing=listitems, window_title=xbmc.getLocalizedString(13511), multiselect=False) @@ -99,7 +102,7 @@ def manual_set_music_artwork(self, artist, album, track, disc): artoptions.append(listitem) # add remaining images as option - allarts = artwork["art"].get(label + "s", []) + allarts = details["art"].get(label + "s", []) if len(allarts) > 1: for item in allarts: listitem = xbmcgui.ListItem(label=item, iconImage=item) @@ -111,9 +114,11 @@ def manual_set_music_artwork(self, artist, album, track, disc): selected_item = dialog.result del dialog if image and selected_item == 1: - artwork["art"][label] = "" + details["art"][label] = "" + changemade = True elif image and selected_item > 2: - artwork["art"][label] = artoptions[selected_item].getProperty("icon") + details["art"][label] = artoptions[selected_item].getProperty("icon") + changemade = True elif (image and selected_item == 2) or not image and selected_item == 0: # manual browse... dialog = xbmcgui.Dialog() @@ -121,10 +126,25 @@ def manual_set_music_artwork(self, artist, album, track, disc): 'files', mask='.gif|.png|.jpg').decode("utf-8") del dialog if image: - artwork["art"][label] = image - - # save results in cache - self.artutils.cache.set(cache_str, artwork, expiration=timedelta(days=120)) + details["art"][label] = image + changemade = True + + # save results if any changes made + if changemade: + # download artwork to music folder if needed + refresh_needed = False + if details.get("diskpath") and self.artutils.addon.getSetting("music_art_download") == "true": + details["art"] = download_artwork(details["diskpath"], details["art"]) + refresh_needed = True + # download artwork to custom folder if needed + if details.get("customartpath") and self.artutils.addon.getSetting("music_art_download_custom") == "true": + details["art"] = download_artwork(details["customartpath"], details["art"]) + refresh_needed = True + self.artutils.cache.set(cache_str, details, expiration=timedelta(days=120)) + # reload skin to make sure new artwork is visible + if refresh_needed: + xbmc.sleep(500) + xbmc.executebuiltin("ReloadSkin()") def music_artwork_options(self, artist, album, track, disc): '''show options for music artwork''' @@ -178,6 +198,7 @@ def get_artist_metadata(self, artist, album, track, ignore_cache=False): if diskpath: details["art"] = extend_dict(details["art"], self.lookup_artistart_in_folder(diskpath)) local_path_custom = diskpath + details["customartpath"] = diskpath # lookup online metadata if self.artutils.addon.getSetting("music_art_scraper") == "true": if not album and not track: @@ -210,7 +231,7 @@ def get_artist_metadata(self, artist, album, track, ignore_cache=False): # set default details if not details.get("artist"): details["artist"] = artist - if not details["art"].get("artistthumb") and details["art"].get("thumb"): + if details["art"].get("thumb"): details["art"]["artistthumb"] = details["art"]["thumb"] # store results in cache and return results @@ -245,6 +266,7 @@ def get_album_metadata(self, artist, album, track, disc, ignore_cache=False): if diskpath: details["art"] = extend_dict(details["art"], self.lookup_albumart_in_folder(diskpath)) local_path_custom = diskpath + details["customartpath"] = diskpath # lookup online metadata if self.artutils.addon.getSetting("music_art_scraper") == "true": mb_albumid = self.get_mb_album_id(artist, album, track) @@ -271,7 +293,7 @@ def get_album_metadata(self, artist, album, track, disc, ignore_cache=False): # set default details if not details.get("album") and details.get("title"): details["album"] = details["title"] - if not details["art"].get("albumthumb") and details["art"].get("thumb"): + if details["art"].get("thumb"): details["art"]["albumthumb"] = details["art"]["thumb"] # store results in cache and return results @@ -291,6 +313,10 @@ def get_artist_kodi_metadata(self, artist): filters = [{"artistid": details["artistid"]}] artist_albums = self.artutils.kodidb.albums(filters=filters) details["albums"] = [] + details["tracks"] = [] + bullet = "•".decode("utf-8") + details["tracks.formatted"] = u"" + details["tracks.formatted2"] = "" for item in artist_albums: details["albums"].append(item["label"]) if not song_path: @@ -302,18 +328,19 @@ def get_artist_kodi_metadata(self, artist): details["diskpath"] = self.get_artistpath_by_songpath(song_path, artist) details["ref_album"] = item["title"] details["ref_track"] = album_tracks[0]["title"] + for item in album_tracks: + details["tracks"].append(item["title"]) + details["tracks.formatted"] += u"%s %s [CR]" % (bullet, item["title"]) + duration = item["duration"] + total_seconds = int(duration) + minutes = total_seconds / 60 + seconds = total_seconds - (minutes * 60) + duration = "%s:%s" % (minutes, str(seconds).zfill(2)) + details["tracks.formatted2"] += u"%s %s (%s)[CR]" % (bullet, item["title"], duration) joinchar = "[CR]• ".decode("utf-8") details["albums.formatted"] = joinchar.join(details["albums"]) - details["art"] = {} - fanart = get_clean_image(details["fanart"]) - if xbmcvfs.exists(fanart): - details["art"]["fanart"] = fanart - del details["fanart"] - thumbnail = get_clean_image(details["thumbnail"]) - if xbmcvfs.exists(thumbnail): - details["art"]["thumb"] = thumbnail - details["art"]["artistthumb"] = thumbnail - del details["thumbnail"] + # do not retrieve artwork from item as there's no way to write it back + # and it will already be retrieved if user enables to get the artwork from the song path return details def get_album_kodi_metadata(self, artist, album, track, disc): @@ -352,15 +379,8 @@ def get_album_kodi_metadata(self, artist, album, track, disc): details["diskpath"] = self.get_albumpath_by_songpath(item["file"]) details["art"] = {} details["songcount"] = len(album_tracks) - fanart = get_clean_image(details["fanart"]) - if xbmcvfs.exists(fanart): - details["art"]["fanart"] = fanart - del details["fanart"] - thumbnail = get_clean_image(details["thumbnail"]) - if xbmcvfs.exists(thumbnail): - details["art"]["thumb"] = thumbnail - details["art"]["albumthumb"] = thumbnail - del details["thumbnail"] + # do not retrieve artwork from item as there's no way to write it back + # and it will already be retrieved if user enables to get the artwork from the song path return details def get_mb_artist_id(self, artist, album, track): @@ -480,3 +500,16 @@ def get_customfolder_path(self, customfolder, foldername): else: return self.get_customfolder_path(curpath, foldername) return "" + + @staticmethod + def get_clean_title(title): + '''strip all unwanted characters from artist, album or track name''' + title = title.split("/")[0] + title = title.split("(")[0] + title = title.split("[")[0] + title = title.split("ft.")[0] + title = title.split("Ft.")[0] + title = title.split("Feat.")[0] + title = title.split("Featuring")[0] + title = title.split("featuring")[0] + return title.strip() diff --git a/lib/helpers/utils.py b/lib/helpers/utils.py index 0682934..a5fd182 100644 --- a/lib/helpers/utils.py +++ b/lib/helpers/utils.py @@ -406,11 +406,11 @@ def download_artwork(folderpath, artwork): new_dict[key] = download_image(os.path.join(folderpath, "poster.jpg"), value) elif key == "landscape": new_dict[key] = download_image(os.path.join(folderpath, "landscape.jpg"), value) - elif key == "fanarts" and value: + elif key == "fanarts" and value and not xbmcvfs.exists(efa_path): + # copy extrafanarts only if the directory doesn't exist at all delim = "\\" if "\\" in folderpath else "/" efa_path = "%sextrafanart" % folderpath + delim - if not xbmcvfs.exists(efa_path): - xbmcvfs.mkdir(efa_path) + xbmcvfs.mkdir(efa_path) images = [] for count, image in enumerate(value): image = download_image(os.path.join(efa_path, "fanart%s.jpg" % count), image) @@ -426,16 +426,35 @@ def download_image(filename, url): '''download specific image to local folder''' if not url: return url + refresh_needed = False if xbmcvfs.exists(filename) and filename == url: # only overwrite if new image is different return filename else: if xbmcvfs.exists(filename): xbmcvfs.delete(filename) + refresh_needed = True if xbmcvfs.copy(url, filename): + if refresh_needed: + refresh_image(filename) return filename + return url +def refresh_image(imagepath): + '''tell kodi texture cache to refresh a particular image''' + import sqlite3 + dbpath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8') + connection = sqlite3.connect(dbpath, timeout=30, isolation_level=None) + try: + cache_image = connection.execute('SELECT cachedurl FROM texture WHERE url = ?', (imagepath,)).fetchone() + if cache_image: + xbmcvfs.delete("special://profile/Thumbnails/%s" % cache_image) + connection.execute('DELETE FROM texture WHERE url = ?', (imagepath,)) + finally: + connection.close() + del connection + class DialogSelect(xbmcgui.WindowXMLDialog): '''wrapper around Kodi dialogselect to present a list of items'''