diff --git a/resources/lib/musicutils.py b/resources/lib/musicutils.py index 97db089d..b058c5c5 100644 --- a/resources/lib/musicutils.py +++ b/resources/lib/musicutils.py @@ -1,286 +1,287 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import os - -import xbmc -import xbmcaddon -import xbmcvfs - -from mutagen.flac import FLAC, Picture -from mutagen.id3 import ID3 -from mutagen import id3 -import base64 - -import read_embyserver as embyserver -import utils - -################################################################################################# - -# 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, isTemp=False): - #get the filename path accessible by python if possible... - - 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... - isTemp = True - 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 (isTemp,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): - - emby = embyserver.Read_EmbyServer() - - 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) - utils.window("ignore-update-%s" %embyid, "true") #set temp windows prop to ignore the update from webclient update - emby.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 ? - utils.logMsg("Exception in getSongTags", str(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 - - f = xbmcvfs.File(file) - org_size = f.size() - f.close() - - #create tempfile - if "/" in file: filepart = file.split("/")[-1] - else: filepart = file.split("\\")[-1] - tempfile = "special://temp/"+filepart - xbmcvfs.copy(file, tempfile) - tempfile = xbmc.translatePath(tempfile).decode("utf-8") - - logMsg( "setting song rating: %s for filename: %s - using tempfile: %s" %(rating,file,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 the file size of the temp file before proceeding with overwite of original file - f = xbmcvfs.File(tempfile) - checksum_size = f.size() - f.close() - if checksum_size >= org_size: - xbmcvfs.delete(file) - xbmcvfs.copy(tempfile,file) - else: - logMsg( "Checksum mismatch for filename: %s - using tempfile: %s - not proceeding with file overwite!" %(rating,file,tempfile)) - - #always delete the tempfile - xbmcvfs.delete(tempfile) - - except Exception as e: - #file in use ? - logMsg("Exception in updateRatingToFile %s" %e,0) - - +# -*- coding: utf-8 -*- + +################################################################################################# + +import os + +import xbmc +import xbmcaddon +import xbmcvfs + +from mutagen.flac import FLAC, Picture +from mutagen.id3 import ID3 +from mutagen import id3 +import base64 + +import read_embyserver as embyserver +import utils + +################################################################################################# + +# 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, isTemp=False): + #get the filename path accessible by python if possible... + + 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... + isTemp = True + 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 (isTemp,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): + + emby = embyserver.Read_EmbyServer() + + 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) + utils.window("ignore-update-%s" %embyid, "true") #set temp windows prop to ignore the update from webclient update + emby.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 + break + 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 ? + utils.logMsg("Exception in getSongTags", str(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 + + f = xbmcvfs.File(file) + org_size = f.size() + f.close() + + #create tempfile + if "/" in file: filepart = file.split("/")[-1] + else: filepart = file.split("\\")[-1] + tempfile = "special://temp/"+filepart + xbmcvfs.copy(file, tempfile) + tempfile = xbmc.translatePath(tempfile).decode("utf-8") + + logMsg( "setting song rating: %s for filename: %s - using tempfile: %s" %(rating,file,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 the file size of the temp file before proceeding with overwite of original file + f = xbmcvfs.File(tempfile) + checksum_size = f.size() + f.close() + if checksum_size >= org_size: + xbmcvfs.delete(file) + xbmcvfs.copy(tempfile,file) + else: + logMsg( "Checksum mismatch for filename: %s - using tempfile: %s - not proceeding with file overwite!" %(rating,file,tempfile)) + + #always delete the tempfile + xbmcvfs.delete(tempfile) + + except Exception as e: + #file in use ? + logMsg("Exception in updateRatingToFile %s" %e,0) + + \ No newline at end of file