# -*- coding: utf-8 -*- ################################################################################################# import os import xbmc import xbmcvfs from mutagen.flac import FLAC from mutagen.id3 import ID3 from mutagen import id3 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 = utils.tryEncode(filename) # 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 = utils.tryDecode(xbmc.translatePath(tempfile)) 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(plexid, plex_rating, API, kodicursor, emby_db, enableimportsongrating, enableexportsongrating, enableupdatesongrating): emby = embyserver.Read_EmbyServer() previous_values = None filename = API.getFilePath() rating = 0 plex_rating = int(round(plex_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(plexid) 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 (plex_rating, comment, False) logMsg("getAdditionalSongTags --> plexid: %s - plex_rating: %s - file_rating: %s - current rating in kodidb: %s" %(plexid, plex_rating, file_rating, currentvalue)) updateFileRating = False updateEmbyRating = False if currentvalue != None: # we need to translate the emby values... if plex_rating == 1 and currentvalue == 2: plex_rating = 2 if plex_rating == 3 and currentvalue == 4: plex_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: plex_rating = currentvalue if (plex_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 (plex_rating != currentvalue) and (file_rating == currentvalue): #emby rating changed - update the file rating = plex_rating updateFileRating = True elif (file_rating != currentvalue) and (plex_rating == currentvalue): #file rating was updated, sync change to emby rating = file_rating updateEmbyRating = True elif (plex_rating != currentvalue) and (file_rating != currentvalue): #both ratings have changed (corner case) - the highest rating wins... if plex_rating > file_rating: rating = plex_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 plex_rating == 1 and file_rating == 2: plex_rating = 2 if plex_rating == 3 and file_rating == 4: plex_rating = 4 if plex_rating != file_rating: updateEmbyRating = True elif enableexportsongrating: #set the initial rating to emby value rating = plex_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" %plexid, "true") #set temp windows prop to ignore the update from webclient update emby.updateUserRating(plexid, 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 = utils.tryDecode(xbmc.translatePath(tempfile)) 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)