# -*- coding: utf-8 -*- ################################################################################################# import os import xbmc, xbmcaddon, xbmcvfs import utils from mutagen.flac import FLAC, Picture from mutagen.id3 import ID3 from mutagen import id3 import base64 ################################################################################################# # Helper for the music library, intended to fix missing song ID3 tags on Emby def logMsg(msg, lvl=1): utils.logMsg("%s %s" % ("Emby", "musictools"), msg, lvl) def getRealFileName(filename,useTemp = False): #get the filename path accessible by python if possible... useTemp = False if not xbmcvfs.exists(filename): logMsg( "File does not exist! %s" %(filename), 0) return (False, "") #if we use os.path method on older python versions (sunch as some android builds), we need to pass arguments as string if os.path.supports_unicode_filenames: checkfile = filename else: checkfile = filename.encode("utf-8") # determine if our python module is able to access the file directly... if os.path.exists(checkfile): filename = filename elif os.path.exists(checkfile.replace("smb://","\\\\").replace("/","\\")): filename = filename.replace("smb://","\\\\").replace("/","\\") else: #file can not be accessed by python directly, we copy it for processing... useTemp = True if useTemp: if "/" in filename: filepart = filename.split("/")[-1] else: filepart = filename.split("\\")[-1] tempfile = "special://temp/"+filepart xbmcvfs.copy(filename, tempfile) filename = xbmc.translatePath(tempfile).decode("utf-8") return (useTemp,filename) def getEmbyRatingFromKodiRating(rating): # Translation needed between Kodi/ID3 rating and emby likes/favourites: # 3+ rating in ID3 = emby like # 5+ rating in ID3 = emby favourite # rating 0 = emby dislike # rating 1-2 = emby no likes or dislikes (returns 1 in results) favourite = False deletelike = False like = False if (rating >= 3): like = True if (rating == 0): like = False if (rating == 1 or rating == 2): deletelike = True if (rating >= 5): favourite = True return(like, favourite, deletelike) def getAdditionalSongTags(embyid, emby_rating, API, kodicursor, emby_db, enableimportsongrating, enableexportsongrating, enableupdatesongrating): previous_values = None filename = API.getFilePath() rating = 0 emby_rating = int(round(emby_rating, 0)) #get file rating and comment tag from file itself. if enableimportsongrating: file_rating, comment, hasEmbeddedCover = getSongTags(filename) else: file_rating = 0 comment = "" hasEmbeddedCover = False emby_dbitem = emby_db.getItem_byId(embyid) try: kodiid = emby_dbitem[0] except TypeError: # Item is not in database. currentvalue = None else: query = "SELECT rating FROM song WHERE idSong = ?" kodicursor.execute(query, (kodiid,)) try: currentvalue = int(round(float(kodicursor.fetchone()[0]),0)) except: currentvalue = None # Only proceed if we actually have a rating from the file if file_rating is None and currentvalue: return (currentvalue, comment, False) elif file_rating is None and not currentvalue: return (emby_rating, comment, False) logMsg("getAdditionalSongTags --> embyid: %s - emby_rating: %s - file_rating: %s - current rating in kodidb: %s" %(embyid, emby_rating, file_rating, currentvalue)) updateFileRating = False updateEmbyRating = False if currentvalue != None: # we need to translate the emby values... if emby_rating == 1 and currentvalue == 2: emby_rating = 2 if emby_rating == 3 and currentvalue == 4: emby_rating = 4 #if updating rating into file is disabled, we ignore the rating in the file... if not enableupdatesongrating: file_rating = currentvalue #if convert emby likes/favourites convert to song rating is disabled, we ignore the emby rating... if not enableexportsongrating: emby_rating = currentvalue if (emby_rating == file_rating) and (file_rating != currentvalue): #the rating has been updated from kodi itself, update change to both emby ands file rating = currentvalue updateFileRating = True updateEmbyRating = True elif (emby_rating != currentvalue) and (file_rating == currentvalue): #emby rating changed - update the file rating = emby_rating updateFileRating = True elif (file_rating != currentvalue) and (emby_rating == currentvalue): #file rating was updated, sync change to emby rating = file_rating updateEmbyRating = True elif (emby_rating != currentvalue) and (file_rating != currentvalue): #both ratings have changed (corner case) - the highest rating wins... if emby_rating > file_rating: rating = emby_rating updateFileRating = True else: rating = file_rating updateEmbyRating = True else: #nothing has changed, just return the current value rating = currentvalue else: # no rating yet in DB if enableimportsongrating: #prefer the file rating rating = file_rating #determine if we should also send the rating to emby server if enableexportsongrating: if emby_rating == 1 and file_rating == 2: emby_rating = 2 if emby_rating == 3 and file_rating == 4: emby_rating = 4 if emby_rating != file_rating: updateEmbyRating = True elif enableexportsongrating: #set the initial rating to emby value rating = emby_rating if updateFileRating and enableupdatesongrating: updateRatingToFile(rating, filename) if updateEmbyRating and enableexportsongrating: # sync details to emby server. Translation needed between ID3 rating and emby likes/favourites: like, favourite, deletelike = getEmbyRatingFromKodiRating(rating) API.updateUserRating(embyid, like, favourite, deletelike) return (rating, comment, hasEmbeddedCover) def getSongTags(file): # Get the actual ID3 tags for music songs as the server is lacking that info rating = 0 comment = "" hasEmbeddedCover = False isTemp,filename = getRealFileName(file) logMsg( "getting song ID3 tags for " + filename) try: ###### FLAC FILES ############# if filename.lower().endswith(".flac"): audio = FLAC(filename) if audio.get("comment"): comment = audio.get("comment")[0] for pic in audio.pictures: if pic.type == 3 and pic.data: #the file has an embedded cover hasEmbeddedCover = True if audio.get("rating"): rating = float(audio.get("rating")[0]) #flac rating is 0-100 and needs to be converted to 0-5 range if rating > 5: rating = (rating / 100) * 5 ###### MP3 FILES ############# elif filename.lower().endswith(".mp3"): audio = ID3(filename) if audio.get("APIC:Front Cover"): if audio.get("APIC:Front Cover").data: hasEmbeddedCover = True if audio.get("comment"): comment = audio.get("comment")[0] if audio.get("POPM:Windows Media Player 9 Series"): if audio.get("POPM:Windows Media Player 9 Series").rating: rating = float(audio.get("POPM:Windows Media Player 9 Series").rating) #POPM rating is 0-255 and needs to be converted to 0-5 range if rating > 5: rating = (rating / 255) * 5 else: logMsg( "Not supported fileformat or unable to access file: %s" %(filename)) #the rating must be a round value rating = int(round(rating,0)) except Exception as e: #file in use ? logMsg("Exception in getSongTags %s" %e,0) rating = None #remove tempfile if needed.... if isTemp: xbmcvfs.delete(filename) return (rating, comment, hasEmbeddedCover) def updateRatingToFile(rating, file): #update the rating from Emby to the file isTemp,tempfile = getRealFileName(file,True) logMsg( "setting song rating: %s for filename: %s" %(rating,tempfile)) if not tempfile: return try: if tempfile.lower().endswith(".flac"): audio = FLAC(tempfile) calcrating = int(round((float(rating) / 5) * 100, 0)) audio["rating"] = str(calcrating) audio.save() elif tempfile.lower().endswith(".mp3"): audio = ID3(tempfile) calcrating = int(round((float(rating) / 5) * 255, 0)) audio.add(id3.POPM(email="Windows Media Player 9 Series", rating=calcrating, count=1)) audio.save() else: logMsg( "Not supported fileformat: %s" %(tempfile)) #once we have succesfully written the flags we move the temp file to destination, otherwise not proceeding and just delete the temp #safety check: we check if we can read the new tags successfully from the tempfile before deleting anything checksum_rating, checksum_comment, checksum_hasEmbeddedCover = getSongTags(tempfile) if checksum_rating == rating: xbmcvfs.delete(file) xbmcvfs.copy(tempfile,file) xbmcvfs.delete(tempfile) except Exception as e: #file in use ? logMsg("Exception in updateRatingToFile %s" %e,0)