diff --git a/contextmenu.py b/contextmenu.py
index 3b367ab2..c91da6a1 100644
--- a/contextmenu.py
+++ b/contextmenu.py
@@ -1,159 +1,158 @@
-# -*- coding: utf-8 -*-
-
-#################################################################################################
-
-import os
-import sys
-import urlparse
-
-import xbmc
-import xbmcaddon
-import xbmcgui
-
-addon_ = xbmcaddon.Addon(id='plugin.video.emby')
-addon_path = addon_.getAddonInfo('path').decode('utf-8')
-base_resource = xbmc.translatePath(os.path.join(addon_path, 'resources', 'lib')).decode('utf-8')
-sys.path.append(base_resource)
-
-import artwork
-import utils
-import clientinfo
-import downloadutils
-import librarysync
-import read_embyserver as embyserver
-import embydb_functions as embydb
-import kodidb_functions as kodidb
-import musicutils as musicutils
-import api
-
-def logMsg(msg, lvl=1):
- utils.logMsg("%s %s" % ("EMBY", "Contextmenu"), msg, lvl)
-
-
-#Kodi contextmenu item to configure the emby settings
-#for now used to set ratings but can later be used to sync individual items etc.
-if __name__ == '__main__':
- itemid = xbmc.getInfoLabel("ListItem.DBID").decode("utf-8")
- itemtype = xbmc.getInfoLabel("ListItem.DBTYPE").decode("utf-8")
-
- emby = embyserver.Read_EmbyServer()
-
- embyid = ""
- if not itemtype and xbmc.getCondVisibility("Container.Content(albums)"): itemtype = "album"
- if not itemtype and xbmc.getCondVisibility("Container.Content(artists)"): itemtype = "artist"
- if not itemtype and xbmc.getCondVisibility("Container.Content(songs)"): itemtype = "song"
- if not itemtype and xbmc.getCondVisibility("Container.Content(pictures)"): itemtype = "picture"
-
- if (not itemid or itemid == "-1") and xbmc.getInfoLabel("ListItem.Property(embyid)"):
- embyid = xbmc.getInfoLabel("ListItem.Property(embyid)")
- else:
- embyconn = utils.kodiSQL('emby')
- embycursor = embyconn.cursor()
- emby_db = embydb.Embydb_Functions(embycursor)
- item = emby_db.getItem_byKodiId(itemid, itemtype)
- embycursor.close()
- if item: embyid = item[0]
-
- logMsg("Contextmenu opened for embyid: %s - itemtype: %s" %(embyid,itemtype))
-
- if embyid:
- item = emby.getItem(embyid)
- API = api.API(item)
- userdata = API.getUserData()
- likes = userdata['Likes']
- favourite = userdata['Favorite']
-
- options=[]
- if likes == True:
- #clear like for the item
- options.append(utils.language(30402))
- if likes == False or likes == None:
- #Like the item
- options.append(utils.language(30403))
- if likes == True or likes == None:
- #Dislike the item
- options.append(utils.language(30404))
- if favourite == False:
- #Add to emby favourites
- options.append(utils.language(30405))
- if favourite == True:
- #Remove from emby favourites
- options.append(utils.language(30406))
- if itemtype == "song":
- #Set custom song rating
- options.append(utils.language(30407))
-
- #delete item
- options.append(utils.language(30409))
-
- #addon settings
- options.append(utils.language(30408))
-
- #display select dialog and process results
- header = utils.language(30401)
- ret = xbmcgui.Dialog().select(header, options)
- if ret != -1:
- if options[ret] == utils.language(30402):
- emby.updateUserRating(embyid, deletelike=True)
- if options[ret] == utils.language(30403):
- emby.updateUserRating(embyid, like=True)
- if options[ret] == utils.language(30404):
- emby.updateUserRating(embyid, like=False)
- if options[ret] == utils.language(30405):
- emby.updateUserRating(embyid, favourite=True)
- if options[ret] == utils.language(30406):
- emby.updateUserRating(embyid, favourite=False)
- if options[ret] == utils.language(30407):
- kodiconn = utils.kodiSQL('music')
- kodicursor = kodiconn.cursor()
- query = ' '.join(("SELECT rating", "FROM song", "WHERE idSong = ?" ))
- kodicursor.execute(query, (itemid,))
- currentvalue = int(round(float(kodicursor.fetchone()[0]),0))
- newvalue = xbmcgui.Dialog().numeric(0, "Set custom song rating (0-5)", str(currentvalue))
- if newvalue:
- newvalue = int(newvalue)
- if newvalue > 5: newvalue = "5"
- if utils.settings('enableUpdateSongRating') == "true":
- musicutils.updateRatingToFile(newvalue, API.getFilePath())
- if utils.settings('enableExportSongRating') == "true":
- like, favourite, deletelike = musicutils.getEmbyRatingFromKodiRating(newvalue)
- emby.updateUserRating(embyid, like, favourite, deletelike)
- query = ' '.join(( "UPDATE song","SET rating = ?", "WHERE idSong = ?" ))
- kodicursor.execute(query, (newvalue,itemid,))
- kodiconn.commit()
-
- if options[ret] == utils.language(30408):
- #Open addon settings
- xbmc.executebuiltin("Addon.OpenSettings(plugin.video.emby)")
-
- if options[ret] == utils.language(30409):
- #delete item from the server
- delete = True
- if utils.settings('skipContextMenu') != "true":
- resp = xbmcgui.Dialog().yesno(
- heading="Confirm delete",
- line1=("Delete file from Emby Server? This will "
- "also delete the file(s) from disk!"))
- if not resp:
- logMsg("User skipped deletion for: %s." % embyid, 1)
- delete = False
-
- if delete:
- import downloadutils
- doUtils = downloadutils.DownloadUtils()
- url = "{server}/emby/Items/%s?format=json" % embyid
- logMsg("Deleting request: %s" % embyid, 0)
- doUtils.downloadUrl(url, type="DELETE")
-
- '''if utils.settings('skipContextMenu') != "true":
- if xbmcgui.Dialog().yesno(
- heading="Confirm delete",
- line1=("Delete file on Emby Server? This will "
- "also delete the file(s) from disk!")):
- import downloadutils
- doUtils = downloadutils.DownloadUtils()
- url = "{server}/emby/Items/%s?format=json" % embyid
- doUtils.downloadUrl(url, type="DELETE")'''
-
- xbmc.sleep(500)
+# -*- coding: utf-8 -*-
+
+#################################################################################################
+
+import os
+import sys
+import urlparse
+
+import xbmc
+import xbmcaddon
+import xbmcgui
+
+addon_ = xbmcaddon.Addon(id='plugin.video.emby')
+addon_path = addon_.getAddonInfo('path').decode('utf-8')
+base_resource = xbmc.translatePath(os.path.join(addon_path, 'resources', 'lib')).decode('utf-8')
+sys.path.append(base_resource)
+
+import artwork
+import utils
+import clientinfo
+import downloadutils
+import librarysync
+import read_embyserver as embyserver
+import embydb_functions as embydb
+import kodidb_functions as kodidb
+import musicutils as musicutils
+import api
+
+def logMsg(msg, lvl=1):
+ utils.logMsg("%s %s" % ("EMBY", "Contextmenu"), msg, lvl)
+
+
+#Kodi contextmenu item to configure the emby settings
+#for now used to set ratings but can later be used to sync individual items etc.
+if __name__ == '__main__':
+ itemid = xbmc.getInfoLabel("ListItem.DBID").decode("utf-8")
+ itemtype = xbmc.getInfoLabel("ListItem.DBTYPE").decode("utf-8")
+
+ emby = embyserver.Read_EmbyServer()
+
+ embyid = ""
+ if not itemtype and xbmc.getCondVisibility("Container.Content(albums)"): itemtype = "album"
+ if not itemtype and xbmc.getCondVisibility("Container.Content(artists)"): itemtype = "artist"
+ if not itemtype and xbmc.getCondVisibility("Container.Content(songs)"): itemtype = "song"
+ if not itemtype and xbmc.getCondVisibility("Container.Content(pictures)"): itemtype = "picture"
+
+ if (not itemid or itemid == "-1") and xbmc.getInfoLabel("ListItem.Property(embyid)"):
+ embyid = xbmc.getInfoLabel("ListItem.Property(embyid)")
+ else:
+ embyconn = utils.kodiSQL('emby')
+ embycursor = embyconn.cursor()
+ emby_db = embydb.Embydb_Functions(embycursor)
+ item = emby_db.getItem_byKodiId(itemid, itemtype)
+ embycursor.close()
+ if item: embyid = item[0]
+
+ logMsg("Contextmenu opened for embyid: %s - itemtype: %s" %(embyid,itemtype))
+
+ if embyid:
+ item = emby.getItem(embyid)
+ API = api.API(item)
+ userdata = API.getUserData()
+ likes = userdata['Likes']
+ favourite = userdata['Favorite']
+
+ options=[]
+ if likes == True:
+ #clear like for the item
+ options.append(utils.language(30402))
+ if likes == False or likes == None:
+ #Like the item
+ options.append(utils.language(30403))
+ if likes == True or likes == None:
+ #Dislike the item
+ options.append(utils.language(30404))
+ if favourite == False:
+ #Add to emby favourites
+ options.append(utils.language(30405))
+ if favourite == True:
+ #Remove from emby favourites
+ options.append(utils.language(30406))
+ if itemtype == "song":
+ #Set custom song rating
+ options.append(utils.language(30407))
+
+ #delete item
+ options.append(utils.language(30409))
+
+ #addon settings
+ options.append(utils.language(30408))
+
+ #display select dialog and process results
+ header = utils.language(30401)
+ ret = xbmcgui.Dialog().select(header, options)
+ if ret != -1:
+ if options[ret] == utils.language(30402):
+ emby.updateUserRating(embyid, deletelike=True)
+ if options[ret] == utils.language(30403):
+ emby.updateUserRating(embyid, like=True)
+ if options[ret] == utils.language(30404):
+ emby.updateUserRating(embyid, like=False)
+ if options[ret] == utils.language(30405):
+ emby.updateUserRating(embyid, favourite=True)
+ if options[ret] == utils.language(30406):
+ emby.updateUserRating(embyid, favourite=False)
+ if options[ret] == utils.language(30407):
+ kodiconn = utils.kodiSQL('music')
+ kodicursor = kodiconn.cursor()
+ query = ' '.join(("SELECT rating", "FROM song", "WHERE idSong = ?" ))
+ kodicursor.execute(query, (itemid,))
+ currentvalue = int(round(float(kodicursor.fetchone()[0]),0))
+ newvalue = xbmcgui.Dialog().numeric(0, "Set custom song rating (0-5)", str(currentvalue))
+ if newvalue:
+ newvalue = int(newvalue)
+ if newvalue > 5: newvalue = "5"
+ if utils.settings('enableUpdateSongRating') == "true":
+ musicutils.updateRatingToFile(newvalue, API.getFilePath())
+ if utils.settings('enableExportSongRating') == "true":
+ like, favourite, deletelike = musicutils.getEmbyRatingFromKodiRating(newvalue)
+ emby.updateUserRating(embyid, like, favourite, deletelike)
+ query = ' '.join(( "UPDATE song","SET rating = ?", "WHERE idSong = ?" ))
+ kodicursor.execute(query, (newvalue,itemid,))
+ kodiconn.commit()
+
+ if options[ret] == utils.language(30408):
+ #Open addon settings
+ xbmc.executebuiltin("Addon.OpenSettings(plugin.video.emby)")
+
+ if options[ret] == utils.language(30409):
+ #delete item from the server
+ delete = True
+ if utils.settings('skipContextMenu') != "true":
+ resp = xbmcgui.Dialog().yesno(
+ heading="Confirm delete",
+ line1=("Delete file from Emby Server? This will "
+ "also delete the file(s) from disk!"))
+ if not resp:
+ logMsg("User skipped deletion for: %s." % embyid, 1)
+ delete = False
+
+ if delete:
+ import downloadutils
+ doUtils = downloadutils.DownloadUtils()
+ url = "{server}/emby/Items/%s?format=json" % embyid
+ logMsg("Deleting request: %s" % embyid, 0)
+ doUtils.downloadUrl(url, action_type="DELETE")
+
+ '''if utils.settings('skipContextMenu') != "true":
+ if xbmcgui.Dialog().yesno(
+ heading="Confirm delete",
+ line1=("Delete file on Emby Server? This will "
+ "also delete the file(s) from disk!")):
+ import downloadutils
+ doUtils = downloadutils.DownloadUtils()
+ doUtils.downloadUrl("{server}/emby/Items/%s?format=json" % embyid, action_type="DELETE")'''
+
+ xbmc.sleep(500)
xbmc.executebuiltin("Container.Update")
\ No newline at end of file
diff --git a/resources/language/English/strings.xml b/resources/language/English/strings.xml
index 776e06a1..78a94417 100644
--- a/resources/language/English/strings.xml
+++ b/resources/language/English/strings.xml
@@ -323,7 +323,7 @@
Gathering tv shows from:
Gathering:
Detected the database needs to be recreated for this version of Emby for Kodi. Proceed?
- Emby for Kod may not work correctly until the database is reset.
+ Emby for Kodi may not work correctly until the database is reset.
Cancelling the database syncing process. The current Kodi version is unsupported.
completed in:
Comparing movies from:
diff --git a/resources/lib/api.py b/resources/lib/api.py
index af993adb..97ad4178 100644
--- a/resources/lib/api.py
+++ b/resources/lib/api.py
@@ -37,7 +37,7 @@ class API():
try:
userdata = self.item['UserData']
-
+
except KeyError: # No userdata found.
pass
@@ -57,7 +57,7 @@ class API():
lastPlayedDate = userdata.get('LastPlayedDate')
if lastPlayedDate:
lastPlayedDate = lastPlayedDate.split('.')[0].replace('T', " ")
-
+
if userdata['Played']:
# Playcount is tied to the watch status
played = True
@@ -91,10 +91,10 @@ class API():
try:
people = self.item['People']
-
+
except KeyError:
pass
-
+
else:
for person in people:
@@ -116,17 +116,16 @@ class API():
}
def getMediaStreams(self):
- item = self.item
videotracks = []
audiotracks = []
subtitlelanguages = []
try:
- media_streams = item['MediaSources'][0]['MediaStreams']
+ media_streams = self.item['MediaSources'][0]['MediaStreams']
except KeyError:
- if not item.get("MediaStreams"): return None
- media_streams = item['MediaStreams']
+ if not self.item.get("MediaStreams"): return None
+ media_streams = self.item['MediaStreams']
for media_stream in media_streams:
# Sort through Video, Audio, Subtitle
@@ -141,12 +140,12 @@ class API():
'codec': codec,
'height': media_stream.get('Height'),
'width': media_stream.get('Width'),
- 'video3DFormat': item.get('Video3DFormat'),
+ 'video3DFormat': self.item.get('Video3DFormat'),
'aspect': 1.85
}
try:
- container = item['MediaSources'][0]['Container'].lower()
+ container = self.item['MediaSources'][0]['Container'].lower()
except:
container = ""
@@ -161,16 +160,16 @@ class API():
track['codec'] = "avc1"
# Aspect ratio
- if item.get('AspectRatio'):
+ if self.item.get('AspectRatio'):
# Metadata AR
- aspect = item['AspectRatio']
+ aspect = self.item['AspectRatio']
else: # File AR
aspect = media_stream.get('AspectRatio', "0")
try:
aspectwidth, aspectheight = aspect.split(':')
track['aspect'] = round(float(aspectwidth) / float(aspectheight), 6)
-
+
except (ValueError, ZeroDivisionError):
width = track.get('width')
height = track.get('height')
@@ -179,16 +178,16 @@ class API():
track['aspect'] = round(float(width / height), 6)
else:
track['aspect'] = 1.85
-
- if item.get("RunTimeTicks"):
- track['duration'] = item.get("RunTimeTicks") / 10000000.0
-
+
+ if self.item.get("RunTimeTicks"):
+ track['duration'] = self.item.get("RunTimeTicks") / 10000000.0
+
videotracks.append(track)
elif stream_type == "Audio":
# Codec, Channels, language
track = {
-
+
'codec': codec,
'channels': media_stream.get('Channels'),
'language': media_stream.get('Language')
@@ -205,18 +204,17 @@ class API():
return {
- 'video': videotracks,
+ 'video': videotracks,
'audio': audiotracks,
'subtitle': subtitlelanguages
}
def getRuntime(self):
- item = self.item
try:
- runtime = item['RunTimeTicks'] / 10000000.0
-
+ runtime = self.item['RunTimeTicks'] / 10000000.0
+
except KeyError:
- runtime = item.get('CumulativeRunTimeTicks', 0) / 10000000.0
+ runtime = self.item.get('CumulativeRunTimeTicks', 0) / 10000000.0
return runtime
@@ -234,20 +232,19 @@ class API():
def getStudios(self):
# Process Studios
- item = self.item
studios = []
try:
- studio = item['SeriesStudio']
+ studio = self.item['SeriesStudio']
studios.append(self.verifyStudio(studio))
-
+
except KeyError:
- studioList = item['Studios']
+ studioList = self.item['Studios']
for studio in studioList:
name = studio['Name']
studios.append(self.verifyStudio(name))
-
+
return studios
def verifyStudio(self, studioName):
@@ -265,12 +262,11 @@ class API():
def getChecksum(self):
# Use the etags checksum and userdata
- item = self.item
- userdata = item['UserData']
+ userdata = self.item['UserData']
checksum = "%s%s%s%s%s%s%s" % (
-
- item['Etag'],
+
+ self.item['Etag'],
userdata['Played'],
userdata['IsFavorite'],
userdata.get('Likes',''),
@@ -282,9 +278,8 @@ class API():
return checksum
def getGenres(self):
- item = self.item
all_genres = ""
- genres = item.get('Genres', item.get('SeriesGenres'))
+ genres = self.item.get('Genres', self.item.get('SeriesGenres'))
if genres:
all_genres = " / ".join(genres)
@@ -344,7 +339,7 @@ class API():
def getMpaa(self):
# Convert more complex cases
mpaa = self.item.get('OfficialRating', "")
-
+
if mpaa in ("NR", "UR"):
# Kodi seems to not like NR, but will accept Not Rated
mpaa = "Not Rated"
@@ -362,9 +357,8 @@ class API():
def getFilePath(self):
- item = self.item
try:
- filepath = item['Path']
+ filepath = self.item['Path']
except KeyError:
filepath = ""
@@ -375,17 +369,16 @@ class API():
filepath = filepath.replace("\\\\", "smb://")
filepath = filepath.replace("\\", "/")
- if item.get('VideoType'):
- videotype = item['VideoType']
+ if self.item.get('VideoType'):
+ videotype = self.item['VideoType']
# Specific format modification
if 'Dvd'in videotype:
filepath = "%s/VIDEO_TS/VIDEO_TS.IFO" % filepath
elif 'BluRay' in videotype:
filepath = "%s/BDMV/index.bdmv" % filepath
-
+
if "\\" in filepath:
# Local path scenario, with special videotype
filepath = filepath.replace("/", "\\")
return filepath
-
\ No newline at end of file
diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py
index 2740f09c..79fb617c 100644
--- a/resources/lib/artwork.py
+++ b/resources/lib/artwork.py
@@ -1,606 +1,604 @@
-# -*- coding: utf-8 -*-
-
-#################################################################################################
-
-import json
-import requests
-import os
-import urllib
-from sqlite3 import OperationalError
-
-import xbmc
-import xbmcgui
-import xbmcvfs
-
-import utils
-import clientinfo
-import image_cache_thread
-
-#################################################################################################
-
-
-class Artwork():
-
- xbmc_host = 'localhost'
- xbmc_port = None
- xbmc_username = None
- xbmc_password = None
-
- imageCacheThreads = []
- imageCacheLimitThreads = 0
-
- def __init__(self):
- self.clientinfo = clientinfo.ClientInfo()
- self.addonName = self.clientinfo.getAddonName()
-
- self.enableTextureCache = utils.settings('enableTextureCache') == "true"
- self.imageCacheLimitThreads = int(utils.settings("imageCacheLimit"))
- self.imageCacheLimitThreads = int(self.imageCacheLimitThreads * 5);
- utils.logMsg("Using Image Cache Thread Count: " + str(self.imageCacheLimitThreads), 1)
-
- if not self.xbmc_port and self.enableTextureCache:
- self.setKodiWebServerDetails()
-
- self.userId = utils.window('emby_currUser')
- self.server = utils.window('emby_server%s' % self.userId)
-
- def logMsg(self, msg, lvl=1):
- className = self.__class__.__name__
- utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
-
-
- def double_urlencode(self, text):
- text = self.single_urlencode(text)
- text = self.single_urlencode(text)
-
- return text
-
- def single_urlencode(self, text):
-
- text = urllib.urlencode({'blahblahblah':text.encode("utf-8")}) #urlencode needs a utf- string
- text = text[13:]
-
- return text.decode("utf-8") #return the result again as unicode
-
- def setKodiWebServerDetails(self):
- # Get the Kodi webserver details - used to set the texture cache
- web_query = {
-
- "jsonrpc": "2.0",
- "id": 1,
- "method": "Settings.GetSettingValue",
- "params": {
-
- "setting": "services.webserver"
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(web_query))
- result = json.loads(result)
- try:
- xbmc_webserver_enabled = result['result']['value']
- except TypeError:
- xbmc_webserver_enabled = False
-
- if not xbmc_webserver_enabled:
- # Enable the webserver, it is disabled
- web_port = {
-
- "jsonrpc": "2.0",
- "id": 1,
- "method": "Settings.SetSettingValue",
- "params": {
-
- "setting": "services.webserverport",
- "value": 8080
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(web_port))
- self.xbmc_port = 8080
-
- web_user = {
-
- "jsonrpc": "2.0",
- "id": 1,
- "method": "Settings.SetSettingValue",
- "params": {
-
- "setting": "services.webserver",
- "value": True
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(web_user))
- self.xbmc_username = "kodi"
-
-
- # Webserver already enabled
- web_port = {
-
- "jsonrpc": "2.0",
- "id": 1,
- "method": "Settings.GetSettingValue",
- "params": {
-
- "setting": "services.webserverport"
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(web_port))
- result = json.loads(result)
- try:
- self.xbmc_port = result['result']['value']
- except TypeError:
- pass
-
- web_user = {
-
- "jsonrpc": "2.0",
- "id": 1,
- "method": "Settings.GetSettingValue",
- "params": {
-
- "setting": "services.webserverusername"
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(web_user))
- result = json.loads(result)
- try:
- self.xbmc_username = result['result']['value']
- except TypeError:
- pass
-
- web_pass = {
-
- "jsonrpc": "2.0",
- "id": 1,
- "method": "Settings.GetSettingValue",
- "params": {
-
- "setting": "services.webserverpassword"
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(web_pass))
- result = json.loads(result)
- try:
- self.xbmc_password = result['result']['value']
- except TypeError:
- pass
-
- def FullTextureCacheSync(self):
- # This method will sync all Kodi artwork to textures13.db
- # and cache them locally. This takes diskspace!
-
- if not xbmcgui.Dialog().yesno("Image Texture Cache", "Running the image cache process can take some time.", "Are you sure you want continue?"):
- return
-
- self.logMsg("Doing Image Cache Sync", 1)
-
- dialog = xbmcgui.DialogProgress()
- dialog.create("Emby for Kodi", "Image Cache Sync")
-
- # ask to rest all existing or not
- if xbmcgui.Dialog().yesno("Image Texture Cache", "Reset all existing cache data first?", ""):
- self.logMsg("Resetting all cache data first", 1)
- # Remove all existing textures first
- path = xbmc.translatePath("special://thumbnails/").decode('utf-8')
- if xbmcvfs.exists(path):
- allDirs, allFiles = xbmcvfs.listdir(path)
- for dir in allDirs:
- allDirs, allFiles = xbmcvfs.listdir(path+dir)
- for file in allFiles:
- if os.path.supports_unicode_filenames:
- xbmcvfs.delete(os.path.join(path+dir.decode('utf-8'),file.decode('utf-8')))
- else:
- xbmcvfs.delete(os.path.join(path.encode('utf-8')+dir,file))
-
- # remove all existing data from texture DB
- textureconnection = utils.kodiSQL('texture')
- texturecursor = textureconnection.cursor()
- texturecursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
- rows = texturecursor.fetchall()
- for row in rows:
- tableName = row[0]
- if(tableName != "version"):
- texturecursor.execute("DELETE FROM " + tableName)
- textureconnection.commit()
- texturecursor.close()
-
- # Cache all entries in video DB
- connection = utils.kodiSQL('video')
- cursor = connection.cursor()
- cursor.execute("SELECT url FROM art WHERE media_type != 'actor'") # dont include actors
- result = cursor.fetchall()
- total = len(result)
- count = 1
- percentage = 0
- self.logMsg("Image cache sync about to process " + str(total) + " images", 1)
- for url in result:
- if dialog.iscanceled():
- break
- percentage = int((float(count) / float(total))*100)
- textMessage = str(count) + " of " + str(total) + " (" + str(len(self.imageCacheThreads)) + ")"
- dialog.update(percentage, "Updating Image Cache: " + textMessage)
- self.CacheTexture(url[0])
- count += 1
- cursor.close()
-
- # Cache all entries in music DB
- connection = utils.kodiSQL('music')
- cursor = connection.cursor()
- cursor.execute("SELECT url FROM art")
- result = cursor.fetchall()
- total = len(result)
- count = 1
- percentage = 0
- self.logMsg("Image cache sync about to process " + str(total) + " images", 1)
- for url in result:
- if dialog.iscanceled():
- break
- percentage = int((float(count) / float(total))*100)
- textMessage = str(count) + " of " + str(total)
- dialog.update(percentage, "Updating Image Cache: " + textMessage)
- self.CacheTexture(url[0])
- count += 1
- cursor.close()
-
- dialog.update(100, "Waiting for all threads to exit: " + str(len(self.imageCacheThreads)))
- self.logMsg("Waiting for all threads to exit", 1)
- while len(self.imageCacheThreads) > 0:
- for thread in self.imageCacheThreads:
- if thread.isFinished:
- self.imageCacheThreads.remove(thread)
- dialog.update(100, "Waiting for all threads to exit: " + str(len(self.imageCacheThreads)))
- self.logMsg("Waiting for all threads to exit: " + str(len(self.imageCacheThreads)), 1)
- xbmc.sleep(500)
-
- dialog.close()
-
- def addWorkerImageCacheThread(self, urlToAdd):
-
- while(True):
- # removed finished
- for thread in self.imageCacheThreads:
- if thread.isFinished:
- self.imageCacheThreads.remove(thread)
-
- # add a new thread or wait and retry if we hit our limit
- if(len(self.imageCacheThreads) < self.imageCacheLimitThreads):
- newThread = image_cache_thread.image_cache_thread()
- newThread.setUrl(self.double_urlencode(urlToAdd))
- newThread.setHost(self.xbmc_host, self.xbmc_port)
- newThread.setAuth(self.xbmc_username, self.xbmc_password)
- newThread.start()
- self.imageCacheThreads.append(newThread)
- return
- else:
- self.logMsg("Waiting for empty queue spot: " + str(len(self.imageCacheThreads)), 2)
- xbmc.sleep(50)
-
-
- def CacheTexture(self, url):
- # Cache a single image url to the texture cache
- if url and self.enableTextureCache:
- self.logMsg("Processing: %s" % url, 2)
-
- if(self.imageCacheLimitThreads == 0 or self.imageCacheLimitThreads == None):
- #Add image to texture cache by simply calling it at the http endpoint
-
- url = self.double_urlencode(url)
- try: # Extreme short timeouts so we will have a exception.
- response = requests.head(
- url=(
- "http://%s:%s/image/image://%s"
- % (self.xbmc_host, self.xbmc_port, url)),
- auth=(self.xbmc_username, self.xbmc_password),
- timeout=(0.01, 0.01))
- # We don't need the result
- except: pass
-
- else:
- self.addWorkerImageCacheThread(url)
-
-
- def addArtwork(self, artwork, kodiId, mediaType, cursor):
- # Kodi conversion table
- kodiart = {
-
- 'Primary': ["thumb", "poster"],
- 'Banner': "banner",
- 'Logo': "clearlogo",
- 'Art': "clearart",
- 'Thumb': "landscape",
- 'Disc': "discart",
- 'Backdrop': "fanart",
- 'BoxRear': "poster"
- }
-
- # Artwork is a dictionary
- for art in artwork:
-
- if art == "Backdrop":
- # Backdrop entry is a list
- # Process extra fanart for artwork downloader (fanart, fanart1, fanart2...)
- backdrops = artwork[art]
- backdropsNumber = len(backdrops)
-
- query = ' '.join((
-
- "SELECT url",
- "FROM art",
- "WHERE media_id = ?",
- "AND media_type = ?",
- "AND type LIKE ?"
- ))
- cursor.execute(query, (kodiId, mediaType, "fanart%",))
- rows = cursor.fetchall()
-
- if len(rows) > backdropsNumber:
- # More backdrops in database. Delete extra fanart.
- query = ' '.join((
-
- "DELETE FROM art",
- "WHERE media_id = ?",
- "AND media_type = ?",
- "AND type LIKE ?"
- ))
- cursor.execute(query, (kodiId, mediaType, "fanart_",))
-
- # Process backdrops and extra fanart
- index = ""
- for backdrop in backdrops:
- self.addOrUpdateArt(
- imageUrl=backdrop,
- kodiId=kodiId,
- mediaType=mediaType,
- imageType="%s%s" % ("fanart", index),
- cursor=cursor)
-
- if backdropsNumber > 1:
- try: # Will only fail on the first try, str to int.
- index += 1
- except TypeError:
- index = 1
-
- elif art == "Primary":
- # Primary art is processed as thumb and poster for Kodi.
- for artType in kodiart[art]:
- self.addOrUpdateArt(
- imageUrl=artwork[art],
- kodiId=kodiId,
- mediaType=mediaType,
- imageType=artType,
- cursor=cursor)
-
- elif kodiart.get(art):
- # Process the rest artwork type that Kodi can use
- self.addOrUpdateArt(
- imageUrl=artwork[art],
- kodiId=kodiId,
- mediaType=mediaType,
- imageType=kodiart[art],
- cursor=cursor)
-
- def addOrUpdateArt(self, imageUrl, kodiId, mediaType, imageType, cursor):
- # Possible that the imageurl is an empty string
- if imageUrl:
- cacheimage = False
-
- query = ' '.join((
-
- "SELECT url",
- "FROM art",
- "WHERE media_id = ?",
- "AND media_type = ?",
- "AND type = ?"
- ))
- cursor.execute(query, (kodiId, mediaType, imageType,))
- try: # Update the artwork
- url = cursor.fetchone()[0]
-
- except TypeError: # Add the artwork
- cacheimage = True
- self.logMsg("Adding Art Link for kodiId: %s (%s)" % (kodiId, imageUrl), 2)
-
- query = (
- '''
- INSERT INTO art(media_id, media_type, type, url)
-
- VALUES (?, ?, ?, ?)
- '''
- )
- cursor.execute(query, (kodiId, mediaType, imageType, imageUrl))
-
- else: # Only cache artwork if it changed
- if url != imageUrl:
- cacheimage = True
-
- # Only for the main backdrop, poster
- if (utils.window('emby_initialScan') != "true" and
- imageType in ("fanart", "poster")):
- # Delete current entry before updating with the new one
- self.deleteCachedArtwork(url)
-
- self.logMsg(
- "Updating Art url for %s kodiId: %s (%s) -> (%s)"
- % (imageType, kodiId, url, imageUrl), 1)
-
- query = ' '.join((
-
- "UPDATE art",
- "SET url = ?",
- "WHERE media_id = ?",
- "AND media_type = ?",
- "AND type = ?"
- ))
- cursor.execute(query, (imageUrl, kodiId, mediaType, imageType))
-
- # Cache fanart and poster in Kodi texture cache
- if cacheimage and imageType in ("fanart", "poster"):
- self.CacheTexture(imageUrl)
-
- def deleteArtwork(self, kodiid, mediatype, cursor):
-
- query = ' '.join((
-
- "SELECT url, type",
- "FROM art",
- "WHERE media_id = ?",
- "AND media_type = ?"
- ))
- cursor.execute(query, (kodiid, mediatype,))
- rows = cursor.fetchall()
- for row in rows:
-
- url = row[0]
- imagetype = row[1]
- if imagetype in ("poster", "fanart"):
- self.deleteCachedArtwork(url)
-
- def deleteCachedArtwork(self, url):
- # Only necessary to remove and apply a new backdrop or poster
- connection = utils.kodiSQL('texture')
- cursor = connection.cursor()
-
- try:
- cursor.execute("SELECT cachedurl FROM texture WHERE url = ?", (url,))
- cachedurl = cursor.fetchone()[0]
-
- except TypeError:
- self.logMsg("Could not find cached url.", 1)
-
- except OperationalError:
- self.logMsg("Database is locked. Skip deletion process.", 1)
-
- else: # Delete thumbnail as well as the entry
- thumbnails = xbmc.translatePath("special://thumbnails/%s" % cachedurl).decode('utf-8')
- self.logMsg("Deleting cached thumbnail: %s" % thumbnails, 1)
- xbmcvfs.delete(thumbnails)
-
- try:
- cursor.execute("DELETE FROM texture WHERE url = ?", (url,))
- connection.commit()
- except OperationalError:
- self.logMsg("Issue deleting url from cache. Skipping.", 2)
-
- finally:
- cursor.close()
-
- def getPeopleArtwork(self, people):
- # append imageurl if existing
- for person in people:
-
- personId = person['Id']
- tag = person.get('PrimaryImageTag')
-
- image = ""
- if tag:
- image = (
- "%s/emby/Items/%s/Images/Primary?"
- "MaxWidth=400&MaxHeight=400&Index=0&Tag=%s"
- % (self.server, personId, tag))
-
- person['imageurl'] = image
-
- return people
-
- def getUserArtwork(self, itemid, itemtype):
- # Load user information set by UserClient
- image = ("%s/emby/Users/%s/Images/%s?Format=original"
- % (self.server, itemid, itemtype))
- return image
-
- def getAllArtwork(self, item, parentInfo=False):
-
- server = self.server
-
- itemid = item['Id']
- artworks = item['ImageTags']
- backdrops = item.get('BackdropImageTags',[])
-
- maxHeight = 10000
- maxWidth = 10000
- customquery = ""
-
- if utils.settings('compressArt') == "true":
- customquery = "&Quality=90"
-
- if utils.settings('enableCoverArt') == "false":
- customquery += "&EnableImageEnhancers=false"
-
- allartworks = {
-
- 'Primary': "",
- 'Art': "",
- 'Banner': "",
- 'Logo': "",
- 'Thumb': "",
- 'Disc': "",
- 'Backdrop': []
- }
-
- # Process backdrops
- for index, tag in enumerate(backdrops):
- artwork = (
- "%s/emby/Items/%s/Images/Backdrop/%s?"
- "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
- % (server, itemid, index, maxWidth, maxHeight, tag, customquery))
- allartworks['Backdrop'].append(artwork)
-
- # Process the rest of the artwork
- for art in artworks:
- # Filter backcover
- if art != "BoxRear":
- tag = artworks[art]
- artwork = (
- "%s/emby/Items/%s/Images/%s/0?"
- "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
- % (server, itemid, art, maxWidth, maxHeight, tag, customquery))
- allartworks[art] = artwork
-
- # Process parent items if the main item is missing artwork
- if parentInfo:
-
- # Process parent backdrops
- if not allartworks['Backdrop']:
-
- parentId = item.get('ParentBackdropItemId')
- if parentId:
- # If there is a parentId, go through the parent backdrop list
- parentbackdrops = item['ParentBackdropImageTags']
-
- for index, tag in enumerate(parentbackdrops):
- artwork = (
- "%s/emby/Items/%s/Images/Backdrop/%s?"
- "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
- % (server, parentId, index, maxWidth, maxHeight, tag, customquery))
- allartworks['Backdrop'].append(artwork)
-
- # Process the rest of the artwork
- parentartwork = ['Logo', 'Art', 'Thumb']
- for parentart in parentartwork:
-
- if not allartworks[parentart]:
-
- parentId = item.get('Parent%sItemId' % parentart)
- if parentId:
-
- parentTag = item['Parent%sImageTag' % parentart]
- artwork = (
- "%s/emby/Items/%s/Images/%s/0?"
- "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
- % (server, parentId, parentart,
- maxWidth, maxHeight, parentTag, customquery))
- allartworks[parentart] = artwork
-
- # Parent album works a bit differently
- if not allartworks['Primary']:
-
- parentId = item.get('AlbumId')
- if parentId and item.get('AlbumPrimaryImageTag'):
-
- parentTag = item['AlbumPrimaryImageTag']
- artwork = (
- "%s/emby/Items/%s/Images/Primary/0?"
- "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
- % (server, parentId, maxWidth, maxHeight, parentTag, customquery))
- allartworks['Primary'] = artwork
-
+# -*- coding: utf-8 -*-
+
+#################################################################################################
+
+import json
+import requests
+import os
+import urllib
+from sqlite3 import OperationalError
+
+import xbmc
+import xbmcgui
+import xbmcvfs
+
+import utils
+import clientinfo
+import image_cache_thread
+
+#################################################################################################
+
+
+class Artwork():
+
+ xbmc_host = 'localhost'
+ xbmc_port = None
+ xbmc_username = None
+ xbmc_password = None
+
+ imageCacheThreads = []
+ imageCacheLimitThreads = 0
+
+ def __init__(self):
+ self.clientinfo = clientinfo.ClientInfo()
+ self.addonName = self.clientinfo.getAddonName()
+
+ self.enableTextureCache = utils.settings('enableTextureCache') == "true"
+ self.imageCacheLimitThreads = int(utils.settings("imageCacheLimit"))
+ self.imageCacheLimitThreads = int(self.imageCacheLimitThreads * 5)
+ utils.logMsg("Using Image Cache Thread Count: " + str(self.imageCacheLimitThreads), 1)
+
+ if not self.xbmc_port and self.enableTextureCache:
+ self.setKodiWebServerDetails()
+
+ self.userId = utils.window('emby_currUser')
+ self.server = utils.window('emby_server%s' % self.userId)
+
+ def logMsg(self, msg, lvl=1):
+ className = self.__class__.__name__
+ utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
+
+
+ def double_urlencode(self, text):
+ text = self.single_urlencode(text)
+ text = self.single_urlencode(text)
+
+ return text
+
+ def single_urlencode(self, text):
+
+ text = urllib.urlencode({'blahblahblah':text.encode("utf-8")}) #urlencode needs a utf- string
+ text = text[13:]
+
+ return text.decode("utf-8") #return the result again as unicode
+
+ def setKodiWebServerDetails(self):
+ # Get the Kodi webserver details - used to set the texture cache
+ web_query = {
+
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "Settings.GetSettingValue",
+ "params": {
+
+ "setting": "services.webserver"
+ }
+ }
+ result = xbmc.executeJSONRPC(json.dumps(web_query))
+ result = json.loads(result)
+ try:
+ xbmc_webserver_enabled = result['result']['value']
+ except TypeError:
+ xbmc_webserver_enabled = False
+
+ if not xbmc_webserver_enabled:
+ # Enable the webserver, it is disabled
+ web_port = {
+
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "Settings.SetSettingValue",
+ "params": {
+
+ "setting": "services.webserverport",
+ "value": 8080
+ }
+ }
+ result = xbmc.executeJSONRPC(json.dumps(web_port))
+ self.xbmc_port = 8080
+
+ web_user = {
+
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "Settings.SetSettingValue",
+ "params": {
+
+ "setting": "services.webserver",
+ "value": True
+ }
+ }
+ result = xbmc.executeJSONRPC(json.dumps(web_user))
+ self.xbmc_username = "kodi"
+
+
+ # Webserver already enabled
+ web_port = {
+
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "Settings.GetSettingValue",
+ "params": {
+
+ "setting": "services.webserverport"
+ }
+ }
+ result = xbmc.executeJSONRPC(json.dumps(web_port))
+ result = json.loads(result)
+ try:
+ self.xbmc_port = result['result']['value']
+ except TypeError:
+ pass
+
+ web_user = {
+
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "Settings.GetSettingValue",
+ "params": {
+
+ "setting": "services.webserverusername"
+ }
+ }
+ result = xbmc.executeJSONRPC(json.dumps(web_user))
+ result = json.loads(result)
+ try:
+ self.xbmc_username = result['result']['value']
+ except TypeError:
+ pass
+
+ web_pass = {
+
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "Settings.GetSettingValue",
+ "params": {
+
+ "setting": "services.webserverpassword"
+ }
+ }
+ result = xbmc.executeJSONRPC(json.dumps(web_pass))
+ result = json.loads(result)
+ try:
+ self.xbmc_password = result['result']['value']
+ except TypeError:
+ pass
+
+ def FullTextureCacheSync(self):
+ # This method will sync all Kodi artwork to textures13.db
+ # and cache them locally. This takes diskspace!
+
+ if not xbmcgui.Dialog().yesno("Image Texture Cache", "Running the image cache process can take some time.", "Are you sure you want continue?"):
+ return
+
+ self.logMsg("Doing Image Cache Sync", 1)
+
+ dialog = xbmcgui.DialogProgress()
+ dialog.create("Emby for Kodi", "Image Cache Sync")
+
+ # ask to rest all existing or not
+ if xbmcgui.Dialog().yesno("Image Texture Cache", "Reset all existing cache data first?", ""):
+ self.logMsg("Resetting all cache data first", 1)
+ # Remove all existing textures first
+ path = xbmc.translatePath("special://thumbnails/").decode('utf-8')
+ if xbmcvfs.exists(path):
+ allDirs, allFiles = xbmcvfs.listdir(path)
+ for dir in allDirs:
+ allDirs, allFiles = xbmcvfs.listdir(path+dir)
+ for file in allFiles:
+ if os.path.supports_unicode_filenames:
+ xbmcvfs.delete(os.path.join(path+dir.decode('utf-8'),file.decode('utf-8')))
+ else:
+ xbmcvfs.delete(os.path.join(path.encode('utf-8')+dir,file))
+
+ # remove all existing data from texture DB
+ textureconnection = utils.kodiSQL('texture')
+ texturecursor = textureconnection.cursor()
+ texturecursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
+ rows = texturecursor.fetchall()
+ for row in rows:
+ tableName = row[0]
+ if(tableName != "version"):
+ texturecursor.execute("DELETE FROM " + tableName)
+ textureconnection.commit()
+ texturecursor.close()
+
+ # Cache all entries in video DB
+ connection = utils.kodiSQL('video')
+ cursor = connection.cursor()
+ cursor.execute("SELECT url FROM art WHERE media_type != 'actor'") # dont include actors
+ result = cursor.fetchall()
+ total = len(result)
+ count = 1
+ percentage = 0
+ self.logMsg("Image cache sync about to process " + str(total) + " images", 1)
+ for url in result:
+ if dialog.iscanceled():
+ break
+ percentage = int((float(count) / float(total))*100)
+ textMessage = str(count) + " of " + str(total) + " (" + str(len(self.imageCacheThreads)) + ")"
+ dialog.update(percentage, "Updating Image Cache: " + textMessage)
+ self.CacheTexture(url[0])
+ count += 1
+ cursor.close()
+
+ # Cache all entries in music DB
+ connection = utils.kodiSQL('music')
+ cursor = connection.cursor()
+ cursor.execute("SELECT url FROM art")
+ result = cursor.fetchall()
+ total = len(result)
+ count = 1
+ percentage = 0
+ self.logMsg("Image cache sync about to process " + str(total) + " images", 1)
+ for url in result:
+ if dialog.iscanceled():
+ break
+ percentage = int((float(count) / float(total))*100)
+ textMessage = str(count) + " of " + str(total)
+ dialog.update(percentage, "Updating Image Cache: " + textMessage)
+ self.CacheTexture(url[0])
+ count += 1
+ cursor.close()
+
+ dialog.update(100, "Waiting for all threads to exit: " + str(len(self.imageCacheThreads)))
+ self.logMsg("Waiting for all threads to exit", 1)
+ while len(self.imageCacheThreads) > 0:
+ for thread in self.imageCacheThreads:
+ if thread.isFinished:
+ self.imageCacheThreads.remove(thread)
+ dialog.update(100, "Waiting for all threads to exit: " + str(len(self.imageCacheThreads)))
+ self.logMsg("Waiting for all threads to exit: " + str(len(self.imageCacheThreads)), 1)
+ xbmc.sleep(500)
+
+ dialog.close()
+
+ def addWorkerImageCacheThread(self, urlToAdd):
+
+ while(True):
+ # removed finished
+ for thread in self.imageCacheThreads:
+ if thread.isFinished:
+ self.imageCacheThreads.remove(thread)
+
+ # add a new thread or wait and retry if we hit our limit
+ if(len(self.imageCacheThreads) < self.imageCacheLimitThreads):
+ newThread = image_cache_thread.image_cache_thread()
+ newThread.setUrl(self.double_urlencode(urlToAdd))
+ newThread.setHost(self.xbmc_host, self.xbmc_port)
+ newThread.setAuth(self.xbmc_username, self.xbmc_password)
+ newThread.start()
+ self.imageCacheThreads.append(newThread)
+ return
+ else:
+ self.logMsg("Waiting for empty queue spot: " + str(len(self.imageCacheThreads)), 2)
+ xbmc.sleep(50)
+
+
+ def CacheTexture(self, url):
+ # Cache a single image url to the texture cache
+ if url and self.enableTextureCache:
+ self.logMsg("Processing: %s" % url, 2)
+
+ if(self.imageCacheLimitThreads == 0 or self.imageCacheLimitThreads == None):
+ #Add image to texture cache by simply calling it at the http endpoint
+
+ url = self.double_urlencode(url)
+ try: # Extreme short timeouts so we will have a exception.
+ response = requests.head(
+ url=(
+ "http://%s:%s/image/image://%s"
+ % (self.xbmc_host, self.xbmc_port, url)),
+ auth=(self.xbmc_username, self.xbmc_password),
+ timeout=(0.01, 0.01))
+ # We don't need the result
+ except: pass
+
+ else:
+ self.addWorkerImageCacheThread(url)
+
+
+ def addArtwork(self, artwork, kodiId, mediaType, cursor):
+ # Kodi conversion table
+ kodiart = {
+
+ 'Primary': ["thumb", "poster"],
+ 'Banner': "banner",
+ 'Logo': "clearlogo",
+ 'Art': "clearart",
+ 'Thumb': "landscape",
+ 'Disc': "discart",
+ 'Backdrop': "fanart",
+ 'BoxRear': "poster"
+ }
+
+ # Artwork is a dictionary
+ for art in artwork:
+
+ if art == "Backdrop":
+ # Backdrop entry is a list
+ # Process extra fanart for artwork downloader (fanart, fanart1, fanart2...)
+ backdrops = artwork[art]
+ backdropsNumber = len(backdrops)
+
+ query = ' '.join((
+
+ "SELECT url",
+ "FROM art",
+ "WHERE media_id = ?",
+ "AND media_type = ?",
+ "AND type LIKE ?"
+ ))
+ cursor.execute(query, (kodiId, mediaType, "fanart%",))
+ rows = cursor.fetchall()
+
+ if len(rows) > backdropsNumber:
+ # More backdrops in database. Delete extra fanart.
+ query = ' '.join((
+
+ "DELETE FROM art",
+ "WHERE media_id = ?",
+ "AND media_type = ?",
+ "AND type LIKE ?"
+ ))
+ cursor.execute(query, (kodiId, mediaType, "fanart_",))
+
+ # Process backdrops and extra fanart
+ index = ""
+ for backdrop in backdrops:
+ self.addOrUpdateArt(
+ imageUrl=backdrop,
+ kodiId=kodiId,
+ mediaType=mediaType,
+ imageType="%s%s" % ("fanart", index),
+ cursor=cursor)
+
+ if backdropsNumber > 1:
+ try: # Will only fail on the first try, str to int.
+ index += 1
+ except TypeError:
+ index = 1
+
+ elif art == "Primary":
+ # Primary art is processed as thumb and poster for Kodi.
+ for artType in kodiart[art]:
+ self.addOrUpdateArt(
+ imageUrl=artwork[art],
+ kodiId=kodiId,
+ mediaType=mediaType,
+ imageType=artType,
+ cursor=cursor)
+
+ elif kodiart.get(art):
+ # Process the rest artwork type that Kodi can use
+ self.addOrUpdateArt(
+ imageUrl=artwork[art],
+ kodiId=kodiId,
+ mediaType=mediaType,
+ imageType=kodiart[art],
+ cursor=cursor)
+
+ def addOrUpdateArt(self, imageUrl, kodiId, mediaType, imageType, cursor):
+ # Possible that the imageurl is an empty string
+ if imageUrl:
+ cacheimage = False
+
+ query = ' '.join((
+
+ "SELECT url",
+ "FROM art",
+ "WHERE media_id = ?",
+ "AND media_type = ?",
+ "AND type = ?"
+ ))
+ cursor.execute(query, (kodiId, mediaType, imageType,))
+ try: # Update the artwork
+ url = cursor.fetchone()[0]
+
+ except TypeError: # Add the artwork
+ cacheimage = True
+ self.logMsg("Adding Art Link for kodiId: %s (%s)" % (kodiId, imageUrl), 2)
+
+ query = (
+ '''
+ INSERT INTO art(media_id, media_type, type, url)
+
+ VALUES (?, ?, ?, ?)
+ '''
+ )
+ cursor.execute(query, (kodiId, mediaType, imageType, imageUrl))
+
+ else: # Only cache artwork if it changed
+ if url != imageUrl:
+ cacheimage = True
+
+ # Only for the main backdrop, poster
+ if (utils.window('emby_initialScan') != "true" and
+ imageType in ("fanart", "poster")):
+ # Delete current entry before updating with the new one
+ self.deleteCachedArtwork(url)
+
+ self.logMsg(
+ "Updating Art url for %s kodiId: %s (%s) -> (%s)"
+ % (imageType, kodiId, url, imageUrl), 1)
+
+ query = ' '.join((
+
+ "UPDATE art",
+ "SET url = ?",
+ "WHERE media_id = ?",
+ "AND media_type = ?",
+ "AND type = ?"
+ ))
+ cursor.execute(query, (imageUrl, kodiId, mediaType, imageType))
+
+ # Cache fanart and poster in Kodi texture cache
+ if cacheimage and imageType in ("fanart", "poster"):
+ self.CacheTexture(imageUrl)
+
+ def deleteArtwork(self, kodiid, mediatype, cursor):
+
+ query = ' '.join((
+
+ "SELECT url, type",
+ "FROM art",
+ "WHERE media_id = ?",
+ "AND media_type = ?"
+ ))
+ cursor.execute(query, (kodiid, mediatype,))
+ rows = cursor.fetchall()
+ for row in rows:
+
+ url = row[0]
+ imagetype = row[1]
+ if imagetype in ("poster", "fanart"):
+ self.deleteCachedArtwork(url)
+
+ def deleteCachedArtwork(self, url):
+ # Only necessary to remove and apply a new backdrop or poster
+ connection = utils.kodiSQL('texture')
+ cursor = connection.cursor()
+
+ try:
+ cursor.execute("SELECT cachedurl FROM texture WHERE url = ?", (url,))
+ cachedurl = cursor.fetchone()[0]
+
+ except TypeError:
+ self.logMsg("Could not find cached url.", 1)
+
+ except OperationalError:
+ self.logMsg("Database is locked. Skip deletion process.", 1)
+
+ else: # Delete thumbnail as well as the entry
+ thumbnails = xbmc.translatePath("special://thumbnails/%s" % cachedurl).decode('utf-8')
+ self.logMsg("Deleting cached thumbnail: %s" % thumbnails, 1)
+ xbmcvfs.delete(thumbnails)
+
+ try:
+ cursor.execute("DELETE FROM texture WHERE url = ?", (url,))
+ connection.commit()
+ except OperationalError:
+ self.logMsg("Issue deleting url from cache. Skipping.", 2)
+
+ finally:
+ cursor.close()
+
+ def getPeopleArtwork(self, people):
+ # append imageurl if existing
+ for person in people:
+
+ personId = person['Id']
+ tag = person.get('PrimaryImageTag')
+
+ image = ""
+ if tag:
+ image = (
+ "%s/emby/Items/%s/Images/Primary?"
+ "MaxWidth=400&MaxHeight=400&Index=0&Tag=%s"
+ % (self.server, personId, tag))
+
+ person['imageurl'] = image
+
+ return people
+
+ def getUserArtwork(self, itemid, itemtype):
+ # Load user information set by UserClient
+ image = ("%s/emby/Users/%s/Images/%s?Format=original"
+ % (self.server, itemid, itemtype))
+ return image
+
+ def getAllArtwork(self, item, parentInfo=False):
+
+ itemid = item['Id']
+ artworks = item['ImageTags']
+ backdrops = item.get('BackdropImageTags',[])
+
+ maxHeight = 10000
+ maxWidth = 10000
+ customquery = ""
+
+ if utils.settings('compressArt') == "true":
+ customquery = "&Quality=90"
+
+ if utils.settings('enableCoverArt') == "false":
+ customquery += "&EnableImageEnhancers=false"
+
+ allartworks = {
+
+ 'Primary': "",
+ 'Art': "",
+ 'Banner': "",
+ 'Logo': "",
+ 'Thumb': "",
+ 'Disc': "",
+ 'Backdrop': []
+ }
+
+ # Process backdrops
+ for index, tag in enumerate(backdrops):
+ artwork = (
+ "%s/emby/Items/%s/Images/Backdrop/%s?"
+ "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
+ % (self.server, itemid, index, maxWidth, maxHeight, tag, customquery))
+ allartworks['Backdrop'].append(artwork)
+
+ # Process the rest of the artwork
+ for art in artworks:
+ # Filter backcover
+ if art != "BoxRear":
+ tag = artworks[art]
+ artwork = (
+ "%s/emby/Items/%s/Images/%s/0?"
+ "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
+ % (self.server, itemid, art, maxWidth, maxHeight, tag, customquery))
+ allartworks[art] = artwork
+
+ # Process parent items if the main item is missing artwork
+ if parentInfo:
+
+ # Process parent backdrops
+ if not allartworks['Backdrop']:
+
+ parentId = item.get('ParentBackdropItemId')
+ if parentId:
+ # If there is a parentId, go through the parent backdrop list
+ parentbackdrops = item['ParentBackdropImageTags']
+
+ for index, tag in enumerate(parentbackdrops):
+ artwork = (
+ "%s/emby/Items/%s/Images/Backdrop/%s?"
+ "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
+ % (self.server, parentId, index, maxWidth, maxHeight, tag, customquery))
+ allartworks['Backdrop'].append(artwork)
+
+ # Process the rest of the artwork
+ parentartwork = ['Logo', 'Art', 'Thumb']
+ for parentart in parentartwork:
+
+ if not allartworks[parentart]:
+
+ parentId = item.get('Parent%sItemId' % parentart)
+ if parentId:
+
+ parentTag = item['Parent%sImageTag' % parentart]
+ artwork = (
+ "%s/emby/Items/%s/Images/%s/0?"
+ "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
+ % (self.server, parentId, parentart,
+ maxWidth, maxHeight, parentTag, customquery))
+ allartworks[parentart] = artwork
+
+ # Parent album works a bit differently
+ if not allartworks['Primary']:
+
+ parentId = item.get('AlbumId')
+ if parentId and item.get('AlbumPrimaryImageTag'):
+
+ parentTag = item['AlbumPrimaryImageTag']
+ artwork = (
+ "%s/emby/Items/%s/Images/Primary/0?"
+ "MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
+ % (self.server, parentId, maxWidth, maxHeight, parentTag, customquery))
+ allartworks['Primary'] = artwork
+
return allartworks
\ No newline at end of file
diff --git a/resources/lib/connect.py b/resources/lib/connect.py
index 05b563a3..2bd5c05d 100644
--- a/resources/lib/connect.py
+++ b/resources/lib/connect.py
@@ -21,7 +21,7 @@ requests.packages.urllib3.disable_warnings(InsecurePlatformWarning)
class ConnectUtils():
-
+
# Borg - multiple instances, shared state
_shared_state = {}
clientInfo = clientinfo.ClientInfo()
@@ -60,8 +60,6 @@ class ConnectUtils():
def startSession(self):
- log = self.logMsg
-
self.deviceId = self.clientInfo.getDeviceId()
# User is identified from this point
@@ -75,8 +73,8 @@ class ConnectUtils():
if self.sslclient is not None:
verify = self.sslclient
except:
- log("Could not load SSL settings.", 1)
-
+ self.logMsg("Could not load SSL settings.", 1)
+
# Start session
self.c = requests.Session()
self.c.headers = header
@@ -85,7 +83,7 @@ class ConnectUtils():
self.c.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
self.c.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
- log("Requests session started on: %s" % self.server, 1)
+ self.logMsg("Requests session started on: %s" % self.server, 1)
def stopSession(self):
try:
@@ -95,8 +93,7 @@ class ConnectUtils():
def getHeader(self, authenticate=True):
- clientInfo = self.clientInfo
- version = clientInfo.getVersion()
+ version = self.clientInfo.getVersion()
if not authenticate:
# If user is not authenticated
@@ -105,9 +102,9 @@ class ConnectUtils():
'X-Application': "Kodi/%s" % version,
'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Accept': "application/json"
- }
+ }
self.logMsg("Header: %s" % header, 1)
-
+
else:
token = self.token
# Attached to the requests session
@@ -117,18 +114,17 @@ class ConnectUtils():
'Accept': "application/json",
'X-Application': "Kodi/%s" % version,
'X-Connect-UserToken': token
- }
+ }
self.logMsg("Header: %s" % header, 1)
-
+
return header
def doUrl(self, url, data=None, postBody=None, rtype="GET",
parameters=None, authenticate=True, timeout=None):
- log = self.logMsg
window = utils.window
- log("=== ENTER connectUrl ===", 2)
+ self.logMsg("=== ENTER connectUrl ===", 2)
default_link = ""
if timeout is None:
timeout = self.timeout
@@ -137,7 +133,7 @@ class ConnectUtils():
try:
# If connect user is authenticated
if authenticate:
- try:
+ try:
c = self.c
# Replace for the real values
url = url.replace("{server}", self.server)
@@ -167,7 +163,7 @@ class ConnectUtils():
verifyssl = self.sslclient
except AttributeError:
pass
-
+
# Prepare request
if rtype == "GET":
r = requests.get(url,
@@ -195,7 +191,7 @@ class ConnectUtils():
verifyssl = self.sslclient
except AttributeError:
pass
-
+
# Prepare request
if rtype == "GET":
r = requests.get(url,
@@ -213,28 +209,28 @@ class ConnectUtils():
verify=verifyssl)
##### THE RESPONSE #####
- log(r.url, 1)
- log(r, 1)
+ self.logMsg(r.url, 1)
+ self.logMsg(r, 1)
if r.status_code == 204:
# No body in the response
- log("====== 204 Success ======", 1)
+ self.logMsg("====== 204 Success ======", 1)
elif r.status_code == requests.codes.ok:
-
- try:
+
+ try:
# UNICODE - JSON object
r = r.json()
- log("====== 200 Success ======", 1)
- log("Response: %s" % r, 1)
+ self.logMsg("====== 200 Success ======", 1)
+ self.logMsg("Response: %s" % r, 1)
return r
except:
if r.headers.get('content-type') != "text/html":
- log("Unable to convert the response for: %s" % url, 1)
+ self.logMsg("Unable to convert the response for: %s" % url, 1)
else:
r.raise_for_status()
-
+
##### EXCEPTIONS #####
except requests.exceptions.ConnectionError as e:
@@ -242,8 +238,8 @@ class ConnectUtils():
pass
except requests.exceptions.ConnectTimeout as e:
- log("Server timeout at: %s" % url, 0)
- log(e, 1)
+ self.logMsg("Server timeout at: %s" % url, 0)
+ self.logMsg(e, 1)
except requests.exceptions.HTTPError as e:
@@ -259,11 +255,11 @@ class ConnectUtils():
pass
except requests.exceptions.SSLError as e:
- log("Invalid SSL certificate for: %s" % url, 0)
- log(e, 1)
+ self.logMsg("Invalid SSL certificate for: %s" % url, 0)
+ self.logMsg(e, 1)
except requests.exceptions.RequestException as e:
- log("Unknown error connecting to: %s" % url, 0)
- log(e, 1)
+ self.logMsg("Unknown error connecting to: %s" % url, 0)
+ self.logMsg(e, 1)
return default_link
diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py
index de340f07..a74ee6f2 100644
--- a/resources/lib/downloadutils.py
+++ b/resources/lib/downloadutils.py
@@ -23,7 +23,7 @@ requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
class DownloadUtils():
-
+
# Borg - multiple instances, shared state
_shared_state = {}
clientInfo = clientinfo.ClientInfo()
@@ -77,11 +77,11 @@ class DownloadUtils():
# Post settings to session
url = "{server}/emby/Sessions/Capabilities/Full?format=json"
data = {
-
+
'PlayableMediaTypes': "Audio,Video",
'SupportsMediaControl': True,
'SupportedCommands': (
-
+
"MoveUp,MoveDown,MoveLeft,MoveRight,Select,"
"Back,ToggleContextMenu,ToggleFullscreen,ToggleOsdMenu,"
"GoHome,PageUp,NextLetter,GoToSearch,"
@@ -97,7 +97,7 @@ class DownloadUtils():
self.logMsg("Capabilities URL: %s" % url, 2)
self.logMsg("Postdata: %s" % data, 2)
- self.downloadUrl(url, postBody=data, type="POST")
+ self.downloadUrl(url, postBody=data, action_type="POST")
self.logMsg("Posted capabilities to %s" % self.server, 2)
# Attempt at getting sessionId
@@ -105,19 +105,19 @@ class DownloadUtils():
result = self.downloadUrl(url)
try:
sessionId = result[0]['Id']
-
+
except (KeyError, TypeError):
self.logMsg("Failed to retrieve sessionId.", 1)
-
+
else:
self.logMsg("Session: %s" % result, 2)
self.logMsg("SessionId: %s" % sessionId, 1)
utils.window('emby_sessionId', value=sessionId)
-
+
# Post any permanent additional users
additionalUsers = utils.settings('additionalUsers')
if additionalUsers:
-
+
additionalUsers = additionalUsers.split(',')
self.logMsg(
"List of permanent users added to the session: %s"
@@ -140,13 +140,11 @@ class DownloadUtils():
"{server}/emby/Sessions/%s/Users/%s?format=json"
% (sessionId, userId)
)
- self.downloadUrl(url, postBody={}, type="POST")
+ self.downloadUrl(url, postBody={}, action_type="POST")
def startSession(self):
- log = self.logMsg
-
self.deviceId = self.clientInfo.getDeviceId()
# User is identified from this point
@@ -160,8 +158,8 @@ class DownloadUtils():
if self.sslclient is not None:
verify = self.sslclient
except:
- log("Could not load SSL settings.", 1)
-
+ self.logMsg("Could not load SSL settings.", 1)
+
# Start session
self.s = requests.Session()
self.s.headers = header
@@ -170,7 +168,7 @@ class DownloadUtils():
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
- log("Requests session started on: %s" % self.server, 1)
+ self.logMsg("Requests session started on: %s" % self.server, 1)
def stopSession(self):
try:
@@ -180,12 +178,10 @@ class DownloadUtils():
def getHeader(self, authenticate=True):
- clientInfo = self.clientInfo
-
- deviceName = clientInfo.getDeviceName()
+ deviceName = self.clientInfo.getDeviceName()
deviceName = utils.normalize_string(deviceName.encode('utf-8'))
- deviceId = clientInfo.getDeviceId()
- version = clientInfo.getVersion()
+ deviceId = self.clientInfo.getDeviceId()
+ version = self.clientInfo.getVersion()
if not authenticate:
# If user is not authenticated
@@ -198,9 +194,9 @@ class DownloadUtils():
'Accept-encoding': 'gzip',
'Accept-Charset': 'UTF-8,*',
'Authorization': auth
- }
+ }
self.logMsg("Header: %s" % header, 2)
-
+
else:
userId = self.userId
token = self.token
@@ -215,36 +211,35 @@ class DownloadUtils():
'Accept-Charset': 'UTF-8,*',
'Authorization': auth,
'X-MediaBrowser-Token': token
- }
+ }
self.logMsg("Header: %s" % header, 2)
-
+
return header
- def downloadUrl(self, url, postBody=None, type="GET", parameters=None, authenticate=True):
-
+ def downloadUrl(self, url, postBody=None, action_type="GET", parameters=None, authenticate=True):
+
self.logMsg("=== ENTER downloadUrl ===", 2)
- timeout = self.timeout
default_link = ""
try:
# If user is authenticated
if (authenticate):
# Get requests session
- try:
+ try:
s = self.s
# Replace for the real values
url = url.replace("{server}", self.server)
url = url.replace("{UserId}", self.userId)
# Prepare request
- if type == "GET":
- r = s.get(url, json=postBody, params=parameters, timeout=timeout)
- elif type == "POST":
- r = s.post(url, json=postBody, timeout=timeout)
- elif type == "DELETE":
- r = s.delete(url, json=postBody, timeout=timeout)
-
+ if action_type == "GET":
+ r = s.get(url, json=postBody, params=parameters, timeout=self.timeout)
+ elif action_type == "POST":
+ r = s.post(url, json=postBody, timeout=self.timeout)
+ elif action_type == "DELETE":
+ r = s.delete(url, json=postBody, timeout=self.timeout)
+
except AttributeError:
# request session does not exists
# Get user information
@@ -266,26 +261,26 @@ class DownloadUtils():
url = url.replace("{UserId}", self.userId)
# Prepare request
- if type == "GET":
+ if action_type == "GET":
r = requests.get(url,
json=postBody,
params=parameters,
headers=header,
- timeout=timeout,
+ timeout=self.timeout,
verify=verifyssl)
- elif type == "POST":
+ elif action_type == "POST":
r = requests.post(url,
json=postBody,
headers=header,
- timeout=timeout,
+ timeout=self.timeout,
verify=verifyssl)
- elif type == "DELETE":
+ elif action_type == "DELETE":
r = requests.delete(url,
json=postBody,
headers=header,
- timeout=timeout,
+ timeout=self.timeout,
verify=verifyssl)
# If user is not authenticated
@@ -301,23 +296,23 @@ class DownloadUtils():
verifyssl = self.sslclient
except AttributeError:
pass
-
+
# Prepare request
- if type == "GET":
+ if action_type == "GET":
r = requests.get(url,
json=postBody,
params=parameters,
headers=header,
- timeout=timeout,
+ timeout=self.timeout,
verify=verifyssl)
- elif type == "POST":
+ elif action_type == "POST":
r = requests.post(url,
json=postBody,
headers=header,
- timeout=timeout,
+ timeout=self.timeout,
verify=verifyssl)
-
+
##### THE RESPONSE #####
self.logMsg(r.url, 2)
if r.status_code == 204:
@@ -325,8 +320,8 @@ class DownloadUtils():
self.logMsg("====== 204 Success ======", 2)
elif r.status_code == requests.codes.ok:
-
- try:
+
+ try:
# UNICODE - JSON object
r = r.json()
self.logMsg("====== 200 Success ======", 2)
@@ -338,7 +333,7 @@ class DownloadUtils():
self.logMsg("Unable to convert the response for: %s" % url, 1)
else:
r.raise_for_status()
-
+
##### EXCEPTIONS #####
except requests.exceptions.ConnectionError as e:
@@ -369,7 +364,7 @@ class DownloadUtils():
icon=xbmcgui.NOTIFICATION_ERROR,
time=5000)
return False
-
+
elif r.headers['X-Application-Error-Code'] == "UnauthorizedAccessException":
# User tried to do something his emby account doesn't allow
pass
diff --git a/resources/lib/embydb_functions.py b/resources/lib/embydb_functions.py
index 518498f5..cfebd5ff 100644
--- a/resources/lib/embydb_functions.py
+++ b/resources/lib/embydb_functions.py
@@ -1,325 +1,292 @@
-# -*- coding: utf-8 -*-
-
-#################################################################################################
-
-import utils
-import clientinfo
-
-#################################################################################################
-
-
-class Embydb_Functions():
-
-
- def __init__(self, embycursor):
-
- self.embycursor = embycursor
-
- self.clientInfo = clientinfo.ClientInfo()
- self.addonName = self.clientInfo.getAddonName()
-
- def logMsg(self, msg, lvl=1):
-
- className = self.__class__.__name__
- utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
-
- def getViews(self):
-
- embycursor = self.embycursor
- views = []
-
- query = ' '.join((
-
- "SELECT view_id",
- "FROM view"
- ))
- embycursor.execute(query)
- rows = embycursor.fetchall()
- for row in rows:
- views.append(row[0])
-
- return views
-
- def getView_byId(self, viewid):
-
- embycursor = self.embycursor
-
- query = ' '.join((
-
- "SELECT view_name, media_type, kodi_tagid",
- "FROM view",
- "WHERE view_id = ?"
- ))
- embycursor.execute(query, (viewid,))
- view = embycursor.fetchone()
-
- return view
-
- def getView_byType(self, mediatype):
-
- embycursor = self.embycursor
- views = []
-
- query = ' '.join((
-
- "SELECT view_id, view_name",
- "FROM view",
- "WHERE media_type = ?"
- ))
- embycursor.execute(query, (mediatype,))
- rows = embycursor.fetchall()
- for row in rows:
- views.append({
-
- 'id': row[0],
- 'name': row[1]
- })
-
- return views
-
- def getView_byName(self, tagname):
-
- embycursor = self.embycursor
-
- query = ' '.join((
-
- "SELECT view_id",
- "FROM view",
- "WHERE view_name = ?"
- ))
- embycursor.execute(query, (tagname,))
- try:
- view = embycursor.fetchone()[0]
-
- except TypeError:
- view = None
-
- return view
-
- def addView(self, embyid, name, mediatype, tagid):
-
- query = (
- '''
- INSERT INTO view(
- view_id, view_name, media_type, kodi_tagid)
-
- VALUES (?, ?, ?, ?)
- '''
- )
- self.embycursor.execute(query, (embyid, name, mediatype, tagid))
-
- def updateView(self, name, tagid, mediafolderid):
-
- query = ' '.join((
-
- "UPDATE view",
- "SET view_name = ?, kodi_tagid = ?",
- "WHERE view_id = ?"
- ))
- self.embycursor.execute(query, (name, tagid, mediafolderid))
-
- def removeView(self, viewid):
-
- query = ' '.join((
-
- "DELETE FROM view",
- "WHERE view_id = ?"
- ))
- self.embycursor.execute(query, (viewid,))
-
- def getItem_byId(self, embyid):
-
- embycursor = self.embycursor
-
- query = ' '.join((
-
- "SELECT kodi_id, kodi_fileid, kodi_pathid, parent_id, media_type, emby_type",
- "FROM emby",
- "WHERE emby_id = ?"
- ))
- try:
- embycursor.execute(query, (embyid,))
- item = embycursor.fetchone()
- return item
- except: return None
-
- def getItem_byWildId(self, embyid):
-
- embycursor = self.embycursor
-
- query = ' '.join((
-
- "SELECT kodi_id, media_type",
- "FROM emby",
- "WHERE emby_id LIKE ?"
- ))
- embycursor.execute(query, (embyid+"%",))
- items = embycursor.fetchall()
-
- return items
-
- def getItem_byView(self, mediafolderid):
-
- embycursor = self.embycursor
-
- query = ' '.join((
-
- "SELECT kodi_id",
- "FROM emby",
- "WHERE media_folder = ?"
- ))
- embycursor.execute(query, (mediafolderid,))
- items = embycursor.fetchall()
-
- return items
-
- def getItem_byKodiId(self, kodiid, mediatype):
-
- embycursor = self.embycursor
-
- query = ' '.join((
-
- "SELECT emby_id, parent_id",
- "FROM emby",
- "WHERE kodi_id = ?",
- "AND media_type = ?"
- ))
- embycursor.execute(query, (kodiid, mediatype,))
- item = embycursor.fetchone()
-
- return item
-
- def getItem_byParentId(self, parentid, mediatype):
-
- embycursor = self.embycursor
-
- query = ' '.join((
-
- "SELECT emby_id, kodi_id, kodi_fileid",
- "FROM emby",
- "WHERE parent_id = ?",
- "AND media_type = ?"
- ))
- embycursor.execute(query, (parentid, mediatype,))
- items = embycursor.fetchall()
-
- return items
-
- def getItemId_byParentId(self, parentid, mediatype):
-
- embycursor = self.embycursor
-
- query = ' '.join((
-
- "SELECT emby_id, kodi_id",
- "FROM emby",
- "WHERE parent_id = ?",
- "AND media_type = ?"
- ))
- embycursor.execute(query, (parentid, mediatype,))
- items = embycursor.fetchall()
-
- return items
-
- def getChecksum(self, mediatype):
-
- embycursor = self.embycursor
-
- query = ' '.join((
-
- "SELECT emby_id, checksum",
- "FROM emby",
- "WHERE emby_type = ?"
- ))
- embycursor.execute(query, (mediatype,))
- items = embycursor.fetchall()
-
- return items
-
- def getMediaType_byId(self, embyid):
-
- embycursor = self.embycursor
-
- query = ' '.join((
-
- "SELECT emby_type",
- "FROM emby",
- "WHERE emby_id = ?"
- ))
- embycursor.execute(query, (embyid,))
- try:
- itemtype = embycursor.fetchone()[0]
-
- except TypeError:
- itemtype = None
-
- return itemtype
-
- def sortby_mediaType(self, itemids, unsorted=True):
-
- sorted_items = {}
-
- for itemid in itemids:
-
- mediatype = self.getMediaType_byId(itemid)
- if mediatype:
- sorted_items.setdefault(mediatype, []).append(itemid)
- elif unsorted:
- sorted_items.setdefault('Unsorted', []).append(itemid)
-
- return sorted_items
-
- def addReference(self, embyid, kodiid, embytype, mediatype, fileid=None, pathid=None,
- parentid=None, checksum=None, mediafolderid=None):
- query = (
- '''
- INSERT OR REPLACE INTO emby(
- emby_id, kodi_id, kodi_fileid, kodi_pathid, emby_type, media_type, parent_id,
- checksum, media_folder)
-
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
- '''
- )
- self.embycursor.execute(query, (embyid, kodiid, fileid, pathid, embytype, mediatype,
- parentid, checksum, mediafolderid))
-
- def updateReference(self, embyid, checksum):
-
- query = "UPDATE emby SET checksum = ? WHERE emby_id = ?"
- self.embycursor.execute(query, (checksum, embyid))
-
- def updateParentId(self, embyid, parent_kodiid):
-
- query = "UPDATE emby SET parent_id = ? WHERE emby_id = ?"
- self.embycursor.execute(query, (parent_kodiid, embyid))
-
- def removeItems_byParentId(self, parent_kodiid, mediatype):
-
- query = ' '.join((
-
- "DELETE FROM emby",
- "WHERE parent_id = ?",
- "AND media_type = ?"
- ))
- self.embycursor.execute(query, (parent_kodiid, mediatype,))
-
- def removeItem_byKodiId(self, kodiid, mediatype):
-
- query = ' '.join((
-
- "DELETE FROM emby",
- "WHERE kodi_id = ?",
- "AND media_type = ?"
- ))
- self.embycursor.execute(query, (kodiid, mediatype,))
-
- def removeItem(self, embyid):
-
- query = "DELETE FROM emby WHERE emby_id = ?"
- self.embycursor.execute(query, (embyid,))
-
- def removeWildItem(self, embyid):
-
- query = "DELETE FROM emby WHERE emby_id LIKE ?"
- self.embycursor.execute(query, (embyid+"%",))
+# -*- coding: utf-8 -*-
+
+#################################################################################################
+
+import utils
+import clientinfo
+
+#################################################################################################
+
+
+class Embydb_Functions():
+
+
+ def __init__(self, embycursor):
+
+ self.embycursor = embycursor
+
+ self.clientInfo = clientinfo.ClientInfo()
+ self.addonName = self.clientInfo.getAddonName()
+
+ def logMsg(self, msg, lvl=1):
+
+ className = self.__class__.__name__
+ utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
+
+ def getViews(self):
+
+ views = []
+
+ query = ' '.join((
+
+ "SELECT view_id",
+ "FROM view"
+ ))
+ self.embycursor.execute(query)
+ rows = self.embycursor.fetchall()
+ for row in rows:
+ views.append(row[0])
+
+ return views
+
+ def getView_byId(self, viewid):
+
+
+ query = ' '.join((
+
+ "SELECT view_name, media_type, kodi_tagid",
+ "FROM view",
+ "WHERE view_id = ?"
+ ))
+ self.embycursor.execute(query, (viewid,))
+ view = self.embycursor.fetchone()
+
+ return view
+
+ def getView_byType(self, mediatype):
+
+ views = []
+
+ query = ' '.join((
+
+ "SELECT view_id, view_name",
+ "FROM view",
+ "WHERE media_type = ?"
+ ))
+ self.embycursor.execute(query, (mediatype,))
+ rows = self.embycursor.fetchall()
+ for row in rows:
+ views.append({
+
+ 'id': row[0],
+ 'name': row[1]
+ })
+
+ return views
+
+ def getView_byName(self, tagname):
+
+ query = ' '.join((
+
+ "SELECT view_id",
+ "FROM view",
+ "WHERE view_name = ?"
+ ))
+ self.embycursor.execute(query, (tagname,))
+ try:
+ view = self.embycursor.fetchone()[0]
+
+ except TypeError:
+ view = None
+
+ return view
+
+ def addView(self, embyid, name, mediatype, tagid):
+
+ query = (
+ '''
+ INSERT INTO view(
+ view_id, view_name, media_type, kodi_tagid)
+
+ VALUES (?, ?, ?, ?)
+ '''
+ )
+ self.embycursor.execute(query, (embyid, name, mediatype, tagid))
+
+ def updateView(self, name, tagid, mediafolderid):
+
+ query = ' '.join((
+
+ "UPDATE view",
+ "SET view_name = ?, kodi_tagid = ?",
+ "WHERE view_id = ?"
+ ))
+ self.embycursor.execute(query, (name, tagid, mediafolderid))
+
+ def removeView(self, viewid):
+
+ query = ' '.join((
+
+ "DELETE FROM view",
+ "WHERE view_id = ?"
+ ))
+ self.embycursor.execute(query, (viewid,))
+
+ def getItem_byId(self, embyid):
+
+ query = ' '.join((
+
+ "SELECT kodi_id, kodi_fileid, kodi_pathid, parent_id, media_type, emby_type",
+ "FROM emby",
+ "WHERE emby_id = ?"
+ ))
+ try:
+ self.embycursor.execute(query, (embyid,))
+ item = self.embycursor.fetchone()
+ return item
+ except: return None
+
+ def getItem_byWildId(self, embyid):
+
+ query = ' '.join((
+
+ "SELECT kodi_id, media_type",
+ "FROM emby",
+ "WHERE emby_id LIKE ?"
+ ))
+ self.embycursor.execute(query, (embyid+"%",))
+ return self.embycursor.fetchall()
+
+ def getItem_byView(self, mediafolderid):
+
+ query = ' '.join((
+
+ "SELECT kodi_id",
+ "FROM emby",
+ "WHERE media_folder = ?"
+ ))
+ self.embycursor.execute(query, (mediafolderid,))
+ return self.embycursor.fetchall()
+
+ def getItem_byKodiId(self, kodiid, mediatype):
+
+ query = ' '.join((
+
+ "SELECT emby_id, parent_id",
+ "FROM emby",
+ "WHERE kodi_id = ?",
+ "AND media_type = ?"
+ ))
+ self.embycursor.execute(query, (kodiid, mediatype,))
+ return self.embycursor.fetchone()
+
+ def getItem_byParentId(self, parentid, mediatype):
+
+ query = ' '.join((
+
+ "SELECT emby_id, kodi_id, kodi_fileid",
+ "FROM emby",
+ "WHERE parent_id = ?",
+ "AND media_type = ?"
+ ))
+ self.embycursor.execute(query, (parentid, mediatype,))
+ return self.embycursor.fetchall()
+
+ def getItemId_byParentId(self, parentid, mediatype):
+
+ query = ' '.join((
+
+ "SELECT emby_id, kodi_id",
+ "FROM emby",
+ "WHERE parent_id = ?",
+ "AND media_type = ?"
+ ))
+ self.embycursor.execute(query, (parentid, mediatype,))
+ return self.embycursor.fetchall()
+
+ def getChecksum(self, mediatype):
+
+ query = ' '.join((
+
+ "SELECT emby_id, checksum",
+ "FROM emby",
+ "WHERE emby_type = ?"
+ ))
+ self.embycursor.execute(query, (mediatype,))
+ return self.embycursor.fetchall()
+
+ def getMediaType_byId(self, embyid):
+
+ query = ' '.join((
+
+ "SELECT emby_type",
+ "FROM emby",
+ "WHERE emby_id = ?"
+ ))
+ self.embycursor.execute(query, (embyid,))
+ try:
+ itemtype = self.embycursor.fetchone()[0]
+
+ except TypeError:
+ itemtype = None
+
+ return itemtype
+
+ def sortby_mediaType(self, itemids, unsorted=True):
+
+ sorted_items = {}
+
+ for itemid in itemids:
+
+ mediatype = self.getMediaType_byId(itemid)
+ if mediatype:
+ sorted_items.setdefault(mediatype, []).append(itemid)
+ elif unsorted:
+ sorted_items.setdefault('Unsorted', []).append(itemid)
+
+ return sorted_items
+
+ def addReference(self, embyid, kodiid, embytype, mediatype, fileid=None, pathid=None,
+ parentid=None, checksum=None, mediafolderid=None):
+ query = (
+ '''
+ INSERT OR REPLACE INTO emby(
+ emby_id, kodi_id, kodi_fileid, kodi_pathid, emby_type, media_type, parent_id,
+ checksum, media_folder)
+
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ '''
+ )
+ self.embycursor.execute(query, (embyid, kodiid, fileid, pathid, embytype, mediatype,
+ parentid, checksum, mediafolderid))
+
+ def updateReference(self, embyid, checksum):
+
+ query = "UPDATE emby SET checksum = ? WHERE emby_id = ?"
+ self.embycursor.execute(query, (checksum, embyid))
+
+ def updateParentId(self, embyid, parent_kodiid):
+
+ query = "UPDATE emby SET parent_id = ? WHERE emby_id = ?"
+ self.embycursor.execute(query, (parent_kodiid, embyid))
+
+ def removeItems_byParentId(self, parent_kodiid, mediatype):
+
+ query = ' '.join((
+
+ "DELETE FROM emby",
+ "WHERE parent_id = ?",
+ "AND media_type = ?"
+ ))
+ self.embycursor.execute(query, (parent_kodiid, mediatype,))
+
+ def removeItem_byKodiId(self, kodiid, mediatype):
+
+ query = ' '.join((
+
+ "DELETE FROM emby",
+ "WHERE kodi_id = ?",
+ "AND media_type = ?"
+ ))
+ self.embycursor.execute(query, (kodiid, mediatype,))
+
+ def removeItem(self, embyid):
+
+ query = "DELETE FROM emby WHERE emby_id = ?"
+ self.embycursor.execute(query, (embyid,))
+
+ def removeWildItem(self, embyid):
+
+ query = "DELETE FROM emby WHERE emby_id LIKE ?"
+ self.embycursor.execute(query, (embyid+"%",))
\ No newline at end of file
diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py
index 11c66f5d..bc81ad0a 100644
--- a/resources/lib/entrypoint.py
+++ b/resources/lib/entrypoint.py
@@ -1,1089 +1,1089 @@
-# -*- coding: utf-8 -*-
-
-#################################################################################################
-
-import json
-import os
-import sys
-import urlparse
-
-import xbmc
-import xbmcaddon
-import xbmcgui
-import xbmcvfs
-import xbmcplugin
-
-import artwork
-import utils
-import clientinfo
-import downloadutils
-import librarysync
-import read_embyserver as embyserver
-import embydb_functions as embydb
-import playlist
-import playbackutils as pbutils
-import playutils
-import api
-
-
-#################################################################################################
-
-
-def doPlayback(itemid, dbid):
-
- emby = embyserver.Read_EmbyServer()
- item = emby.getItem(itemid)
- pbutils.PlaybackUtils(item).play(itemid, dbid)
-
-##### DO RESET AUTH #####
-def resetAuth():
- # User tried login and failed too many times
- resp = xbmcgui.Dialog().yesno(
- heading="Warning",
- line1=(
- "Emby might lock your account if you fail to log in too many times. "
- "Proceed anyway?"))
- if resp == 1:
- utils.logMsg("EMBY", "Reset login attempts.", 1)
- utils.window('emby_serverStatus', value="Auth")
- else:
- xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)')
-
-def addDirectoryItem(label, path, folder=True):
- li = xbmcgui.ListItem(label, path=path)
- li.setThumbnailImage("special://home/addons/plugin.video.emby/icon.png")
- li.setArt({"fanart":"special://home/addons/plugin.video.emby/fanart.jpg"})
- li.setArt({"landscape":"special://home/addons/plugin.video.emby/fanart.jpg"})
- xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=folder)
-
-def doMainListing():
- xbmcplugin.setContent(int(sys.argv[1]), 'files')
- # Get emby nodes from the window props
- embyprops = utils.window('Emby.nodes.total')
- if embyprops:
- totalnodes = int(embyprops)
- for i in range(totalnodes):
- path = utils.window('Emby.nodes.%s.index' % i)
- if not path:
- path = utils.window('Emby.nodes.%s.content' % i)
- label = utils.window('Emby.nodes.%s.title' % i)
- type = utils.window('Emby.nodes.%s.type' % i)
- #because we do not use seperate entrypoints for each content type, we need to figure out which items to show in each listing.
- #for now we just only show picture nodes in the picture library video nodes in the video library and all nodes in any other window
- if path and xbmc.getCondVisibility("Window.IsActive(Pictures)") and type == "photos":
- addDirectoryItem(label, path)
- elif path and xbmc.getCondVisibility("Window.IsActive(VideoLibrary)") and type != "photos":
- addDirectoryItem(label, path)
- elif path and not xbmc.getCondVisibility("Window.IsActive(VideoLibrary) | Window.IsActive(Pictures) | Window.IsActive(MusicLibrary)"):
- addDirectoryItem(label, path)
-
- #experimental live tv nodes
- addDirectoryItem("Live Tv Channels (experimental)", "plugin://plugin.video.emby/?mode=browsecontent&type=tvchannels&folderid=root")
- addDirectoryItem("Live Tv Recordings (experimental)", "plugin://plugin.video.emby/?mode=browsecontent&type=recordings&folderid=root")
-
- # some extra entries for settings and stuff. TODO --> localize the labels
- addDirectoryItem("Network credentials", "plugin://plugin.video.emby/?mode=passwords")
- addDirectoryItem("Settings", "plugin://plugin.video.emby/?mode=settings")
- addDirectoryItem("Add user to session", "plugin://plugin.video.emby/?mode=adduser")
- addDirectoryItem("Refresh Emby playlists/nodes", "plugin://plugin.video.emby/?mode=refreshplaylist")
- addDirectoryItem("Perform manual sync", "plugin://plugin.video.emby/?mode=manualsync")
- addDirectoryItem("Repair local database (force update all content)", "plugin://plugin.video.emby/?mode=repair")
- addDirectoryItem("Perform local database reset (full resync)", "plugin://plugin.video.emby/?mode=reset")
- addDirectoryItem("Cache all images to Kodi texture cache", "plugin://plugin.video.emby/?mode=texturecache")
- addDirectoryItem("Sync Emby Theme Media to Kodi", "plugin://plugin.video.emby/?mode=thememedia")
-
- xbmcplugin.endOfDirectory(int(sys.argv[1]))
-
-
-##### Generate a new deviceId
-def resetDeviceId():
-
- dialog = xbmcgui.Dialog()
- language = utils.language
-
- deviceId_old = utils.window('emby_deviceId')
- try:
- utils.window('emby_deviceId', clear=True)
- deviceId = clientinfo.ClientInfo().getDeviceId(reset=True)
- except Exception as e:
- utils.logMsg("EMBY", "Failed to generate a new device Id: %s" % e, 1)
- dialog.ok(
- heading="Emby for Kodi",
- line1=language(33032))
- else:
- utils.logMsg("EMBY", "Successfully removed old deviceId: %s New deviceId: %s"
- % (deviceId_old, deviceId), 1)
- dialog.ok(
- heading="Emby for Kodi",
- line1=language(33033))
- xbmc.executebuiltin('RestartApp')
-
-##### Delete Item
-def deleteItem():
-
- # Serves as a keymap action
- if xbmc.getInfoLabel('ListItem.Property(embyid)'): # If we already have the embyid
- embyid = xbmc.getInfoLabel('ListItem.Property(embyid)')
- else:
- dbid = xbmc.getInfoLabel('ListItem.DBID')
- itemtype = xbmc.getInfoLabel('ListItem.DBTYPE')
-
- if not itemtype:
-
- if xbmc.getCondVisibility('Container.Content(albums)'):
- itemtype = "album"
- elif xbmc.getCondVisibility('Container.Content(artists)'):
- itemtype = "artist"
- elif xbmc.getCondVisibility('Container.Content(songs)'):
- itemtype = "song"
- elif xbmc.getCondVisibility('Container.Content(pictures)'):
- itemtype = "picture"
- else:
- utils.logMsg("EMBY delete", "Unknown type, unable to proceed.", 1)
- return
-
- embyconn = utils.kodiSQL('emby')
- embycursor = embyconn.cursor()
- emby_db = embydb.Embydb_Functions(embycursor)
- item = emby_db.getItem_byKodiId(dbid, itemtype)
- embycursor.close()
-
- try:
- embyid = item[0]
- except TypeError:
- utils.logMsg("EMBY delete", "Unknown embyId, unable to proceed.", 1)
- return
-
- if utils.settings('skipContextMenu') != "true":
- resp = xbmcgui.Dialog().yesno(
- heading="Confirm delete",
- line1=("Delete file from Emby Server? This will "
- "also delete the file(s) from disk!"))
- if not resp:
- utils.logMsg("EMBY delete", "User skipped deletion for: %s." % embyid, 1)
- return
-
- doUtils = downloadutils.DownloadUtils()
- url = "{server}/emby/Items/%s?format=json" % embyid
- utils.logMsg("EMBY delete", "Deleting request: %s" % embyid, 0)
- doUtils.downloadUrl(url, type="DELETE")
-
-##### ADD ADDITIONAL USERS #####
-def addUser():
-
- doUtils = downloadutils.DownloadUtils()
- art = artwork.Artwork()
- clientInfo = clientinfo.ClientInfo()
- deviceId = clientInfo.getDeviceId()
- deviceName = clientInfo.getDeviceName()
- userid = utils.window('emby_currUser')
- dialog = xbmcgui.Dialog()
-
- # Get session
- url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId
- result = doUtils.downloadUrl(url)
-
- try:
- sessionId = result[0]['Id']
- additionalUsers = result[0]['AdditionalUsers']
- # Add user to session
- userlist = {}
- users = []
- url = "{server}/emby/Users?IsDisabled=false&IsHidden=false&format=json"
- result = doUtils.downloadUrl(url)
-
- # pull the list of users
- for user in result:
- name = user['Name']
- userId = user['Id']
- if userid != userId:
- userlist[name] = userId
- users.append(name)
-
- # Display dialog if there's additional users
- if additionalUsers:
-
- option = dialog.select("Add/Remove user from the session", ["Add user", "Remove user"])
- # Users currently in the session
- additionalUserlist = {}
- additionalUsername = []
- # Users currently in the session
- for user in additionalUsers:
- name = user['UserName']
- userId = user['UserId']
- additionalUserlist[name] = userId
- additionalUsername.append(name)
-
- if option == 1:
- # User selected Remove user
- resp = dialog.select("Remove user from the session", additionalUsername)
- if resp > -1:
- selected = additionalUsername[resp]
- selected_userId = additionalUserlist[selected]
- url = "{server}/emby/Sessions/%s/Users/%s" % (sessionId, selected_userId)
- doUtils.downloadUrl(url, postBody={}, type="DELETE")
- dialog.notification(
- heading="Success!",
- message="%s removed from viewing session" % selected,
- icon="special://home/addons/plugin.video.emby/icon.png",
- time=1000)
-
- # clear picture
- position = utils.window('EmbyAdditionalUserPosition.%s' % selected_userId)
- utils.window('EmbyAdditionalUserImage.%s' % position, clear=True)
- return
- else:
- return
-
- elif option == 0:
- # User selected Add user
- for adduser in additionalUsername:
- try: # Remove from selected already added users. It is possible they are hidden.
- users.remove(adduser)
- except: pass
-
- elif option < 0:
- # User cancelled
- return
-
- # Subtract any additional users
- utils.logMsg("EMBY", "Displaying list of users: %s" % users)
- resp = dialog.select("Add user to the session", users)
- # post additional user
- if resp > -1:
- selected = users[resp]
- selected_userId = userlist[selected]
- url = "{server}/emby/Sessions/%s/Users/%s" % (sessionId, selected_userId)
- doUtils.downloadUrl(url, postBody={}, type="POST")
- dialog.notification(
- heading="Success!",
- message="%s added to viewing session" % selected,
- icon="special://home/addons/plugin.video.emby/icon.png",
- time=1000)
-
- except:
- utils.logMsg("EMBY", "Failed to add user to session.")
- dialog.notification(
- heading="Error",
- message="Unable to add/remove user from the session.",
- icon=xbmcgui.NOTIFICATION_ERROR)
-
- # Add additional user images
- # always clear the individual items first
- totalNodes = 10
- for i in range(totalNodes):
- if not utils.window('EmbyAdditionalUserImage.%s' % i):
- break
- utils.window('EmbyAdditionalUserImage.%s' % i, clear=True)
-
- url = "{server}/emby/Sessions?DeviceId=%s" % deviceId
- result = doUtils.downloadUrl(url)
- additionalUsers = result[0]['AdditionalUsers']
- count = 0
- for additionaluser in additionalUsers:
- userid = additionaluser['UserId']
- url = "{server}/emby/Users/%s?format=json" % userid
- result = doUtils.downloadUrl(url)
- utils.window('EmbyAdditionalUserImage.%s' % count,
- value=art.getUserArtwork(result['Id'], 'Primary'))
- utils.window('EmbyAdditionalUserPosition.%s' % userid, value=str(count))
- count +=1
-
-##### THEME MUSIC/VIDEOS #####
-def getThemeMedia():
-
- doUtils = downloadutils.DownloadUtils()
- dialog = xbmcgui.Dialog()
- playback = None
-
- # Choose playback method
- resp = dialog.select("Playback method for your themes", ["Direct Play", "Direct Stream"])
- if resp == 0:
- playback = "DirectPlay"
- elif resp == 1:
- playback = "DirectStream"
- else:
- return
-
- library = xbmc.translatePath(
- "special://profile/addon_data/plugin.video.emby/library/").decode('utf-8')
- # Create library directory
- if not xbmcvfs.exists(library):
- xbmcvfs.mkdir(library)
-
- # Set custom path for user
- tvtunes_path = xbmc.translatePath(
- "special://profile/addon_data/script.tvtunes/").decode('utf-8')
- if xbmcvfs.exists(tvtunes_path):
- tvtunes = xbmcaddon.Addon(id="script.tvtunes")
- tvtunes.setSetting('custom_path_enable', "true")
- tvtunes.setSetting('custom_path', library)
- utils.logMsg("EMBY", "TV Tunes custom path is enabled and set.", 1)
- else:
- # if it does not exist this will not work so warn user
- # often they need to edit the settings first for it to be created.
- dialog.ok(
- heading="Warning",
- line1=(
- "The settings file does not exist in tvtunes. ",
- "Go to the tvtunes addon and change a setting, then come back and re-run."))
- xbmc.executebuiltin('Addon.OpenSettings(script.tvtunes)')
- return
-
- # Get every user view Id
- embyconn = utils.kodiSQL('emby')
- embycursor = embyconn.cursor()
- emby_db = embydb.Embydb_Functions(embycursor)
- viewids = emby_db.getViews()
- embycursor.close()
-
- # Get Ids with Theme Videos
- itemIds = {}
- for view in viewids:
- url = "{server}/emby/Users/{UserId}/Items?HasThemeVideo=True&ParentId=%s&format=json" % view
- result = doUtils.downloadUrl(url)
- if result['TotalRecordCount'] != 0:
- for item in result['Items']:
- itemId = item['Id']
- folderName = item['Name']
- folderName = utils.normalize_string(folderName.encode('utf-8'))
- itemIds[itemId] = folderName
-
- # Get paths for theme videos
- for itemId in itemIds:
- nfo_path = xbmc.translatePath(
- "special://profile/addon_data/plugin.video.emby/library/%s/" % itemIds[itemId])
- # Create folders for each content
- if not xbmcvfs.exists(nfo_path):
- xbmcvfs.mkdir(nfo_path)
- # Where to put the nfos
- nfo_path = "%s%s" % (nfo_path, "tvtunes.nfo")
-
- url = "{server}/emby/Items/%s/ThemeVideos?format=json" % itemId
- result = doUtils.downloadUrl(url)
-
- # Create nfo and write themes to it
- nfo_file = xbmcvfs.File(nfo_path, 'w')
- pathstowrite = ""
- # May be more than one theme
- for theme in result['Items']:
- putils = playutils.PlayUtils(theme)
- if playback == "DirectPlay":
- playurl = putils.directPlay()
- else:
- playurl = putils.directStream()
- pathstowrite += ('%s' % playurl.encode('utf-8'))
-
- # Check if the item has theme songs and add them
- url = "{server}/emby/Items/%s/ThemeSongs?format=json" % itemId
- result = doUtils.downloadUrl(url)
-
- # May be more than one theme
- for theme in result['Items']:
- putils = playutils.PlayUtils(theme)
- if playback == "DirectPlay":
- playurl = putils.directPlay()
- else:
- playurl = putils.directStream()
- pathstowrite += ('%s' % playurl.encode('utf-8'))
-
- nfo_file.write(
- '%s' % pathstowrite
- )
- # Close nfo file
- nfo_file.close()
-
- # Get Ids with Theme songs
- musicitemIds = {}
- for view in viewids:
- url = "{server}/emby/Users/{UserId}/Items?HasThemeSong=True&ParentId=%s&format=json" % view
- result = doUtils.downloadUrl(url)
- if result['TotalRecordCount'] != 0:
- for item in result['Items']:
- itemId = item['Id']
- folderName = item['Name']
- folderName = utils.normalize_string(folderName.encode('utf-8'))
- musicitemIds[itemId] = folderName
-
- # Get paths
- for itemId in musicitemIds:
-
- # if the item was already processed with video themes back out
- if itemId in itemIds:
- continue
-
- nfo_path = xbmc.translatePath(
- "special://profile/addon_data/plugin.video.emby/library/%s/" % musicitemIds[itemId])
- # Create folders for each content
- if not xbmcvfs.exists(nfo_path):
- xbmcvfs.mkdir(nfo_path)
- # Where to put the nfos
- nfo_path = "%s%s" % (nfo_path, "tvtunes.nfo")
-
- url = "{server}/emby/Items/%s/ThemeSongs?format=json" % itemId
- result = doUtils.downloadUrl(url)
-
- # Create nfo and write themes to it
- nfo_file = xbmcvfs.File(nfo_path, 'w')
- pathstowrite = ""
- # May be more than one theme
- for theme in result['Items']:
- putils = playutils.PlayUtils(theme)
- if playback == "DirectPlay":
- playurl = putils.directPlay()
- else:
- playurl = putils.directStream()
- pathstowrite += ('%s' % playurl.encode('utf-8'))
-
- nfo_file.write(
- '%s' % pathstowrite
- )
- # Close nfo file
- nfo_file.close()
-
- dialog.notification(
- heading="Emby for Kodi",
- message="Themes added!",
- icon="special://home/addons/plugin.video.emby/icon.png",
- time=1000,
- sound=False)
-
-##### REFRESH EMBY PLAYLISTS #####
-def refreshPlaylist():
-
- lib = librarysync.LibrarySync()
- dialog = xbmcgui.Dialog()
- try:
- # First remove playlists
- utils.deletePlaylists()
- # Remove video nodes
- utils.deleteNodes()
- # Refresh views
- lib.refreshViews()
- dialog.notification(
- heading="Emby for Kodi",
- message="Emby playlists/nodes refreshed",
- icon="special://home/addons/plugin.video.emby/icon.png",
- time=1000,
- sound=False)
-
- except Exception as e:
- utils.logMsg("EMBY", "Refresh playlists/nodes failed: %s" % e, 1)
- dialog.notification(
- heading="Emby for Kodi",
- message="Emby playlists/nodes refresh failed",
- icon=xbmcgui.NOTIFICATION_ERROR,
- time=1000,
- sound=False)
-
-#### SHOW SUBFOLDERS FOR NODE #####
-def GetSubFolders(nodeindex):
- nodetypes = ["",".recent",".recentepisodes",".inprogress",".inprogressepisodes",".unwatched",".nextepisodes",".sets",".genres",".random",".recommended"]
- for node in nodetypes:
- title = utils.window('Emby.nodes.%s%s.title' %(nodeindex,node))
- if title:
- path = utils.window('Emby.nodes.%s%s.content' %(nodeindex,node))
- type = utils.window('Emby.nodes.%s%s.type' %(nodeindex,node))
- addDirectoryItem(title, path)
- xbmcplugin.endOfDirectory(int(sys.argv[1]))
-
-##### BROWSE EMBY NODES DIRECTLY #####
-def BrowseContent(viewname, type="", folderid=""):
-
- emby = embyserver.Read_EmbyServer()
- art = artwork.Artwork()
- doUtils = downloadutils.DownloadUtils()
-
- #folderid used as filter ?
- if folderid in ["recent","recentepisodes","inprogress","inprogressepisodes","unwatched","nextepisodes","sets","genres","random","recommended"]:
- filter = folderid
- folderid = ""
- else:
- filter = ""
-
- xbmcplugin.setPluginCategory(int(sys.argv[1]), viewname)
- #get views for root level
- if not folderid:
- views = emby.getViews(type)
- for view in views:
- if view.get("name") == viewname.decode('utf-8'):
- folderid = view.get("id")
-
- if viewname is not None:
- utils.logMsg("BrowseContent","viewname: %s - type: %s - folderid: %s - filter: %s" %(viewname.decode('utf-8'), type.decode('utf-8'), folderid.decode('utf-8'), filter.decode('utf-8')))
- #set the correct params for the content type
- #only proceed if we have a folderid
- if folderid:
- if type.lower() == "homevideos":
- xbmcplugin.setContent(int(sys.argv[1]), 'episodes')
- itemtype = "Video,Folder,PhotoAlbum"
- elif type.lower() == "photos":
- xbmcplugin.setContent(int(sys.argv[1]), 'files')
- itemtype = "Photo,PhotoAlbum,Folder"
- else:
- itemtype = ""
-
- #get the actual listing
- if type == "recordings":
- listing = emby.getTvRecordings(folderid)
- elif type == "tvchannels":
- listing = emby.getTvChannels()
- elif filter == "recent":
- listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="DateCreated", recursive=True, limit=25, sortorder="Descending")
- elif filter == "random":
- listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="Random", recursive=True, limit=150, sortorder="Descending")
- elif filter == "recommended":
- listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter="IsFavorite")
- elif filter == "sets":
- listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[1], sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter="IsFavorite")
- else:
- listing = emby.getFilteredSection(folderid, itemtype=itemtype, recursive=False)
-
- #process the listing
- if listing:
- for item in listing.get("Items"):
- li = createListItemFromEmbyItem(item,art,doUtils)
- if item.get("IsFolder") == True:
- #for folders we add an additional browse request, passing the folderId
- path = "%s?id=%s&mode=browsecontent&type=%s&folderid=%s" % (sys.argv[0].decode('utf-8'), viewname.decode('utf-8'), type.decode('utf-8'), item.get("Id").decode('utf-8'))
- xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=True)
- else:
- #playable item, set plugin path and mediastreams
- xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=li.getProperty("path"), listitem=li)
-
-
- if filter == "recent":
- xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_DATE)
- else:
- xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_TITLE)
- xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_DATE)
- xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_RATING)
- xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_RUNTIME)
-
- xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
-
-##### CREATE LISTITEM FROM EMBY METADATA #####
-def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils.DownloadUtils()):
- API = api.API(item)
- itemid = item['Id']
-
- title = item.get('Name')
- li = xbmcgui.ListItem(title)
-
- premieredate = item.get('PremiereDate',"")
- if not premieredate: premieredate = item.get('DateCreated',"")
- if premieredate:
- premieredatelst = premieredate.split('T')[0].split("-")
- premieredate = "%s.%s.%s" %(premieredatelst[2],premieredatelst[1],premieredatelst[0])
-
- li.setProperty("embyid",itemid)
-
- allart = art.getAllArtwork(item)
-
- if item["Type"] == "Photo":
- #listitem setup for pictures...
- img_path = allart.get('Primary')
- li.setProperty("path",img_path)
- picture = doUtils.downloadUrl("{server}/Items/%s/Images" %itemid)
- if picture:
- picture = picture[0]
- if picture.get("Width") > picture.get("Height"):
- li.setArt( {"fanart": img_path}) #add image as fanart for use with skinhelper auto thumb/backgrund creation
- li.setInfo('pictures', infoLabels={ "picturepath": img_path, "date": premieredate, "size": picture.get("Size"), "exif:width": str(picture.get("Width")), "exif:height": str(picture.get("Height")), "title": title})
- li.setThumbnailImage(img_path)
- li.setProperty("plot",API.getOverview())
- li.setIconImage('DefaultPicture.png')
- else:
- #normal video items
- li.setProperty('IsPlayable', 'true')
- path = "%s?id=%s&mode=play" % (sys.argv[0], item.get("Id"))
- li.setProperty("path",path)
- genre = API.getGenres()
- overlay = 0
- userdata = API.getUserData()
- runtime = item.get("RunTimeTicks",0)/ 10000000.0
- seektime = userdata['Resume']
- if seektime:
- li.setProperty("resumetime", str(seektime))
- li.setProperty("totaltime", str(runtime))
-
- played = userdata['Played']
- if played: overlay = 7
- else: overlay = 6
- playcount = userdata['PlayCount']
- if playcount is None:
- playcount = 0
-
- rating = item.get('CommunityRating')
- if not rating: rating = userdata['UserRating']
-
- # Populate the extradata list and artwork
- extradata = {
- 'id': itemid,
- 'rating': rating,
- 'year': item.get('ProductionYear'),
- 'genre': genre,
- 'playcount': str(playcount),
- 'title': title,
- 'plot': API.getOverview(),
- 'Overlay': str(overlay),
- 'duration': runtime
- }
- if premieredate:
- extradata["premieredate"] = premieredate
- extradata["date"] = premieredate
- li.setInfo('video', infoLabels=extradata)
- if allart.get('Primary'):
- li.setThumbnailImage(allart.get('Primary'))
- else: li.setThumbnailImage('DefaultTVShows.png')
- li.setIconImage('DefaultTVShows.png')
- if not allart.get('Background'): #add image as fanart for use with skinhelper auto thumb/backgrund creation
- li.setArt( {"fanart": allart.get('Primary') } )
- else:
- pbutils.PlaybackUtils(item).setArtwork(li)
-
- mediastreams = API.getMediaStreams()
- videostreamFound = False
- if mediastreams:
- for key, value in mediastreams.iteritems():
- if key == "video" and value: videostreamFound = True
- if value: li.addStreamInfo(key, value[0])
- if not videostreamFound:
- #just set empty streamdetails to prevent errors in the logs
- li.addStreamInfo("video", {'duration': runtime})
-
- return li
-
-##### BROWSE EMBY CHANNELS #####
-def BrowseChannels(itemid, folderid=None):
-
- _addon_id = int(sys.argv[1])
- _addon_url = sys.argv[0]
- doUtils = downloadutils.DownloadUtils()
- art = artwork.Artwork()
-
- xbmcplugin.setContent(int(sys.argv[1]), 'files')
- if folderid:
- url = (
- "{server}/emby/Channels/%s/Items?userid={UserId}&folderid=%s&format=json"
- % (itemid, folderid))
- elif itemid == "0":
- # id 0 is the root channels folder
- url = "{server}/emby/Channels?{UserId}&format=json"
- else:
- url = "{server}/emby/Channels/%s/Items?UserId={UserId}&format=json" % itemid
-
- result = doUtils.downloadUrl(url)
- if result and result.get("Items"):
- for item in result.get("Items"):
- itemid = item['Id']
- itemtype = item['Type']
- li = createListItemFromEmbyItem(item,art,doUtils)
-
- isFolder = item.get('IsFolder', False)
-
- channelId = item.get('ChannelId', "")
- channelName = item.get('ChannelName', "")
- if itemtype == "Channel":
- path = "%s?id=%s&mode=channels" % (_addon_url, itemid)
- xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li, isFolder=True)
- elif isFolder:
- path = "%s?id=%s&mode=channelsfolder&folderid=%s" % (_addon_url, channelId, itemid)
- xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li, isFolder=True)
- else:
- path = "%s?id=%s&mode=play" % (_addon_url, itemid)
- li.setProperty('IsPlayable', 'true')
- xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li)
-
- xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
-
-##### LISTITEM SETUP FOR VIDEONODES #####
-def createListItem(item):
-
- title = item['title']
- li = xbmcgui.ListItem(title)
- li.setProperty('IsPlayable', "true")
-
- metadata = {
-
- 'Title': title,
- 'duration': str(item['runtime']/60),
- 'Plot': item['plot'],
- 'Playcount': item['playcount']
- }
-
- if "episode" in item:
- episode = item['episode']
- metadata['Episode'] = episode
-
- if "season" in item:
- season = item['season']
- metadata['Season'] = season
-
- if season and episode:
- li.setProperty('episodeno', "s%.2de%.2d" % (season, episode))
-
- if "firstaired" in item:
- metadata['Premiered'] = item['firstaired']
-
- if "showtitle" in item:
- metadata['TVshowTitle'] = item['showtitle']
-
- if "rating" in item:
- metadata['Rating'] = str(round(float(item['rating']),1))
-
- if "director" in item:
- metadata['Director'] = " / ".join(item['director'])
-
- if "writer" in item:
- metadata['Writer'] = " / ".join(item['writer'])
-
- if "cast" in item:
- cast = []
- castandrole = []
- for person in item['cast']:
- name = person['name']
- cast.append(name)
- castandrole.append((name, person['role']))
- metadata['Cast'] = cast
- metadata['CastAndRole'] = castandrole
-
- li.setInfo(type="Video", infoLabels=metadata)
- li.setProperty('resumetime', str(item['resume']['position']))
- li.setProperty('totaltime', str(item['resume']['total']))
- li.setArt(item['art'])
- li.setThumbnailImage(item['art'].get('thumb',''))
- li.setIconImage('DefaultTVShows.png')
- li.setProperty('dbid', str(item['episodeid']))
- li.setProperty('fanart_image', item['art'].get('tvshow.fanart',''))
- for key, value in item['streamdetails'].iteritems():
- for stream in value:
- li.addStreamInfo(key, stream)
-
- return li
-
-##### GET NEXTUP EPISODES FOR TAGNAME #####
-def getNextUpEpisodes(tagname, limit):
-
- count = 0
- # if the addon is called with nextup parameter,
- # we return the nextepisodes list of the given tagname
- xbmcplugin.setContent(int(sys.argv[1]), 'episodes')
- # First we get a list of all the TV shows - filtered by tag
- query = {
-
- 'jsonrpc': "2.0",
- 'id': "libTvShows",
- 'method': "VideoLibrary.GetTVShows",
- 'params': {
-
- 'sort': {'order': "descending", 'method': "lastplayed"},
- 'filter': {
- 'and': [
- {'operator': "true", 'field': "inprogress", 'value': ""},
- {'operator': "is", 'field': "tag", 'value': "%s" % tagname}
- ]},
- 'properties': ['title', 'studio', 'mpaa', 'file', 'art']
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(query))
- result = json.loads(result)
- # If we found any, find the oldest unwatched show for each one.
- try:
- items = result['result']['tvshows']
- except (KeyError, TypeError):
- pass
- else:
- for item in items:
- if utils.settings('ignoreSpecialsNextEpisodes') == "true":
- query = {
-
- 'jsonrpc': "2.0",
- 'id': 1,
- 'method': "VideoLibrary.GetEpisodes",
- 'params': {
-
- 'tvshowid': item['tvshowid'],
- 'sort': {'method': "episode"},
- 'filter': {
- 'and': [
- {'operator': "lessthan", 'field': "playcount", 'value': "1"},
- {'operator': "greaterthan", 'field': "season", 'value': "0"}
- ]},
- 'properties': [
- "title", "playcount", "season", "episode", "showtitle",
- "plot", "file", "rating", "resume", "tvshowid", "art",
- "streamdetails", "firstaired", "runtime", "writer",
- "dateadded", "lastplayed"
- ],
- 'limits': {"end": 1}
- }
- }
- else:
- query = {
-
- 'jsonrpc': "2.0",
- 'id': 1,
- 'method': "VideoLibrary.GetEpisodes",
- 'params': {
-
- 'tvshowid': item['tvshowid'],
- 'sort': {'method': "episode"},
- 'filter': {'operator': "lessthan", 'field': "playcount", 'value': "1"},
- 'properties': [
- "title", "playcount", "season", "episode", "showtitle",
- "plot", "file", "rating", "resume", "tvshowid", "art",
- "streamdetails", "firstaired", "runtime", "writer",
- "dateadded", "lastplayed"
- ],
- 'limits': {"end": 1}
- }
- }
-
- result = xbmc.executeJSONRPC(json.dumps(query))
- result = json.loads(result)
- try:
- episodes = result['result']['episodes']
- except (KeyError, TypeError):
- pass
- else:
- for episode in episodes:
- li = createListItem(episode)
- xbmcplugin.addDirectoryItem(
- handle=int(sys.argv[1]),
- url=episode['file'],
- listitem=li)
- count += 1
-
- if count == limit:
- break
-
- xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
-
-##### GET INPROGRESS EPISODES FOR TAGNAME #####
-def getInProgressEpisodes(tagname, limit):
-
- count = 0
- # if the addon is called with inprogressepisodes parameter,
- # we return the inprogressepisodes list of the given tagname
- xbmcplugin.setContent(int(sys.argv[1]), 'episodes')
- # First we get a list of all the in-progress TV shows - filtered by tag
- query = {
-
- 'jsonrpc': "2.0",
- 'id': "libTvShows",
- 'method': "VideoLibrary.GetTVShows",
- 'params': {
-
- 'sort': {'order': "descending", 'method': "lastplayed"},
- 'filter': {
- 'and': [
- {'operator': "true", 'field': "inprogress", 'value': ""},
- {'operator': "is", 'field': "tag", 'value': "%s" % tagname}
- ]},
- 'properties': ['title', 'studio', 'mpaa', 'file', 'art']
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(query))
- result = json.loads(result)
- # If we found any, find the oldest unwatched show for each one.
- try:
- items = result['result']['tvshows']
- except (KeyError, TypeError):
- pass
- else:
- for item in items:
- query = {
-
- 'jsonrpc': "2.0",
- 'id': 1,
- 'method': "VideoLibrary.GetEpisodes",
- 'params': {
-
- 'tvshowid': item['tvshowid'],
- 'sort': {'method': "episode"},
- 'filter': {'operator': "true", 'field': "inprogress", 'value': ""},
- 'properties': [
- "title", "playcount", "season", "episode", "showtitle", "plot",
- "file", "rating", "resume", "tvshowid", "art", "cast",
- "streamdetails", "firstaired", "runtime", "writer",
- "dateadded", "lastplayed"
- ]
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(query))
- result = json.loads(result)
- try:
- episodes = result['result']['episodes']
- except (KeyError, TypeError):
- pass
- else:
- for episode in episodes:
- li = createListItem(episode)
- xbmcplugin.addDirectoryItem(
- handle=int(sys.argv[1]),
- url=episode['file'],
- listitem=li)
- count += 1
-
- if count == limit:
- break
-
- xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
-
-##### GET RECENT EPISODES FOR TAGNAME #####
-def getRecentEpisodes(tagname, limit):
-
- count = 0
- # if the addon is called with recentepisodes parameter,
- # we return the recentepisodes list of the given tagname
- xbmcplugin.setContent(int(sys.argv[1]), 'episodes')
- # First we get a list of all the TV shows - filtered by tag
- query = {
-
- 'jsonrpc': "2.0",
- 'id': "libTvShows",
- 'method': "VideoLibrary.GetTVShows",
- 'params': {
-
- 'sort': {'order': "descending", 'method': "dateadded"},
- 'filter': {'operator': "is", 'field': "tag", 'value': "%s" % tagname},
- 'properties': ["title","sorttitle"]
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(query))
- result = json.loads(result)
- # If we found any, find the oldest unwatched show for each one.
- try:
- items = result['result']['tvshows']
- except (KeyError, TypeError):
- pass
- else:
- allshowsIds = set()
- for item in items:
- allshowsIds.add(item['tvshowid'])
-
- query = {
-
- 'jsonrpc': "2.0",
- 'id': 1,
- 'method': "VideoLibrary.GetEpisodes",
- 'params': {
-
- 'sort': {'order': "descending", 'method': "dateadded"},
- 'filter': {'operator': "lessthan", 'field': "playcount", 'value': "1"},
- 'properties': [
- "title", "playcount", "season", "episode", "showtitle", "plot",
- "file", "rating", "resume", "tvshowid", "art", "streamdetails",
- "firstaired", "runtime", "cast", "writer", "dateadded", "lastplayed"
- ],
- "limits": {"end": limit}
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(query))
- result = json.loads(result)
- try:
- episodes = result['result']['episodes']
- except (KeyError, TypeError):
- pass
- else:
- for episode in episodes:
- if episode['tvshowid'] in allshowsIds:
- li = createListItem(episode)
- xbmcplugin.addDirectoryItem(
- handle=int(sys.argv[1]),
- url=episode['file'],
- listitem=li)
- count += 1
-
- if count == limit:
- break
-
- xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
-
-##### GET VIDEO EXTRAS FOR LISTITEM #####
-def getVideoFiles(embyId,embyPath):
- #returns the video files for the item as plugin listing, can be used for browsing the actual files or videoextras etc.
- emby = embyserver.Read_EmbyServer()
- if not embyId:
- if "plugin.video.emby" in embyPath:
- embyId = embyPath.split("/")[-2]
- if embyId:
- item = emby.getItem(embyId)
- putils = playutils.PlayUtils(item)
- if putils.isDirectPlay():
- #only proceed if we can access the files directly. TODO: copy local on the fly if accessed outside
- filelocation = putils.directPlay()
- if not filelocation.endswith("/"):
- filelocation = filelocation.rpartition("/")[0]
- dirs, files = xbmcvfs.listdir(filelocation)
- for file in files:
- file = filelocation + file
- li = xbmcgui.ListItem(file, path=file)
- xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=file, listitem=li)
- for dir in dirs:
- dir = filelocation + dir
- li = xbmcgui.ListItem(dir, path=dir)
- xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=dir, listitem=li, isFolder=True)
- xbmcplugin.endOfDirectory(int(sys.argv[1]))
-
-##### GET EXTRAFANART FOR LISTITEM #####
-def getExtraFanArt(embyId,embyPath):
-
- emby = embyserver.Read_EmbyServer()
- art = artwork.Artwork()
-
- # Get extrafanart for listitem
- # will be called by skinhelper script to get the extrafanart
- try:
- # for tvshows we get the embyid just from the path
- if not embyId:
- if "plugin.video.emby" in embyPath:
- embyId = embyPath.split("/")[-2]
-
- if embyId:
- #only proceed if we actually have a emby id
- utils.logMsg("EMBY", "Requesting extrafanart for Id: %s" % embyId, 0)
-
- # We need to store the images locally for this to work
- # because of the caching system in xbmc
- fanartDir = xbmc.translatePath("special://thumbnails/emby/%s/" % embyId).decode('utf-8')
-
- if not xbmcvfs.exists(fanartDir):
- # Download the images to the cache directory
- xbmcvfs.mkdirs(fanartDir)
- item = emby.getItem(embyId)
- if item:
- backdrops = art.getAllArtwork(item)['Backdrop']
- tags = item['BackdropImageTags']
- count = 0
- for backdrop in backdrops:
- # Same ordering as in artwork
- tag = tags[count]
- if os.path.supports_unicode_filenames:
- fanartFile = os.path.join(fanartDir, "fanart%s.jpg" % tag)
- else:
- fanartFile = os.path.join(fanartDir.encode("utf-8"), "fanart%s.jpg" % tag.encode("utf-8"))
- li = xbmcgui.ListItem(tag, path=fanartFile)
- xbmcplugin.addDirectoryItem(
- handle=int(sys.argv[1]),
- url=fanartFile,
- listitem=li)
- xbmcvfs.copy(backdrop, fanartFile)
- count += 1
- else:
- utils.logMsg("EMBY", "Found cached backdrop.", 2)
- # Use existing cached images
- dirs, files = xbmcvfs.listdir(fanartDir)
- for file in files:
- fanartFile = os.path.join(fanartDir, file.decode('utf-8'))
- li = xbmcgui.ListItem(file, path=fanartFile)
- xbmcplugin.addDirectoryItem(
- handle=int(sys.argv[1]),
- url=fanartFile,
- listitem=li)
- except Exception as e:
- utils.logMsg("EMBY", "Error getting extrafanart: %s" % e, 0)
-
- # Always do endofdirectory to prevent errors in the logs
+# -*- coding: utf-8 -*-
+
+#################################################################################################
+
+import json
+import os
+import sys
+import urlparse
+
+import xbmc
+import xbmcaddon
+import xbmcgui
+import xbmcvfs
+import xbmcplugin
+
+import artwork
+import utils
+import clientinfo
+import downloadutils
+import librarysync
+import read_embyserver as embyserver
+import embydb_functions as embydb
+import playlist
+import playbackutils as pbutils
+import playutils
+import api
+
+
+#################################################################################################
+
+
+def doPlayback(itemid, dbid):
+
+ emby = embyserver.Read_EmbyServer()
+ item = emby.getItem(itemid)
+ pbutils.PlaybackUtils(item).play(itemid, dbid)
+
+##### DO RESET AUTH #####
+def resetAuth():
+ # User tried login and failed too many times
+ resp = xbmcgui.Dialog().yesno(
+ heading="Warning",
+ line1=(
+ "Emby might lock your account if you fail to log in too many times. "
+ "Proceed anyway?"))
+ if resp == 1:
+ utils.logMsg("EMBY", "Reset login attempts.", 1)
+ utils.window('emby_serverStatus', value="Auth")
+ else:
+ xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)')
+
+def addDirectoryItem(label, path, folder=True):
+ li = xbmcgui.ListItem(label, path=path)
+ li.setThumbnailImage("special://home/addons/plugin.video.emby/icon.png")
+ li.setArt({"fanart":"special://home/addons/plugin.video.emby/fanart.jpg"})
+ li.setArt({"landscape":"special://home/addons/plugin.video.emby/fanart.jpg"})
+ xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=folder)
+
+def doMainListing():
+ xbmcplugin.setContent(int(sys.argv[1]), 'files')
+ # Get emby nodes from the window props
+ embyprops = utils.window('Emby.nodes.total')
+ if embyprops:
+ totalnodes = int(embyprops)
+ for i in range(totalnodes):
+ path = utils.window('Emby.nodes.%s.index' % i)
+ if not path:
+ path = utils.window('Emby.nodes.%s.content' % i)
+ label = utils.window('Emby.nodes.%s.title' % i)
+ node_type = utils.window('Emby.nodes.%s.type' % i)
+ #because we do not use seperate entrypoints for each content type, we need to figure out which items to show in each listing.
+ #for now we just only show picture nodes in the picture library video nodes in the video library and all nodes in any other window
+ if path and xbmc.getCondVisibility("Window.IsActive(Pictures)") and node_type == "photos":
+ addDirectoryItem(label, path)
+ elif path and xbmc.getCondVisibility("Window.IsActive(VideoLibrary)") and node_type != "photos":
+ addDirectoryItem(label, path)
+ elif path and not xbmc.getCondVisibility("Window.IsActive(VideoLibrary) | Window.IsActive(Pictures) | Window.IsActive(MusicLibrary)"):
+ addDirectoryItem(label, path)
+
+ #experimental live tv nodes
+ addDirectoryItem("Live Tv Channels (experimental)", "plugin://plugin.video.emby/?mode=browsecontent&type=tvchannels&folderid=root")
+ addDirectoryItem("Live Tv Recordings (experimental)", "plugin://plugin.video.emby/?mode=browsecontent&type=recordings&folderid=root")
+
+ # some extra entries for settings and stuff. TODO --> localize the labels
+ addDirectoryItem("Network credentials", "plugin://plugin.video.emby/?mode=passwords")
+ addDirectoryItem("Settings", "plugin://plugin.video.emby/?mode=settings")
+ addDirectoryItem("Add user to session", "plugin://plugin.video.emby/?mode=adduser")
+ addDirectoryItem("Refresh Emby playlists/nodes", "plugin://plugin.video.emby/?mode=refreshplaylist")
+ addDirectoryItem("Perform manual sync", "plugin://plugin.video.emby/?mode=manualsync")
+ addDirectoryItem("Repair local database (force update all content)", "plugin://plugin.video.emby/?mode=repair")
+ addDirectoryItem("Perform local database reset (full resync)", "plugin://plugin.video.emby/?mode=reset")
+ addDirectoryItem("Cache all images to Kodi texture cache", "plugin://plugin.video.emby/?mode=texturecache")
+ addDirectoryItem("Sync Emby Theme Media to Kodi", "plugin://plugin.video.emby/?mode=thememedia")
+
+ xbmcplugin.endOfDirectory(int(sys.argv[1]))
+
+
+##### Generate a new deviceId
+def resetDeviceId():
+
+ dialog = xbmcgui.Dialog()
+ language = utils.language
+
+ deviceId_old = utils.window('emby_deviceId')
+ try:
+ utils.window('emby_deviceId', clear=True)
+ deviceId = clientinfo.ClientInfo().getDeviceId(reset=True)
+ except Exception as e:
+ utils.logMsg("EMBY", "Failed to generate a new device Id: %s" % e, 1)
+ dialog.ok(
+ heading="Emby for Kodi",
+ line1=language(33032))
+ else:
+ utils.logMsg("EMBY", "Successfully removed old deviceId: %s New deviceId: %s"
+ % (deviceId_old, deviceId), 1)
+ dialog.ok(
+ heading="Emby for Kodi",
+ line1=language(33033))
+ xbmc.executebuiltin('RestartApp')
+
+##### Delete Item
+def deleteItem():
+
+ # Serves as a keymap action
+ if xbmc.getInfoLabel('ListItem.Property(embyid)'): # If we already have the embyid
+ embyid = xbmc.getInfoLabel('ListItem.Property(embyid)')
+ else:
+ dbid = xbmc.getInfoLabel('ListItem.DBID')
+ itemtype = xbmc.getInfoLabel('ListItem.DBTYPE')
+
+ if not itemtype:
+
+ if xbmc.getCondVisibility('Container.Content(albums)'):
+ itemtype = "album"
+ elif xbmc.getCondVisibility('Container.Content(artists)'):
+ itemtype = "artist"
+ elif xbmc.getCondVisibility('Container.Content(songs)'):
+ itemtype = "song"
+ elif xbmc.getCondVisibility('Container.Content(pictures)'):
+ itemtype = "picture"
+ else:
+ utils.logMsg("EMBY delete", "Unknown type, unable to proceed.", 1)
+ return
+
+ embyconn = utils.kodiSQL('emby')
+ embycursor = embyconn.cursor()
+ emby_db = embydb.Embydb_Functions(embycursor)
+ item = emby_db.getItem_byKodiId(dbid, itemtype)
+ embycursor.close()
+
+ try:
+ embyid = item[0]
+ except TypeError:
+ utils.logMsg("EMBY delete", "Unknown embyId, unable to proceed.", 1)
+ return
+
+ if utils.settings('skipContextMenu') != "true":
+ resp = xbmcgui.Dialog().yesno(
+ heading="Confirm delete",
+ line1=("Delete file from Emby Server? This will "
+ "also delete the file(s) from disk!"))
+ if not resp:
+ utils.logMsg("EMBY delete", "User skipped deletion for: %s." % embyid, 1)
+ return
+
+ doUtils = downloadutils.DownloadUtils()
+ url = "{server}/emby/Items/%s?format=json" % embyid
+ utils.logMsg("EMBY delete", "Deleting request: %s" % embyid, 0)
+ doUtils.downloadUrl(url, action_type="DELETE")
+
+##### ADD ADDITIONAL USERS #####
+def addUser():
+
+ doUtils = downloadutils.DownloadUtils()
+ art = artwork.Artwork()
+ clientInfo = clientinfo.ClientInfo()
+ deviceId = clientInfo.getDeviceId()
+ deviceName = clientInfo.getDeviceName()
+ userid = utils.window('emby_currUser')
+ dialog = xbmcgui.Dialog()
+
+ # Get session
+ url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId
+ result = doUtils.downloadUrl(url)
+
+ try:
+ sessionId = result[0]['Id']
+ additionalUsers = result[0]['AdditionalUsers']
+ # Add user to session
+ userlist = {}
+ users = []
+ url = "{server}/emby/Users?IsDisabled=false&IsHidden=false&format=json"
+ result = doUtils.downloadUrl(url)
+
+ # pull the list of users
+ for user in result:
+ name = user['Name']
+ userId = user['Id']
+ if userid != userId:
+ userlist[name] = userId
+ users.append(name)
+
+ # Display dialog if there's additional users
+ if additionalUsers:
+
+ option = dialog.select("Add/Remove user from the session", ["Add user", "Remove user"])
+ # Users currently in the session
+ additionalUserlist = {}
+ additionalUsername = []
+ # Users currently in the session
+ for user in additionalUsers:
+ name = user['UserName']
+ userId = user['UserId']
+ additionalUserlist[name] = userId
+ additionalUsername.append(name)
+
+ if option == 1:
+ # User selected Remove user
+ resp = dialog.select("Remove user from the session", additionalUsername)
+ if resp > -1:
+ selected = additionalUsername[resp]
+ selected_userId = additionalUserlist[selected]
+ url = "{server}/emby/Sessions/%s/Users/%s" % (sessionId, selected_userId)
+ doUtils.downloadUrl(url, postBody={}, action_type="DELETE")
+ dialog.notification(
+ heading="Success!",
+ message="%s removed from viewing session" % selected,
+ icon="special://home/addons/plugin.video.emby/icon.png",
+ time=1000)
+
+ # clear picture
+ position = utils.window('EmbyAdditionalUserPosition.%s' % selected_userId)
+ utils.window('EmbyAdditionalUserImage.%s' % position, clear=True)
+ return
+ else:
+ return
+
+ elif option == 0:
+ # User selected Add user
+ for adduser in additionalUsername:
+ try: # Remove from selected already added users. It is possible they are hidden.
+ users.remove(adduser)
+ except: pass
+
+ elif option < 0:
+ # User cancelled
+ return
+
+ # Subtract any additional users
+ utils.logMsg("EMBY", "Displaying list of users: %s" % users)
+ resp = dialog.select("Add user to the session", users)
+ # post additional user
+ if resp > -1:
+ selected = users[resp]
+ selected_userId = userlist[selected]
+ url = "{server}/emby/Sessions/%s/Users/%s" % (sessionId, selected_userId)
+ doUtils.downloadUrl(url, postBody={}, action_type="POST")
+ dialog.notification(
+ heading="Success!",
+ message="%s added to viewing session" % selected,
+ icon="special://home/addons/plugin.video.emby/icon.png",
+ time=1000)
+
+ except:
+ utils.logMsg("EMBY", "Failed to add user to session.")
+ dialog.notification(
+ heading="Error",
+ message="Unable to add/remove user from the session.",
+ icon=xbmcgui.NOTIFICATION_ERROR)
+
+ # Add additional user images
+ # always clear the individual items first
+ totalNodes = 10
+ for i in range(totalNodes):
+ if not utils.window('EmbyAdditionalUserImage.%s' % i):
+ break
+ utils.window('EmbyAdditionalUserImage.%s' % i, clear=True)
+
+ url = "{server}/emby/Sessions?DeviceId=%s" % deviceId
+ result = doUtils.downloadUrl(url)
+ additionalUsers = result[0]['AdditionalUsers']
+ count = 0
+ for additionaluser in additionalUsers:
+ userid = additionaluser['UserId']
+ url = "{server}/emby/Users/%s?format=json" % userid
+ result = doUtils.downloadUrl(url)
+ utils.window('EmbyAdditionalUserImage.%s' % count,
+ value=art.getUserArtwork(result['Id'], 'Primary'))
+ utils.window('EmbyAdditionalUserPosition.%s' % userid, value=str(count))
+ count +=1
+
+##### THEME MUSIC/VIDEOS #####
+def getThemeMedia():
+
+ doUtils = downloadutils.DownloadUtils()
+ dialog = xbmcgui.Dialog()
+ playback = None
+
+ # Choose playback method
+ resp = dialog.select("Playback method for your themes", ["Direct Play", "Direct Stream"])
+ if resp == 0:
+ playback = "DirectPlay"
+ elif resp == 1:
+ playback = "DirectStream"
+ else:
+ return
+
+ library = xbmc.translatePath(
+ "special://profile/addon_data/plugin.video.emby/library/").decode('utf-8')
+ # Create library directory
+ if not xbmcvfs.exists(library):
+ xbmcvfs.mkdir(library)
+
+ # Set custom path for user
+ tvtunes_path = xbmc.translatePath(
+ "special://profile/addon_data/script.tvtunes/").decode('utf-8')
+ if xbmcvfs.exists(tvtunes_path):
+ tvtunes = xbmcaddon.Addon(id="script.tvtunes")
+ tvtunes.setSetting('custom_path_enable', "true")
+ tvtunes.setSetting('custom_path', library)
+ utils.logMsg("EMBY", "TV Tunes custom path is enabled and set.", 1)
+ else:
+ # if it does not exist this will not work so warn user
+ # often they need to edit the settings first for it to be created.
+ dialog.ok(
+ heading="Warning",
+ line1=(
+ "The settings file does not exist in tvtunes. ",
+ "Go to the tvtunes addon and change a setting, then come back and re-run."))
+ xbmc.executebuiltin('Addon.OpenSettings(script.tvtunes)')
+ return
+
+ # Get every user view Id
+ embyconn = utils.kodiSQL('emby')
+ embycursor = embyconn.cursor()
+ emby_db = embydb.Embydb_Functions(embycursor)
+ viewids = emby_db.getViews()
+ embycursor.close()
+
+ # Get Ids with Theme Videos
+ itemIds = {}
+ for view in viewids:
+ url = "{server}/emby/Users/{UserId}/Items?HasThemeVideo=True&ParentId=%s&format=json" % view
+ result = doUtils.downloadUrl(url)
+ if result['TotalRecordCount'] != 0:
+ for item in result['Items']:
+ itemId = item['Id']
+ folderName = item['Name']
+ folderName = utils.normalize_string(folderName.encode('utf-8'))
+ itemIds[itemId] = folderName
+
+ # Get paths for theme videos
+ for itemId in itemIds:
+ nfo_path = xbmc.translatePath(
+ "special://profile/addon_data/plugin.video.emby/library/%s/" % itemIds[itemId])
+ # Create folders for each content
+ if not xbmcvfs.exists(nfo_path):
+ xbmcvfs.mkdir(nfo_path)
+ # Where to put the nfos
+ nfo_path = "%s%s" % (nfo_path, "tvtunes.nfo")
+
+ url = "{server}/emby/Items/%s/ThemeVideos?format=json" % itemId
+ result = doUtils.downloadUrl(url)
+
+ # Create nfo and write themes to it
+ nfo_file = xbmcvfs.File(nfo_path, 'w')
+ pathstowrite = ""
+ # May be more than one theme
+ for theme in result['Items']:
+ putils = playutils.PlayUtils(theme)
+ if playback == "DirectPlay":
+ playurl = putils.directPlay()
+ else:
+ playurl = putils.directStream()
+ pathstowrite += ('%s' % playurl.encode('utf-8'))
+
+ # Check if the item has theme songs and add them
+ url = "{server}/emby/Items/%s/ThemeSongs?format=json" % itemId
+ result = doUtils.downloadUrl(url)
+
+ # May be more than one theme
+ for theme in result['Items']:
+ putils = playutils.PlayUtils(theme)
+ if playback == "DirectPlay":
+ playurl = putils.directPlay()
+ else:
+ playurl = putils.directStream()
+ pathstowrite += ('%s' % playurl.encode('utf-8'))
+
+ nfo_file.write(
+ '%s' % pathstowrite
+ )
+ # Close nfo file
+ nfo_file.close()
+
+ # Get Ids with Theme songs
+ musicitemIds = {}
+ for view in viewids:
+ url = "{server}/emby/Users/{UserId}/Items?HasThemeSong=True&ParentId=%s&format=json" % view
+ result = doUtils.downloadUrl(url)
+ if result['TotalRecordCount'] != 0:
+ for item in result['Items']:
+ itemId = item['Id']
+ folderName = item['Name']
+ folderName = utils.normalize_string(folderName.encode('utf-8'))
+ musicitemIds[itemId] = folderName
+
+ # Get paths
+ for itemId in musicitemIds:
+
+ # if the item was already processed with video themes back out
+ if itemId in itemIds:
+ continue
+
+ nfo_path = xbmc.translatePath(
+ "special://profile/addon_data/plugin.video.emby/library/%s/" % musicitemIds[itemId])
+ # Create folders for each content
+ if not xbmcvfs.exists(nfo_path):
+ xbmcvfs.mkdir(nfo_path)
+ # Where to put the nfos
+ nfo_path = "%s%s" % (nfo_path, "tvtunes.nfo")
+
+ url = "{server}/emby/Items/%s/ThemeSongs?format=json" % itemId
+ result = doUtils.downloadUrl(url)
+
+ # Create nfo and write themes to it
+ nfo_file = xbmcvfs.File(nfo_path, 'w')
+ pathstowrite = ""
+ # May be more than one theme
+ for theme in result['Items']:
+ putils = playutils.PlayUtils(theme)
+ if playback == "DirectPlay":
+ playurl = putils.directPlay()
+ else:
+ playurl = putils.directStream()
+ pathstowrite += ('%s' % playurl.encode('utf-8'))
+
+ nfo_file.write(
+ '%s' % pathstowrite
+ )
+ # Close nfo file
+ nfo_file.close()
+
+ dialog.notification(
+ heading="Emby for Kodi",
+ message="Themes added!",
+ icon="special://home/addons/plugin.video.emby/icon.png",
+ time=1000,
+ sound=False)
+
+##### REFRESH EMBY PLAYLISTS #####
+def refreshPlaylist():
+
+ lib = librarysync.LibrarySync()
+ dialog = xbmcgui.Dialog()
+ try:
+ # First remove playlists
+ utils.deletePlaylists()
+ # Remove video nodes
+ utils.deleteNodes()
+ # Refresh views
+ lib.refreshViews()
+ dialog.notification(
+ heading="Emby for Kodi",
+ message="Emby playlists/nodes refreshed",
+ icon="special://home/addons/plugin.video.emby/icon.png",
+ time=1000,
+ sound=False)
+
+ except Exception as e:
+ utils.logMsg("EMBY", "Refresh playlists/nodes failed: %s" % e, 1)
+ dialog.notification(
+ heading="Emby for Kodi",
+ message="Emby playlists/nodes refresh failed",
+ icon=xbmcgui.NOTIFICATION_ERROR,
+ time=1000,
+ sound=False)
+
+#### SHOW SUBFOLDERS FOR NODE #####
+def GetSubFolders(nodeindex):
+ nodetypes = ["",".recent",".recentepisodes",".inprogress",".inprogressepisodes",".unwatched",".nextepisodes",".sets",".genres",".random",".recommended"]
+ for node in nodetypes:
+ title = utils.window('Emby.nodes.%s%s.title' %(nodeindex,node))
+ if title:
+ path = utils.window('Emby.nodes.%s%s.content' %(nodeindex,node))
+ addDirectoryItem(title, path)
+ xbmcplugin.endOfDirectory(int(sys.argv[1]))
+
+##### BROWSE EMBY NODES DIRECTLY #####
+def BrowseContent(viewname, browse_type="", folderid=""):
+
+ emby = embyserver.Read_EmbyServer()
+ art = artwork.Artwork()
+ doUtils = downloadutils.DownloadUtils()
+
+ #folderid used as filter ?
+ if folderid in ["recent","recentepisodes","inprogress","inprogressepisodes","unwatched","nextepisodes","sets","genres","random","recommended"]:
+ filter_type = folderid
+ folderid = ""
+ else:
+ filter_type = ""
+
+ xbmcplugin.setPluginCategory(int(sys.argv[1]), viewname)
+ #get views for root level
+ if not folderid:
+ views = emby.getViews(browse_type)
+ for view in views:
+ if view.get("name") == viewname.decode('utf-8'):
+ folderid = view.get("id")
+ break
+
+ if viewname is not None:
+ utils.logMsg("BrowseContent","viewname: %s - type: %s - folderid: %s - filter: %s" %(viewname.decode('utf-8'), browse_type.decode('utf-8'), folderid.decode('utf-8'), filter_type.decode('utf-8')))
+ #set the correct params for the content type
+ #only proceed if we have a folderid
+ if folderid:
+ if browse_type.lower() == "homevideos":
+ xbmcplugin.setContent(int(sys.argv[1]), 'episodes')
+ itemtype = "Video,Folder,PhotoAlbum"
+ elif browse_type.lower() == "photos":
+ xbmcplugin.setContent(int(sys.argv[1]), 'files')
+ itemtype = "Photo,PhotoAlbum,Folder"
+ else:
+ itemtype = ""
+
+ #get the actual listing
+ if browse_type == "recordings":
+ listing = emby.getTvRecordings(folderid)
+ elif browse_type == "tvchannels":
+ listing = emby.getTvChannels()
+ elif filter_type == "recent":
+ listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="DateCreated", recursive=True, limit=25, sortorder="Descending")
+ elif filter_type == "random":
+ listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="Random", recursive=True, limit=150, sortorder="Descending")
+ elif filter_type == "recommended":
+ listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter_type="IsFavorite")
+ elif filter_type == "sets":
+ listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[1], sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter_type="IsFavorite")
+ else:
+ listing = emby.getFilteredSection(folderid, itemtype=itemtype, recursive=False)
+
+ #process the listing
+ if listing:
+ for item in listing.get("Items"):
+ li = createListItemFromEmbyItem(item,art,doUtils)
+ if item.get("IsFolder") == True:
+ #for folders we add an additional browse request, passing the folderId
+ path = "%s?id=%s&mode=browsecontent&type=%s&folderid=%s" % (sys.argv[0].decode('utf-8'), viewname.decode('utf-8'), browse_type.decode('utf-8'), item.get("Id").decode('utf-8'))
+ xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=True)
+ else:
+ #playable item, set plugin path and mediastreams
+ xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=li.getProperty("path"), listitem=li)
+
+
+ if filter_type == "recent":
+ xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_DATE)
+ else:
+ xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_TITLE)
+ xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_DATE)
+ xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_RATING)
+ xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_RUNTIME)
+
+ xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
+
+##### CREATE LISTITEM FROM EMBY METADATA #####
+def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils.DownloadUtils()):
+ API = api.API(item)
+ itemid = item['Id']
+
+ title = item.get('Name')
+ li = xbmcgui.ListItem(title)
+
+ premieredate = item.get('PremiereDate',"")
+ if not premieredate: premieredate = item.get('DateCreated',"")
+ if premieredate:
+ premieredatelst = premieredate.split('T')[0].split("-")
+ premieredate = "%s.%s.%s" %(premieredatelst[2],premieredatelst[1],premieredatelst[0])
+
+ li.setProperty("embyid",itemid)
+
+ allart = art.getAllArtwork(item)
+
+ if item["Type"] == "Photo":
+ #listitem setup for pictures...
+ img_path = allart.get('Primary')
+ li.setProperty("path",img_path)
+ picture = doUtils.downloadUrl("{server}/Items/%s/Images" %itemid)
+ if picture:
+ picture = picture[0]
+ if picture.get("Width") > picture.get("Height"):
+ li.setArt( {"fanart": img_path}) #add image as fanart for use with skinhelper auto thumb/backgrund creation
+ li.setInfo('pictures', infoLabels={ "picturepath": img_path, "date": premieredate, "size": picture.get("Size"), "exif:width": str(picture.get("Width")), "exif:height": str(picture.get("Height")), "title": title})
+ li.setThumbnailImage(img_path)
+ li.setProperty("plot",API.getOverview())
+ li.setIconImage('DefaultPicture.png')
+ else:
+ #normal video items
+ li.setProperty('IsPlayable', 'true')
+ path = "%s?id=%s&mode=play" % (sys.argv[0], item.get("Id"))
+ li.setProperty("path",path)
+ genre = API.getGenres()
+ overlay = 0
+ userdata = API.getUserData()
+ runtime = item.get("RunTimeTicks",0)/ 10000000.0
+ seektime = userdata['Resume']
+ if seektime:
+ li.setProperty("resumetime", str(seektime))
+ li.setProperty("totaltime", str(runtime))
+
+ played = userdata['Played']
+ if played: overlay = 7
+ else: overlay = 6
+ playcount = userdata['PlayCount']
+ if playcount is None:
+ playcount = 0
+
+ rating = item.get('CommunityRating')
+ if not rating: rating = userdata['UserRating']
+
+ # Populate the extradata list and artwork
+ extradata = {
+ 'id': itemid,
+ 'rating': rating,
+ 'year': item.get('ProductionYear'),
+ 'genre': genre,
+ 'playcount': str(playcount),
+ 'title': title,
+ 'plot': API.getOverview(),
+ 'Overlay': str(overlay),
+ 'duration': runtime
+ }
+ if premieredate:
+ extradata["premieredate"] = premieredate
+ extradata["date"] = premieredate
+ li.setInfo('video', infoLabels=extradata)
+ if allart.get('Primary'):
+ li.setThumbnailImage(allart.get('Primary'))
+ else: li.setThumbnailImage('DefaultTVShows.png')
+ li.setIconImage('DefaultTVShows.png')
+ if not allart.get('Background'): #add image as fanart for use with skinhelper auto thumb/backgrund creation
+ li.setArt( {"fanart": allart.get('Primary') } )
+ else:
+ pbutils.PlaybackUtils(item).setArtwork(li)
+
+ mediastreams = API.getMediaStreams()
+ videostreamFound = False
+ if mediastreams:
+ for key, value in mediastreams.iteritems():
+ if key == "video" and value: videostreamFound = True
+ if value: li.addStreamInfo(key, value[0])
+ if not videostreamFound:
+ #just set empty streamdetails to prevent errors in the logs
+ li.addStreamInfo("video", {'duration': runtime})
+
+ return li
+
+##### BROWSE EMBY CHANNELS #####
+def BrowseChannels(itemid, folderid=None):
+
+ _addon_id = int(sys.argv[1])
+ _addon_url = sys.argv[0]
+ doUtils = downloadutils.DownloadUtils()
+ art = artwork.Artwork()
+
+ xbmcplugin.setContent(int(sys.argv[1]), 'files')
+ if folderid:
+ url = (
+ "{server}/emby/Channels/%s/Items?userid={UserId}&folderid=%s&format=json"
+ % (itemid, folderid))
+ elif itemid == "0":
+ # id 0 is the root channels folder
+ url = "{server}/emby/Channels?{UserId}&format=json"
+ else:
+ url = "{server}/emby/Channels/%s/Items?UserId={UserId}&format=json" % itemid
+
+ result = doUtils.downloadUrl(url)
+ if result and result.get("Items"):
+ for item in result.get("Items"):
+ itemid = item['Id']
+ itemtype = item['Type']
+ li = createListItemFromEmbyItem(item,art,doUtils)
+
+ isFolder = item.get('IsFolder', False)
+
+ channelId = item.get('ChannelId', "")
+ channelName = item.get('ChannelName', "")
+ if itemtype == "Channel":
+ path = "%s?id=%s&mode=channels" % (_addon_url, itemid)
+ xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li, isFolder=True)
+ elif isFolder:
+ path = "%s?id=%s&mode=channelsfolder&folderid=%s" % (_addon_url, channelId, itemid)
+ xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li, isFolder=True)
+ else:
+ path = "%s?id=%s&mode=play" % (_addon_url, itemid)
+ li.setProperty('IsPlayable', 'true')
+ xbmcplugin.addDirectoryItem(handle=_addon_id, url=path, listitem=li)
+
+ xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
+
+##### LISTITEM SETUP FOR VIDEONODES #####
+def createListItem(item):
+
+ title = item['title']
+ li = xbmcgui.ListItem(title)
+ li.setProperty('IsPlayable', "true")
+
+ metadata = {
+
+ 'Title': title,
+ 'duration': str(item['runtime']/60),
+ 'Plot': item['plot'],
+ 'Playcount': item['playcount']
+ }
+
+ if "episode" in item:
+ episode = item['episode']
+ metadata['Episode'] = episode
+
+ if "season" in item:
+ season = item['season']
+ metadata['Season'] = season
+
+ if season and episode:
+ li.setProperty('episodeno', "s%.2de%.2d" % (season, episode))
+
+ if "firstaired" in item:
+ metadata['Premiered'] = item['firstaired']
+
+ if "showtitle" in item:
+ metadata['TVshowTitle'] = item['showtitle']
+
+ if "rating" in item:
+ metadata['Rating'] = str(round(float(item['rating']),1))
+
+ if "director" in item:
+ metadata['Director'] = " / ".join(item['director'])
+
+ if "writer" in item:
+ metadata['Writer'] = " / ".join(item['writer'])
+
+ if "cast" in item:
+ cast = []
+ castandrole = []
+ for person in item['cast']:
+ name = person['name']
+ cast.append(name)
+ castandrole.append((name, person['role']))
+ metadata['Cast'] = cast
+ metadata['CastAndRole'] = castandrole
+
+ li.setInfo(type="Video", infoLabels=metadata)
+ li.setProperty('resumetime', str(item['resume']['position']))
+ li.setProperty('totaltime', str(item['resume']['total']))
+ li.setArt(item['art'])
+ li.setThumbnailImage(item['art'].get('thumb',''))
+ li.setIconImage('DefaultTVShows.png')
+ li.setProperty('dbid', str(item['episodeid']))
+ li.setProperty('fanart_image', item['art'].get('tvshow.fanart',''))
+ for key, value in item['streamdetails'].iteritems():
+ for stream in value:
+ li.addStreamInfo(key, stream)
+
+ return li
+
+##### GET NEXTUP EPISODES FOR TAGNAME #####
+def getNextUpEpisodes(tagname, limit):
+
+ count = 0
+ # if the addon is called with nextup parameter,
+ # we return the nextepisodes list of the given tagname
+ xbmcplugin.setContent(int(sys.argv[1]), 'episodes')
+ # First we get a list of all the TV shows - filtered by tag
+ query = {
+
+ 'jsonrpc': "2.0",
+ 'id': "libTvShows",
+ 'method': "VideoLibrary.GetTVShows",
+ 'params': {
+
+ 'sort': {'order': "descending", 'method': "lastplayed"},
+ 'filter': {
+ 'and': [
+ {'operator': "true", 'field': "inprogress", 'value': ""},
+ {'operator': "is", 'field': "tag", 'value': "%s" % tagname}
+ ]},
+ 'properties': ['title', 'studio', 'mpaa', 'file', 'art']
+ }
+ }
+ result = xbmc.executeJSONRPC(json.dumps(query))
+ result = json.loads(result)
+ # If we found any, find the oldest unwatched show for each one.
+ try:
+ items = result['result']['tvshows']
+ except (KeyError, TypeError):
+ pass
+ else:
+ for item in items:
+ if utils.settings('ignoreSpecialsNextEpisodes') == "true":
+ query = {
+
+ 'jsonrpc': "2.0",
+ 'id': 1,
+ 'method': "VideoLibrary.GetEpisodes",
+ 'params': {
+
+ 'tvshowid': item['tvshowid'],
+ 'sort': {'method': "episode"},
+ 'filter': {
+ 'and': [
+ {'operator': "lessthan", 'field': "playcount", 'value': "1"},
+ {'operator': "greaterthan", 'field': "season", 'value': "0"}
+ ]},
+ 'properties': [
+ "title", "playcount", "season", "episode", "showtitle",
+ "plot", "file", "rating", "resume", "tvshowid", "art",
+ "streamdetails", "firstaired", "runtime", "writer",
+ "dateadded", "lastplayed"
+ ],
+ 'limits': {"end": 1}
+ }
+ }
+ else:
+ query = {
+
+ 'jsonrpc': "2.0",
+ 'id': 1,
+ 'method': "VideoLibrary.GetEpisodes",
+ 'params': {
+
+ 'tvshowid': item['tvshowid'],
+ 'sort': {'method': "episode"},
+ 'filter': {'operator': "lessthan", 'field': "playcount", 'value': "1"},
+ 'properties': [
+ "title", "playcount", "season", "episode", "showtitle",
+ "plot", "file", "rating", "resume", "tvshowid", "art",
+ "streamdetails", "firstaired", "runtime", "writer",
+ "dateadded", "lastplayed"
+ ],
+ 'limits': {"end": 1}
+ }
+ }
+
+ result = xbmc.executeJSONRPC(json.dumps(query))
+ result = json.loads(result)
+ try:
+ episodes = result['result']['episodes']
+ except (KeyError, TypeError):
+ pass
+ else:
+ for episode in episodes:
+ li = createListItem(episode)
+ xbmcplugin.addDirectoryItem(
+ handle=int(sys.argv[1]),
+ url=episode['file'],
+ listitem=li)
+ count += 1
+
+ if count == limit:
+ break
+
+ xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
+
+##### GET INPROGRESS EPISODES FOR TAGNAME #####
+def getInProgressEpisodes(tagname, limit):
+
+ count = 0
+ # if the addon is called with inprogressepisodes parameter,
+ # we return the inprogressepisodes list of the given tagname
+ xbmcplugin.setContent(int(sys.argv[1]), 'episodes')
+ # First we get a list of all the in-progress TV shows - filtered by tag
+ query = {
+
+ 'jsonrpc': "2.0",
+ 'id': "libTvShows",
+ 'method': "VideoLibrary.GetTVShows",
+ 'params': {
+
+ 'sort': {'order': "descending", 'method': "lastplayed"},
+ 'filter': {
+ 'and': [
+ {'operator': "true", 'field': "inprogress", 'value': ""},
+ {'operator': "is", 'field': "tag", 'value': "%s" % tagname}
+ ]},
+ 'properties': ['title', 'studio', 'mpaa', 'file', 'art']
+ }
+ }
+ result = xbmc.executeJSONRPC(json.dumps(query))
+ result = json.loads(result)
+ # If we found any, find the oldest unwatched show for each one.
+ try:
+ items = result['result']['tvshows']
+ except (KeyError, TypeError):
+ pass
+ else:
+ for item in items:
+ query = {
+
+ 'jsonrpc': "2.0",
+ 'id': 1,
+ 'method': "VideoLibrary.GetEpisodes",
+ 'params': {
+
+ 'tvshowid': item['tvshowid'],
+ 'sort': {'method': "episode"},
+ 'filter': {'operator': "true", 'field': "inprogress", 'value': ""},
+ 'properties': [
+ "title", "playcount", "season", "episode", "showtitle", "plot",
+ "file", "rating", "resume", "tvshowid", "art", "cast",
+ "streamdetails", "firstaired", "runtime", "writer",
+ "dateadded", "lastplayed"
+ ]
+ }
+ }
+ result = xbmc.executeJSONRPC(json.dumps(query))
+ result = json.loads(result)
+ try:
+ episodes = result['result']['episodes']
+ except (KeyError, TypeError):
+ pass
+ else:
+ for episode in episodes:
+ li = createListItem(episode)
+ xbmcplugin.addDirectoryItem(
+ handle=int(sys.argv[1]),
+ url=episode['file'],
+ listitem=li)
+ count += 1
+
+ if count == limit:
+ break
+
+ xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
+
+##### GET RECENT EPISODES FOR TAGNAME #####
+def getRecentEpisodes(tagname, limit):
+
+ count = 0
+ # if the addon is called with recentepisodes parameter,
+ # we return the recentepisodes list of the given tagname
+ xbmcplugin.setContent(int(sys.argv[1]), 'episodes')
+ # First we get a list of all the TV shows - filtered by tag
+ query = {
+
+ 'jsonrpc': "2.0",
+ 'id': "libTvShows",
+ 'method': "VideoLibrary.GetTVShows",
+ 'params': {
+
+ 'sort': {'order': "descending", 'method': "dateadded"},
+ 'filter': {'operator': "is", 'field': "tag", 'value': "%s" % tagname},
+ 'properties': ["title","sorttitle"]
+ }
+ }
+ result = xbmc.executeJSONRPC(json.dumps(query))
+ result = json.loads(result)
+ # If we found any, find the oldest unwatched show for each one.
+ try:
+ items = result['result']['tvshows']
+ except (KeyError, TypeError):
+ pass
+ else:
+ allshowsIds = set()
+ for item in items:
+ allshowsIds.add(item['tvshowid'])
+
+ query = {
+
+ 'jsonrpc': "2.0",
+ 'id': 1,
+ 'method': "VideoLibrary.GetEpisodes",
+ 'params': {
+
+ 'sort': {'order': "descending", 'method': "dateadded"},
+ 'filter': {'operator': "lessthan", 'field': "playcount", 'value': "1"},
+ 'properties': [
+ "title", "playcount", "season", "episode", "showtitle", "plot",
+ "file", "rating", "resume", "tvshowid", "art", "streamdetails",
+ "firstaired", "runtime", "cast", "writer", "dateadded", "lastplayed"
+ ],
+ "limits": {"end": limit}
+ }
+ }
+ result = xbmc.executeJSONRPC(json.dumps(query))
+ result = json.loads(result)
+ try:
+ episodes = result['result']['episodes']
+ except (KeyError, TypeError):
+ pass
+ else:
+ for episode in episodes:
+ if episode['tvshowid'] in allshowsIds:
+ li = createListItem(episode)
+ xbmcplugin.addDirectoryItem(
+ handle=int(sys.argv[1]),
+ url=episode['file'],
+ listitem=li)
+ count += 1
+
+ if count == limit:
+ break
+
+ xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
+
+##### GET VIDEO EXTRAS FOR LISTITEM #####
+def getVideoFiles(embyId,embyPath):
+ #returns the video files for the item as plugin listing, can be used for browsing the actual files or videoextras etc.
+ emby = embyserver.Read_EmbyServer()
+ if not embyId:
+ if "plugin.video.emby" in embyPath:
+ embyId = embyPath.split("/")[-2]
+ if embyId:
+ item = emby.getItem(embyId)
+ putils = playutils.PlayUtils(item)
+ if putils.isDirectPlay():
+ #only proceed if we can access the files directly. TODO: copy local on the fly if accessed outside
+ filelocation = putils.directPlay()
+ if not filelocation.endswith("/"):
+ filelocation = filelocation.rpartition("/")[0]
+ dirs, files = xbmcvfs.listdir(filelocation)
+ for file in files:
+ file = filelocation + file
+ li = xbmcgui.ListItem(file, path=file)
+ xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=file, listitem=li)
+ for dir in dirs:
+ dir = filelocation + dir
+ li = xbmcgui.ListItem(dir, path=dir)
+ xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=dir, listitem=li, isFolder=True)
+ xbmcplugin.endOfDirectory(int(sys.argv[1]))
+
+##### GET EXTRAFANART FOR LISTITEM #####
+def getExtraFanArt(embyId,embyPath):
+
+ emby = embyserver.Read_EmbyServer()
+ art = artwork.Artwork()
+
+ # Get extrafanart for listitem
+ # will be called by skinhelper script to get the extrafanart
+ try:
+ # for tvshows we get the embyid just from the path
+ if not embyId:
+ if "plugin.video.emby" in embyPath:
+ embyId = embyPath.split("/")[-2]
+
+ if embyId:
+ #only proceed if we actually have a emby id
+ utils.logMsg("EMBY", "Requesting extrafanart for Id: %s" % embyId, 0)
+
+ # We need to store the images locally for this to work
+ # because of the caching system in xbmc
+ fanartDir = xbmc.translatePath("special://thumbnails/emby/%s/" % embyId).decode('utf-8')
+
+ if not xbmcvfs.exists(fanartDir):
+ # Download the images to the cache directory
+ xbmcvfs.mkdirs(fanartDir)
+ item = emby.getItem(embyId)
+ if item:
+ backdrops = art.getAllArtwork(item)['Backdrop']
+ tags = item['BackdropImageTags']
+ count = 0
+ for backdrop in backdrops:
+ # Same ordering as in artwork
+ tag = tags[count]
+ if os.path.supports_unicode_filenames:
+ fanartFile = os.path.join(fanartDir, "fanart%s.jpg" % tag)
+ else:
+ fanartFile = os.path.join(fanartDir.encode("utf-8"), "fanart%s.jpg" % tag.encode("utf-8"))
+ li = xbmcgui.ListItem(tag, path=fanartFile)
+ xbmcplugin.addDirectoryItem(
+ handle=int(sys.argv[1]),
+ url=fanartFile,
+ listitem=li)
+ xbmcvfs.copy(backdrop, fanartFile)
+ count += 1
+ else:
+ utils.logMsg("EMBY", "Found cached backdrop.", 2)
+ # Use existing cached images
+ dirs, files = xbmcvfs.listdir(fanartDir)
+ for file in files:
+ fanartFile = os.path.join(fanartDir, file.decode('utf-8'))
+ li = xbmcgui.ListItem(file, path=fanartFile)
+ xbmcplugin.addDirectoryItem(
+ handle=int(sys.argv[1]),
+ url=fanartFile,
+ listitem=li)
+ except Exception as e:
+ utils.logMsg("EMBY", "Error getting extrafanart: %s" % e, 0)
+
+ # Always do endofdirectory to prevent errors in the logs
xbmcplugin.endOfDirectory(int(sys.argv[1]))
\ No newline at end of file
diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py
index e23c9001..7bf0dbb9 100644
--- a/resources/lib/initialsetup.py
+++ b/resources/lib/initialsetup.py
@@ -176,8 +176,8 @@ class InitialSetup():
sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1)
- self.logMsg("MultiGroup : %s" % str(MULTI_GROUP), 2);
- self.logMsg("Sending UDP Data: %s" % MESSAGE, 2);
+ self.logMsg("MultiGroup : %s" % str(MULTI_GROUP), 2)
+ self.logMsg("Sending UDP Data: %s" % MESSAGE, 2)
sock.sendto(MESSAGE, MULTI_GROUP)
try:
diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py
index f3f03ca6..18c90517 100644
--- a/resources/lib/itemtypes.py
+++ b/resources/lib/itemtypes.py
@@ -1,2470 +1,2433 @@
-# -*- coding: utf-8 -*-
-
-##################################################################################################
-
-import urllib
-from ntpath import dirname
-from datetime import datetime
-
-import xbmc
-import xbmcgui
-import xbmcvfs
-
-import api
-import artwork
-import clientinfo
-import downloadutils
-import utils
-import embydb_functions as embydb
-import kodidb_functions as kodidb
-import read_embyserver as embyserver
-import musicutils
-
-##################################################################################################
-
-
-class Items(object):
-
-
- def __init__(self, embycursor, kodicursor):
-
- self.embycursor = embycursor
- self.kodicursor = kodicursor
-
- self.clientInfo = clientinfo.ClientInfo()
- self.addonName = self.clientInfo.getAddonName()
- self.doUtils = downloadutils.DownloadUtils()
-
- self.kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
- self.directpath = utils.settings('useDirectPaths') == "1"
- self.music_enabled = utils.settings('enableMusic') == "true"
- self.contentmsg = utils.settings('newContent') == "true"
- self.newvideo_time = int(utils.settings('newvideotime'))*1000
- self.newmusic_time = int(utils.settings('newmusictime'))*1000
-
- self.artwork = artwork.Artwork()
- self.emby = embyserver.Read_EmbyServer()
- self.emby_db = embydb.Embydb_Functions(embycursor)
- self.kodi_db = kodidb.Kodidb_Functions(kodicursor)
-
- def logMsg(self, msg, lvl=1):
-
- className = self.__class__.__name__
- utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
-
-
- def itemsbyId(self, items, process, pdialog=None):
- # Process items by itemid. Process can be added, update, userdata, remove
- emby = self.emby
- embycursor = self.embycursor
- kodicursor = self.kodicursor
- music_enabled = self.music_enabled
-
- itemtypes = {
-
- 'Movie': Movies,
- 'BoxSet': Movies,
- 'Series': TVShows,
- 'Season': TVShows,
- 'Episode': TVShows,
- 'MusicAlbum': Music,
- 'MusicArtist': Music,
- 'AlbumArtist': Music,
- 'Audio': Music
- }
-
- update_videolibrary = False
- total = 0
- for item in items:
- total += len(items[item])
-
- if total == 0:
- return False
-
- self.logMsg("Processing %s: %s" % (process, items), 1)
- if pdialog:
- pdialog.update(heading="Processing %s: %s items" % (process, total))
-
- count = 0
- for itemtype in items:
-
- # Safety check
- if not itemtypes.get(itemtype):
- # We don't process this type of item
- continue
-
- itemlist = items[itemtype]
- if not itemlist:
- # The list to process is empty
- continue
-
- musicconn = None
-
- if itemtype in ('MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio'):
- if music_enabled:
- musicconn = utils.kodiSQL('music')
- musiccursor = musicconn.cursor()
- items_process = itemtypes[itemtype](embycursor, musiccursor)
- else:
- # Music is not enabled, do not proceed with itemtype
- continue
- else:
- update_videolibrary = True
- items_process = itemtypes[itemtype](embycursor, kodicursor)
-
- if itemtype == "Movie":
- actions = {
- 'added': items_process.added,
- 'update': items_process.add_update,
- 'userdata': items_process.updateUserdata,
- 'remove': items_process.remove
- }
- elif itemtype == "BoxSet":
- actions = {
- 'added': items_process.added_boxset,
- 'update': items_process.add_updateBoxset,
- 'remove': items_process.remove
- }
- elif itemtype == "MusicVideo":
- actions = {
- 'added': items_process.added,
- 'update': items_process.add_update,
- 'userdata': items_process.updateUserdata,
- 'remove': items_process.remove
- }
- elif itemtype == "Series":
- actions = {
- 'added': items_process.added,
- 'update': items_process.add_update,
- 'userdata': items_process.updateUserdata,
- 'remove': items_process.remove
- }
- elif itemtype == "Season":
- actions = {
- 'added': items_process.added_season,
- 'update': items_process.add_updateSeason,
- 'remove': items_process.remove
- }
- elif itemtype == "Episode":
- actions = {
- 'added': items_process.added_episode,
- 'update': items_process.add_updateEpisode,
- 'userdata': items_process.updateUserdata,
- 'remove': items_process.remove
- }
- elif itemtype == "MusicAlbum":
- actions = {
- 'added': items_process.added_album,
- 'update': items_process.add_updateAlbum,
- 'userdata': items_process.updateUserdata,
- 'remove': items_process.remove
- }
- elif itemtype in ("MusicArtist", "AlbumArtist"):
- actions = {
- 'added': items_process.added,
- 'update': items_process.add_updateArtist,
- 'remove': items_process.remove
- }
- elif itemtype == "Audio":
- actions = {
- 'added': items_process.added_song,
- 'update': items_process.add_updateSong,
- 'userdata': items_process.updateUserdata,
- 'remove': items_process.remove
- }
- else:
- self.logMsg("Unsupported itemtype: %s." % itemtype, 1)
- actions = {}
-
- if actions.get(process):
-
- if process == "remove":
- for item in itemlist:
- actions[process](item)
-
- elif process == "added":
- actions[process](itemlist, pdialog)
-
- else:
- processItems = emby.getFullItems(itemlist)
- for item in processItems:
-
- title = item['Name']
-
- if itemtype == "Episode":
- title = "%s - %s" % (item['SeriesName'], title)
-
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
-
- actions[process](item)
-
-
- if musicconn is not None:
- # close connection for special types
- self.logMsg("Updating music database.", 1)
- musicconn.commit()
- musiccursor.close()
-
- return (True, update_videolibrary)
-
- def contentPop(self, name, time=5000):
-
- if time:
- # It's possible for the time to be 0. It should be considered disabled in this case.
- xbmcgui.Dialog().notification(
- heading="Emby for Kodi",
- message="Added: %s" % name,
- icon="special://home/addons/plugin.video.emby/icon.png",
- time=time,
- sound=False)
-
-
-class Movies(Items):
-
-
- def __init__(self, embycursor, kodicursor):
- Items.__init__(self, embycursor, kodicursor)
-
- def added(self, items, pdialog):
-
- total = len(items)
- count = 0
- for movie in items:
-
- title = movie['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
- self.add_update(movie)
- if not pdialog and self.contentmsg:
- self.contentPop(title, self.newvideo_time)
-
- def added_boxset(self, items, pdialog):
-
- total = len(items)
- count = 0
- for boxset in items:
-
- title = boxset['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
- self.add_updateBoxset(boxset)
-
-
- def add_update(self, item, viewtag=None, viewid=None):
- # Process single movie
- kodicursor = self.kodicursor
- emby_db = self.emby_db
- kodi_db = self.kodi_db
- artwork = self.artwork
- API = api.API(item)
-
- # If the item already exist in the local Kodi DB we'll perform a full item update
- # If the item doesn't exist, we'll add it to the database
- update_item = True
- itemid = item['Id']
- emby_dbitem = emby_db.getItem_byId(itemid)
- try:
- movieid = emby_dbitem[0]
- fileid = emby_dbitem[1]
- pathid = emby_dbitem[2]
- self.logMsg("movieid: %s fileid: %s pathid: %s" % (movieid, fileid, pathid), 1)
-
- except TypeError:
- update_item = False
- self.logMsg("movieid: %s not found." % itemid, 2)
- # movieid
- kodicursor.execute("select coalesce(max(idMovie),0) from movie")
- movieid = kodicursor.fetchone()[0] + 1
-
- else:
- # Verification the item is still in Kodi
- query = "SELECT * FROM movie WHERE idMovie = ?"
- kodicursor.execute(query, (movieid,))
- try:
- kodicursor.fetchone()[0]
- except TypeError:
- # item is not found, let's recreate it.
- update_item = False
- self.logMsg("movieid: %s missing from Kodi, repairing the entry." % movieid, 1)
-
- if not viewtag or not viewid:
- # Get view tag from emby
- viewtag, viewid, mediatype = self.emby.getView_embyId(itemid)
- self.logMsg("View tag found: %s" % viewtag, 2)
-
- # fileId information
- checksum = API.getChecksum()
- dateadded = API.getDateCreated()
- userdata = API.getUserData()
- playcount = userdata['PlayCount']
- dateplayed = userdata['LastPlayedDate']
-
- # item details
- people = API.getPeople()
- writer = " / ".join(people['Writer'])
- director = " / ".join(people['Director'])
- genres = item['Genres']
- title = item['Name']
- plot = API.getOverview()
- shortplot = item.get('ShortOverview')
- tagline = API.getTagline()
- votecount = item.get('VoteCount')
- rating = item.get('CommunityRating')
- year = item.get('ProductionYear')
- imdb = API.getProvider('Imdb')
- sorttitle = item['SortName']
- runtime = API.getRuntime()
- mpaa = API.getMpaa()
- genre = " / ".join(genres)
- country = API.getCountry()
- studios = API.getStudios()
- try:
- studio = studios[0]
- except IndexError:
- studio = None
-
- if item.get('LocalTrailerCount'):
- # There's a local trailer
- url = (
- "{server}/emby/Users/{UserId}/Items/%s/LocalTrailers?format=json"
- % itemid
- )
- result = self.doUtils.downloadUrl(url)
- try:
- trailer = "plugin://plugin.video.emby/trailer/?id=%s&mode=play" % result[0]['Id']
- except IndexError:
- self.logMsg("Failed to process local trailer.", 1)
- trailer = None
- else:
- # Try to get the youtube trailer
- try:
- trailer = item['RemoteTrailers'][0]['Url']
- except (KeyError, IndexError):
- trailer = None
- else:
- try:
- trailerId = trailer.rsplit('=', 1)[1]
- except IndexError:
- self.logMsg("Failed to process trailer: %s" % trailer, 1)
- trailer = None
- else:
- trailer = "plugin://plugin.video.youtube/play/?video_id=%s" % trailerId
-
-
- ##### GET THE FILE AND PATH #####
- playurl = API.getFilePath()
-
- if "\\" in playurl:
- # Local path
- filename = playurl.rsplit("\\", 1)[1]
- else: # Network share
- filename = playurl.rsplit("/", 1)[1]
-
- if self.directpath:
- # Direct paths is set the Kodi way
- if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(playurl):
- # Validate the path is correct with user intervention
- resp = xbmcgui.Dialog().yesno(
- heading="Can't validate path",
- line1=(
- "Kodi can't locate file: %s. Verify the path. "
- "You may to verify your network credentials in the "
- "add-on settings or use the emby path substitution "
- "to format your path correctly. Stop syncing?"
- % playurl))
- if resp:
- utils.window('emby_shouldStop', value="true")
- return False
-
- path = playurl.replace(filename, "")
- utils.window('emby_pathverified', value="true")
- else:
- # Set plugin path and media flags using real filename
- path = "plugin://plugin.video.emby.movies/"
- params = {
-
- 'filename': filename.encode('utf-8'),
- 'id': itemid,
- 'dbid': movieid,
- 'mode': "play"
- }
- filename = "%s?%s" % (path, urllib.urlencode(params))
-
-
- ##### UPDATE THE MOVIE #####
- if update_item:
- self.logMsg("UPDATE movie itemid: %s - Title: %s" % (itemid, title), 1)
-
- # Update the movie entry
- query = ' '.join((
-
- "UPDATE movie",
- "SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?,",
- "c07 = ?, c09 = ?, c10 = ?, c11 = ?, c12 = ?, c14 = ?, c15 = ?,",
- "c16 = ?, c18 = ?, c19 = ?, c21 = ?",
- "WHERE idMovie = ?"
- ))
- kodicursor.execute(query, (title, plot, shortplot, tagline, votecount, rating, writer,
- year, imdb, sorttitle, runtime, mpaa, genre, director, title, studio, trailer,
- country, movieid))
-
- # Update the checksum in emby table
- emby_db.updateReference(itemid, checksum)
-
- ##### OR ADD THE MOVIE #####
- else:
- self.logMsg("ADD movie itemid: %s - Title: %s" % (itemid, title), 1)
-
- # Add path
- pathid = kodi_db.addPath(path)
- # Add the file
- fileid = kodi_db.addFile(filename, pathid)
-
- # Create the movie entry
- query = (
- '''
- INSERT INTO movie(
- idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07,
- c09, c10, c11, c12, c14, c15, c16, c18, c19, c21)
-
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- '''
- )
- kodicursor.execute(query, (movieid, fileid, title, plot, shortplot, tagline, votecount,
- rating, writer, year, imdb, sorttitle, runtime, mpaa, genre, director, title,
- studio, trailer, country))
-
- # Create the reference in emby table
- emby_db.addReference(itemid, movieid, "Movie", "movie", fileid, pathid, None, checksum, viewid)
-
- # Update the path
- query = ' '.join((
-
- "UPDATE path",
- "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?",
- "WHERE idPath = ?"
- ))
- kodicursor.execute(query, (path, "movies", "metadata.local", 1, pathid))
-
- # Update the file
- query = ' '.join((
-
- "UPDATE files",
- "SET idPath = ?, strFilename = ?, dateAdded = ?",
- "WHERE idFile = ?"
- ))
- kodicursor.execute(query, (pathid, filename, dateadded, fileid))
-
- # Process countries
- kodi_db.addCountries(movieid, item['ProductionLocations'], "movie")
- # Process cast
- people = artwork.getPeopleArtwork(item['People'])
- kodi_db.addPeople(movieid, people, "movie")
- # Process genres
- kodi_db.addGenres(movieid, genres, "movie")
- # Process artwork
- artwork.addArtwork(artwork.getAllArtwork(item), movieid, "movie", kodicursor)
- # Process stream details
- streams = API.getMediaStreams()
- kodi_db.addStreams(fileid, streams, runtime)
- # Process studios
- kodi_db.addStudios(movieid, studios, "movie")
- # Process tags: view, emby tags
- tags = [viewtag]
- tags.extend(item['Tags'])
- if userdata['Favorite']:
- tags.append("Favorite movies")
- kodi_db.addTags(movieid, tags, "movie")
- # Process playstates
- resume = API.adjustResume(userdata['Resume'])
- total = round(float(runtime), 6)
- kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed)
-
- def add_updateBoxset(self, boxset):
-
- emby = self.emby
- emby_db = self.emby_db
- kodi_db = self.kodi_db
- artwork = self.artwork
-
- boxsetid = boxset['Id']
- title = boxset['Name']
- checksum = boxset['Etag']
- emby_dbitem = emby_db.getItem_byId(boxsetid)
- try:
- setid = emby_dbitem[0]
-
- except TypeError:
- setid = kodi_db.createBoxset(title)
-
- # Process artwork
- artwork.addArtwork(artwork.getAllArtwork(boxset), setid, "set", self.kodicursor)
-
- # Process movies inside boxset
- current_movies = emby_db.getItemId_byParentId(setid, "movie")
- process = []
- try:
- # Try to convert tuple to dictionary
- current = dict(current_movies)
- except ValueError:
- current = {}
-
- # Sort current titles
- for current_movie in current:
- process.append(current_movie)
-
- # New list to compare
- boxsetMovies = emby.getMovies_byBoxset(boxsetid)
- for movie in boxsetMovies['Items']:
-
- itemid = movie['Id']
-
- if not current.get(itemid):
- # Assign boxset to movie
- emby_dbitem = emby_db.getItem_byId(itemid)
- try:
- movieid = emby_dbitem[0]
- except TypeError:
- self.logMsg("Failed to add: %s to boxset." % movie['Name'], 1)
- continue
-
- self.logMsg("New addition to boxset %s: %s" % (title, movie['Name']), 1)
- kodi_db.assignBoxset(setid, movieid)
- # Update emby reference
- emby_db.updateParentId(itemid, setid)
- else:
- # Remove from process, because the item still belongs
- process.remove(itemid)
-
- # Process removals from boxset
- for movie in process:
- movieid = current[movie]
- self.logMsg("Remove from boxset %s: %s" % (title, movieid))
- kodi_db.removefromBoxset(movieid)
- # Update emby reference
- emby_db.updateParentId(movie, None)
-
- # Update the reference in the emby table
- emby_db.addReference(boxsetid, setid, "BoxSet", mediatype="set", checksum=checksum)
-
- def updateUserdata(self, item):
- # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
- # Poster with progress bar
- emby_db = self.emby_db
- kodi_db = self.kodi_db
- API = api.API(item)
-
- # Get emby information
- itemid = item['Id']
- checksum = API.getChecksum()
- userdata = API.getUserData()
- runtime = API.getRuntime()
-
- # Get Kodi information
- emby_dbitem = emby_db.getItem_byId(itemid)
- try:
- movieid = emby_dbitem[0]
- fileid = emby_dbitem[1]
- self.logMsg(
- "Update playstate for movie: %s fileid: %s"
- % (item['Name'], fileid), 1)
- except TypeError:
- return
-
- # Process favorite tags
- if userdata['Favorite']:
- kodi_db.addTag(movieid, "Favorite movies", "movie")
- else:
- kodi_db.removeTag(movieid, "Favorite movies", "movie")
-
- # Process playstates
- playcount = userdata['PlayCount']
- dateplayed = userdata['LastPlayedDate']
- resume = API.adjustResume(userdata['Resume'])
- total = round(float(runtime), 6)
-
- self.logMsg("%s New resume point: %s" % (itemid, resume))
-
- kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed)
- emby_db.updateReference(itemid, checksum)
-
- def remove(self, itemid):
- # Remove movieid, fileid, emby reference
- emby_db = self.emby_db
- kodicursor = self.kodicursor
- artwork = self.artwork
-
- emby_dbitem = emby_db.getItem_byId(itemid)
- try:
- kodiid = emby_dbitem[0]
- fileid = emby_dbitem[1]
- mediatype = emby_dbitem[4]
- self.logMsg("Removing %sid: %s fileid: %s" % (mediatype, kodiid, fileid), 1)
- except TypeError:
- return
-
- # Remove the emby reference
- emby_db.removeItem(itemid)
- # Remove artwork
- artwork.deleteArtwork(kodiid, mediatype, kodicursor)
-
- if mediatype == "movie":
- # Delete kodi movie and file
- kodicursor.execute("DELETE FROM movie WHERE idMovie = ?", (kodiid,))
- kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,))
-
- elif mediatype == "set":
- # Delete kodi boxset
- boxset_movies = emby_db.getItem_byParentId(kodiid, "movie")
- for movie in boxset_movies:
- embyid = movie[0]
- movieid = movie[1]
- self.kodi_db.removefromBoxset(movieid)
- # Update emby reference
- emby_db.updateParentId(embyid, None)
-
- kodicursor.execute("DELETE FROM sets WHERE idSet = ?", (kodiid,))
-
- self.logMsg("Deleted %s %s from kodi database" % (mediatype, itemid), 1)
-
-class MusicVideos(Items):
-
-
- def __init__(self, embycursor, kodicursor):
- Items.__init__(self, embycursor, kodicursor)
-
- def added(self, items, pdialog):
-
- total = len(items)
- count = 0
- for mvideo in items:
-
- title = mvideo['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
- self.add_update(mvideo)
- if not pdialog and self.contentmsg:
- self.contentPop(title, self.newvideo_time)
-
-
- def add_update(self, item, viewtag=None, viewid=None):
- # Process single music video
- kodicursor = self.kodicursor
- emby_db = self.emby_db
- kodi_db = self.kodi_db
- artwork = self.artwork
- API = api.API(item)
-
- # If the item already exist in the local Kodi DB we'll perform a full item update
- # If the item doesn't exist, we'll add it to the database
- update_item = True
- itemid = item['Id']
- emby_dbitem = emby_db.getItem_byId(itemid)
- try:
- mvideoid = emby_dbitem[0]
- fileid = emby_dbitem[1]
- pathid = emby_dbitem[2]
- self.logMsg("mvideoid: %s fileid: %s pathid: %s" % (mvideoid, fileid, pathid), 1)
-
- except TypeError:
- update_item = False
- self.logMsg("mvideoid: %s not found." % itemid, 2)
- # mvideoid
- kodicursor.execute("select coalesce(max(idMVideo),0) from musicvideo")
- mvideoid = kodicursor.fetchone()[0] + 1
-
- else:
- # Verification the item is still in Kodi
- query = "SELECT * FROM musicvideo WHERE idMVideo = ?"
- kodicursor.execute(query, (mvideoid,))
- try:
- kodicursor.fetchone()[0]
- except TypeError:
- # item is not found, let's recreate it.
- update_item = False
- self.logMsg("mvideoid: %s missing from Kodi, repairing the entry." % mvideoid, 1)
-
- if not viewtag or not viewid:
- # Get view tag from emby
- viewtag, viewid, mediatype = self.emby.getView_embyId(itemid)
- self.logMsg("View tag found: %s" % viewtag, 2)
-
- # fileId information
- checksum = API.getChecksum()
- dateadded = API.getDateCreated()
- userdata = API.getUserData()
- playcount = userdata['PlayCount']
- dateplayed = userdata['LastPlayedDate']
-
- # item details
- runtime = API.getRuntime()
- plot = API.getOverview()
- title = item['Name']
- year = item.get('ProductionYear')
- genres = item['Genres']
- genre = " / ".join(genres)
- studios = API.getStudios()
- studio = " / ".join(studios)
- artist = " / ".join(item.get('Artists'))
- album = item.get('Album')
- track = item.get('Track')
- people = API.getPeople()
- director = " / ".join(people['Director'])
-
-
- ##### GET THE FILE AND PATH #####
- playurl = API.getFilePath()
-
- if "\\" in playurl:
- # Local path
- filename = playurl.rsplit("\\", 1)[1]
- else: # Network share
- filename = playurl.rsplit("/", 1)[1]
-
- if self.directpath:
- # Direct paths is set the Kodi way
- if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(playurl):
- # Validate the path is correct with user intervention
- resp = xbmcgui.Dialog().yesno(
- heading="Can't validate path",
- line1=(
- "Kodi can't locate file: %s. Verify the path. "
- "You may to verify your network credentials in the "
- "add-on settings or use the emby path substitution "
- "to format your path correctly. Stop syncing?"
- % playurl))
- if resp:
- utils.window('emby_shouldStop', value="true")
- return False
-
- path = playurl.replace(filename, "")
- utils.window('emby_pathverified', value="true")
- else:
- # Set plugin path and media flags using real filename
- path = "plugin://plugin.video.emby.musicvideos/"
- params = {
-
- 'filename': filename.encode('utf-8'),
- 'id': itemid,
- 'dbid': mvideoid,
- 'mode': "play"
- }
- filename = "%s?%s" % (path, urllib.urlencode(params))
-
-
- ##### UPDATE THE MUSIC VIDEO #####
- if update_item:
- self.logMsg("UPDATE mvideo itemid: %s - Title: %s" % (itemid, title), 1)
-
- # Update path
- query = "UPDATE path SET strPath = ? WHERE idPath = ?"
- kodicursor.execute(query, (path, pathid))
-
- # Update the filename
- query = "UPDATE files SET strFilename = ?, dateAdded = ? WHERE idFile = ?"
- kodicursor.execute(query, (filename, dateadded, fileid))
-
- # Update the music video entry
- query = ' '.join((
-
- "UPDATE musicvideo",
- "SET c00 = ?, c04 = ?, c05 = ?, c06 = ?, c07 = ?, c08 = ?, c09 = ?, c10 = ?,",
- "c11 = ?, c12 = ?"
- "WHERE idMVideo = ?"
- ))
- kodicursor.execute(query, (title, runtime, director, studio, year, plot, album,
- artist, genre, track, mvideoid))
-
- # Update the checksum in emby table
- emby_db.updateReference(itemid, checksum)
-
- ##### OR ADD THE MUSIC VIDEO #####
- else:
- self.logMsg("ADD mvideo itemid: %s - Title: %s" % (itemid, title), 1)
-
- # Add path
- query = ' '.join((
-
- "SELECT idPath",
- "FROM path",
- "WHERE strPath = ?"
- ))
- kodicursor.execute(query, (path,))
- try:
- pathid = kodicursor.fetchone()[0]
- except TypeError:
- kodicursor.execute("select coalesce(max(idPath),0) from path")
- pathid = kodicursor.fetchone()[0] + 1
- query = (
- '''
- INSERT OR REPLACE INTO path(
- idPath, strPath, strContent, strScraper, noUpdate)
-
- VALUES (?, ?, ?, ?, ?)
- '''
- )
- kodicursor.execute(query, (pathid, path, "musicvideos", "metadata.local", 1))
-
- # Add the file
- kodicursor.execute("select coalesce(max(idFile),0) from files")
- fileid = kodicursor.fetchone()[0] + 1
- query = (
- '''
- INSERT INTO files(
- idFile, idPath, strFilename, dateAdded)
-
- VALUES (?, ?, ?, ?)
- '''
- )
- kodicursor.execute(query, (fileid, pathid, filename, dateadded))
-
- # Create the musicvideo entry
- query = (
- '''
- INSERT INTO musicvideo(
- idMVideo, idFile, c00, c04, c05, c06, c07, c08, c09, c10, c11, c12)
-
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- '''
- )
- kodicursor.execute(query, (mvideoid, fileid, title, runtime, director, studio,
- year, plot, album, artist, genre, track))
-
- # Create the reference in emby table
- emby_db.addReference(itemid, mvideoid, "MusicVideo", "musicvideo", fileid, pathid,
- checksum=checksum, mediafolderid=viewid)
-
-
- # Process cast
- people = item['People']
- artists = item['ArtistItems']
- for artist in artists:
- artist['Type'] = "Artist"
- people.extend(artists)
- people = artwork.getPeopleArtwork(people)
- kodi_db.addPeople(mvideoid, people, "musicvideo")
- # Process genres
- kodi_db.addGenres(mvideoid, genres, "musicvideo")
- # Process artwork
- artwork.addArtwork(artwork.getAllArtwork(item), mvideoid, "musicvideo", kodicursor)
- # Process stream details
- streams = API.getMediaStreams()
- kodi_db.addStreams(fileid, streams, runtime)
- # Process studios
- kodi_db.addStudios(mvideoid, studios, "musicvideo")
- # Process tags: view, emby tags
- tags = [viewtag]
- tags.extend(item['Tags'])
- if userdata['Favorite']:
- tags.append("Favorite musicvideos")
- kodi_db.addTags(mvideoid, tags, "musicvideo")
- # Process playstates
- resume = API.adjustResume(userdata['Resume'])
- total = round(float(runtime), 6)
- kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed)
-
- def updateUserdata(self, item):
- # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
- # Poster with progress bar
- emby_db = self.emby_db
- kodi_db = self.kodi_db
- API = api.API(item)
-
- # Get emby information
- itemid = item['Id']
- checksum = API.getChecksum()
- userdata = API.getUserData()
- runtime = API.getRuntime()
-
- # Get Kodi information
- emby_dbitem = emby_db.getItem_byId(itemid)
- try:
- mvideoid = emby_dbitem[0]
- fileid = emby_dbitem[1]
- self.logMsg(
- "Update playstate for musicvideo: %s fileid: %s"
- % (item['Name'], fileid), 1)
- except TypeError:
- return
-
- # Process favorite tags
- if userdata['Favorite']:
- kodi_db.addTag(mvideoid, "Favorite musicvideos", "musicvideo")
- else:
- kodi_db.removeTag(mvideoid, "Favorite musicvideos", "musicvideo")
-
- # Process playstates
- playcount = userdata['PlayCount']
- dateplayed = userdata['LastPlayedDate']
- resume = API.adjustResume(userdata['Resume'])
- total = round(float(runtime), 6)
-
- kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed)
- emby_db.updateReference(itemid, checksum)
-
- def remove(self, itemid):
- # Remove mvideoid, fileid, pathid, emby reference
- emby_db = self.emby_db
- kodicursor = self.kodicursor
- artwork = self.artwork
-
- emby_dbitem = emby_db.getItem_byId(itemid)
- try:
- mvideoid = emby_dbitem[0]
- fileid = emby_dbitem[1]
- pathid = emby_dbitem[2]
- self.logMsg("Removing mvideoid: %s fileid: %s" % (mvideoid, fileid, pathid), 1)
- except TypeError:
- return
-
- # Remove artwork
- query = ' '.join((
-
- "SELECT url, type",
- "FROM art",
- "WHERE media_id = ?",
- "AND media_type = 'musicvideo'"
- ))
- kodicursor.execute(query, (mvideoid,))
- rows = kodicursor.fetchall()
- for row in rows:
-
- url = row[0]
- imagetype = row[1]
- if imagetype in ("poster", "fanart"):
- artwork.deleteCachedArtwork(url)
-
- kodicursor.execute("DELETE FROM musicvideo WHERE idMVideo = ?", (mvideoid,))
- kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,))
- if self.directpath:
- kodicursor.execute("DELETE FROM path WHERE idPath = ?", (pathid,))
- self.embycursor.execute("DELETE FROM emby WHERE emby_id = ?", (itemid,))
-
- self.logMsg("Deleted musicvideo %s from kodi database" % itemid, 1)
-
-class TVShows(Items):
-
-
- def __init__(self, embycursor, kodicursor):
- Items.__init__(self, embycursor, kodicursor)
-
- def added(self, items, pdialog):
-
- total = len(items)
- count = 0
- for tvshow in items:
-
- title = tvshow['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
- self.add_update(tvshow)
- # Add episodes
- all_episodes = self.emby.getEpisodesbyShow(tvshow['Id'])
- self.added_episode(all_episodes['Items'], pdialog)
-
- def added_season(self, items, pdialog):
-
- total = len(items)
- count = 0
- for season in items:
-
- title = "%s - %s" % (season.get('SeriesName', "Unknown"), season['Name'])
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
- self.add_updateSeason(season)
- # Add episodes
- all_episodes = self.emby.getEpisodesbySeason(season['Id'])
- self.added_episode(all_episodes['Items'], pdialog)
-
- def added_episode(self, items, pdialog):
-
- total = len(items)
- count = 0
- for episode in items:
- title = "%s - %s" % (episode.get('SeriesName', "Unknown"), episode['Name'])
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
- self.add_updateEpisode(episode)
- if not pdialog and self.contentmsg:
- self.contentPop(title, self.newvideo_time)
-
-
- def add_update(self, item, viewtag=None, viewid=None):
- # Process single tvshow
- kodicursor = self.kodicursor
- emby = self.emby
- emby_db = self.emby_db
- kodi_db = self.kodi_db
- artwork = self.artwork
- API = api.API(item)
-
- if utils.settings('syncEmptyShows') == "false" and not item['RecursiveItemCount']:
- self.logMsg("Skipping empty show: %s" % item['Name'], 1)
- return
- # If the item already exist in the local Kodi DB we'll perform a full item update
- # If the item doesn't exist, we'll add it to the database
- update_item = True
- force_episodes = False
- itemid = item['Id']
- emby_dbitem = emby_db.getItem_byId(itemid)
- try:
- showid = emby_dbitem[0]
- pathid = emby_dbitem[2]
- self.logMsg("showid: %s pathid: %s" % (showid, pathid), 1)
-
- except TypeError:
- update_item = False
- self.logMsg("showid: %s not found." % itemid, 2)
- kodicursor.execute("select coalesce(max(idShow),0) from tvshow")
- showid = kodicursor.fetchone()[0] + 1
-
- else:
- # Verification the item is still in Kodi
- query = "SELECT * FROM tvshow WHERE idShow = ?"
- kodicursor.execute(query, (showid,))
- try:
- kodicursor.fetchone()[0]
- except TypeError:
- # item is not found, let's recreate it.
- update_item = False
- self.logMsg("showid: %s missing from Kodi, repairing the entry." % showid, 1)
- # Force re-add episodes after the show is re-created.
- force_episodes = True
-
-
- if viewtag is None or viewid is None:
- # Get view tag from emby
- viewtag, viewid, mediatype = emby.getView_embyId(itemid)
- self.logMsg("View tag found: %s" % viewtag, 2)
-
- # fileId information
- checksum = API.getChecksum()
- dateadded = API.getDateCreated()
- userdata = API.getUserData()
- playcount = userdata['PlayCount']
- dateplayed = userdata['LastPlayedDate']
-
- # item details
- genres = item['Genres']
- title = item['Name']
- plot = API.getOverview()
- rating = item.get('CommunityRating')
- premieredate = API.getPremiereDate()
- tvdb = API.getProvider('Tvdb')
- sorttitle = item['SortName']
- mpaa = API.getMpaa()
- genre = " / ".join(genres)
- studios = API.getStudios()
- studio = " / ".join(studios)
-
-
- ##### GET THE FILE AND PATH #####
- playurl = API.getFilePath()
-
- if self.directpath:
- # Direct paths is set the Kodi way
- if "\\" in playurl:
- # Local path
- path = "%s\\" % playurl
- toplevelpath = "%s\\" % dirname(dirname(path))
- else:
- # Network path
- path = "%s/" % playurl
- toplevelpath = "%s/" % dirname(dirname(path))
-
- if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(path):
- # Validate the path is correct with user intervention
- resp = xbmcgui.Dialog().yesno(
- heading="Can't validate path",
- line1=(
- "Kodi can't locate file: %s. Verify the path. "
- "You may to verify your network credentials in the "
- "add-on settings or use the emby path substitution "
- "to format your path correctly. Stop syncing?"
- % playurl))
- if resp:
- utils.window('emby_shouldStop', value="true")
- return False
-
- utils.window('emby_pathverified', value="true")
- else:
- # Set plugin path
- toplevelpath = "plugin://plugin.video.emby.tvshows/"
- path = "%s%s/" % (toplevelpath, itemid)
-
-
- ##### UPDATE THE TVSHOW #####
- if update_item:
- self.logMsg("UPDATE tvshow itemid: %s - Title: %s" % (itemid, title), 1)
-
- # Update the tvshow entry
- query = ' '.join((
-
- "UPDATE tvshow",
- "SET c00 = ?, c01 = ?, c04 = ?, c05 = ?, c08 = ?, c09 = ?,",
- "c12 = ?, c13 = ?, c14 = ?, c15 = ?",
- "WHERE idShow = ?"
- ))
- kodicursor.execute(query, (title, plot, rating, premieredate, genre, title,
- tvdb, mpaa, studio, sorttitle, showid))
-
- # Update the checksum in emby table
- emby_db.updateReference(itemid, checksum)
-
- ##### OR ADD THE TVSHOW #####
- else:
- self.logMsg("ADD tvshow itemid: %s - Title: %s" % (itemid, title), 1)
-
- # Add top path
- toppathid = kodi_db.addPath(toplevelpath)
- query = ' '.join((
-
- "UPDATE path",
- "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?",
- "WHERE idPath = ?"
- ))
- kodicursor.execute(query, (toplevelpath, "tvshows", "metadata.local", 1, toppathid))
-
- # Add path
- pathid = kodi_db.addPath(path)
-
- # Create the tvshow entry
- query = (
- '''
- INSERT INTO tvshow(
- idShow, c00, c01, c04, c05, c08, c09, c12, c13, c14, c15)
-
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- '''
- )
- kodicursor.execute(query, (showid, title, plot, rating, premieredate, genre,
- title, tvdb, mpaa, studio, sorttitle))
-
- # Link the path
- query = "INSERT INTO tvshowlinkpath(idShow, idPath) values(?, ?)"
- kodicursor.execute(query, (showid, pathid))
-
- # Create the reference in emby table
- emby_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid,
- checksum=checksum, mediafolderid=viewid)
-
- # Update the path
- query = ' '.join((
-
- "UPDATE path",
- "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?",
- "WHERE idPath = ?"
- ))
- kodicursor.execute(query, (path, None, None, 1, pathid))
-
- # Process cast
- people = artwork.getPeopleArtwork(item['People'])
- kodi_db.addPeople(showid, people, "tvshow")
- # Process genres
- kodi_db.addGenres(showid, genres, "tvshow")
- # Process artwork
- artwork.addArtwork(artwork.getAllArtwork(item), showid, "tvshow", kodicursor)
- # Process studios
- kodi_db.addStudios(showid, studios, "tvshow")
- # Process tags: view, emby tags
- tags = [viewtag]
- tags.extend(item['Tags'])
- if userdata['Favorite']:
- tags.append("Favorite tvshows")
- kodi_db.addTags(showid, tags, "tvshow")
- # Process seasons
- all_seasons = emby.getSeasons(itemid)
- for season in all_seasons['Items']:
- self.add_updateSeason(season, showid=showid)
- else:
- # Finally, refresh the all season entry
- seasonid = kodi_db.addSeason(showid, -1)
- # Process artwork
- artwork.addArtwork(artwork.getAllArtwork(item), seasonid, "season", kodicursor)
-
- if force_episodes:
- # We needed to recreate the show entry. Re-add episodes now.
- self.logMsg("Repairing episodes for showid: %s %s" % (showid, title), 1)
- all_episodes = emby.getEpisodesbyShow(itemid)
- self.added_episode(all_episodes['Items'], None)
-
- def add_updateSeason(self, item, showid=None):
-
- kodicursor = self.kodicursor
- emby_db = self.emby_db
- kodi_db = self.kodi_db
- artwork = self.artwork
-
- seasonnum = item.get('IndexNumber', 1)
- itemid = item['Id']
-
- if showid is None:
- try:
- seriesId = item['SeriesId']
- showid = emby_db.getItem_byId(seriesId)[0]
- except KeyError:
- return
- except TypeError:
- # Show is missing, update show instead.
- show = self.emby.getItem(seriesId)
- self.add_update(show)
- return
-
- seasonid = kodi_db.addSeason(showid, seasonnum)
-
- if item['LocationType'] != "Virtual":
- # Create the reference in emby table
- emby_db.addReference(itemid, seasonid, "Season", "season", parentid=showid)
-
- # Process artwork
- artwork.addArtwork(artwork.getAllArtwork(item), seasonid, "season", kodicursor)
-
- def add_updateEpisode(self, item):
- # Process single episode
- kodiversion = self.kodiversion
- kodicursor = self.kodicursor
- emby_db = self.emby_db
- kodi_db = self.kodi_db
- artwork = self.artwork
- API = api.API(item)
-
- # If the item already exist in the local Kodi DB we'll perform a full item update
- # If the item doesn't exist, we'll add it to the database
- update_item = True
- itemid = item['Id']
- emby_dbitem = emby_db.getItem_byId(itemid)
- try:
- episodeid = emby_dbitem[0]
- fileid = emby_dbitem[1]
- pathid = emby_dbitem[2]
- self.logMsg("episodeid: %s fileid: %s pathid: %s" % (episodeid, fileid, pathid), 1)
-
- except TypeError:
- update_item = False
- self.logMsg("episodeid: %s not found." % itemid, 2)
- # episodeid
- kodicursor.execute("select coalesce(max(idEpisode),0) from episode")
- episodeid = kodicursor.fetchone()[0] + 1
-
- else:
- # Verification the item is still in Kodi
- query = "SELECT * FROM episode WHERE idEpisode = ?"
- kodicursor.execute(query, (episodeid,))
- try:
- kodicursor.fetchone()[0]
- except TypeError:
- # item is not found, let's recreate it.
- update_item = False
- self.logMsg("episodeid: %s missing from Kodi, repairing the entry." % episodeid, 1)
-
- # fileId information
- checksum = API.getChecksum()
- dateadded = API.getDateCreated()
- userdata = API.getUserData()
- playcount = userdata['PlayCount']
- dateplayed = userdata['LastPlayedDate']
-
- # item details
- people = API.getPeople()
- writer = " / ".join(people['Writer'])
- director = " / ".join(people['Director'])
- title = item['Name']
- plot = API.getOverview()
- rating = item.get('CommunityRating')
- runtime = API.getRuntime()
- premieredate = API.getPremiereDate()
-
- # episode details
- try:
- seriesId = item['SeriesId']
- except KeyError:
- # Missing seriesId, skip
- self.logMsg("Skipping: %s. SeriesId is missing." % itemid, 1)
- return False
-
- seriesName = item['SeriesName']
- season = item.get('ParentIndexNumber')
- episode = item.get('IndexNumber', -1)
-
- if season is None:
- if item.get('AbsoluteEpisodeNumber'):
- # Anime scenario
- season = 1
- episode = item['AbsoluteEpisodeNumber']
- else:
- season = -1
-
- # Specials ordering within season
- if item.get('AirsAfterSeasonNumber'):
- airsBeforeSeason = item['AirsAfterSeasonNumber']
- airsBeforeEpisode = 4096 # Kodi default number for afterseason ordering
- else:
- airsBeforeSeason = item.get('AirsBeforeSeasonNumber')
- airsBeforeEpisode = item.get('AirsBeforeEpisodeNumber')
-
- # Append multi episodes to title
- if item.get('IndexNumberEnd'):
- title = "| %02d | %s" % (item['IndexNumberEnd'], title)
-
- # Get season id
- show = emby_db.getItem_byId(seriesId)
- try:
- showid = show[0]
- except TypeError:
- # Show is missing from database
- show = self.emby.getItem(seriesId)
- self.add_update(show)
- show = emby_db.getItem_byId(seriesId)
- try:
- showid = show[0]
- except TypeError:
- self.logMsg("Skipping: %s. Unable to add series: %s." % (itemid, seriesId))
- return False
-
- seasonid = kodi_db.addSeason(showid, season)
-
-
- ##### GET THE FILE AND PATH #####
- playurl = API.getFilePath()
-
- if "\\" in playurl:
- # Local path
- filename = playurl.rsplit("\\", 1)[1]
- else: # Network share
- filename = playurl.rsplit("/", 1)[1]
-
- if self.directpath:
- # Direct paths is set the Kodi way
- if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(playurl):
- # Validate the path is correct with user intervention
- resp = xbmcgui.Dialog().yesno(
- heading="Can't validate path",
- line1=(
- "Kodi can't locate file: %s. Verify the path. "
- "You may to verify your network credentials in the "
- "add-on settings or use the emby path substitution "
- "to format your path correctly. Stop syncing?"
- % playurl))
- if resp:
- utils.window('emby_shouldStop', value="true")
- return False
-
- path = playurl.replace(filename, "")
- utils.window('emby_pathverified', value="true")
- else:
- # Set plugin path and media flags using real filename
- path = "plugin://plugin.video.emby.tvshows/%s/" % seriesId
- params = {
-
- 'filename': filename.encode('utf-8'),
- 'id': itemid,
- 'dbid': episodeid,
- 'mode': "play"
- }
- filename = "%s?%s" % (path, urllib.urlencode(params))
-
-
- ##### UPDATE THE EPISODE #####
- if update_item:
- self.logMsg("UPDATE episode itemid: %s - Title: %s" % (itemid, title), 1)
-
- # Update the movie entry
- if kodiversion in (16, 17):
- # Kodi Jarvis, Krypton
- query = ' '.join((
-
- "UPDATE episode",
- "SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,",
- "c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?, idSeason = ?",
- "WHERE idEpisode = ?"
- ))
- kodicursor.execute(query, (title, plot, rating, writer, premieredate,
- runtime, director, season, episode, title, airsBeforeSeason,
- airsBeforeEpisode, seasonid, episodeid))
- else:
- query = ' '.join((
-
- "UPDATE episode",
- "SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,",
- "c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?",
- "WHERE idEpisode = ?"
- ))
- kodicursor.execute(query, (title, plot, rating, writer, premieredate,
- runtime, director, season, episode, title, airsBeforeSeason,
- airsBeforeEpisode, episodeid))
-
- # Update the checksum in emby table
- emby_db.updateReference(itemid, checksum)
- # Update parentid reference
- emby_db.updateParentId(itemid, seasonid)
-
- ##### OR ADD THE EPISODE #####
- else:
- self.logMsg("ADD episode itemid: %s - Title: %s" % (itemid, title), 1)
-
- # Add path
- pathid = kodi_db.addPath(path)
- # Add the file
- fileid = kodi_db.addFile(filename, pathid)
-
- # Create the episode entry
- if kodiversion in (16, 17):
- # Kodi Jarvis, Krypton
- query = (
- '''
- INSERT INTO episode(
- idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14,
- idShow, c15, c16, idSeason)
-
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- '''
- )
- kodicursor.execute(query, (episodeid, fileid, title, plot, rating, writer,
- premieredate, runtime, director, season, episode, title, showid,
- airsBeforeSeason, airsBeforeEpisode, seasonid))
- else:
- query = (
- '''
- INSERT INTO episode(
- idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14,
- idShow, c15, c16)
-
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- '''
- )
- kodicursor.execute(query, (episodeid, fileid, title, plot, rating, writer,
- premieredate, runtime, director, season, episode, title, showid,
- airsBeforeSeason, airsBeforeEpisode))
-
- # Create the reference in emby table
- emby_db.addReference(itemid, episodeid, "Episode", "episode", fileid, pathid,
- seasonid, checksum)
-
- # Update the path
- query = ' '.join((
-
- "UPDATE path",
- "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?",
- "WHERE idPath = ?"
- ))
- kodicursor.execute(query, (path, None, None, 1, pathid))
-
- # Update the file
- query = ' '.join((
-
- "UPDATE files",
- "SET idPath = ?, strFilename = ?, dateAdded = ?",
- "WHERE idFile = ?"
- ))
- kodicursor.execute(query, (pathid, filename, dateadded, fileid))
-
- # Process cast
- people = artwork.getPeopleArtwork(item['People'])
- kodi_db.addPeople(episodeid, people, "episode")
- # Process artwork
- artworks = artwork.getAllArtwork(item)
- artwork.addOrUpdateArt(artworks['Primary'], episodeid, "episode", "thumb", kodicursor)
- # Process stream details
- streams = API.getMediaStreams()
- kodi_db.addStreams(fileid, streams, runtime)
- # Process playstates
- resume = API.adjustResume(userdata['Resume'])
- total = round(float(runtime), 6)
- kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed)
- if not self.directpath and resume:
- # Create additional entry for widgets. This is only required for plugin/episode.
- temppathid = kodi_db.getPath("plugin://plugin.video.emby.tvshows/")
- tempfileid = kodi_db.addFile(filename, temppathid)
- query = ' '.join((
-
- "UPDATE files",
- "SET idPath = ?, strFilename = ?, dateAdded = ?",
- "WHERE idFile = ?"
- ))
- kodicursor.execute(query, (temppathid, filename, dateadded, tempfileid))
- kodi_db.addPlaystate(tempfileid, resume, total, playcount, dateplayed)
-
- def updateUserdata(self, item):
- # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
- # Poster with progress bar
- emby_db = self.emby_db
- kodi_db = self.kodi_db
- API = api.API(item)
-
- # Get emby information
- itemid = item['Id']
- checksum = API.getChecksum()
- userdata = API.getUserData()
- runtime = API.getRuntime()
- dateadded = API.getDateCreated()
-
- # Get Kodi information
- emby_dbitem = emby_db.getItem_byId(itemid)
- try:
- kodiid = emby_dbitem[0]
- fileid = emby_dbitem[1]
- mediatype = emby_dbitem[4]
- self.logMsg(
- "Update playstate for %s: %s fileid: %s"
- % (mediatype, item['Name'], fileid), 1)
- except TypeError:
- return
-
- # Process favorite tags
- if mediatype == "tvshow":
- if userdata['Favorite']:
- kodi_db.addTag(kodiid, "Favorite tvshows", "tvshow")
- else:
- kodi_db.removeTag(kodiid, "Favorite tvshows", "tvshow")
-
- # Process playstates
- if mediatype == "episode":
- playcount = userdata['PlayCount']
- dateplayed = userdata['LastPlayedDate']
- resume = API.adjustResume(userdata['Resume'])
- total = round(float(runtime), 6)
-
- self.logMsg("%s New resume point: %s" % (itemid, resume))
-
- kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed)
- if not self.directpath and not resume:
- # Make sure there's no other bookmarks created by widget.
- filename = kodi_db.getFile(fileid)
- kodi_db.removeFile("plugin://plugin.video.emby.tvshows/", filename)
-
- if not self.directpath and resume:
- # Create additional entry for widgets. This is only required for plugin/episode.
- filename = kodi_db.getFile(fileid)
- temppathid = kodi_db.getPath("plugin://plugin.video.emby.tvshows/")
- tempfileid = kodi_db.addFile(filename, temppathid)
- query = ' '.join((
-
- "UPDATE files",
- "SET idPath = ?, strFilename = ?, dateAdded = ?",
- "WHERE idFile = ?"
- ))
- self.kodicursor.execute(query, (temppathid, filename, dateadded, tempfileid))
- kodi_db.addPlaystate(tempfileid, resume, total, playcount, dateplayed)
-
- emby_db.updateReference(itemid, checksum)
-
- def remove(self, itemid):
- # Remove showid, fileid, pathid, emby reference
- emby_db = self.emby_db
- embycursor = self.embycursor
- kodicursor = self.kodicursor
- artwork = self.artwork
-
- emby_dbitem = emby_db.getItem_byId(itemid)
- try:
- kodiid = emby_dbitem[0]
- fileid = emby_dbitem[1]
- pathid = emby_dbitem[2]
- parentid = emby_dbitem[3]
- mediatype = emby_dbitem[4]
- self.logMsg("Removing %s kodiid: %s fileid: %s" % (mediatype, kodiid, fileid), 1)
- except TypeError:
- return
-
- ##### PROCESS ITEM #####
-
- # Remove the emby reference
- emby_db.removeItem(itemid)
-
-
- ##### IF EPISODE #####
-
- if mediatype == "episode":
- # Delete kodi episode and file, verify season and tvshow
- self.removeEpisode(kodiid, fileid)
-
- # Season verification
- season = emby_db.getItem_byKodiId(parentid, "season")
- try:
- showid = season[1]
- except TypeError:
- return
-
- season_episodes = emby_db.getItem_byParentId(parentid, "episode")
- if not season_episodes:
- self.removeSeason(parentid)
- emby_db.removeItem(season[0])
-
- # Show verification
- show = emby_db.getItem_byKodiId(showid, "tvshow")
- query = ' '.join((
-
- "SELECT totalCount",
- "FROM tvshowcounts",
- "WHERE idShow = ?"
- ))
- kodicursor.execute(query, (showid,))
- result = kodicursor.fetchone()
- if result and result[0] is None:
- # There's no episodes left, delete show and any possible remaining seasons
- seasons = emby_db.getItem_byParentId(showid, "season")
- for season in seasons:
- self.removeSeason(season[1])
- else:
- # Delete emby season entries
- emby_db.removeItems_byParentId(showid, "season")
- self.removeShow(showid)
- emby_db.removeItem(show[0])
-
- ##### IF TVSHOW #####
-
- elif mediatype == "tvshow":
- # Remove episodes, seasons, tvshow
- seasons = emby_db.getItem_byParentId(kodiid, "season")
- for season in seasons:
- seasonid = season[1]
- season_episodes = emby_db.getItem_byParentId(seasonid, "episode")
- for episode in season_episodes:
- self.removeEpisode(episode[1], episode[2])
- else:
- # Remove emby episodes
- emby_db.removeItems_byParentId(seasonid, "episode")
- else:
- # Remove emby seasons
- emby_db.removeItems_byParentId(kodiid, "season")
-
- # Remove tvshow
- self.removeShow(kodiid)
-
- ##### IF SEASON #####
-
- elif mediatype == "season":
- # Remove episodes, season, verify tvshow
- season_episodes = emby_db.getItem_byParentId(kodiid, "episode")
- for episode in season_episodes:
- self.removeEpisode(episode[1], episode[2])
- else:
- # Remove emby episodes
- emby_db.removeItems_byParentId(kodiid, "episode")
-
- # Remove season
- self.removeSeason(kodiid)
-
- # Show verification
- seasons = emby_db.getItem_byParentId(parentid, "season")
- if not seasons:
- # There's no seasons, delete the show
- self.removeShow(parentid)
- emby_db.removeItem_byKodiId(parentid, "tvshow")
-
- self.logMsg("Deleted %s: %s from kodi database" % (mediatype, itemid), 1)
-
- def removeShow(self, kodiid):
-
- kodicursor = self.kodicursor
- artwork = self.artwork
-
- artwork.deleteArtwork(kodiid, "tvshow", kodicursor)
- kodicursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodiid,))
- self.logMsg("Removed tvshow: %s." % kodiid, 2)
-
- def removeSeason(self, kodiid):
-
- kodicursor = self.kodicursor
- artwork = self.artwork
-
- artwork.deleteArtwork(kodiid, "season", kodicursor)
- kodicursor.execute("DELETE FROM seasons WHERE idSeason = ?", (kodiid,))
- self.logMsg("Removed season: %s." % kodiid, 2)
-
- def removeEpisode(self, kodiid, fileid):
-
- kodicursor = self.kodicursor
- artwork = self.artwork
-
- artwork.deleteArtwork(kodiid, "episode", kodicursor)
- kodicursor.execute("DELETE FROM episode WHERE idEpisode = ?", (kodiid,))
- kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,))
- self.logMsg("Removed episode: %s." % kodiid, 2)
-
-class Music(Items):
-
-
- def __init__(self, embycursor, musiccursor):
-
- Items.__init__(self, embycursor, musiccursor)
-
- self.directstream = utils.settings('streamMusic') == "true"
- self.enableimportsongrating = utils.settings('enableImportSongRating') == "true"
- self.enableexportsongrating = utils.settings('enableExportSongRating') == "true"
- self.enableupdatesongrating = utils.settings('enableUpdateSongRating') == "true"
- self.userid = utils.window('emby_currUser')
- self.server = utils.window('emby_server%s' % self.userid)
-
- def added(self, items, pdialog):
-
- total = len(items)
- count = 0
- for artist in items:
-
- title = artist['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
- self.add_updateArtist(artist)
- # Add albums
- all_albums = self.emby.getAlbumsbyArtist(artist['Id'])
- self.added_album(all_albums['Items'], pdialog)
-
- def added_album(self, items, pdialog):
-
- total = len(items)
- count = 0
- for album in items:
-
- title = album['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
- self.add_updateAlbum(album)
- # Add songs
- all_songs = self.emby.getSongsbyAlbum(album['Id'])
- self.added_song(all_songs['Items'], pdialog)
-
- def added_song(self, items, pdialog):
-
- total = len(items)
- count = 0
- for song in items:
-
- title = song['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
- self.add_updateSong(song)
- if not pdialog and self.contentmsg:
- self.contentPop(title, self.newmusic_time)
-
- def add_updateArtist(self, item, artisttype="MusicArtist"):
- # Process a single artist
- kodiversion = self.kodiversion
- kodicursor = self.kodicursor
- emby_db = self.emby_db
- kodi_db = self.kodi_db
- artwork = self.artwork
- API = api.API(item)
-
- update_item = True
- itemid = item['Id']
- emby_dbitem = emby_db.getItem_byId(itemid)
- try:
- artistid = emby_dbitem[0]
- except TypeError:
- update_item = False
- self.logMsg("artistid: %s not found." % itemid, 2)
-
- ##### The artist details #####
- lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- dateadded = API.getDateCreated()
- checksum = API.getChecksum()
-
- name = item['Name']
- musicBrainzId = API.getProvider('MusicBrainzArtist')
- genres = " / ".join(item.get('Genres'))
- bio = API.getOverview()
-
- # Associate artwork
- artworks = artwork.getAllArtwork(item, parentInfo=True)
- thumb = artworks['Primary']
- backdrops = artworks['Backdrop'] # List
-
- if thumb:
- thumb = "%s" % thumb
- if backdrops:
- fanart = "%s" % backdrops[0]
- else:
- fanart = ""
-
-
- ##### UPDATE THE ARTIST #####
- if update_item:
- self.logMsg("UPDATE artist itemid: %s - Name: %s" % (itemid, name), 1)
- # Update the checksum in emby table
- emby_db.updateReference(itemid, checksum)
-
- ##### OR ADD THE ARTIST #####
- else:
- self.logMsg("ADD artist itemid: %s - Name: %s" % (itemid, name), 1)
- # safety checks: It looks like Emby supports the same artist multiple times.
- # Kodi doesn't allow that. In case that happens we just merge the artist entries.
- artistid = kodi_db.addArtist(name, musicBrainzId)
- # Create the reference in emby table
- emby_db.addReference(itemid, artistid, artisttype, "artist", checksum=checksum)
-
-
- # Process the artist
- if self.kodiversion in (16, 17):
- query = ' '.join((
-
- "UPDATE artist",
- "SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,",
- "lastScraped = ?",
- "WHERE idArtist = ?"
- ))
- kodicursor.execute(query, (genres, bio, thumb, fanart, lastScraped, artistid))
- else:
- query = ' '.join((
-
- "UPDATE artist",
- "SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,",
- "lastScraped = ?, dateAdded = ?",
- "WHERE idArtist = ?"
- ))
- kodicursor.execute(query, (genres, bio, thumb, fanart, lastScraped,
- dateadded, artistid))
-
-
- # Update artwork
- artwork.addArtwork(artworks, artistid, "artist", kodicursor)
-
- def add_updateAlbum(self, item):
- # Process a single artist
- emby = self.emby
- kodiversion = self.kodiversion
- kodicursor = self.kodicursor
- emby_db = self.emby_db
- kodi_db = self.kodi_db
- artwork = self.artwork
- API = api.API(item)
-
- update_item = True
- itemid = item['Id']
- emby_dbitem = emby_db.getItem_byId(itemid)
- try:
- albumid = emby_dbitem[0]
- except TypeError:
- update_item = False
- self.logMsg("albumid: %s not found." % itemid, 2)
-
- ##### The album details #####
- lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- dateadded = API.getDateCreated()
- userdata = API.getUserData()
- checksum = API.getChecksum()
-
- name = item['Name']
- musicBrainzId = API.getProvider('MusicBrainzAlbum')
- year = item.get('ProductionYear')
- genres = item.get('Genres')
- genre = " / ".join(genres)
- bio = API.getOverview()
- rating = userdata['UserRating']
- artists = item['AlbumArtists']
- if not artists:
- artists = item['ArtistItems']
- artistname = []
- for artist in artists:
- artistname.append(artist['Name'])
- artistname = " / ".join(artistname)
-
- # Associate artwork
- artworks = artwork.getAllArtwork(item, parentInfo=True)
- thumb = artworks['Primary']
- if thumb:
- thumb = "%s" % thumb
-
- ##### UPDATE THE ALBUM #####
- if update_item:
- self.logMsg("UPDATE album itemid: %s - Name: %s" % (itemid, name), 1)
- # Update the checksum in emby table
- emby_db.updateReference(itemid, checksum)
-
- ##### OR ADD THE ALBUM #####
- else:
- self.logMsg("ADD album itemid: %s - Name: %s" % (itemid, name), 1)
- # safety checks: It looks like Emby supports the same artist multiple times.
- # Kodi doesn't allow that. In case that happens we just merge the artist entries.
- albumid = kodi_db.addAlbum(name, musicBrainzId)
- # Create the reference in emby table
- emby_db.addReference(itemid, albumid, "MusicAlbum", "album", checksum=checksum)
-
-
- # Process the album info
- if kodiversion == 17:
- # Kodi Krypton
- query = ' '.join((
-
- "UPDATE album",
- "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
- "iUserrating = ?, lastScraped = ?, strReleaseType = ?",
- "WHERE idAlbum = ?"
- ))
- kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped,
- "album", albumid))
- elif kodiversion == 16:
- # Kodi Jarvis
- query = ' '.join((
-
- "UPDATE album",
- "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
- "iRating = ?, lastScraped = ?, strReleaseType = ?",
- "WHERE idAlbum = ?"
- ))
- kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped,
- "album", albumid))
- elif kodiversion == 15:
- # Kodi Isengard
- query = ' '.join((
-
- "UPDATE album",
- "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
- "iRating = ?, lastScraped = ?, dateAdded = ?, strReleaseType = ?",
- "WHERE idAlbum = ?"
- ))
- kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped,
- dateadded, "album", albumid))
- else:
- # Kodi Helix
- query = ' '.join((
-
- "UPDATE album",
- "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
- "iRating = ?, lastScraped = ?, dateAdded = ?",
- "WHERE idAlbum = ?"
- ))
- kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped,
- dateadded, albumid))
-
- # Associate the parentid for emby reference
- parentId = item.get('ParentId')
- if parentId is not None:
- emby_dbartist = emby_db.getItem_byId(parentId)
- try:
- artistid = emby_dbartist[0]
- except TypeError:
- # Artist does not exist in emby database.
- artist = emby.getItem(parentId)
- # Item may not be an artist, verification necessary.
- if artist['Type'] == "MusicArtist":
- # Update with the parentId, for remove reference
- emby_db.addReference(parentId, parentId, "MusicArtist", "artist")
- emby_db.updateParentId(itemid, parentId)
- else:
- # Update emby reference with the artistid
- emby_db.updateParentId(itemid, artistid)
-
- # Assign main artists to album
- for artist in artists:
- artistname = artist['Name']
- artistId = artist['Id']
- emby_dbartist = emby_db.getItem_byId(artistId)
- try:
- artistid = emby_dbartist[0]
- except TypeError:
- # Artist does not exist in emby database, create the reference
- artist = emby.getItem(artistId)
- self.add_updateArtist(artist, artisttype="AlbumArtist")
- emby_dbartist = emby_db.getItem_byId(artistId)
- artistid = emby_dbartist[0]
- else:
- # Best take this name over anything else.
- query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?"
- kodicursor.execute(query, (artistname, artistid,))
-
- # Add artist to album
- query = (
- '''
- INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist)
-
- VALUES (?, ?, ?)
- '''
- )
- kodicursor.execute(query, (artistid, albumid, artistname))
- # Update discography
- query = (
- '''
- INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear)
-
- VALUES (?, ?, ?)
- '''
- )
- kodicursor.execute(query, (artistid, name, year))
- # Update emby reference with parentid
- emby_db.updateParentId(artistId, albumid)
-
- # Add genres
- kodi_db.addMusicGenres(albumid, genres, "album")
- # Update artwork
- artwork.addArtwork(artworks, albumid, "album", kodicursor)
-
- def add_updateSong(self, item):
- # Process single song
- kodiversion = self.kodiversion
- kodicursor = self.kodicursor
- emby = self.emby
- emby_db = self.emby_db
- kodi_db = self.kodi_db
- artwork = self.artwork
- API = api.API(item)
-
- update_item = True
- itemid = item['Id']
- emby_dbitem = emby_db.getItem_byId(itemid)
- try:
- songid = emby_dbitem[0]
- pathid = emby_dbitem[2]
- albumid = emby_dbitem[3]
- except TypeError:
- update_item = False
- self.logMsg("songid: %s not found." % itemid, 2)
-
- ##### The song details #####
- checksum = API.getChecksum()
- dateadded = API.getDateCreated()
- userdata = API.getUserData()
- playcount = userdata['PlayCount']
- dateplayed = userdata['LastPlayedDate']
-
- # item details
- title = item['Name']
- musicBrainzId = API.getProvider('MusicBrainzTrackId')
- genres = item.get('Genres')
- genre = " / ".join(genres)
- artists = " / ".join(item['Artists'])
- tracknumber = item.get('IndexNumber', 0)
- disc = item.get('ParentIndexNumber', 1)
- if disc == 1:
- track = tracknumber
- else:
- track = disc*2**16 + tracknumber
- year = item.get('ProductionYear')
- duration = API.getRuntime()
- rating = userdata['UserRating']
-
- #if enabled, try to get the rating from file and/or emby
- if not self.directstream:
- rating, comment, hasEmbeddedCover = musicutils.getAdditionalSongTags(itemid, rating, API, kodicursor, emby_db, self.enableimportsongrating, self.enableexportsongrating, self.enableupdatesongrating)
- else:
- hasEmbeddedCover = False
- comment = API.getOverview()
-
-
- ##### GET THE FILE AND PATH #####
- if self.directstream:
- path = "%s/emby/Audio/%s/" % (self.server, itemid)
- filename = "stream.mp3"
- else:
- playurl = API.getFilePath()
-
- if "\\" in playurl:
- # Local path
- filename = playurl.rsplit("\\", 1)[1]
- else: # Network share
- filename = playurl.rsplit("/", 1)[1]
-
- # Direct paths is set the Kodi way
- if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(playurl):
- # Validate the path is correct with user intervention
- utils.window('emby_directPath', clear=True)
- resp = xbmcgui.Dialog().yesno(
- heading="Can't validate path",
- line1=(
- "Kodi can't locate file: %s. Verify the path. "
- "You may to verify your network credentials in the "
- "add-on settings or use the emby path substitution "
- "to format your path correctly. Stop syncing?"
- % playurl))
- if resp:
- utils.window('emby_shouldStop', value="true")
- return False
-
- path = playurl.replace(filename, "")
- utils.window('emby_pathverified', value="true")
-
- ##### UPDATE THE SONG #####
- if update_item:
- self.logMsg("UPDATE song itemid: %s - Title: %s" % (itemid, title), 1)
-
- # Update path
- query = "UPDATE path SET strPath = ? WHERE idPath = ?"
- kodicursor.execute(query, (path, pathid))
-
- # Update the song entry
- query = ' '.join((
-
- "UPDATE song",
- "SET idAlbum = ?, strArtists = ?, strGenres = ?, strTitle = ?, iTrack = ?,",
- "iDuration = ?, iYear = ?, strFilename = ?, iTimesPlayed = ?, lastplayed = ?,",
- "rating = ?, comment = ?",
- "WHERE idSong = ?"
- ))
- kodicursor.execute(query, (albumid, artists, genre, title, track, duration, year,
- filename, playcount, dateplayed, rating, comment, songid))
-
- # Update the checksum in emby table
- emby_db.updateReference(itemid, checksum)
-
- ##### OR ADD THE SONG #####
- else:
- self.logMsg("ADD song itemid: %s - Title: %s" % (itemid, title), 1)
-
- # Add path
- pathid = kodi_db.addPath(path)
-
- try:
- # Get the album
- emby_dbalbum = emby_db.getItem_byId(item['AlbumId'])
- albumid = emby_dbalbum[0]
- except KeyError:
- # Verify if there's an album associated.
- album_name = item.get('Album')
- if album_name:
- self.logMsg("Creating virtual music album for song: %s." % itemid, 1)
- albumid = kodi_db.addAlbum(album_name, API.getProvider('MusicBrainzAlbum'))
- emby_db.addReference("%salbum%s" % (itemid, albumid), albumid, "MusicAlbum_", "album")
- else:
- # No album Id associated to the song.
- self.logMsg("Song itemid: %s has no albumId associated." % itemid, 1)
- return False
-
- except TypeError:
- # No album found. Let's create it
- self.logMsg("Album database entry missing.", 1)
- emby_albumId = item['AlbumId']
- album = emby.getItem(emby_albumId)
- self.add_updateAlbum(album)
- emby_dbalbum = emby_db.getItem_byId(emby_albumId)
- try:
- albumid = emby_dbalbum[0]
- self.logMsg("Found albumid: %s" % albumid, 1)
- except TypeError:
- # No album found, create a single's album
- self.logMsg("Failed to add album. Creating singles.", 1)
- kodicursor.execute("select coalesce(max(idAlbum),0) from album")
- albumid = kodicursor.fetchone()[0] + 1
- if kodiversion == 16:
- # Kodi Jarvis
- query = (
- '''
- INSERT INTO album(idAlbum, strGenres, iYear, strReleaseType)
-
- VALUES (?, ?, ?, ?)
- '''
- )
- kodicursor.execute(query, (albumid, genre, year, "single"))
- elif kodiversion == 15:
- # Kodi Isengard
- query = (
- '''
- INSERT INTO album(idAlbum, strGenres, iYear, dateAdded, strReleaseType)
-
- VALUES (?, ?, ?, ?, ?)
- '''
- )
- kodicursor.execute(query, (albumid, genre, year, dateadded, "single"))
- else:
- # Kodi Helix
- query = (
- '''
- INSERT INTO album(idAlbum, strGenres, iYear, dateAdded)
-
- VALUES (?, ?, ?, ?)
- '''
- )
- kodicursor.execute(query, (albumid, genre, year, dateadded))
-
- # Create the song entry
- kodicursor.execute("select coalesce(max(idSong),0) from song")
- songid = kodicursor.fetchone()[0] + 1
- query = (
- '''
- INSERT INTO song(
- idSong, idAlbum, idPath, strArtists, strGenres, strTitle, iTrack,
- iDuration, iYear, strFileName, strMusicBrainzTrackID, iTimesPlayed, lastplayed,
- rating)
-
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- '''
- )
- kodicursor.execute(query, (songid, albumid, pathid, artists, genre, title, track,
- duration, year, filename, musicBrainzId, playcount, dateplayed, rating))
-
- # Create the reference in emby table
- emby_db.addReference(itemid, songid, "Audio", "song", pathid=pathid, parentid=albumid,
- checksum=checksum)
-
-
- # Link song to album
- query = (
- '''
- INSERT OR REPLACE INTO albuminfosong(
- idAlbumInfoSong, idAlbumInfo, iTrack, strTitle, iDuration)
-
- VALUES (?, ?, ?, ?, ?)
- '''
- )
- kodicursor.execute(query, (songid, albumid, track, title, duration))
-
- # Link song to artists
- for index, artist in enumerate(item['ArtistItems']):
-
- artist_name = artist['Name']
- artist_eid = artist['Id']
- artist_edb = emby_db.getItem_byId(artist_eid)
- try:
- artistid = artist_edb[0]
- except TypeError:
- # Artist is missing from emby database, add it.
- artist_full = emby.getItem(artist_eid)
- self.add_updateArtist(artist_full)
- artist_edb = emby_db.getItem_byId(artist_eid)
- artistid = artist_edb[0]
- finally:
- query = (
- '''
- INSERT OR REPLACE INTO song_artist(idArtist, idSong, iOrder, strArtist)
-
- VALUES (?, ?, ?, ?)
- '''
- )
- kodicursor.execute(query, (artistid, songid, index, artist_name))
-
- # Verify if album artist exists
- album_artists = []
- for artist in item['AlbumArtists']:
-
- artist_name = artist['Name']
- album_artists.append(artist_name)
- artist_eid = artist['Id']
- artist_edb = emby_db.getItem_byId(artist_eid)
- try:
- artistid = artist_edb[0]
- except TypeError:
- # Artist is missing from emby database, add it.
- artist_full = emby.getItem(artist_eid)
- self.add_updateArtist(artist_full)
- artist_edb = emby_db.getItem_byId(artist_eid)
- artistid = artist_edb[0]
- finally:
- query = (
- '''
- INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist)
-
- VALUES (?, ?, ?)
- '''
- )
- kodicursor.execute(query, (artistid, albumid, artist_name))
- # Update discography
- if item.get('Album'):
- query = (
- '''
- INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear)
-
- VALUES (?, ?, ?)
- '''
- )
- kodicursor.execute(query, (artistid, item['Album'], 0))
- else:
- album_artists = " / ".join(album_artists)
- query = ' '.join((
-
- "SELECT strArtists",
- "FROM album",
- "WHERE idAlbum = ?"
- ))
- kodicursor.execute(query, (albumid,))
- result = kodicursor.fetchone()
- if result and result[0] != album_artists:
- # Field is empty
- if kodiversion in (16, 17):
- # Kodi Jarvis, Krypton
- query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?"
- kodicursor.execute(query, (album_artists, albumid))
- elif kodiversion == 15:
- # Kodi Isengard
- query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?"
- kodicursor.execute(query, (album_artists, albumid))
- else:
- # Kodi Helix
- query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?"
- kodicursor.execute(query, (album_artists, albumid))
-
- # Add genres
- kodi_db.addMusicGenres(songid, genres, "song")
-
- # Update artwork
- allart = artwork.getAllArtwork(item, parentInfo=True)
- if hasEmbeddedCover:
- allart["Primary"] = "image://music@" + artwork.single_urlencode( playurl )
- artwork.addArtwork(allart, songid, "song", kodicursor)
-
- if item.get('AlbumId') is None:
- # Update album artwork
- artwork.addArtwork(allart, albumid, "album", kodicursor)
-
- def updateUserdata(self, item):
- # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
- # Poster with progress bar
- kodicursor = self.kodicursor
- emby_db = self.emby_db
- kodi_db = self.kodi_db
- API = api.API(item)
-
- # Get emby information
- itemid = item['Id']
- checksum = API.getChecksum()
- userdata = API.getUserData()
- runtime = API.getRuntime()
- rating = userdata['UserRating']
-
- # Get Kodi information
- emby_dbitem = emby_db.getItem_byId(itemid)
- try:
- kodiid = emby_dbitem[0]
- mediatype = emby_dbitem[4]
- self.logMsg("Update playstate for %s: %s" % (mediatype, item['Name']), 1)
- except TypeError:
- return
-
- if mediatype == "song":
-
- #should we ignore this item ?
- #happens when userdata updated by ratings method
- if utils.window("ignore-update-%s" %itemid):
- utils.window("ignore-update-%s" %itemid,clear=True)
- return
-
- # Process playstates
- playcount = userdata['PlayCount']
- dateplayed = userdata['LastPlayedDate']
-
- #process item ratings
- rating, comment, hasEmbeddedCover = musicutils.getAdditionalSongTags(itemid, rating, API, kodicursor, emby_db, self.enableimportsongrating, self.enableexportsongrating, self.enableupdatesongrating)
-
- query = "UPDATE song SET iTimesPlayed = ?, lastplayed = ?, rating = ? WHERE idSong = ?"
- kodicursor.execute(query, (playcount, dateplayed, rating, kodiid))
-
- elif mediatype == "album":
- # Process playstates
- query = "UPDATE album SET iRating = ? WHERE idAlbum = ?"
- kodicursor.execute(query, (rating, kodiid))
-
- emby_db.updateReference(itemid, checksum)
-
- def remove(self, itemid):
- # Remove kodiid, fileid, pathid, emby reference
- emby_db = self.emby_db
- kodicursor = self.kodicursor
- artwork = self.artwork
-
- emby_dbitem = emby_db.getItem_byId(itemid)
- try:
- kodiid = emby_dbitem[0]
- mediatype = emby_dbitem[4]
- self.logMsg("Removing %s kodiid: %s" % (mediatype, kodiid), 1)
- except TypeError:
- return
-
- ##### PROCESS ITEM #####
-
- # Remove the emby reference
- emby_db.removeItem(itemid)
-
-
- ##### IF SONG #####
-
- if mediatype == "song":
- # Delete song
- self.removeSong(kodiid)
- # This should only address single song scenario, where server doesn't actually
- # create an album for the song.
- customitems = emby_db.getItem_byWildId(itemid)
- emby_db.removeWildItem(itemid)
-
- for item in customitems:
-
- item_kid = item[0]
- item_mediatype = item[1]
-
- if item_mediatype == "album":
- childs = emby_db.getItem_byParentId(item_kid, "song")
- if not childs:
- # Delete album
- self.removeAlbum(item_kid)
-
- ##### IF ALBUM #####
-
- elif mediatype == "album":
- # Delete songs, album
- album_songs = emby_db.getItem_byParentId(kodiid, "song")
- for song in album_songs:
- self.removeSong(song[1])
- else:
- # Remove emby songs
- emby_db.removeItems_byParentId(kodiid, "song")
-
- # Remove the album
- self.removeAlbum(kodiid)
-
- ##### IF ARTIST #####
-
- elif mediatype == "artist":
- # Delete songs, album, artist
- albums = emby_db.getItem_byParentId(kodiid, "album")
- for album in albums:
- albumid = album[1]
- album_songs = emby_db.getItem_byParentId(albumid, "song")
- for song in album_songs:
- self.removeSong(song[1])
- else:
- # Remove emby song
- emby_db.removeItems_byParentId(albumid, "song")
- # Remove emby artist
- emby_db.removeItems_byParentId(albumid, "artist")
- # Remove kodi album
- self.removeAlbum(albumid)
- else:
- # Remove emby albums
- emby_db.removeItems_byParentId(kodiid, "album")
-
- # Remove artist
- self.removeArtist(kodiid)
-
- self.logMsg("Deleted %s: %s from kodi database" % (mediatype, itemid), 1)
-
- def removeSong(self, kodiid):
-
- kodicursor = self.kodicursor
- artwork = self.artwork
-
- artwork.deleteArtwork(kodiid, "song", kodicursor)
- kodicursor.execute("DELETE FROM song WHERE idSong = ?", (kodiid,))
-
- def removeAlbum(self, kodiid):
-
- kodicursor = self.kodicursor
- artwork = self.artwork
-
- artwork.deleteArtwork(kodiid, "album", kodicursor)
- kodicursor.execute("DELETE FROM album WHERE idAlbum = ?", (kodiid,))
-
- def removeArtist(self, kodiid):
-
- kodicursor = self.kodicursor
- artwork = self.artwork
-
- artwork.deleteArtwork(kodiid, "artist", kodicursor)
- kodicursor.execute("DELETE FROM artist WHERE idArtist = ?", (kodiid,))
\ No newline at end of file
+# -*- coding: utf-8 -*-
+
+##################################################################################################
+
+import urllib
+from ntpath import dirname
+from datetime import datetime
+
+import xbmc
+import xbmcgui
+import xbmcvfs
+
+import api
+import artwork
+import clientinfo
+import downloadutils
+import utils
+import embydb_functions as embydb
+import kodidb_functions as kodidb
+import read_embyserver as embyserver
+import musicutils
+
+##################################################################################################
+
+
+class Items(object):
+
+
+ def __init__(self, embycursor, kodicursor):
+
+ self.embycursor = embycursor
+ self.kodicursor = kodicursor
+
+ self.clientInfo = clientinfo.ClientInfo()
+ self.addonName = self.clientInfo.getAddonName()
+ self.doUtils = downloadutils.DownloadUtils()
+
+ self.kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
+ self.directpath = utils.settings('useDirectPaths') == "1"
+ self.music_enabled = utils.settings('enableMusic') == "true"
+ self.contentmsg = utils.settings('newContent') == "true"
+ self.newvideo_time = int(utils.settings('newvideotime'))*1000
+ self.newmusic_time = int(utils.settings('newmusictime'))*1000
+
+ self.artwork = artwork.Artwork()
+ self.emby = embyserver.Read_EmbyServer()
+ self.emby_db = embydb.Embydb_Functions(embycursor)
+ self.kodi_db = kodidb.Kodidb_Functions(kodicursor)
+
+ def logMsg(self, msg, lvl=1):
+
+ className = self.__class__.__name__
+ utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
+
+
+ def itemsbyId(self, items, process, pdialog=None):
+ # Process items by itemid. Process can be added, update, userdata, remove
+ emby = self.emby
+ embycursor = self.embycursor
+ kodicursor = self.kodicursor
+ music_enabled = self.music_enabled
+
+ itemtypes = {
+
+ 'Movie': Movies,
+ 'BoxSet': Movies,
+ 'Series': TVShows,
+ 'Season': TVShows,
+ 'Episode': TVShows,
+ 'MusicAlbum': Music,
+ 'MusicArtist': Music,
+ 'AlbumArtist': Music,
+ 'Audio': Music
+ }
+
+ update_videolibrary = False
+ total = 0
+ for item in items:
+ total += len(items[item])
+
+ if total == 0:
+ return False
+
+ self.logMsg("Processing %s: %s" % (process, items), 1)
+ if pdialog:
+ pdialog.update(heading="Processing %s: %s items" % (process, total))
+
+ count = 0
+ for itemtype in items:
+
+ # Safety check
+ if not itemtypes.get(itemtype):
+ # We don't process this type of item
+ continue
+
+ itemlist = items[itemtype]
+ if not itemlist:
+ # The list to process is empty
+ continue
+
+ musicconn = None
+
+ if itemtype in ('MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio'):
+ if music_enabled:
+ musicconn = utils.kodiSQL('music')
+ musiccursor = musicconn.cursor()
+ items_process = itemtypes[itemtype](embycursor, musiccursor)
+ else:
+ # Music is not enabled, do not proceed with itemtype
+ continue
+ else:
+ update_videolibrary = True
+ items_process = itemtypes[itemtype](embycursor, kodicursor)
+
+ if itemtype == "Movie":
+ actions = {
+ 'added': items_process.added,
+ 'update': items_process.add_update,
+ 'userdata': items_process.updateUserdata,
+ 'remove': items_process.remove
+ }
+ elif itemtype == "BoxSet":
+ actions = {
+ 'added': items_process.added_boxset,
+ 'update': items_process.add_updateBoxset,
+ 'remove': items_process.remove
+ }
+ elif itemtype == "MusicVideo":
+ actions = {
+ 'added': items_process.added,
+ 'update': items_process.add_update,
+ 'userdata': items_process.updateUserdata,
+ 'remove': items_process.remove
+ }
+ elif itemtype == "Series":
+ actions = {
+ 'added': items_process.added,
+ 'update': items_process.add_update,
+ 'userdata': items_process.updateUserdata,
+ 'remove': items_process.remove
+ }
+ elif itemtype == "Season":
+ actions = {
+ 'added': items_process.added_season,
+ 'update': items_process.add_updateSeason,
+ 'remove': items_process.remove
+ }
+ elif itemtype == "Episode":
+ actions = {
+ 'added': items_process.added_episode,
+ 'update': items_process.add_updateEpisode,
+ 'userdata': items_process.updateUserdata,
+ 'remove': items_process.remove
+ }
+ elif itemtype == "MusicAlbum":
+ actions = {
+ 'added': items_process.added_album,
+ 'update': items_process.add_updateAlbum,
+ 'userdata': items_process.updateUserdata,
+ 'remove': items_process.remove
+ }
+ elif itemtype in ("MusicArtist", "AlbumArtist"):
+ actions = {
+ 'added': items_process.added,
+ 'update': items_process.add_updateArtist,
+ 'remove': items_process.remove
+ }
+ elif itemtype == "Audio":
+ actions = {
+ 'added': items_process.added_song,
+ 'update': items_process.add_updateSong,
+ 'userdata': items_process.updateUserdata,
+ 'remove': items_process.remove
+ }
+ else:
+ self.logMsg("Unsupported itemtype: %s." % itemtype, 1)
+ actions = {}
+
+ if actions.get(process):
+
+ if process == "remove":
+ for item in itemlist:
+ actions[process](item)
+
+ elif process == "added":
+ actions[process](itemlist, pdialog)
+
+ else:
+ processItems = emby.getFullItems(itemlist)
+ for item in processItems:
+
+ title = item['Name']
+
+ if itemtype == "Episode":
+ title = "%s - %s" % (item['SeriesName'], title)
+
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=title)
+ count += 1
+
+ actions[process](item)
+
+
+ if musicconn is not None:
+ # close connection for special types
+ self.logMsg("Updating music database.", 1)
+ musicconn.commit()
+ musiccursor.close()
+
+ return (True, update_videolibrary)
+
+ def contentPop(self, name, time=5000):
+
+ if time:
+ # It's possible for the time to be 0. It should be considered disabled in this case.
+ xbmcgui.Dialog().notification(
+ heading="Emby for Kodi",
+ message="Added: %s" % name,
+ icon="special://home/addons/plugin.video.emby/icon.png",
+ time=time,
+ sound=False)
+
+
+class Movies(Items):
+
+
+ def __init__(self, embycursor, kodicursor):
+ Items.__init__(self, embycursor, kodicursor)
+
+ def added(self, items, pdialog):
+
+ total = len(items)
+ count = 0
+ for movie in items:
+
+ title = movie['Name']
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=title)
+ count += 1
+ self.add_update(movie)
+ if not pdialog and self.contentmsg:
+ self.contentPop(title, self.newvideo_time)
+
+ def added_boxset(self, items, pdialog):
+
+ total = len(items)
+ count = 0
+ for boxset in items:
+
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=boxset['Name'])
+ count += 1
+ self.add_updateBoxset(boxset)
+
+
+ def add_update(self, item, viewtag=None, viewid=None):
+ # Process single movie
+ kodicursor = self.kodicursor
+ emby_db = self.emby_db
+ artwork = self.artwork
+ API = api.API(item)
+
+ # If the item already exist in the local Kodi DB we'll perform a full item update
+ # If the item doesn't exist, we'll add it to the database
+ update_item = True
+ itemid = item['Id']
+ emby_dbitem = emby_db.getItem_byId(itemid)
+ try:
+ movieid = emby_dbitem[0]
+ fileid = emby_dbitem[1]
+ pathid = emby_dbitem[2]
+ self.logMsg("movieid: %s fileid: %s pathid: %s" % (movieid, fileid, pathid), 1)
+
+ except TypeError:
+ update_item = False
+ self.logMsg("movieid: %s not found." % itemid, 2)
+ # movieid
+ kodicursor.execute("select coalesce(max(idMovie),0) from movie")
+ movieid = kodicursor.fetchone()[0] + 1
+
+ else:
+ # Verification the item is still in Kodi
+ query = "SELECT * FROM movie WHERE idMovie = ?"
+ kodicursor.execute(query, (movieid,))
+ try:
+ kodicursor.fetchone()[0]
+ except TypeError:
+ # item is not found, let's recreate it.
+ update_item = False
+ self.logMsg("movieid: %s missing from Kodi, repairing the entry." % movieid, 1)
+
+ if not viewtag or not viewid:
+ # Get view tag from emby
+ viewtag, viewid, mediatype = self.emby.getView_embyId(itemid)
+ self.logMsg("View tag found: %s" % viewtag, 2)
+
+ # fileId information
+ checksum = API.getChecksum()
+ dateadded = API.getDateCreated()
+ userdata = API.getUserData()
+ playcount = userdata['PlayCount']
+ dateplayed = userdata['LastPlayedDate']
+
+ # item details
+ people = API.getPeople()
+ writer = " / ".join(people['Writer'])
+ director = " / ".join(people['Director'])
+ genres = item['Genres']
+ title = item['Name']
+ plot = API.getOverview()
+ shortplot = item.get('ShortOverview')
+ tagline = API.getTagline()
+ votecount = item.get('VoteCount')
+ rating = item.get('CommunityRating')
+ year = item.get('ProductionYear')
+ imdb = API.getProvider('Imdb')
+ sorttitle = item['SortName']
+ runtime = API.getRuntime()
+ mpaa = API.getMpaa()
+ genre = " / ".join(genres)
+ country = API.getCountry()
+ studios = API.getStudios()
+ try:
+ studio = studios[0]
+ except IndexError:
+ studio = None
+
+ if item.get('LocalTrailerCount'):
+ # There's a local trailer
+ url = (
+ "{server}/emby/Users/{UserId}/Items/%s/LocalTrailers?format=json"
+ % itemid
+ )
+ result = self.doUtils.downloadUrl(url)
+ try:
+ trailer = "plugin://plugin.video.emby/trailer/?id=%s&mode=play" % result[0]['Id']
+ except IndexError:
+ self.logMsg("Failed to process local trailer.", 1)
+ trailer = None
+ else:
+ # Try to get the youtube trailer
+ try:
+ trailer = item['RemoteTrailers'][0]['Url']
+ except (KeyError, IndexError):
+ trailer = None
+ else:
+ try:
+ trailerId = trailer.rsplit('=', 1)[1]
+ except IndexError:
+ self.logMsg("Failed to process trailer: %s" % trailer, 1)
+ trailer = None
+ else:
+ trailer = "plugin://plugin.video.youtube/play/?video_id=%s" % trailerId
+
+
+ ##### GET THE FILE AND PATH #####
+ playurl = API.getFilePath()
+
+ if "\\" in playurl:
+ # Local path
+ filename = playurl.rsplit("\\", 1)[1]
+ else: # Network share
+ filename = playurl.rsplit("/", 1)[1]
+
+ if self.directpath:
+ # Direct paths is set the Kodi way
+ if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(playurl):
+ # Validate the path is correct with user intervention
+ resp = xbmcgui.Dialog().yesno(
+ heading="Can't validate path",
+ line1=(
+ "Kodi can't locate file: %s. Verify the path. "
+ "You may to verify your network credentials in the "
+ "add-on settings or use the emby path substitution "
+ "to format your path correctly. Stop syncing?"
+ % playurl))
+ if resp:
+ utils.window('emby_shouldStop', value="true")
+ return False
+
+ path = playurl.replace(filename, "")
+ utils.window('emby_pathverified', value="true")
+ else:
+ # Set plugin path and media flags using real filename
+ path = "plugin://plugin.video.emby.movies/"
+ params = {
+
+ 'filename': filename.encode('utf-8'),
+ 'id': itemid,
+ 'dbid': movieid,
+ 'mode': "play"
+ }
+ filename = "%s?%s" % (path, urllib.urlencode(params))
+
+
+ ##### UPDATE THE MOVIE #####
+ if update_item:
+ self.logMsg("UPDATE movie itemid: %s - Title: %s" % (itemid, title), 1)
+
+ # Update the movie entry
+ query = ' '.join((
+
+ "UPDATE movie",
+ "SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?,",
+ "c07 = ?, c09 = ?, c10 = ?, c11 = ?, c12 = ?, c14 = ?, c15 = ?,",
+ "c16 = ?, c18 = ?, c19 = ?, c21 = ?",
+ "WHERE idMovie = ?"
+ ))
+ kodicursor.execute(query, (title, plot, shortplot, tagline, votecount, rating, writer,
+ year, imdb, sorttitle, runtime, mpaa, genre, director, title, studio, trailer,
+ country, movieid))
+
+ # Update the checksum in emby table
+ emby_db.updateReference(itemid, checksum)
+
+ ##### OR ADD THE MOVIE #####
+ else:
+ self.logMsg("ADD movie itemid: %s - Title: %s" % (itemid, title), 1)
+
+ # Add path
+ pathid = self.kodi_db.addPath(path)
+ # Add the file
+ fileid = self.kodi_db.addFile(filename, pathid)
+
+ # Create the movie entry
+ query = (
+ '''
+ INSERT INTO movie(
+ idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07,
+ c09, c10, c11, c12, c14, c15, c16, c18, c19, c21)
+
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ '''
+ )
+ kodicursor.execute(query, (movieid, fileid, title, plot, shortplot, tagline, votecount,
+ rating, writer, year, imdb, sorttitle, runtime, mpaa, genre, director, title,
+ studio, trailer, country))
+
+ # Create the reference in emby table
+ emby_db.addReference(itemid, movieid, "Movie", "movie", fileid, pathid, None, checksum, viewid)
+
+ # Update the path
+ query = ' '.join((
+
+ "UPDATE path",
+ "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?",
+ "WHERE idPath = ?"
+ ))
+ kodicursor.execute(query, (path, "movies", "metadata.local", 1, pathid))
+
+ # Update the file
+ query = ' '.join((
+
+ "UPDATE files",
+ "SET idPath = ?, strFilename = ?, dateAdded = ?",
+ "WHERE idFile = ?"
+ ))
+ kodicursor.execute(query, (pathid, filename, dateadded, fileid))
+
+ # Process countries
+ self.kodi_db.addCountries(movieid, item['ProductionLocations'], "movie")
+ # Process cast
+ people = artwork.getPeopleArtwork(item['People'])
+ self.kodi_db.addPeople(movieid, people, "movie")
+ # Process genres
+ self.kodi_db.addGenres(movieid, genres, "movie")
+ # Process artwork
+ artwork.addArtwork(artwork.getAllArtwork(item), movieid, "movie", kodicursor)
+ # Process stream details
+ streams = API.getMediaStreams()
+ self.kodi_db.addStreams(fileid, streams, runtime)
+ # Process studios
+ self.kodi_db.addStudios(movieid, studios, "movie")
+ # Process tags: view, emby tags
+ tags = [viewtag]
+ tags.extend(item['Tags'])
+ if userdata['Favorite']:
+ tags.append("Favorite movies")
+ self.kodi_db.addTags(movieid, tags, "movie")
+ # Process playstates
+ resume = API.adjustResume(userdata['Resume'])
+ total = round(float(runtime), 6)
+ self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed)
+
+ def add_updateBoxset(self, boxset):
+
+ emby = self.emby
+ emby_db = self.emby_db
+ artwork = self.artwork
+
+ boxsetid = boxset['Id']
+ title = boxset['Name']
+ checksum = boxset['Etag']
+ emby_dbitem = emby_db.getItem_byId(boxsetid)
+ try:
+ setid = emby_dbitem[0]
+
+ except TypeError:
+ setid = self.kodi_db.createBoxset(title)
+
+ # Process artwork
+ artwork.addArtwork(artwork.getAllArtwork(boxset), setid, "set", self.kodicursor)
+
+ # Process movies inside boxset
+ current_movies = emby_db.getItemId_byParentId(setid, "movie")
+ process = []
+ try:
+ # Try to convert tuple to dictionary
+ current = dict(current_movies)
+ except ValueError:
+ current = {}
+
+ # Sort current titles
+ for current_movie in current:
+ process.append(current_movie)
+
+ # New list to compare
+ for movie in emby.getMovies_byBoxset(boxsetid)['Items']:
+
+ itemid = movie['Id']
+
+ if not current.get(itemid):
+ # Assign boxset to movie
+ emby_dbitem = emby_db.getItem_byId(itemid)
+ try:
+ movieid = emby_dbitem[0]
+ except TypeError:
+ self.logMsg("Failed to add: %s to boxset." % movie['Name'], 1)
+ continue
+
+ self.logMsg("New addition to boxset %s: %s" % (title, movie['Name']), 1)
+ self.kodi_db.assignBoxset(setid, movieid)
+ # Update emby reference
+ emby_db.updateParentId(itemid, setid)
+ else:
+ # Remove from process, because the item still belongs
+ process.remove(itemid)
+
+ # Process removals from boxset
+ for movie in process:
+ movieid = current[movie]
+ self.logMsg("Remove from boxset %s: %s" % (title, movieid))
+ self.kodi_db.removefromBoxset(movieid)
+ # Update emby reference
+ emby_db.updateParentId(movie, None)
+
+ # Update the reference in the emby table
+ emby_db.addReference(boxsetid, setid, "BoxSet", mediatype="set", checksum=checksum)
+
+ def updateUserdata(self, item):
+ # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
+ # Poster with progress bar
+ emby_db = self.emby_db
+ API = api.API(item)
+
+ # Get emby information
+ itemid = item['Id']
+ checksum = API.getChecksum()
+ userdata = API.getUserData()
+ runtime = API.getRuntime()
+
+ # Get Kodi information
+ emby_dbitem = emby_db.getItem_byId(itemid)
+ try:
+ movieid = emby_dbitem[0]
+ fileid = emby_dbitem[1]
+ self.logMsg(
+ "Update playstate for movie: %s fileid: %s"
+ % (item['Name'], fileid), 1)
+ except TypeError:
+ return
+
+ # Process favorite tags
+ if userdata['Favorite']:
+ self.kodi_db.addTag(movieid, "Favorite movies", "movie")
+ else:
+ self.kodi_db.removeTag(movieid, "Favorite movies", "movie")
+
+ # Process playstates
+ playcount = userdata['PlayCount']
+ dateplayed = userdata['LastPlayedDate']
+ resume = API.adjustResume(userdata['Resume'])
+ total = round(float(runtime), 6)
+
+ self.logMsg("%s New resume point: %s" % (itemid, resume))
+
+ self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed)
+ emby_db.updateReference(itemid, checksum)
+
+ def remove(self, itemid):
+ # Remove movieid, fileid, emby reference
+ emby_db = self.emby_db
+ kodicursor = self.kodicursor
+ artwork = self.artwork
+
+ emby_dbitem = emby_db.getItem_byId(itemid)
+ try:
+ kodiid = emby_dbitem[0]
+ fileid = emby_dbitem[1]
+ mediatype = emby_dbitem[4]
+ self.logMsg("Removing %sid: %s fileid: %s" % (mediatype, kodiid, fileid), 1)
+ except TypeError:
+ return
+
+ # Remove the emby reference
+ emby_db.removeItem(itemid)
+ # Remove artwork
+ artwork.deleteArtwork(kodiid, mediatype, kodicursor)
+
+ if mediatype == "movie":
+ # Delete kodi movie and file
+ kodicursor.execute("DELETE FROM movie WHERE idMovie = ?", (kodiid,))
+ kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,))
+
+ elif mediatype == "set":
+ # Delete kodi boxset
+ boxset_movies = emby_db.getItem_byParentId(kodiid, "movie")
+ for movie in boxset_movies:
+ embyid = movie[0]
+ movieid = movie[1]
+ self.kodi_db.removefromBoxset(movieid)
+ # Update emby reference
+ emby_db.updateParentId(embyid, None)
+
+ kodicursor.execute("DELETE FROM sets WHERE idSet = ?", (kodiid,))
+
+ self.logMsg("Deleted %s %s from kodi database" % (mediatype, itemid), 1)
+
+class MusicVideos(Items):
+
+
+ def __init__(self, embycursor, kodicursor):
+ Items.__init__(self, embycursor, kodicursor)
+
+ def added(self, items, pdialog):
+
+ total = len(items)
+ count = 0
+ for mvideo in items:
+
+ title = mvideo['Name']
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=title)
+ count += 1
+ self.add_update(mvideo)
+ if not pdialog and self.contentmsg:
+ self.contentPop(title, self.newvideo_time)
+
+
+ def add_update(self, item, viewtag=None, viewid=None):
+ # Process single music video
+ kodicursor = self.kodicursor
+ emby_db = self.emby_db
+ artwork = self.artwork
+ API = api.API(item)
+
+ # If the item already exist in the local Kodi DB we'll perform a full item update
+ # If the item doesn't exist, we'll add it to the database
+ update_item = True
+ itemid = item['Id']
+ emby_dbitem = emby_db.getItem_byId(itemid)
+ try:
+ mvideoid = emby_dbitem[0]
+ fileid = emby_dbitem[1]
+ pathid = emby_dbitem[2]
+ self.logMsg("mvideoid: %s fileid: %s pathid: %s" % (mvideoid, fileid, pathid), 1)
+
+ except TypeError:
+ update_item = False
+ self.logMsg("mvideoid: %s not found." % itemid, 2)
+ # mvideoid
+ kodicursor.execute("select coalesce(max(idMVideo),0) from musicvideo")
+ mvideoid = kodicursor.fetchone()[0] + 1
+
+ else:
+ # Verification the item is still in Kodi
+ query = "SELECT * FROM musicvideo WHERE idMVideo = ?"
+ kodicursor.execute(query, (mvideoid,))
+ try:
+ kodicursor.fetchone()[0]
+ except TypeError:
+ # item is not found, let's recreate it.
+ update_item = False
+ self.logMsg("mvideoid: %s missing from Kodi, repairing the entry." % mvideoid, 1)
+
+ if not viewtag or not viewid:
+ # Get view tag from emby
+ viewtag, viewid, mediatype = self.emby.getView_embyId(itemid)
+ self.logMsg("View tag found: %s" % viewtag, 2)
+
+ # fileId information
+ checksum = API.getChecksum()
+ dateadded = API.getDateCreated()
+ userdata = API.getUserData()
+ playcount = userdata['PlayCount']
+ dateplayed = userdata['LastPlayedDate']
+
+ # item details
+ runtime = API.getRuntime()
+ plot = API.getOverview()
+ title = item['Name']
+ year = item.get('ProductionYear')
+ genres = item['Genres']
+ genre = " / ".join(genres)
+ studios = API.getStudios()
+ studio = " / ".join(studios)
+ artist = " / ".join(item.get('Artists'))
+ album = item.get('Album')
+ track = item.get('Track')
+ people = API.getPeople()
+ director = " / ".join(people['Director'])
+
+
+ ##### GET THE FILE AND PATH #####
+ playurl = API.getFilePath()
+
+ if "\\" in playurl:
+ # Local path
+ filename = playurl.rsplit("\\", 1)[1]
+ else: # Network share
+ filename = playurl.rsplit("/", 1)[1]
+
+ if self.directpath:
+ # Direct paths is set the Kodi way
+ if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(playurl):
+ # Validate the path is correct with user intervention
+ resp = xbmcgui.Dialog().yesno(
+ heading="Can't validate path",
+ line1=(
+ "Kodi can't locate file: %s. Verify the path. "
+ "You may to verify your network credentials in the "
+ "add-on settings or use the emby path substitution "
+ "to format your path correctly. Stop syncing?"
+ % playurl))
+ if resp:
+ utils.window('emby_shouldStop', value="true")
+ return False
+
+ path = playurl.replace(filename, "")
+ utils.window('emby_pathverified', value="true")
+ else:
+ # Set plugin path and media flags using real filename
+ path = "plugin://plugin.video.emby.musicvideos/"
+ params = {
+
+ 'filename': filename.encode('utf-8'),
+ 'id': itemid,
+ 'dbid': mvideoid,
+ 'mode': "play"
+ }
+ filename = "%s?%s" % (path, urllib.urlencode(params))
+
+
+ ##### UPDATE THE MUSIC VIDEO #####
+ if update_item:
+ self.logMsg("UPDATE mvideo itemid: %s - Title: %s" % (itemid, title), 1)
+
+ # Update path
+ query = "UPDATE path SET strPath = ? WHERE idPath = ?"
+ kodicursor.execute(query, (path, pathid))
+
+ # Update the filename
+ query = "UPDATE files SET strFilename = ?, dateAdded = ? WHERE idFile = ?"
+ kodicursor.execute(query, (filename, dateadded, fileid))
+
+ # Update the music video entry
+ query = ' '.join((
+
+ "UPDATE musicvideo",
+ "SET c00 = ?, c04 = ?, c05 = ?, c06 = ?, c07 = ?, c08 = ?, c09 = ?, c10 = ?,",
+ "c11 = ?, c12 = ?"
+ "WHERE idMVideo = ?"
+ ))
+ kodicursor.execute(query, (title, runtime, director, studio, year, plot, album,
+ artist, genre, track, mvideoid))
+
+ # Update the checksum in emby table
+ emby_db.updateReference(itemid, checksum)
+
+ ##### OR ADD THE MUSIC VIDEO #####
+ else:
+ self.logMsg("ADD mvideo itemid: %s - Title: %s" % (itemid, title), 1)
+
+ # Add path
+ query = ' '.join((
+
+ "SELECT idPath",
+ "FROM path",
+ "WHERE strPath = ?"
+ ))
+ kodicursor.execute(query, (path,))
+ try:
+ pathid = kodicursor.fetchone()[0]
+ except TypeError:
+ kodicursor.execute("select coalesce(max(idPath),0) from path")
+ pathid = kodicursor.fetchone()[0] + 1
+ query = (
+ '''
+ INSERT OR REPLACE INTO path(
+ idPath, strPath, strContent, strScraper, noUpdate)
+
+ VALUES (?, ?, ?, ?, ?)
+ '''
+ )
+ kodicursor.execute(query, (pathid, path, "musicvideos", "metadata.local", 1))
+
+ # Add the file
+ kodicursor.execute("select coalesce(max(idFile),0) from files")
+ fileid = kodicursor.fetchone()[0] + 1
+ query = (
+ '''
+ INSERT INTO files(
+ idFile, idPath, strFilename, dateAdded)
+
+ VALUES (?, ?, ?, ?)
+ '''
+ )
+ kodicursor.execute(query, (fileid, pathid, filename, dateadded))
+
+ # Create the musicvideo entry
+ query = (
+ '''
+ INSERT INTO musicvideo(
+ idMVideo, idFile, c00, c04, c05, c06, c07, c08, c09, c10, c11, c12)
+
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ '''
+ )
+ kodicursor.execute(query, (mvideoid, fileid, title, runtime, director, studio,
+ year, plot, album, artist, genre, track))
+
+ # Create the reference in emby table
+ emby_db.addReference(itemid, mvideoid, "MusicVideo", "musicvideo", fileid, pathid,
+ checksum=checksum, mediafolderid=viewid)
+
+
+ # Process cast
+ people = item['People']
+ artists = item['ArtistItems']
+ for artist in artists:
+ artist['Type'] = "Artist"
+ people.extend(artists)
+ people = artwork.getPeopleArtwork(people)
+ self.kodi_db.addPeople(mvideoid, people, "musicvideo")
+ # Process genres
+ self.kodi_db.addGenres(mvideoid, genres, "musicvideo")
+ # Process artwork
+ artwork.addArtwork(artwork.getAllArtwork(item), mvideoid, "musicvideo", kodicursor)
+ # Process stream details
+ streams = API.getMediaStreams()
+ self.kodi_db.addStreams(fileid, streams, runtime)
+ # Process studios
+ self.kodi_db.addStudios(mvideoid, studios, "musicvideo")
+ # Process tags: view, emby tags
+ tags = [viewtag]
+ tags.extend(item['Tags'])
+ if userdata['Favorite']:
+ tags.append("Favorite musicvideos")
+ self.kodi_db.addTags(mvideoid, tags, "musicvideo")
+ # Process playstates
+ resume = API.adjustResume(userdata['Resume'])
+ total = round(float(runtime), 6)
+ self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed)
+
+ def updateUserdata(self, item):
+ # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
+ # Poster with progress bar
+ emby_db = self.emby_db
+ API = api.API(item)
+
+ # Get emby information
+ itemid = item['Id']
+ checksum = API.getChecksum()
+ userdata = API.getUserData()
+ runtime = API.getRuntime()
+
+ # Get Kodi information
+ emby_dbitem = emby_db.getItem_byId(itemid)
+ try:
+ mvideoid = emby_dbitem[0]
+ fileid = emby_dbitem[1]
+ self.logMsg(
+ "Update playstate for musicvideo: %s fileid: %s"
+ % (item['Name'], fileid), 1)
+ except TypeError:
+ return
+
+ # Process favorite tags
+ if userdata['Favorite']:
+ self.kodi_db.addTag(mvideoid, "Favorite musicvideos", "musicvideo")
+ else:
+ self.kodi_db.removeTag(mvideoid, "Favorite musicvideos", "musicvideo")
+
+ # Process playstates
+ playcount = userdata['PlayCount']
+ dateplayed = userdata['LastPlayedDate']
+ resume = API.adjustResume(userdata['Resume'])
+ total = round(float(runtime), 6)
+
+ self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed)
+ emby_db.updateReference(itemid, checksum)
+
+ def remove(self, itemid):
+ # Remove mvideoid, fileid, pathid, emby reference
+ emby_db = self.emby_db
+ kodicursor = self.kodicursor
+ artwork = self.artwork
+
+ emby_dbitem = emby_db.getItem_byId(itemid)
+ try:
+ mvideoid = emby_dbitem[0]
+ fileid = emby_dbitem[1]
+ pathid = emby_dbitem[2]
+ self.logMsg("Removing mvideoid: %s fileid: %s" % (mvideoid, fileid, pathid), 1)
+ except TypeError:
+ return
+
+ # Remove artwork
+ query = ' '.join((
+
+ "SELECT url, type",
+ "FROM art",
+ "WHERE media_id = ?",
+ "AND media_type = 'musicvideo'"
+ ))
+ kodicursor.execute(query, (mvideoid,))
+ for row in kodicursor.fetchall():
+
+ url = row[0]
+ imagetype = row[1]
+ if imagetype in ("poster", "fanart"):
+ artwork.deleteCachedArtwork(url)
+
+ kodicursor.execute("DELETE FROM musicvideo WHERE idMVideo = ?", (mvideoid,))
+ kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,))
+ if self.directpath:
+ kodicursor.execute("DELETE FROM path WHERE idPath = ?", (pathid,))
+ self.embycursor.execute("DELETE FROM emby WHERE emby_id = ?", (itemid,))
+
+ self.logMsg("Deleted musicvideo %s from kodi database" % itemid, 1)
+
+class TVShows(Items):
+
+
+ def __init__(self, embycursor, kodicursor):
+ Items.__init__(self, embycursor, kodicursor)
+
+ def added(self, items, pdialog):
+
+ total = len(items)
+ count = 0
+ for tvshow in items:
+
+ title = tvshow['Name']
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=title)
+ count += 1
+ self.add_update(tvshow)
+ # Add episodes
+ all_episodes = self.emby.getEpisodesbyShow(tvshow['Id'])
+ self.added_episode(all_episodes['Items'], pdialog)
+
+ def added_season(self, items, pdialog):
+
+ total = len(items)
+ count = 0
+ for season in items:
+
+ title = "%s - %s" % (season.get('SeriesName', "Unknown"), season['Name'])
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=title)
+ count += 1
+ self.add_updateSeason(season)
+ # Add episodes
+ all_episodes = self.emby.getEpisodesbySeason(season['Id'])
+ self.added_episode(all_episodes['Items'], pdialog)
+
+ def added_episode(self, items, pdialog):
+
+ total = len(items)
+ count = 0
+ for episode in items:
+ title = "%s - %s" % (episode.get('SeriesName', "Unknown"), episode['Name'])
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=title)
+ count += 1
+ self.add_updateEpisode(episode)
+ if not pdialog and self.contentmsg:
+ self.contentPop(title, self.newvideo_time)
+
+
+ def add_update(self, item, viewtag=None, viewid=None):
+ # Process single tvshow
+ kodicursor = self.kodicursor
+ emby = self.emby
+ emby_db = self.emby_db
+ artwork = self.artwork
+ API = api.API(item)
+
+ if utils.settings('syncEmptyShows') == "false" and not item['RecursiveItemCount']:
+ self.logMsg("Skipping empty show: %s" % item['Name'], 1)
+ return
+ # If the item already exist in the local Kodi DB we'll perform a full item update
+ # If the item doesn't exist, we'll add it to the database
+ update_item = True
+ force_episodes = False
+ itemid = item['Id']
+ emby_dbitem = emby_db.getItem_byId(itemid)
+ try:
+ showid = emby_dbitem[0]
+ pathid = emby_dbitem[2]
+ self.logMsg("showid: %s pathid: %s" % (showid, pathid), 1)
+
+ except TypeError:
+ update_item = False
+ self.logMsg("showid: %s not found." % itemid, 2)
+ kodicursor.execute("select coalesce(max(idShow),0) from tvshow")
+ showid = kodicursor.fetchone()[0] + 1
+
+ else:
+ # Verification the item is still in Kodi
+ query = "SELECT * FROM tvshow WHERE idShow = ?"
+ kodicursor.execute(query, (showid,))
+ try:
+ kodicursor.fetchone()[0]
+ except TypeError:
+ # item is not found, let's recreate it.
+ update_item = False
+ self.logMsg("showid: %s missing from Kodi, repairing the entry." % showid, 1)
+ # Force re-add episodes after the show is re-created.
+ force_episodes = True
+
+
+ if viewtag is None or viewid is None:
+ # Get view tag from emby
+ viewtag, viewid, mediatype = emby.getView_embyId(itemid)
+ self.logMsg("View tag found: %s" % viewtag, 2)
+
+ # fileId information
+ checksum = API.getChecksum()
+ dateadded = API.getDateCreated()
+ userdata = API.getUserData()
+ playcount = userdata['PlayCount']
+ dateplayed = userdata['LastPlayedDate']
+
+ # item details
+ genres = item['Genres']
+ title = item['Name']
+ plot = API.getOverview()
+ rating = item.get('CommunityRating')
+ premieredate = API.getPremiereDate()
+ tvdb = API.getProvider('Tvdb')
+ sorttitle = item['SortName']
+ mpaa = API.getMpaa()
+ genre = " / ".join(genres)
+ studios = API.getStudios()
+ studio = " / ".join(studios)
+
+
+ ##### GET THE FILE AND PATH #####
+ playurl = API.getFilePath()
+
+ if self.directpath:
+ # Direct paths is set the Kodi way
+ if "\\" in playurl:
+ # Local path
+ path = "%s\\" % playurl
+ toplevelpath = "%s\\" % dirname(dirname(path))
+ else:
+ # Network path
+ path = "%s/" % playurl
+ toplevelpath = "%s/" % dirname(dirname(path))
+
+ if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(path):
+ # Validate the path is correct with user intervention
+ resp = xbmcgui.Dialog().yesno(
+ heading="Can't validate path",
+ line1=(
+ "Kodi can't locate file: %s. Verify the path. "
+ "You may to verify your network credentials in the "
+ "add-on settings or use the emby path substitution "
+ "to format your path correctly. Stop syncing?"
+ % playurl))
+ if resp:
+ utils.window('emby_shouldStop', value="true")
+ return False
+
+ utils.window('emby_pathverified', value="true")
+ else:
+ # Set plugin path
+ toplevelpath = "plugin://plugin.video.emby.tvshows/"
+ path = "%s%s/" % (toplevelpath, itemid)
+
+
+ ##### UPDATE THE TVSHOW #####
+ if update_item:
+ self.logMsg("UPDATE tvshow itemid: %s - Title: %s" % (itemid, title), 1)
+
+ # Update the tvshow entry
+ query = ' '.join((
+
+ "UPDATE tvshow",
+ "SET c00 = ?, c01 = ?, c04 = ?, c05 = ?, c08 = ?, c09 = ?,",
+ "c12 = ?, c13 = ?, c14 = ?, c15 = ?",
+ "WHERE idShow = ?"
+ ))
+ kodicursor.execute(query, (title, plot, rating, premieredate, genre, title,
+ tvdb, mpaa, studio, sorttitle, showid))
+
+ # Update the checksum in emby table
+ emby_db.updateReference(itemid, checksum)
+
+ ##### OR ADD THE TVSHOW #####
+ else:
+ self.logMsg("ADD tvshow itemid: %s - Title: %s" % (itemid, title), 1)
+
+ # Add top path
+ toppathid = self.kodi_db.addPath(toplevelpath)
+ query = ' '.join((
+
+ "UPDATE path",
+ "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?",
+ "WHERE idPath = ?"
+ ))
+ kodicursor.execute(query, (toplevelpath, "tvshows", "metadata.local", 1, toppathid))
+
+ # Add path
+ pathid = self.kodi_db.addPath(path)
+
+ # Create the tvshow entry
+ query = (
+ '''
+ INSERT INTO tvshow(
+ idShow, c00, c01, c04, c05, c08, c09, c12, c13, c14, c15)
+
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ '''
+ )
+ kodicursor.execute(query, (showid, title, plot, rating, premieredate, genre,
+ title, tvdb, mpaa, studio, sorttitle))
+
+ # Link the path
+ query = "INSERT INTO tvshowlinkpath(idShow, idPath) values(?, ?)"
+ kodicursor.execute(query, (showid, pathid))
+
+ # Create the reference in emby table
+ emby_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid,
+ checksum=checksum, mediafolderid=viewid)
+
+ # Update the path
+ query = ' '.join((
+
+ "UPDATE path",
+ "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?",
+ "WHERE idPath = ?"
+ ))
+ kodicursor.execute(query, (path, None, None, 1, pathid))
+
+ # Process cast
+ people = artwork.getPeopleArtwork(item['People'])
+ self.kodi_db.addPeople(showid, people, "tvshow")
+ # Process genres
+ self.kodi_db.addGenres(showid, genres, "tvshow")
+ # Process artwork
+ artwork.addArtwork(artwork.getAllArtwork(item), showid, "tvshow", kodicursor)
+ # Process studios
+ self.kodi_db.addStudios(showid, studios, "tvshow")
+ # Process tags: view, emby tags
+ tags = [viewtag]
+ tags.extend(item['Tags'])
+ if userdata['Favorite']:
+ tags.append("Favorite tvshows")
+ self.kodi_db.addTags(showid, tags, "tvshow")
+ # Process seasons
+ all_seasons = emby.getSeasons(itemid)
+ for season in all_seasons['Items']:
+ self.add_updateSeason(season, showid=showid)
+ else:
+ # Finally, refresh the all season entry
+ seasonid = self.kodi_db.addSeason(showid, -1)
+ # Process artwork
+ artwork.addArtwork(artwork.getAllArtwork(item), seasonid, "season", kodicursor)
+
+ if force_episodes:
+ # We needed to recreate the show entry. Re-add episodes now.
+ self.logMsg("Repairing episodes for showid: %s %s" % (showid, title), 1)
+ all_episodes = emby.getEpisodesbyShow(itemid)
+ self.added_episode(all_episodes['Items'], None)
+
+ def add_updateSeason(self, item, showid=None):
+
+ kodicursor = self.kodicursor
+ emby_db = self.emby_db
+ artwork = self.artwork
+
+ seasonnum = item.get('IndexNumber', 1)
+
+ if showid is None:
+ try:
+ seriesId = item['SeriesId']
+ showid = emby_db.getItem_byId(seriesId)[0]
+ except KeyError:
+ return
+ except TypeError:
+ # Show is missing, update show instead.
+ show = self.emby.getItem(seriesId)
+ self.add_update(show)
+ return
+
+ seasonid = self.kodi_db.addSeason(showid, seasonnum)
+
+ if item['LocationType'] != "Virtual":
+ # Create the reference in emby table
+ emby_db.addReference(item['Id'], seasonid, "Season", "season", parentid=showid)
+
+ # Process artwork
+ artwork.addArtwork(artwork.getAllArtwork(item), seasonid, "season", kodicursor)
+
+ def add_updateEpisode(self, item):
+ # Process single episode
+ kodicursor = self.kodicursor
+ emby_db = self.emby_db
+ artwork = self.artwork
+ API = api.API(item)
+
+ # If the item already exist in the local Kodi DB we'll perform a full item update
+ # If the item doesn't exist, we'll add it to the database
+ update_item = True
+ itemid = item['Id']
+ emby_dbitem = emby_db.getItem_byId(itemid)
+ try:
+ episodeid = emby_dbitem[0]
+ fileid = emby_dbitem[1]
+ pathid = emby_dbitem[2]
+ self.logMsg("episodeid: %s fileid: %s pathid: %s" % (episodeid, fileid, pathid), 1)
+
+ except TypeError:
+ update_item = False
+ self.logMsg("episodeid: %s not found." % itemid, 2)
+ # episodeid
+ kodicursor.execute("select coalesce(max(idEpisode),0) from episode")
+ episodeid = kodicursor.fetchone()[0] + 1
+
+ else:
+ # Verification the item is still in Kodi
+ query = "SELECT * FROM episode WHERE idEpisode = ?"
+ kodicursor.execute(query, (episodeid,))
+ try:
+ kodicursor.fetchone()[0]
+ except TypeError:
+ # item is not found, let's recreate it.
+ update_item = False
+ self.logMsg("episodeid: %s missing from Kodi, repairing the entry." % episodeid, 1)
+
+ # fileId information
+ checksum = API.getChecksum()
+ dateadded = API.getDateCreated()
+ userdata = API.getUserData()
+ playcount = userdata['PlayCount']
+ dateplayed = userdata['LastPlayedDate']
+
+ # item details
+ people = API.getPeople()
+ writer = " / ".join(people['Writer'])
+ director = " / ".join(people['Director'])
+ title = item['Name']
+ plot = API.getOverview()
+ rating = item.get('CommunityRating')
+ runtime = API.getRuntime()
+ premieredate = API.getPremiereDate()
+
+ # episode details
+ try:
+ seriesId = item['SeriesId']
+ except KeyError:
+ # Missing seriesId, skip
+ self.logMsg("Skipping: %s. SeriesId is missing." % itemid, 1)
+ return False
+
+ seriesName = item['SeriesName']
+ season = item.get('ParentIndexNumber')
+ episode = item.get('IndexNumber', -1)
+
+ if season is None:
+ if item.get('AbsoluteEpisodeNumber'):
+ # Anime scenario
+ season = 1
+ episode = item['AbsoluteEpisodeNumber']
+ else:
+ season = -1
+
+ # Specials ordering within season
+ if item.get('AirsAfterSeasonNumber'):
+ airsBeforeSeason = item['AirsAfterSeasonNumber']
+ airsBeforeEpisode = 4096 # Kodi default number for afterseason ordering
+ else:
+ airsBeforeSeason = item.get('AirsBeforeSeasonNumber')
+ airsBeforeEpisode = item.get('AirsBeforeEpisodeNumber')
+
+ # Append multi episodes to title
+ if item.get('IndexNumberEnd'):
+ title = "| %02d | %s" % (item['IndexNumberEnd'], title)
+
+ # Get season id
+ show = emby_db.getItem_byId(seriesId)
+ try:
+ showid = show[0]
+ except TypeError:
+ # Show is missing from database
+ show = self.emby.getItem(seriesId)
+ self.add_update(show)
+ show = emby_db.getItem_byId(seriesId)
+ try:
+ showid = show[0]
+ except TypeError:
+ self.logMsg("Skipping: %s. Unable to add series: %s." % (itemid, seriesId))
+ return False
+
+ seasonid = self.kodi_db.addSeason(showid, season)
+
+
+ ##### GET THE FILE AND PATH #####
+ playurl = API.getFilePath()
+
+ if "\\" in playurl:
+ # Local path
+ filename = playurl.rsplit("\\", 1)[1]
+ else: # Network share
+ filename = playurl.rsplit("/", 1)[1]
+
+ if self.directpath:
+ # Direct paths is set the Kodi way
+ if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(playurl):
+ # Validate the path is correct with user intervention
+ resp = xbmcgui.Dialog().yesno(
+ heading="Can't validate path",
+ line1=(
+ "Kodi can't locate file: %s. Verify the path. "
+ "You may to verify your network credentials in the "
+ "add-on settings or use the emby path substitution "
+ "to format your path correctly. Stop syncing?"
+ % playurl))
+ if resp:
+ utils.window('emby_shouldStop', value="true")
+ return False
+
+ path = playurl.replace(filename, "")
+ utils.window('emby_pathverified', value="true")
+ else:
+ # Set plugin path and media flags using real filename
+ path = "plugin://plugin.video.emby.tvshows/%s/" % seriesId
+ params = {
+
+ 'filename': filename.encode('utf-8'),
+ 'id': itemid,
+ 'dbid': episodeid,
+ 'mode': "play"
+ }
+ filename = "%s?%s" % (path, urllib.urlencode(params))
+
+
+ ##### UPDATE THE EPISODE #####
+ if update_item:
+ self.logMsg("UPDATE episode itemid: %s - Title: %s" % (itemid, title), 1)
+
+ # Update the movie entry
+ if self.kodiversion in (16, 17):
+ # Kodi Jarvis, Krypton
+ query = ' '.join((
+
+ "UPDATE episode",
+ "SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,",
+ "c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?, idSeason = ?",
+ "WHERE idEpisode = ?"
+ ))
+ kodicursor.execute(query, (title, plot, rating, writer, premieredate,
+ runtime, director, season, episode, title, airsBeforeSeason,
+ airsBeforeEpisode, seasonid, episodeid))
+ else:
+ query = ' '.join((
+
+ "UPDATE episode",
+ "SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,",
+ "c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?",
+ "WHERE idEpisode = ?"
+ ))
+ kodicursor.execute(query, (title, plot, rating, writer, premieredate,
+ runtime, director, season, episode, title, airsBeforeSeason,
+ airsBeforeEpisode, episodeid))
+
+ # Update the checksum in emby table
+ emby_db.updateReference(itemid, checksum)
+ # Update parentid reference
+ emby_db.updateParentId(itemid, seasonid)
+
+ ##### OR ADD THE EPISODE #####
+ else:
+ self.logMsg("ADD episode itemid: %s - Title: %s" % (itemid, title), 1)
+
+ # Add path
+ pathid = self.kodi_db.addPath(path)
+ # Add the file
+ fileid = self.kodi_db.addFile(filename, pathid)
+
+ # Create the episode entry
+ if self.kodiversion in (16, 17):
+ # Kodi Jarvis, Krypton
+ query = (
+ '''
+ INSERT INTO episode(
+ idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14,
+ idShow, c15, c16, idSeason)
+
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ '''
+ )
+ kodicursor.execute(query, (episodeid, fileid, title, plot, rating, writer,
+ premieredate, runtime, director, season, episode, title, showid,
+ airsBeforeSeason, airsBeforeEpisode, seasonid))
+ else:
+ query = (
+ '''
+ INSERT INTO episode(
+ idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14,
+ idShow, c15, c16)
+
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ '''
+ )
+ kodicursor.execute(query, (episodeid, fileid, title, plot, rating, writer,
+ premieredate, runtime, director, season, episode, title, showid,
+ airsBeforeSeason, airsBeforeEpisode))
+
+ # Create the reference in emby table
+ emby_db.addReference(itemid, episodeid, "Episode", "episode", fileid, pathid,
+ seasonid, checksum)
+
+ # Update the path
+ query = ' '.join((
+
+ "UPDATE path",
+ "SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?",
+ "WHERE idPath = ?"
+ ))
+ kodicursor.execute(query, (path, None, None, 1, pathid))
+
+ # Update the file
+ query = ' '.join((
+
+ "UPDATE files",
+ "SET idPath = ?, strFilename = ?, dateAdded = ?",
+ "WHERE idFile = ?"
+ ))
+ kodicursor.execute(query, (pathid, filename, dateadded, fileid))
+
+ # Process cast
+ people = artwork.getPeopleArtwork(item['People'])
+ self.kodi_db.addPeople(episodeid, people, "episode")
+ # Process artwork
+ artworks = artwork.getAllArtwork(item)
+ artwork.addOrUpdateArt(artworks['Primary'], episodeid, "episode", "thumb", kodicursor)
+ # Process stream details
+ streams = API.getMediaStreams()
+ self.kodi_db.addStreams(fileid, streams, runtime)
+ # Process playstates
+ resume = API.adjustResume(userdata['Resume'])
+ total = round(float(runtime), 6)
+ self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed)
+ if not self.directpath and resume:
+ # Create additional entry for widgets. This is only required for plugin/episode.
+ temppathid = self.kodi_db.getPath("plugin://plugin.video.emby.tvshows/")
+ tempfileid = self.kodi_db.addFile(filename, temppathid)
+ query = ' '.join((
+
+ "UPDATE files",
+ "SET idPath = ?, strFilename = ?, dateAdded = ?",
+ "WHERE idFile = ?"
+ ))
+ kodicursor.execute(query, (temppathid, filename, dateadded, tempfileid))
+ self.kodi_db.addPlaystate(tempfileid, resume, total, playcount, dateplayed)
+
+ def updateUserdata(self, item):
+ # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
+ # Poster with progress bar
+ emby_db = self.emby_db
+ API = api.API(item)
+
+ # Get emby information
+ itemid = item['Id']
+ checksum = API.getChecksum()
+ userdata = API.getUserData()
+ runtime = API.getRuntime()
+ dateadded = API.getDateCreated()
+
+ # Get Kodi information
+ emby_dbitem = emby_db.getItem_byId(itemid)
+ try:
+ kodiid = emby_dbitem[0]
+ fileid = emby_dbitem[1]
+ mediatype = emby_dbitem[4]
+ self.logMsg(
+ "Update playstate for %s: %s fileid: %s"
+ % (mediatype, item['Name'], fileid), 1)
+ except TypeError:
+ return
+
+ # Process favorite tags
+ if mediatype == "tvshow":
+ if userdata['Favorite']:
+ self.kodi_db.addTag(kodiid, "Favorite tvshows", "tvshow")
+ else:
+ self.kodi_db.removeTag(kodiid, "Favorite tvshows", "tvshow")
+ elif mediatype == "episode":
+ # Process playstates
+ playcount = userdata['PlayCount']
+ dateplayed = userdata['LastPlayedDate']
+ resume = API.adjustResume(userdata['Resume'])
+ total = round(float(runtime), 6)
+
+ self.logMsg("%s New resume point: %s" % (itemid, resume))
+
+ self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed)
+ if not self.directpath and not resume:
+ # Make sure there's no other bookmarks created by widget.
+ filename = self.kodi_db.getFile(fileid)
+ self.kodi_db.removeFile("plugin://plugin.video.emby.tvshows/", filename)
+
+ if not self.directpath and resume:
+ # Create additional entry for widgets. This is only required for plugin/episode.
+ filename = self.kodi_db.getFile(fileid)
+ temppathid = self.kodi_db.getPath("plugin://plugin.video.emby.tvshows/")
+ tempfileid = self.kodi_db.addFile(filename, temppathid)
+ query = ' '.join((
+
+ "UPDATE files",
+ "SET idPath = ?, strFilename = ?, dateAdded = ?",
+ "WHERE idFile = ?"
+ ))
+ self.kodicursor.execute(query, (temppathid, filename, dateadded, tempfileid))
+ self.kodi_db.addPlaystate(tempfileid, resume, total, playcount, dateplayed)
+
+ emby_db.updateReference(itemid, checksum)
+
+ def remove(self, itemid):
+ # Remove showid, fileid, pathid, emby reference
+ emby_db = self.emby_db
+ embycursor = self.embycursor
+ kodicursor = self.kodicursor
+ artwork = self.artwork
+
+ emby_dbitem = emby_db.getItem_byId(itemid)
+ try:
+ kodiid = emby_dbitem[0]
+ fileid = emby_dbitem[1]
+ pathid = emby_dbitem[2]
+ parentid = emby_dbitem[3]
+ mediatype = emby_dbitem[4]
+ self.logMsg("Removing %s kodiid: %s fileid: %s" % (mediatype, kodiid, fileid), 1)
+ except TypeError:
+ return
+
+ ##### PROCESS ITEM #####
+
+ # Remove the emby reference
+ emby_db.removeItem(itemid)
+
+
+ ##### IF EPISODE #####
+
+ if mediatype == "episode":
+ # Delete kodi episode and file, verify season and tvshow
+ self.removeEpisode(kodiid, fileid)
+
+ # Season verification
+ season = emby_db.getItem_byKodiId(parentid, "season")
+ try:
+ showid = season[1]
+ except TypeError:
+ return
+
+ season_episodes = emby_db.getItem_byParentId(parentid, "episode")
+ if not season_episodes:
+ self.removeSeason(parentid)
+ emby_db.removeItem(season[0])
+
+ # Show verification
+ show = emby_db.getItem_byKodiId(showid, "tvshow")
+ query = ' '.join((
+
+ "SELECT totalCount",
+ "FROM tvshowcounts",
+ "WHERE idShow = ?"
+ ))
+ kodicursor.execute(query, (showid,))
+ result = kodicursor.fetchone()
+ if result and result[0] is None:
+ # There's no episodes left, delete show and any possible remaining seasons
+ seasons = emby_db.getItem_byParentId(showid, "season")
+ for season in seasons:
+ self.removeSeason(season[1])
+ else:
+ # Delete emby season entries
+ emby_db.removeItems_byParentId(showid, "season")
+ self.removeShow(showid)
+ emby_db.removeItem(show[0])
+
+ ##### IF TVSHOW #####
+
+ elif mediatype == "tvshow":
+ # Remove episodes, seasons, tvshow
+ seasons = emby_db.getItem_byParentId(kodiid, "season")
+ for season in seasons:
+ seasonid = season[1]
+ season_episodes = emby_db.getItem_byParentId(seasonid, "episode")
+ for episode in season_episodes:
+ self.removeEpisode(episode[1], episode[2])
+ else:
+ # Remove emby episodes
+ emby_db.removeItems_byParentId(seasonid, "episode")
+ else:
+ # Remove emby seasons
+ emby_db.removeItems_byParentId(kodiid, "season")
+
+ # Remove tvshow
+ self.removeShow(kodiid)
+
+ ##### IF SEASON #####
+
+ elif mediatype == "season":
+ # Remove episodes, season, verify tvshow
+ season_episodes = emby_db.getItem_byParentId(kodiid, "episode")
+ for episode in season_episodes:
+ self.removeEpisode(episode[1], episode[2])
+ else:
+ # Remove emby episodes
+ emby_db.removeItems_byParentId(kodiid, "episode")
+
+ # Remove season
+ self.removeSeason(kodiid)
+
+ # Show verification
+ seasons = emby_db.getItem_byParentId(parentid, "season")
+ if not seasons:
+ # There's no seasons, delete the show
+ self.removeShow(parentid)
+ emby_db.removeItem_byKodiId(parentid, "tvshow")
+
+ self.logMsg("Deleted %s: %s from kodi database" % (mediatype, itemid), 1)
+
+ def removeShow(self, kodiid):
+
+ kodicursor = self.kodicursor
+ self.artwork.deleteArtwork(kodiid, "tvshow", kodicursor)
+ kodicursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodiid,))
+ self.logMsg("Removed tvshow: %s." % kodiid, 2)
+
+ def removeSeason(self, kodiid):
+
+ kodicursor = self.kodicursor
+
+ self.artwork.deleteArtwork(kodiid, "season", kodicursor)
+ kodicursor.execute("DELETE FROM seasons WHERE idSeason = ?", (kodiid,))
+ self.logMsg("Removed season: %s." % kodiid, 2)
+
+ def removeEpisode(self, kodiid, fileid):
+
+ kodicursor = self.kodicursor
+
+ self.artwork.deleteArtwork(kodiid, "episode", kodicursor)
+ kodicursor.execute("DELETE FROM episode WHERE idEpisode = ?", (kodiid,))
+ kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,))
+ self.logMsg("Removed episode: %s." % kodiid, 2)
+
+class Music(Items):
+
+
+ def __init__(self, embycursor, musiccursor):
+
+ Items.__init__(self, embycursor, musiccursor)
+
+ self.directstream = utils.settings('streamMusic') == "true"
+ self.enableimportsongrating = utils.settings('enableImportSongRating') == "true"
+ self.enableexportsongrating = utils.settings('enableExportSongRating') == "true"
+ self.enableupdatesongrating = utils.settings('enableUpdateSongRating') == "true"
+ self.userid = utils.window('emby_currUser')
+ self.server = utils.window('emby_server%s' % self.userid)
+
+ def added(self, items, pdialog):
+
+ total = len(items)
+ count = 0
+ for artist in items:
+
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=artist['Name'])
+ count += 1
+ self.add_updateArtist(artist)
+ # Add albums
+ all_albums = self.emby.getAlbumsbyArtist(artist['Id'])
+ self.added_album(all_albums['Items'], pdialog)
+
+ def added_album(self, items, pdialog):
+
+ total = len(items)
+ count = 0
+ for album in items:
+
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=album['Name'])
+ count += 1
+ self.add_updateAlbum(album)
+ # Add songs
+ all_songs = self.emby.getSongsbyAlbum(album['Id'])
+ self.added_song(all_songs['Items'], pdialog)
+
+ def added_song(self, items, pdialog):
+
+ total = len(items)
+ count = 0
+ for song in items:
+
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=song['Name'])
+ count += 1
+ self.add_updateSong(song)
+ if not pdialog and self.contentmsg:
+ self.contentPop(song['Name'], self.newmusic_time)
+
+ def add_updateArtist(self, item, artisttype="MusicArtist"):
+ # Process a single artist
+ kodicursor = self.kodicursor
+ emby_db = self.emby_db
+ artwork = self.artwork
+ API = api.API(item)
+
+ update_item = True
+ itemid = item['Id']
+ emby_dbitem = emby_db.getItem_byId(itemid)
+ try:
+ artistid = emby_dbitem[0]
+ except TypeError:
+ update_item = False
+ self.logMsg("artistid: %s not found." % itemid, 2)
+
+ ##### The artist details #####
+ lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ dateadded = API.getDateCreated()
+ checksum = API.getChecksum()
+
+ name = item['Name']
+ musicBrainzId = API.getProvider('MusicBrainzArtist')
+ genres = " / ".join(item.get('Genres'))
+ bio = API.getOverview()
+
+ # Associate artwork
+ artworks = artwork.getAllArtwork(item, parentInfo=True)
+ thumb = artworks['Primary']
+ backdrops = artworks['Backdrop'] # List
+
+ if thumb:
+ thumb = "%s" % thumb
+ if backdrops:
+ fanart = "%s" % backdrops[0]
+ else:
+ fanart = ""
+
+
+ ##### UPDATE THE ARTIST #####
+ if update_item:
+ self.logMsg("UPDATE artist itemid: %s - Name: %s" % (itemid, name), 1)
+ # Update the checksum in emby table
+ emby_db.updateReference(itemid, checksum)
+
+ ##### OR ADD THE ARTIST #####
+ else:
+ self.logMsg("ADD artist itemid: %s - Name: %s" % (itemid, name), 1)
+ # safety checks: It looks like Emby supports the same artist multiple times.
+ # Kodi doesn't allow that. In case that happens we just merge the artist entries.
+ artistid = self.kodi_db.addArtist(name, musicBrainzId)
+ # Create the reference in emby table
+ emby_db.addReference(itemid, artistid, artisttype, "artist", checksum=checksum)
+
+
+ # Process the artist
+ if self.kodiversion in (16, 17):
+ query = ' '.join((
+
+ "UPDATE artist",
+ "SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,",
+ "lastScraped = ?",
+ "WHERE idArtist = ?"
+ ))
+ kodicursor.execute(query, (genres, bio, thumb, fanart, lastScraped, artistid))
+ else:
+ query = ' '.join((
+
+ "UPDATE artist",
+ "SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,",
+ "lastScraped = ?, dateAdded = ?",
+ "WHERE idArtist = ?"
+ ))
+ kodicursor.execute(query, (genres, bio, thumb, fanart, lastScraped,
+ dateadded, artistid))
+
+
+ # Update artwork
+ artwork.addArtwork(artworks, artistid, "artist", kodicursor)
+
+ def add_updateAlbum(self, item):
+ # Process a single artist
+ emby = self.emby
+ kodicursor = self.kodicursor
+ emby_db = self.emby_db
+ artwork = self.artwork
+ API = api.API(item)
+
+ update_item = True
+ itemid = item['Id']
+ emby_dbitem = emby_db.getItem_byId(itemid)
+ try:
+ albumid = emby_dbitem[0]
+ except TypeError:
+ update_item = False
+ self.logMsg("albumid: %s not found." % itemid, 2)
+
+ ##### The album details #####
+ lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ dateadded = API.getDateCreated()
+ userdata = API.getUserData()
+ checksum = API.getChecksum()
+
+ name = item['Name']
+ musicBrainzId = API.getProvider('MusicBrainzAlbum')
+ year = item.get('ProductionYear')
+ genres = item.get('Genres')
+ genre = " / ".join(genres)
+ bio = API.getOverview()
+ rating = userdata['UserRating']
+ artists = item['AlbumArtists']
+ if not artists:
+ artists = item['ArtistItems']
+ artistname = []
+ for artist in artists:
+ artistname.append(artist['Name'])
+ artistname = " / ".join(artistname)
+
+ # Associate artwork
+ artworks = artwork.getAllArtwork(item, parentInfo=True)
+ thumb = artworks['Primary']
+ if thumb:
+ thumb = "%s" % thumb
+
+ ##### UPDATE THE ALBUM #####
+ if update_item:
+ self.logMsg("UPDATE album itemid: %s - Name: %s" % (itemid, name), 1)
+ # Update the checksum in emby table
+ emby_db.updateReference(itemid, checksum)
+
+ ##### OR ADD THE ALBUM #####
+ else:
+ self.logMsg("ADD album itemid: %s - Name: %s" % (itemid, name), 1)
+ # safety checks: It looks like Emby supports the same artist multiple times.
+ # Kodi doesn't allow that. In case that happens we just merge the artist entries.
+ albumid = self.kodi_db.addAlbum(name, musicBrainzId)
+ # Create the reference in emby table
+ emby_db.addReference(itemid, albumid, "MusicAlbum", "album", checksum=checksum)
+
+
+ # Process the album info
+ if self.kodiversion == 17:
+ # Kodi Krypton
+ query = ' '.join((
+
+ "UPDATE album",
+ "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
+ "iUserrating = ?, lastScraped = ?, strReleaseType = ?",
+ "WHERE idAlbum = ?"
+ ))
+ kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped,
+ "album", albumid))
+ elif self.kodiversion == 16:
+ # Kodi Jarvis
+ query = ' '.join((
+
+ "UPDATE album",
+ "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
+ "iRating = ?, lastScraped = ?, strReleaseType = ?",
+ "WHERE idAlbum = ?"
+ ))
+ kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped,
+ "album", albumid))
+ elif self.kodiversion == 15:
+ # Kodi Isengard
+ query = ' '.join((
+
+ "UPDATE album",
+ "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
+ "iRating = ?, lastScraped = ?, dateAdded = ?, strReleaseType = ?",
+ "WHERE idAlbum = ?"
+ ))
+ kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped,
+ dateadded, "album", albumid))
+ else:
+ # Kodi Helix
+ query = ' '.join((
+
+ "UPDATE album",
+ "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
+ "iRating = ?, lastScraped = ?, dateAdded = ?",
+ "WHERE idAlbum = ?"
+ ))
+ kodicursor.execute(query, (artistname, year, genre, bio, thumb, rating, lastScraped,
+ dateadded, albumid))
+
+ # Associate the parentid for emby reference
+ parentId = item.get('ParentId')
+ if parentId is not None:
+ emby_dbartist = emby_db.getItem_byId(parentId)
+ try:
+ artistid = emby_dbartist[0]
+ except TypeError:
+ # Artist does not exist in emby database.
+ artist = emby.getItem(parentId)
+ # Item may not be an artist, verification necessary.
+ if artist['Type'] == "MusicArtist":
+ # Update with the parentId, for remove reference
+ emby_db.addReference(parentId, parentId, "MusicArtist", "artist")
+ emby_db.updateParentId(itemid, parentId)
+ else:
+ # Update emby reference with the artistid
+ emby_db.updateParentId(itemid, artistid)
+
+ # Assign main artists to album
+ for artist in artists:
+ artistname = artist['Name']
+ artistId = artist['Id']
+ emby_dbartist = emby_db.getItem_byId(artistId)
+ try:
+ artistid = emby_dbartist[0]
+ except TypeError:
+ # Artist does not exist in emby database, create the reference
+ artist = emby.getItem(artistId)
+ self.add_updateArtist(artist, artisttype="AlbumArtist")
+ emby_dbartist = emby_db.getItem_byId(artistId)
+ artistid = emby_dbartist[0]
+ else:
+ # Best take this name over anything else.
+ query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?"
+ kodicursor.execute(query, (artistname, artistid,))
+
+ # Add artist to album
+ query = (
+ '''
+ INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist)
+
+ VALUES (?, ?, ?)
+ '''
+ )
+ kodicursor.execute(query, (artistid, albumid, artistname))
+ # Update discography
+ query = (
+ '''
+ INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear)
+
+ VALUES (?, ?, ?)
+ '''
+ )
+ kodicursor.execute(query, (artistid, name, year))
+ # Update emby reference with parentid
+ emby_db.updateParentId(artistId, albumid)
+
+ # Add genres
+ self.kodi_db.addMusicGenres(albumid, genres, "album")
+ # Update artwork
+ artwork.addArtwork(artworks, albumid, "album", kodicursor)
+
+ def add_updateSong(self, item):
+ # Process single song
+ kodicursor = self.kodicursor
+ emby = self.emby
+ emby_db = self.emby_db
+ artwork = self.artwork
+ API = api.API(item)
+
+ update_item = True
+ itemid = item['Id']
+ emby_dbitem = emby_db.getItem_byId(itemid)
+ try:
+ songid = emby_dbitem[0]
+ pathid = emby_dbitem[2]
+ albumid = emby_dbitem[3]
+ except TypeError:
+ update_item = False
+ self.logMsg("songid: %s not found." % itemid, 2)
+
+ ##### The song details #####
+ checksum = API.getChecksum()
+ dateadded = API.getDateCreated()
+ userdata = API.getUserData()
+ playcount = userdata['PlayCount']
+ dateplayed = userdata['LastPlayedDate']
+
+ # item details
+ title = item['Name']
+ musicBrainzId = API.getProvider('MusicBrainzTrackId')
+ genres = item.get('Genres')
+ genre = " / ".join(genres)
+ artists = " / ".join(item['Artists'])
+ tracknumber = item.get('IndexNumber', 0)
+ disc = item.get('ParentIndexNumber', 1)
+ if disc == 1:
+ track = tracknumber
+ else:
+ track = disc*2**16 + tracknumber
+ year = item.get('ProductionYear')
+ duration = API.getRuntime()
+ rating = userdata['UserRating']
+
+ #if enabled, try to get the rating from file and/or emby
+ if not self.directstream:
+ rating, comment, hasEmbeddedCover = musicutils.getAdditionalSongTags(itemid, rating, API, kodicursor, emby_db, self.enableimportsongrating, self.enableexportsongrating, self.enableupdatesongrating)
+ else:
+ hasEmbeddedCover = False
+ comment = API.getOverview()
+
+
+ ##### GET THE FILE AND PATH #####
+ if self.directstream:
+ path = "%s/emby/Audio/%s/" % (self.server, itemid)
+ filename = "stream.mp3"
+ else:
+ playurl = API.getFilePath()
+
+ if "\\" in playurl:
+ # Local path
+ filename = playurl.rsplit("\\", 1)[1]
+ else: # Network share
+ filename = playurl.rsplit("/", 1)[1]
+
+ # Direct paths is set the Kodi way
+ if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(playurl):
+ # Validate the path is correct with user intervention
+ utils.window('emby_directPath', clear=True)
+ resp = xbmcgui.Dialog().yesno(
+ heading="Can't validate path",
+ line1=(
+ "Kodi can't locate file: %s. Verify the path. "
+ "You may to verify your network credentials in the "
+ "add-on settings or use the emby path substitution "
+ "to format your path correctly. Stop syncing?"
+ % playurl))
+ if resp:
+ utils.window('emby_shouldStop', value="true")
+ return False
+
+ path = playurl.replace(filename, "")
+ utils.window('emby_pathverified', value="true")
+
+ ##### UPDATE THE SONG #####
+ if update_item:
+ self.logMsg("UPDATE song itemid: %s - Title: %s" % (itemid, title), 1)
+
+ # Update path
+ query = "UPDATE path SET strPath = ? WHERE idPath = ?"
+ kodicursor.execute(query, (path, pathid))
+
+ # Update the song entry
+ query = ' '.join((
+
+ "UPDATE song",
+ "SET idAlbum = ?, strArtists = ?, strGenres = ?, strTitle = ?, iTrack = ?,",
+ "iDuration = ?, iYear = ?, strFilename = ?, iTimesPlayed = ?, lastplayed = ?,",
+ "rating = ?, comment = ?",
+ "WHERE idSong = ?"
+ ))
+ kodicursor.execute(query, (albumid, artists, genre, title, track, duration, year,
+ filename, playcount, dateplayed, rating, comment, songid))
+
+ # Update the checksum in emby table
+ emby_db.updateReference(itemid, checksum)
+
+ ##### OR ADD THE SONG #####
+ else:
+ self.logMsg("ADD song itemid: %s - Title: %s" % (itemid, title), 1)
+
+ # Add path
+ pathid = self.kodi_db.addPath(path)
+
+ try:
+ # Get the album
+ emby_dbalbum = emby_db.getItem_byId(item['AlbumId'])
+ albumid = emby_dbalbum[0]
+ except KeyError:
+ # Verify if there's an album associated.
+ album_name = item.get('Album')
+ if album_name:
+ self.logMsg("Creating virtual music album for song: %s." % itemid, 1)
+ albumid = self.kodi_db.addAlbum(album_name, API.getProvider('MusicBrainzAlbum'))
+ emby_db.addReference("%salbum%s" % (itemid, albumid), albumid, "MusicAlbum_", "album")
+ else:
+ # No album Id associated to the song.
+ self.logMsg("Song itemid: %s has no albumId associated." % itemid, 1)
+ return False
+
+ except TypeError:
+ # No album found. Let's create it
+ self.logMsg("Album database entry missing.", 1)
+ emby_albumId = item['AlbumId']
+ album = emby.getItem(emby_albumId)
+ self.add_updateAlbum(album)
+ emby_dbalbum = emby_db.getItem_byId(emby_albumId)
+ try:
+ albumid = emby_dbalbum[0]
+ self.logMsg("Found albumid: %s" % albumid, 1)
+ except TypeError:
+ # No album found, create a single's album
+ self.logMsg("Failed to add album. Creating singles.", 1)
+ kodicursor.execute("select coalesce(max(idAlbum),0) from album")
+ albumid = kodicursor.fetchone()[0] + 1
+ if self.kodiversion == 16:
+ # Kodi Jarvis
+ query = (
+ '''
+ INSERT INTO album(idAlbum, strGenres, iYear, strReleaseType)
+
+ VALUES (?, ?, ?, ?)
+ '''
+ )
+ kodicursor.execute(query, (albumid, genre, year, "single"))
+ elif self.kodiversion == 15:
+ # Kodi Isengard
+ query = (
+ '''
+ INSERT INTO album(idAlbum, strGenres, iYear, dateAdded, strReleaseType)
+
+ VALUES (?, ?, ?, ?, ?)
+ '''
+ )
+ kodicursor.execute(query, (albumid, genre, year, dateadded, "single"))
+ else:
+ # Kodi Helix
+ query = (
+ '''
+ INSERT INTO album(idAlbum, strGenres, iYear, dateAdded)
+
+ VALUES (?, ?, ?, ?)
+ '''
+ )
+ kodicursor.execute(query, (albumid, genre, year, dateadded))
+
+ # Create the song entry
+ kodicursor.execute("select coalesce(max(idSong),0) from song")
+ songid = kodicursor.fetchone()[0] + 1
+ query = (
+ '''
+ INSERT INTO song(
+ idSong, idAlbum, idPath, strArtists, strGenres, strTitle, iTrack,
+ iDuration, iYear, strFileName, strMusicBrainzTrackID, iTimesPlayed, lastplayed,
+ rating)
+
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ '''
+ )
+ kodicursor.execute(query, (songid, albumid, pathid, artists, genre, title, track,
+ duration, year, filename, musicBrainzId, playcount, dateplayed, rating))
+
+ # Create the reference in emby table
+ emby_db.addReference(itemid, songid, "Audio", "song", pathid=pathid, parentid=albumid,
+ checksum=checksum)
+
+
+ # Link song to album
+ query = (
+ '''
+ INSERT OR REPLACE INTO albuminfosong(
+ idAlbumInfoSong, idAlbumInfo, iTrack, strTitle, iDuration)
+
+ VALUES (?, ?, ?, ?, ?)
+ '''
+ )
+ kodicursor.execute(query, (songid, albumid, track, title, duration))
+
+ # Link song to artists
+ for index, artist in enumerate(item['ArtistItems']):
+
+ artist_name = artist['Name']
+ artist_eid = artist['Id']
+ artist_edb = emby_db.getItem_byId(artist_eid)
+ try:
+ artistid = artist_edb[0]
+ except TypeError:
+ # Artist is missing from emby database, add it.
+ artist_full = emby.getItem(artist_eid)
+ self.add_updateArtist(artist_full)
+ artist_edb = emby_db.getItem_byId(artist_eid)
+ artistid = artist_edb[0]
+ finally:
+ query = (
+ '''
+ INSERT OR REPLACE INTO song_artist(idArtist, idSong, iOrder, strArtist)
+
+ VALUES (?, ?, ?, ?)
+ '''
+ )
+ kodicursor.execute(query, (artistid, songid, index, artist_name))
+
+ # Verify if album artist exists
+ album_artists = []
+ for artist in item['AlbumArtists']:
+
+ artist_name = artist['Name']
+ album_artists.append(artist_name)
+ artist_eid = artist['Id']
+ artist_edb = emby_db.getItem_byId(artist_eid)
+ try:
+ artistid = artist_edb[0]
+ except TypeError:
+ # Artist is missing from emby database, add it.
+ artist_full = emby.getItem(artist_eid)
+ self.add_updateArtist(artist_full)
+ artist_edb = emby_db.getItem_byId(artist_eid)
+ artistid = artist_edb[0]
+ finally:
+ query = (
+ '''
+ INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist)
+
+ VALUES (?, ?, ?)
+ '''
+ )
+ kodicursor.execute(query, (artistid, albumid, artist_name))
+ # Update discography
+ if item.get('Album'):
+ query = (
+ '''
+ INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear)
+
+ VALUES (?, ?, ?)
+ '''
+ )
+ kodicursor.execute(query, (artistid, item['Album'], 0))
+ else:
+ album_artists = " / ".join(album_artists)
+ query = ' '.join((
+
+ "SELECT strArtists",
+ "FROM album",
+ "WHERE idAlbum = ?"
+ ))
+ kodicursor.execute(query, (albumid,))
+ result = kodicursor.fetchone()
+ if result and result[0] != album_artists:
+ # Field is empty
+ if self.kodiversion in (16, 17):
+ # Kodi Jarvis, Krypton
+ query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?"
+ kodicursor.execute(query, (album_artists, albumid))
+ elif self.kodiversion == 15:
+ # Kodi Isengard
+ query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?"
+ kodicursor.execute(query, (album_artists, albumid))
+ else:
+ # Kodi Helix
+ query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?"
+ kodicursor.execute(query, (album_artists, albumid))
+
+ # Add genres
+ self.kodi_db.addMusicGenres(songid, genres, "song")
+
+ # Update artwork
+ allart = artwork.getAllArtwork(item, parentInfo=True)
+ if hasEmbeddedCover:
+ allart["Primary"] = "image://music@" + artwork.single_urlencode( playurl )
+ artwork.addArtwork(allart, songid, "song", kodicursor)
+
+ if item.get('AlbumId') is None:
+ # Update album artwork
+ artwork.addArtwork(allart, albumid, "album", kodicursor)
+
+ def updateUserdata(self, item):
+ # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
+ # Poster with progress bar
+ kodicursor = self.kodicursor
+ emby_db = self.emby_db
+ API = api.API(item)
+
+ # Get emby information
+ itemid = item['Id']
+ checksum = API.getChecksum()
+ userdata = API.getUserData()
+ runtime = API.getRuntime()
+ rating = userdata['UserRating']
+
+ # Get Kodi information
+ emby_dbitem = emby_db.getItem_byId(itemid)
+ try:
+ kodiid = emby_dbitem[0]
+ mediatype = emby_dbitem[4]
+ self.logMsg("Update playstate for %s: %s" % (mediatype, item['Name']), 1)
+ except TypeError:
+ return
+
+ if mediatype == "song":
+
+ #should we ignore this item ?
+ #happens when userdata updated by ratings method
+ if utils.window("ignore-update-%s" %itemid):
+ utils.window("ignore-update-%s" %itemid,clear=True)
+ return
+
+ # Process playstates
+ playcount = userdata['PlayCount']
+ dateplayed = userdata['LastPlayedDate']
+
+ #process item ratings
+ rating, comment, hasEmbeddedCover = musicutils.getAdditionalSongTags(itemid, rating, API, kodicursor, emby_db, self.enableimportsongrating, self.enableexportsongrating, self.enableupdatesongrating)
+
+ query = "UPDATE song SET iTimesPlayed = ?, lastplayed = ?, rating = ? WHERE idSong = ?"
+ kodicursor.execute(query, (playcount, dateplayed, rating, kodiid))
+
+ elif mediatype == "album":
+ # Process playstates
+ query = "UPDATE album SET iRating = ? WHERE idAlbum = ?"
+ kodicursor.execute(query, (rating, kodiid))
+
+ emby_db.updateReference(itemid, checksum)
+
+ def remove(self, itemid):
+ # Remove kodiid, fileid, pathid, emby reference
+ emby_db = self.emby_db
+ kodicursor = self.kodicursor
+ artwork = self.artwork
+
+ emby_dbitem = emby_db.getItem_byId(itemid)
+ try:
+ kodiid = emby_dbitem[0]
+ mediatype = emby_dbitem[4]
+ self.logMsg("Removing %s kodiid: %s" % (mediatype, kodiid), 1)
+ except TypeError:
+ return
+
+ ##### PROCESS ITEM #####
+
+ # Remove the emby reference
+ emby_db.removeItem(itemid)
+
+
+ ##### IF SONG #####
+
+ if mediatype == "song":
+ # Delete song
+ self.removeSong(kodiid)
+ # This should only address single song scenario, where server doesn't actually
+ # create an album for the song.
+ emby_db.removeWildItem(itemid)
+
+ for item in emby_db.getItem_byWildId(itemid):
+
+ item_kid = item[0]
+ item_mediatype = item[1]
+
+ if item_mediatype == "album":
+ childs = emby_db.getItem_byParentId(item_kid, "song")
+ if not childs:
+ # Delete album
+ self.removeAlbum(item_kid)
+
+ ##### IF ALBUM #####
+
+ elif mediatype == "album":
+ # Delete songs, album
+ album_songs = emby_db.getItem_byParentId(kodiid, "song")
+ for song in album_songs:
+ self.removeSong(song[1])
+ else:
+ # Remove emby songs
+ emby_db.removeItems_byParentId(kodiid, "song")
+
+ # Remove the album
+ self.removeAlbum(kodiid)
+
+ ##### IF ARTIST #####
+
+ elif mediatype == "artist":
+ # Delete songs, album, artist
+ albums = emby_db.getItem_byParentId(kodiid, "album")
+ for album in albums:
+ albumid = album[1]
+ album_songs = emby_db.getItem_byParentId(albumid, "song")
+ for song in album_songs:
+ self.removeSong(song[1])
+ else:
+ # Remove emby song
+ emby_db.removeItems_byParentId(albumid, "song")
+ # Remove emby artist
+ emby_db.removeItems_byParentId(albumid, "artist")
+ # Remove kodi album
+ self.removeAlbum(albumid)
+ else:
+ # Remove emby albums
+ emby_db.removeItems_byParentId(kodiid, "album")
+
+ # Remove artist
+ self.removeArtist(kodiid)
+
+ self.logMsg("Deleted %s: %s from kodi database" % (mediatype, itemid), 1)
+
+ def removeSong(self, kodiid):
+
+ kodicursor = self.kodicursor
+
+ self.artwork.deleteArtwork(kodiid, "song", self.kodicursor)
+ self.kodicursor.execute("DELETE FROM song WHERE idSong = ?", (kodiid,))
+
+ def removeAlbum(self, kodiid):
+
+ self.artwork.deleteArtwork(kodiid, "album", self.kodicursor)
+ self.kodicursor.execute("DELETE FROM album WHERE idAlbum = ?", (kodiid,))
+
+ def removeArtist(self, kodiid):
+
+ self.artwork.deleteArtwork(kodiid, "artist", self.kodicursor)
+ self.kodicursor.execute("DELETE FROM artist WHERE idArtist = ?", (kodiid,))
diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py
index a7d82a3e..6c3dd8b1 100644
--- a/resources/lib/kodidb_functions.py
+++ b/resources/lib/kodidb_functions.py
@@ -1,1190 +1,1149 @@
-# -*- coding: utf-8 -*-
-
-##################################################################################################
-
-import xbmc
-
-import api
-import artwork
-import clientinfo
-import utils
-
-##################################################################################################
-
-
-class Kodidb_Functions():
-
- kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
-
-
- def __init__(self, cursor):
-
- self.cursor = cursor
-
- self.clientInfo = clientinfo.ClientInfo()
- self.addonName = self.clientInfo.getAddonName()
- self.artwork = artwork.Artwork()
-
- def logMsg(self, msg, lvl=1):
-
- className = self.__class__.__name__
- utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
-
-
- def addPath(self, path):
-
- cursor = self.cursor
-
- query = ' '.join((
-
- "SELECT idPath",
- "FROM path",
- "WHERE strPath = ?"
- ))
- cursor.execute(query, (path,))
- try:
- pathid = cursor.fetchone()[0]
- except TypeError:
- cursor.execute("select coalesce(max(idPath),0) from path")
- pathid = cursor.fetchone()[0] + 1
- query = (
- '''
- INSERT INTO path(
- idPath, strPath)
-
- VALUES (?, ?)
- '''
- )
- cursor.execute(query, (pathid, path))
-
- return pathid
-
- def getPath(self, path):
-
- cursor = self.cursor
-
- query = ' '.join((
-
- "SELECT idPath",
- "FROM path",
- "WHERE strPath = ?"
- ))
- cursor.execute(query, (path,))
- try:
- pathid = cursor.fetchone()[0]
- except TypeError:
- pathid = None
-
- return pathid
-
- def addFile(self, filename, pathid):
-
- cursor = self.cursor
-
- query = ' '.join((
-
- "SELECT idFile",
- "FROM files",
- "WHERE strFilename = ?",
- "AND idPath = ?"
- ))
- cursor.execute(query, (filename, pathid,))
- try:
- fileid = cursor.fetchone()[0]
- except TypeError:
- cursor.execute("select coalesce(max(idFile),0) from files")
- fileid = cursor.fetchone()[0] + 1
- query = (
- '''
- INSERT INTO files(
- idFile, strFilename)
-
- VALUES (?, ?)
- '''
- )
- cursor.execute(query, (fileid, filename))
-
- return fileid
-
- def getFile(self, fileid):
-
- cursor = self.cursor
-
- query = ' '.join((
-
- "SELECT strFilename",
- "FROM files",
- "WHERE idFile = ?"
- ))
- cursor.execute(query, (fileid,))
- try:
- filename = cursor.fetchone()[0]
- except TypeError:
- filename = ""
-
- return filename
-
- def removeFile(self, path, filename):
-
- pathid = self.getPath(path)
-
- if pathid is not None:
- query = ' '.join((
-
- "DELETE FROM files",
- "WHERE idPath = ?",
- "AND strFilename = ?"
- ))
- self.cursor.execute(query, (pathid, filename,))
-
- def addCountries(self, kodiid, countries, mediatype):
-
- cursor = self.cursor
-
- if self.kodiversion in (15, 16, 17):
- # Kodi Isengard, Jarvis, Krypton
- for country in countries:
- query = ' '.join((
-
- "SELECT country_id",
- "FROM country",
- "WHERE name = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (country,))
-
- try:
- country_id = cursor.fetchone()[0]
-
- except TypeError:
- # Country entry does not exists
- cursor.execute("select coalesce(max(country_id),0) from country")
- country_id = cursor.fetchone()[0] + 1
-
- query = "INSERT INTO country(country_id, name) values(?, ?)"
- cursor.execute(query, (country_id, country))
- self.logMsg("Add country to media, processing: %s" % country, 2)
-
- finally: # Assign country to content
- query = (
- '''
- INSERT OR REPLACE INTO country_link(
- country_id, media_id, media_type)
-
- VALUES (?, ?, ?)
- '''
- )
- cursor.execute(query, (country_id, kodiid, mediatype))
- else:
- # Kodi Helix
- for country in countries:
- query = ' '.join((
-
- "SELECT idCountry",
- "FROM country",
- "WHERE strCountry = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (country,))
-
- try:
- idCountry = cursor.fetchone()[0]
-
- except TypeError:
- # Country entry does not exists
- cursor.execute("select coalesce(max(idCountry),0) from country")
- idCountry = cursor.fetchone()[0] + 1
-
- query = "INSERT INTO country(idCountry, strCountry) values(?, ?)"
- cursor.execute(query, (idCountry, country))
- self.logMsg("Add country to media, processing: %s" % country, 2)
-
- finally:
- # Only movies have a country field
- if "movie" in mediatype:
- query = (
- '''
- INSERT OR REPLACE INTO countrylinkmovie(
- idCountry, idMovie)
-
- VALUES (?, ?)
- '''
- )
- cursor.execute(query, (idCountry, kodiid))
-
- def addPeople(self, kodiid, people, mediatype):
-
- cursor = self.cursor
- artwork = self.artwork
- kodiversion = self.kodiversion
-
- castorder = 1
- for person in people:
-
- name = person['Name']
- type = person['Type']
- thumb = person['imageurl']
-
- # Kodi Isengard, Jarvis, Krypton
- if kodiversion in (15, 16, 17):
- query = ' '.join((
-
- "SELECT actor_id",
- "FROM actor",
- "WHERE name = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (name,))
-
- try:
- actorid = cursor.fetchone()[0]
-
- except TypeError:
- # Cast entry does not exists
- cursor.execute("select coalesce(max(actor_id),0) from actor")
- actorid = cursor.fetchone()[0] + 1
-
- query = "INSERT INTO actor(actor_id, name) values(?, ?)"
- cursor.execute(query, (actorid, name))
- self.logMsg("Add people to media, processing: %s" % name, 2)
-
- finally:
- # Link person to content
- if "Actor" in type:
- role = person.get('Role')
- query = (
- '''
- INSERT OR REPLACE INTO actor_link(
- actor_id, media_id, media_type, role, cast_order)
-
- VALUES (?, ?, ?, ?, ?)
- '''
- )
- cursor.execute(query, (actorid, kodiid, mediatype, role, castorder))
- castorder += 1
-
- elif "Director" in type:
- query = (
- '''
- INSERT OR REPLACE INTO director_link(
- actor_id, media_id, media_type)
-
- VALUES (?, ?, ?)
- '''
- )
- cursor.execute(query, (actorid, kodiid, mediatype))
-
- elif type in ("Writing", "Writer"):
- query = (
- '''
- INSERT OR REPLACE INTO writer_link(
- actor_id, media_id, media_type)
-
- VALUES (?, ?, ?)
- '''
- )
- cursor.execute(query, (actorid, kodiid, mediatype))
-
- elif "Artist" in type:
- query = (
- '''
- INSERT OR REPLACE INTO actor_link(
- actor_id, media_id, media_type)
-
- VALUES (?, ?, ?)
- '''
- )
- cursor.execute(query, (actorid, kodiid, mediatype))
- # Kodi Helix
- else:
- query = ' '.join((
-
- "SELECT idActor",
- "FROM actors",
- "WHERE strActor = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (name,))
-
- try:
- actorid = cursor.fetchone()[0]
-
- except TypeError:
- # Cast entry does not exists
- cursor.execute("select coalesce(max(idActor),0) from actors")
- actorid = cursor.fetchone()[0] + 1
-
- query = "INSERT INTO actors(idActor, strActor) values(?, ?)"
- cursor.execute(query, (actorid, name))
- self.logMsg("Add people to media, processing: %s" % name, 2)
-
- finally:
- # Link person to content
- if "Actor" in type:
- role = person.get('Role')
-
- if "movie" in mediatype:
- query = (
- '''
- INSERT OR REPLACE INTO actorlinkmovie(
- idActor, idMovie, strRole, iOrder)
-
- VALUES (?, ?, ?, ?)
- '''
- )
- elif "tvshow" in mediatype:
- query = (
- '''
- INSERT OR REPLACE INTO actorlinktvshow(
- idActor, idShow, strRole, iOrder)
-
- VALUES (?, ?, ?, ?)
- '''
- )
- elif "episode" in mediatype:
- query = (
- '''
- INSERT OR REPLACE INTO actorlinkepisode(
- idActor, idEpisode, strRole, iOrder)
-
- VALUES (?, ?, ?, ?)
- '''
- )
- else: return # Item is invalid
-
- cursor.execute(query, (actorid, kodiid, role, castorder))
- castorder += 1
-
- elif "Director" in type:
- if "movie" in mediatype:
- query = (
- '''
- INSERT OR REPLACE INTO directorlinkmovie(
- idDirector, idMovie)
-
- VALUES (?, ?)
- '''
- )
- elif "tvshow" in mediatype:
- query = (
- '''
- INSERT OR REPLACE INTO directorlinktvshow(
- idDirector, idShow)
-
- VALUES (?, ?)
- '''
- )
- elif "musicvideo" in mediatype:
- query = (
- '''
- INSERT OR REPLACE INTO directorlinkmusicvideo(
- idDirector, idMVideo)
-
- VALUES (?, ?)
- '''
- )
-
- elif "episode" in mediatype:
- query = (
- '''
- INSERT OR REPLACE INTO directorlinkepisode(
- idDirector, idEpisode)
-
- VALUES (?, ?)
- '''
- )
- else: return # Item is invalid
-
- cursor.execute(query, (actorid, kodiid))
-
- elif type in ("Writing", "Writer"):
- if "movie" in mediatype:
- query = (
- '''
- INSERT OR REPLACE INTO writerlinkmovie(
- idWriter, idMovie)
-
- VALUES (?, ?)
- '''
- )
- elif "episode" in mediatype:
- query = (
- '''
- INSERT OR REPLACE INTO writerlinkepisode(
- idWriter, idEpisode)
-
- VALUES (?, ?)
- '''
- )
- else: return # Item is invalid
-
- cursor.execute(query, (actorid, kodiid))
-
- elif "Artist" in type:
- query = (
- '''
- INSERT OR REPLACE INTO artistlinkmusicvideo(
- idArtist, idMVideo)
-
- VALUES (?, ?)
- '''
- )
- cursor.execute(query, (actorid, kodiid))
-
- # Add person image to art table
- if thumb:
- arttype = type.lower()
-
- if "writing" in arttype:
- arttype = "writer"
-
- artwork.addOrUpdateArt(thumb, actorid, arttype, "thumb", cursor)
-
- def addGenres(self, kodiid, genres, mediatype):
-
- cursor = self.cursor
-
- # Kodi Isengard, Jarvis, Krypton
- if self.kodiversion in (15, 16, 17):
- # Delete current genres for clean slate
- query = ' '.join((
-
- "DELETE FROM genre_link",
- "WHERE media_id = ?",
- "AND media_type = ?"
- ))
- cursor.execute(query, (kodiid, mediatype,))
-
- # Add genres
- for genre in genres:
-
- query = ' '.join((
-
- "SELECT genre_id",
- "FROM genre",
- "WHERE name = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (genre,))
-
- try:
- genre_id = cursor.fetchone()[0]
-
- except TypeError:
- # Create genre in database
- cursor.execute("select coalesce(max(genre_id),0) from genre")
- genre_id = cursor.fetchone()[0] + 1
-
- query = "INSERT INTO genre(genre_id, name) values(?, ?)"
- cursor.execute(query, (genre_id, genre))
- self.logMsg("Add Genres to media, processing: %s" % genre, 2)
-
- finally:
- # Assign genre to item
- query = (
- '''
- INSERT OR REPLACE INTO genre_link(
- genre_id, media_id, media_type)
-
- VALUES (?, ?, ?)
- '''
- )
- cursor.execute(query, (genre_id, kodiid, mediatype))
- else:
- # Kodi Helix
- # Delete current genres for clean slate
- if "movie" in mediatype:
- cursor.execute("DELETE FROM genrelinkmovie WHERE idMovie = ?", (kodiid,))
- elif "tvshow" in mediatype:
- cursor.execute("DELETE FROM genrelinktvshow WHERE idShow = ?", (kodiid,))
- elif "musicvideo" in mediatype:
- cursor.execute("DELETE FROM genrelinkmusicvideo WHERE idMVideo = ?", (kodiid,))
-
- # Add genres
- for genre in genres:
-
- query = ' '.join((
-
- "SELECT idGenre",
- "FROM genre",
- "WHERE strGenre = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (genre,))
-
- try:
- idGenre = cursor.fetchone()[0]
-
- except TypeError:
- # Create genre in database
- cursor.execute("select coalesce(max(idGenre),0) from genre")
- idGenre = cursor.fetchone()[0] + 1
-
- query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)"
- cursor.execute(query, (idGenre, genre))
- self.logMsg("Add Genres to media, processing: %s" % genre, 2)
-
- finally:
- # Assign genre to item
- if "movie" in mediatype:
- query = (
- '''
- INSERT OR REPLACE into genrelinkmovie(
- idGenre, idMovie)
-
- VALUES (?, ?)
- '''
- )
- elif "tvshow" in mediatype:
- query = (
- '''
- INSERT OR REPLACE into genrelinktvshow(
- idGenre, idShow)
-
- VALUES (?, ?)
- '''
- )
- elif "musicvideo" in mediatype:
- query = (
- '''
- INSERT OR REPLACE into genrelinkmusicvideo(
- idGenre, idMVideo)
-
- VALUES (?, ?)
- '''
- )
- else: return # Item is invalid
-
- cursor.execute(query, (idGenre, kodiid))
-
- def addStudios(self, kodiid, studios, mediatype):
-
- cursor = self.cursor
- kodiversion = self.kodiversion
-
- for studio in studios:
-
- if kodiversion in (15, 16, 17):
- # Kodi Isengard, Jarvis, Krypton
- query = ' '.join((
-
- "SELECT studio_id",
- "FROM studio",
- "WHERE name = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (studio,))
- try:
- studioid = cursor.fetchone()[0]
-
- except TypeError:
- # Studio does not exists.
- cursor.execute("select coalesce(max(studio_id),0) from studio")
- studioid = cursor.fetchone()[0] + 1
-
- query = "INSERT INTO studio(studio_id, name) values(?, ?)"
- cursor.execute(query, (studioid, studio))
- self.logMsg("Add Studios to media, processing: %s" % studio, 2)
-
- finally: # Assign studio to item
- query = (
- '''
- INSERT OR REPLACE INTO studio_link(
- studio_id, media_id, media_type)
-
- VALUES (?, ?, ?)
- ''')
- cursor.execute(query, (studioid, kodiid, mediatype))
- else:
- # Kodi Helix
- query = ' '.join((
-
- "SELECT idstudio",
- "FROM studio",
- "WHERE strstudio = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (studio,))
- try:
- studioid = cursor.fetchone()[0]
-
- except TypeError:
- # Studio does not exists.
- cursor.execute("select coalesce(max(idstudio),0) from studio")
- studioid = cursor.fetchone()[0] + 1
-
- query = "INSERT INTO studio(idstudio, strstudio) values(?, ?)"
- cursor.execute(query, (studioid, studio))
- self.logMsg("Add Studios to media, processing: %s" % studio, 2)
-
- finally: # Assign studio to item
- if "movie" in mediatype:
- query = (
- '''
- INSERT OR REPLACE INTO studiolinkmovie(idstudio, idMovie)
- VALUES (?, ?)
- ''')
- elif "musicvideo" in mediatype:
- query = (
- '''
- INSERT OR REPLACE INTO studiolinkmusicvideo(idstudio, idMVideo)
- VALUES (?, ?)
- ''')
- elif "tvshow" in mediatype:
- query = (
- '''
- INSERT OR REPLACE INTO studiolinktvshow(idstudio, idShow)
- VALUES (?, ?)
- ''')
- elif "episode" in mediatype:
- query = (
- '''
- INSERT OR REPLACE INTO studiolinkepisode(idstudio, idEpisode)
- VALUES (?, ?)
- ''')
- cursor.execute(query, (studioid, kodiid))
-
- def addStreams(self, fileid, streamdetails, runtime):
-
- cursor = self.cursor
-
- # First remove any existing entries
- cursor.execute("DELETE FROM streamdetails WHERE idFile = ?", (fileid,))
- if streamdetails:
- # Video details
- for videotrack in streamdetails['video']:
- query = (
- '''
- INSERT INTO streamdetails(
- idFile, iStreamType, strVideoCodec, fVideoAspect,
- iVideoWidth, iVideoHeight, iVideoDuration ,strStereoMode)
-
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- '''
- )
- cursor.execute(query, (fileid, 0, videotrack['codec'],
- videotrack['aspect'], videotrack['width'], videotrack['height'],
- runtime ,videotrack['video3DFormat']))
-
- # Audio details
- for audiotrack in streamdetails['audio']:
- query = (
- '''
- INSERT INTO streamdetails(
- idFile, iStreamType, strAudioCodec, iAudioChannels, strAudioLanguage)
-
- VALUES (?, ?, ?, ?, ?)
- '''
- )
- cursor.execute(query, (fileid, 1, audiotrack['codec'],
- audiotrack['channels'], audiotrack['language']))
-
- # Subtitles details
- for subtitletrack in streamdetails['subtitle']:
- query = (
- '''
- INSERT INTO streamdetails(
- idFile, iStreamType, strSubtitleLanguage)
-
- VALUES (?, ?, ?)
- '''
- )
- cursor.execute(query, (fileid, 2, subtitletrack))
-
- def addPlaystate(self, fileid, resume_seconds, total_seconds, playcount, dateplayed):
-
- cursor = self.cursor
-
- # Delete existing resume point
- query = ' '.join((
-
- "DELETE FROM bookmark",
- "WHERE idFile = ?"
- ))
- cursor.execute(query, (fileid,))
-
- # Set watched count
- query = ' '.join((
-
- "UPDATE files",
- "SET playCount = ?, lastPlayed = ?",
- "WHERE idFile = ?"
- ))
- cursor.execute(query, (playcount, dateplayed, fileid))
-
- # Set the resume bookmark
- if resume_seconds:
- cursor.execute("select coalesce(max(idBookmark),0) from bookmark")
- bookmarkId = cursor.fetchone()[0] + 1
- query = (
- '''
- INSERT INTO bookmark(
- idBookmark, idFile, timeInSeconds, totalTimeInSeconds, player, type)
-
- VALUES (?, ?, ?, ?, ?, ?)
- '''
- )
- cursor.execute(query, (bookmarkId, fileid, resume_seconds, total_seconds,
- "DVDPlayer", 1))
-
- def addTags(self, kodiid, tags, mediatype):
-
- cursor = self.cursor
-
- # First, delete any existing tags associated to the id
- if self.kodiversion in (15, 16, 17):
- # Kodi Isengard, Jarvis, Krypton
- query = ' '.join((
-
- "DELETE FROM tag_link",
- "WHERE media_id = ?",
- "AND media_type = ?"
- ))
- cursor.execute(query, (kodiid, mediatype))
- else:
- # Kodi Helix
- query = ' '.join((
-
- "DELETE FROM taglinks",
- "WHERE idMedia = ?",
- "AND media_type = ?"
- ))
- cursor.execute(query, (kodiid, mediatype))
-
- # Add tags
- self.logMsg("Adding Tags: %s" % tags, 2)
- for tag in tags:
- self.addTag(kodiid, tag, mediatype)
-
- def addTag(self, kodiid, tag, mediatype):
-
- cursor = self.cursor
-
- if self.kodiversion in (15, 16, 17):
- # Kodi Isengard, Jarvis, Krypton
- query = ' '.join((
-
- "SELECT tag_id",
- "FROM tag",
- "WHERE name = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (tag,))
- try:
- tag_id = cursor.fetchone()[0]
-
- except TypeError:
- # Create the tag, because it does not exist
- tag_id = self.createTag(tag)
- self.logMsg("Adding tag: %s" % tag, 2)
-
- finally:
- # Assign tag to item
- query = (
- '''
- INSERT OR REPLACE INTO tag_link(
- tag_id, media_id, media_type)
-
- VALUES (?, ?, ?)
- '''
- )
- cursor.execute(query, (tag_id, kodiid, mediatype))
- else:
- # Kodi Helix
- query = ' '.join((
-
- "SELECT idTag",
- "FROM tag",
- "WHERE strTag = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (tag,))
- try:
- tag_id = cursor.fetchone()[0]
-
- except TypeError:
- # Create the tag
- tag_id = self.createTag(tag)
- self.logMsg("Adding tag: %s" % tag, 2)
-
- finally:
- # Assign tag to item
- query = (
- '''
- INSERT OR REPLACE INTO taglinks(
- idTag, idMedia, media_type)
-
- VALUES (?, ?, ?)
- '''
- )
- cursor.execute(query, (tag_id, kodiid, mediatype))
-
- def createTag(self, name):
-
- cursor = self.cursor
-
- # This will create and return the tag_id
- if self.kodiversion in (15, 16, 17):
- # Kodi Isengard, Jarvis, Krypton
- query = ' '.join((
-
- "SELECT tag_id",
- "FROM tag",
- "WHERE name = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (name,))
- try:
- tag_id = cursor.fetchone()[0]
-
- except TypeError:
- cursor.execute("select coalesce(max(tag_id),0) from tag")
- tag_id = cursor.fetchone()[0] + 1
-
- query = "INSERT INTO tag(tag_id, name) values(?, ?)"
- cursor.execute(query, (tag_id, name))
- self.logMsg("Create tag_id: %s name: %s" % (tag_id, name), 2)
- else:
- # Kodi Helix
- query = ' '.join((
-
- "SELECT idTag",
- "FROM tag",
- "WHERE strTag = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (name,))
- try:
- tag_id = cursor.fetchone()[0]
-
- except TypeError:
- cursor.execute("select coalesce(max(idTag),0) from tag")
- tag_id = cursor.fetchone()[0] + 1
-
- query = "INSERT INTO tag(idTag, strTag) values(?, ?)"
- cursor.execute(query, (tag_id, name))
- self.logMsg("Create idTag: %s name: %s" % (tag_id, name), 2)
-
- return tag_id
-
- def updateTag(self, oldtag, newtag, kodiid, mediatype):
-
- cursor = self.cursor
- self.logMsg("Updating: %s with %s for %s: %s" % (oldtag, newtag, mediatype, kodiid), 2)
-
- if self.kodiversion in (15, 16, 17):
- # Kodi Isengard, Jarvis, Krypton
- try:
- query = ' '.join((
-
- "UPDATE tag_link",
- "SET tag_id = ?",
- "WHERE media_id = ?",
- "AND media_type = ?",
- "AND tag_id = ?"
- ))
- cursor.execute(query, (newtag, kodiid, mediatype, oldtag,))
- except Exception as e:
- # The new tag we are going to apply already exists for this item
- # delete current tag instead
- self.logMsg("Exception: %s" % e, 1)
- query = ' '.join((
-
- "DELETE FROM tag_link",
- "WHERE media_id = ?",
- "AND media_type = ?",
- "AND tag_id = ?"
- ))
- cursor.execute(query, (kodiid, mediatype, oldtag,))
- else:
- # Kodi Helix
- try:
- query = ' '.join((
-
- "UPDATE taglinks",
- "SET idTag = ?",
- "WHERE idMedia = ?",
- "AND media_type = ?",
- "AND idTag = ?"
- ))
- cursor.execute(query, (newtag, kodiid, mediatype, oldtag,))
- except Exception as e:
- # The new tag we are going to apply already exists for this item
- # delete current tag instead
- self.logMsg("Exception: %s" % e, 1)
- query = ' '.join((
-
- "DELETE FROM taglinks",
- "WHERE idMedia = ?",
- "AND media_type = ?",
- "AND idTag = ?"
- ))
- cursor.execute(query, (kodiid, mediatype, oldtag,))
-
- def removeTag(self, kodiid, tagname, mediatype):
-
- cursor = self.cursor
-
- if self.kodiversion in (15, 16, 17):
- # Kodi Isengard, Jarvis, Krypton
- query = ' '.join((
-
- "SELECT tag_id",
- "FROM tag",
- "WHERE name = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (tagname,))
- try:
- tag_id = cursor.fetchone()[0]
- except TypeError:
- return
- else:
- query = ' '.join((
-
- "DELETE FROM tag_link",
- "WHERE media_id = ?",
- "AND media_type = ?",
- "AND tag_id = ?"
- ))
- cursor.execute(query, (kodiid, mediatype, tag_id,))
- else:
- # Kodi Helix
- query = ' '.join((
-
- "SELECT idTag",
- "FROM tag",
- "WHERE strTag = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (tagname,))
- try:
- tag_id = cursor.fetchone()[0]
- except TypeError:
- return
- else:
- query = ' '.join((
-
- "DELETE FROM taglinks",
- "WHERE idMedia = ?",
- "AND media_type = ?",
- "AND idTag = ?"
- ))
- cursor.execute(query, (kodiid, mediatype, tag_id,))
-
- def createBoxset(self, boxsetname):
-
- cursor = self.cursor
- self.logMsg("Adding boxset: %s" % boxsetname, 2)
- query = ' '.join((
-
- "SELECT idSet",
- "FROM sets",
- "WHERE strSet = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (boxsetname,))
- try:
- setid = cursor.fetchone()[0]
-
- except TypeError:
- cursor.execute("select coalesce(max(idSet),0) from sets")
- setid = cursor.fetchone()[0] + 1
-
- query = "INSERT INTO sets(idSet, strSet) values(?, ?)"
- cursor.execute(query, (setid, boxsetname))
-
- return setid
-
- def assignBoxset(self, setid, movieid):
-
- query = ' '.join((
-
- "UPDATE movie",
- "SET idSet = ?",
- "WHERE idMovie = ?"
- ))
- self.cursor.execute(query, (setid, movieid,))
-
- def removefromBoxset(self, movieid):
-
- query = ' '.join((
-
- "UPDATE movie",
- "SET idSet = null",
- "WHERE idMovie = ?"
- ))
- self.cursor.execute(query, (movieid,))
-
- def addSeason(self, showid, seasonnumber):
-
- cursor = self.cursor
-
- query = ' '.join((
-
- "SELECT idSeason",
- "FROM seasons",
- "WHERE idShow = ?",
- "AND season = ?"
- ))
- cursor.execute(query, (showid, seasonnumber,))
- try:
- seasonid = cursor.fetchone()[0]
- except TypeError:
- cursor.execute("select coalesce(max(idSeason),0) from seasons")
- seasonid = cursor.fetchone()[0] + 1
- query = "INSERT INTO seasons(idSeason, idShow, season) values(?, ?, ?)"
- cursor.execute(query, (seasonid, showid, seasonnumber))
-
- return seasonid
-
- def addArtist(self, name, musicbrainz):
-
- cursor = self.cursor
-
- query = ' '.join((
-
- "SELECT idArtist, strArtist",
- "FROM artist",
- "WHERE strMusicBrainzArtistID = ?"
- ))
- cursor.execute(query, (musicbrainz,))
- try:
- result = cursor.fetchone()
- artistid = result[0]
- artistname = result[1]
-
- except TypeError:
-
- query = ' '.join((
-
- "SELECT idArtist",
- "FROM artist",
- "WHERE strArtist = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (name,))
- try:
- artistid = cursor.fetchone()[0]
- except TypeError:
- cursor.execute("select coalesce(max(idArtist),0) from artist")
- artistid = cursor.fetchone()[0] + 1
- query = (
- '''
- INSERT INTO artist(idArtist, strArtist, strMusicBrainzArtistID)
-
- VALUES (?, ?, ?)
- '''
- )
- cursor.execute(query, (artistid, name, musicbrainz))
- else:
- if artistname != name:
- query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?"
- cursor.execute(query, (name, artistid,))
-
- return artistid
-
- def addAlbum(self, name, musicbrainz):
-
- kodiversion = self.kodiversion
- cursor = self.cursor
-
- query = ' '.join((
-
- "SELECT idAlbum",
- "FROM album",
- "WHERE strMusicBrainzAlbumID = ?"
- ))
- cursor.execute(query, (musicbrainz,))
- try:
- albumid = cursor.fetchone()[0]
- except TypeError:
- # Create the album
- cursor.execute("select coalesce(max(idAlbum),0) from album")
- albumid = cursor.fetchone()[0] + 1
- if kodiversion in (15, 16, 17):
- query = (
- '''
- INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType)
-
- VALUES (?, ?, ?, ?)
- '''
- )
- cursor.execute(query, (albumid, name, musicbrainz, "album"))
- else: # Helix
- query = (
- '''
- INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID)
-
- VALUES (?, ?, ?)
- '''
- )
- cursor.execute(query, (albumid, name, musicbrainz))
-
- return albumid
-
- def addMusicGenres(self, kodiid, genres, mediatype):
-
- cursor = self.cursor
-
- if mediatype == "album":
-
- # Delete current genres for clean slate
- query = ' '.join((
-
- "DELETE FROM album_genre",
- "WHERE idAlbum = ?"
- ))
- cursor.execute(query, (kodiid,))
-
- for genre in genres:
- query = ' '.join((
-
- "SELECT idGenre",
- "FROM genre",
- "WHERE strGenre = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (genre,))
- try:
- genreid = cursor.fetchone()[0]
- except TypeError:
- # Create the genre
- cursor.execute("select coalesce(max(idGenre),0) from genre")
- genreid = cursor.fetchone()[0] + 1
- query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)"
- cursor.execute(query, (genreid, genre))
-
- query = "INSERT OR REPLACE INTO album_genre(idGenre, idAlbum) values(?, ?)"
- cursor.execute(query, (genreid, kodiid))
-
- elif mediatype == "song":
-
- # Delete current genres for clean slate
- query = ' '.join((
-
- "DELETE FROM song_genre",
- "WHERE idSong = ?"
- ))
- cursor.execute(query, (kodiid,))
-
- for genre in genres:
- query = ' '.join((
-
- "SELECT idGenre",
- "FROM genre",
- "WHERE strGenre = ?",
- "COLLATE NOCASE"
- ))
- cursor.execute(query, (genre,))
- try:
- genreid = cursor.fetchone()[0]
- except TypeError:
- # Create the genre
- cursor.execute("select coalesce(max(idGenre),0) from genre")
- genreid = cursor.fetchone()[0] + 1
- query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)"
- cursor.execute(query, (genreid, genre))
-
- query = "INSERT OR REPLACE INTO song_genre(idGenre, idSong) values(?, ?)"
- cursor.execute(query, (genreid, kodiid))
\ No newline at end of file
+# -*- coding: utf-8 -*-
+
+##################################################################################################
+
+import xbmc
+
+import api
+import artwork
+import clientinfo
+import utils
+
+##################################################################################################
+
+
+class Kodidb_Functions():
+
+ kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
+
+
+ def __init__(self, cursor):
+
+ self.cursor = cursor
+
+ self.clientInfo = clientinfo.ClientInfo()
+ self.addonName = self.clientInfo.getAddonName()
+ self.artwork = artwork.Artwork()
+
+ def logMsg(self, msg, lvl=1):
+
+ className = self.__class__.__name__
+ utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
+
+
+ def addPath(self, path):
+
+ query = ' '.join((
+
+ "SELECT idPath",
+ "FROM path",
+ "WHERE strPath = ?"
+ ))
+ self.cursor.execute(query, (path,))
+ try:
+ pathid = self.cursor.fetchone()[0]
+ except TypeError:
+ self.cursor.execute("select coalesce(max(idPath),0) from path")
+ pathid = self.cursor.fetchone()[0] + 1
+ query = (
+ '''
+ INSERT INTO path(
+ idPath, strPath)
+
+ VALUES (?, ?)
+ '''
+ )
+ self.cursor.execute(query, (pathid, path))
+
+ return pathid
+
+ def getPath(self, path):
+
+ query = ' '.join((
+
+ "SELECT idPath",
+ "FROM path",
+ "WHERE strPath = ?"
+ ))
+ self.cursor.execute(query, (path,))
+ try:
+ pathid = self.cursor.fetchone()[0]
+ except TypeError:
+ pathid = None
+
+ return pathid
+
+ def addFile(self, filename, pathid):
+
+ query = ' '.join((
+
+ "SELECT idFile",
+ "FROM files",
+ "WHERE strFilename = ?",
+ "AND idPath = ?"
+ ))
+ self.cursor.execute(query, (filename, pathid,))
+ try:
+ fileid = self.cursor.fetchone()[0]
+ except TypeError:
+ self.cursor.execute("select coalesce(max(idFile),0) from files")
+ fileid = self.cursor.fetchone()[0] + 1
+ query = (
+ '''
+ INSERT INTO files(
+ idFile, strFilename)
+
+ VALUES (?, ?)
+ '''
+ )
+ self.cursor.execute(query, (fileid, filename))
+
+ return fileid
+
+ def getFile(self, fileid):
+
+ query = ' '.join((
+
+ "SELECT strFilename",
+ "FROM files",
+ "WHERE idFile = ?"
+ ))
+ self.cursor.execute(query, (fileid,))
+ try:
+ filename = self.cursor.fetchone()[0]
+ except TypeError:
+ filename = ""
+
+ return filename
+
+ def removeFile(self, path, filename):
+
+ pathid = self.getPath(path)
+
+ if pathid is not None:
+ query = ' '.join((
+
+ "DELETE FROM files",
+ "WHERE idPath = ?",
+ "AND strFilename = ?"
+ ))
+ self.cursor.execute(query, (pathid, filename,))
+
+ def addCountries(self, kodiid, countries, mediatype):
+
+ if self.kodiversion in (15, 16, 17):
+ # Kodi Isengard, Jarvis, Krypton
+ for country in countries:
+ query = ' '.join((
+
+ "SELECT country_id",
+ "FROM country",
+ "WHERE name = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (country,))
+
+ try:
+ country_id = self.cursor.fetchone()[0]
+
+ except TypeError:
+ # Country entry does not exists
+ self.cursor.execute("select coalesce(max(country_id),0) from country")
+ country_id = self.cursor.fetchone()[0] + 1
+
+ query = "INSERT INTO country(country_id, name) values(?, ?)"
+ self.cursor.execute(query, (country_id, country))
+ self.logMsg("Add country to media, processing: %s" % country, 2)
+
+ finally: # Assign country to content
+ query = (
+ '''
+ INSERT OR REPLACE INTO country_link(
+ country_id, media_id, media_type)
+
+ VALUES (?, ?, ?)
+ '''
+ )
+ self.cursor.execute(query, (country_id, kodiid, mediatype))
+ else:
+ # Kodi Helix
+ for country in countries:
+ query = ' '.join((
+
+ "SELECT idCountry",
+ "FROM country",
+ "WHERE strCountry = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (country,))
+
+ try:
+ idCountry = self.cursor.fetchone()[0]
+
+ except TypeError:
+ # Country entry does not exists
+ self.cursor.execute("select coalesce(max(idCountry),0) from country")
+ idCountry = self.cursor.fetchone()[0] + 1
+
+ query = "INSERT INTO country(idCountry, strCountry) values(?, ?)"
+ self.cursor.execute(query, (idCountry, country))
+ self.logMsg("Add country to media, processing: %s" % country, 2)
+
+ finally:
+ # Only movies have a country field
+ if "movie" in mediatype:
+ query = (
+ '''
+ INSERT OR REPLACE INTO countrylinkmovie(
+ idCountry, idMovie)
+
+ VALUES (?, ?)
+ '''
+ )
+ self.cursor.execute(query, (idCountry, kodiid))
+
+ def addPeople(self, kodiid, people, mediatype):
+
+ castorder = 1
+ for person in people:
+
+ name = person['Name']
+ person_type = person['Type']
+ thumb = person['imageurl']
+
+ # Kodi Isengard, Jarvis, Krypton
+ if self.kodiversion in (15, 16, 17):
+ query = ' '.join((
+
+ "SELECT actor_id",
+ "FROM actor",
+ "WHERE name = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (name,))
+
+ try:
+ actorid = self.cursor.fetchone()[0]
+
+ except TypeError:
+ # Cast entry does not exists
+ self.cursor.execute("select coalesce(max(actor_id),0) from actor")
+ actorid = self.cursor.fetchone()[0] + 1
+
+ query = "INSERT INTO actor(actor_id, name) values(?, ?)"
+ self.cursor.execute(query, (actorid, name))
+ self.logMsg("Add people to media, processing: %s" % name, 2)
+
+ finally:
+ # Link person to content
+ if "Actor" in person_type:
+ role = person.get('Role')
+ query = (
+ '''
+ INSERT OR REPLACE INTO actor_link(
+ actor_id, media_id, media_type, role, cast_order)
+
+ VALUES (?, ?, ?, ?, ?)
+ '''
+ )
+ self.cursor.execute(query, (actorid, kodiid, mediatype, role, castorder))
+ castorder += 1
+
+ elif "Director" in person_type:
+ query = (
+ '''
+ INSERT OR REPLACE INTO director_link(
+ actor_id, media_id, media_type)
+
+ VALUES (?, ?, ?)
+ '''
+ )
+ self.cursor.execute(query, (actorid, kodiid, mediatype))
+
+ elif person_type in ("Writing", "Writer"):
+ query = (
+ '''
+ INSERT OR REPLACE INTO writer_link(
+ actor_id, media_id, media_type)
+
+ VALUES (?, ?, ?)
+ '''
+ )
+ self.cursor.execute(query, (actorid, kodiid, mediatype))
+
+ elif "Artist" in person_type:
+ query = (
+ '''
+ INSERT OR REPLACE INTO actor_link(
+ actor_id, media_id, media_type)
+
+ VALUES (?, ?, ?)
+ '''
+ )
+ self.cursor.execute(query, (actorid, kodiid, mediatype))
+ # Kodi Helix
+ else:
+ query = ' '.join((
+
+ "SELECT idActor",
+ "FROM actors",
+ "WHERE strActor = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (name,))
+
+ try:
+ actorid = self.cursor.fetchone()[0]
+
+ except TypeError:
+ # Cast entry does not exists
+ self.cursor.execute("select coalesce(max(idActor),0) from actors")
+ actorid = self.cursor.fetchone()[0] + 1
+
+ query = "INSERT INTO actors(idActor, strActor) values(?, ?)"
+ self.cursor.execute(query, (actorid, name))
+ self.logMsg("Add people to media, processing: %s" % name, 2)
+
+ finally:
+ # Link person to content
+ if "Actor" in person_type:
+ role = person.get('Role')
+
+ if "movie" in mediatype:
+ query = (
+ '''
+ INSERT OR REPLACE INTO actorlinkmovie(
+ idActor, idMovie, strRole, iOrder)
+
+ VALUES (?, ?, ?, ?)
+ '''
+ )
+ elif "tvshow" in mediatype:
+ query = (
+ '''
+ INSERT OR REPLACE INTO actorlinktvshow(
+ idActor, idShow, strRole, iOrder)
+
+ VALUES (?, ?, ?, ?)
+ '''
+ )
+ elif "episode" in mediatype:
+ query = (
+ '''
+ INSERT OR REPLACE INTO actorlinkepisode(
+ idActor, idEpisode, strRole, iOrder)
+
+ VALUES (?, ?, ?, ?)
+ '''
+ )
+ else: return # Item is invalid
+
+ self.cursor.execute(query, (actorid, kodiid, role, castorder))
+ castorder += 1
+
+ elif "Director" in person_type:
+ if "movie" in mediatype:
+ query = (
+ '''
+ INSERT OR REPLACE INTO directorlinkmovie(
+ idDirector, idMovie)
+
+ VALUES (?, ?)
+ '''
+ )
+ elif "tvshow" in mediatype:
+ query = (
+ '''
+ INSERT OR REPLACE INTO directorlinktvshow(
+ idDirector, idShow)
+
+ VALUES (?, ?)
+ '''
+ )
+ elif "musicvideo" in mediatype:
+ query = (
+ '''
+ INSERT OR REPLACE INTO directorlinkmusicvideo(
+ idDirector, idMVideo)
+
+ VALUES (?, ?)
+ '''
+ )
+
+ elif "episode" in mediatype:
+ query = (
+ '''
+ INSERT OR REPLACE INTO directorlinkepisode(
+ idDirector, idEpisode)
+
+ VALUES (?, ?)
+ '''
+ )
+ else: return # Item is invalid
+
+ self.cursor.execute(query, (actorid, kodiid))
+
+ elif person_type in ("Writing", "Writer"):
+ if "movie" in mediatype:
+ query = (
+ '''
+ INSERT OR REPLACE INTO writerlinkmovie(
+ idWriter, idMovie)
+
+ VALUES (?, ?)
+ '''
+ )
+ elif "episode" in mediatype:
+ query = (
+ '''
+ INSERT OR REPLACE INTO writerlinkepisode(
+ idWriter, idEpisode)
+
+ VALUES (?, ?)
+ '''
+ )
+ else: return # Item is invalid
+
+ self.cursor.execute(query, (actorid, kodiid))
+
+ elif "Artist" in person_type:
+ query = (
+ '''
+ INSERT OR REPLACE INTO artistlinkmusicvideo(
+ idArtist, idMVideo)
+
+ VALUES (?, ?)
+ '''
+ )
+ self.cursor.execute(query, (actorid, kodiid))
+
+ # Add person image to art table
+ if thumb:
+ arttype = person_type.lower()
+
+ if "writing" in arttype:
+ arttype = "writer"
+
+ self.artwork.addOrUpdateArt(thumb, actorid, arttype, "thumb", self.cursor)
+
+ def addGenres(self, kodiid, genres, mediatype):
+
+
+ # Kodi Isengard, Jarvis, Krypton
+ if self.kodiversion in (15, 16, 17):
+ # Delete current genres for clean slate
+ query = ' '.join((
+
+ "DELETE FROM genre_link",
+ "WHERE media_id = ?",
+ "AND media_type = ?"
+ ))
+ self.cursor.execute(query, (kodiid, mediatype,))
+
+ # Add genres
+ for genre in genres:
+
+ query = ' '.join((
+
+ "SELECT genre_id",
+ "FROM genre",
+ "WHERE name = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (genre,))
+
+ try:
+ genre_id = self.cursor.fetchone()[0]
+
+ except TypeError:
+ # Create genre in database
+ self.cursor.execute("select coalesce(max(genre_id),0) from genre")
+ genre_id = self.cursor.fetchone()[0] + 1
+
+ query = "INSERT INTO genre(genre_id, name) values(?, ?)"
+ self.cursor.execute(query, (genre_id, genre))
+ self.logMsg("Add Genres to media, processing: %s" % genre, 2)
+
+ finally:
+ # Assign genre to item
+ query = (
+ '''
+ INSERT OR REPLACE INTO genre_link(
+ genre_id, media_id, media_type)
+
+ VALUES (?, ?, ?)
+ '''
+ )
+ self.cursor.execute(query, (genre_id, kodiid, mediatype))
+ else:
+ # Kodi Helix
+ # Delete current genres for clean slate
+ if "movie" in mediatype:
+ self.cursor.execute("DELETE FROM genrelinkmovie WHERE idMovie = ?", (kodiid,))
+ elif "tvshow" in mediatype:
+ self.cursor.execute("DELETE FROM genrelinktvshow WHERE idShow = ?", (kodiid,))
+ elif "musicvideo" in mediatype:
+ self.cursor.execute("DELETE FROM genrelinkmusicvideo WHERE idMVideo = ?", (kodiid,))
+
+ # Add genres
+ for genre in genres:
+
+ query = ' '.join((
+
+ "SELECT idGenre",
+ "FROM genre",
+ "WHERE strGenre = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (genre,))
+
+ try:
+ idGenre = self.cursor.fetchone()[0]
+
+ except TypeError:
+ # Create genre in database
+ self.cursor.execute("select coalesce(max(idGenre),0) from genre")
+ idGenre = self.cursor.fetchone()[0] + 1
+
+ query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)"
+ self.cursor.execute(query, (idGenre, genre))
+ self.logMsg("Add Genres to media, processing: %s" % genre, 2)
+
+ finally:
+ # Assign genre to item
+ if "movie" in mediatype:
+ query = (
+ '''
+ INSERT OR REPLACE into genrelinkmovie(
+ idGenre, idMovie)
+
+ VALUES (?, ?)
+ '''
+ )
+ elif "tvshow" in mediatype:
+ query = (
+ '''
+ INSERT OR REPLACE into genrelinktvshow(
+ idGenre, idShow)
+
+ VALUES (?, ?)
+ '''
+ )
+ elif "musicvideo" in mediatype:
+ query = (
+ '''
+ INSERT OR REPLACE into genrelinkmusicvideo(
+ idGenre, idMVideo)
+
+ VALUES (?, ?)
+ '''
+ )
+ else: return # Item is invalid
+
+ self.cursor.execute(query, (idGenre, kodiid))
+
+ def addStudios(self, kodiid, studios, mediatype):
+
+ for studio in studios:
+
+ if self.kodiversion in (15, 16, 17):
+ # Kodi Isengard, Jarvis, Krypton
+ query = ' '.join((
+
+ "SELECT studio_id",
+ "FROM studio",
+ "WHERE name = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (studio,))
+ try:
+ studioid = self.cursor.fetchone()[0]
+
+ except TypeError:
+ # Studio does not exists.
+ self.cursor.execute("select coalesce(max(studio_id),0) from studio")
+ studioid = self.cursor.fetchone()[0] + 1
+
+ query = "INSERT INTO studio(studio_id, name) values(?, ?)"
+ self.cursor.execute(query, (studioid, studio))
+ self.logMsg("Add Studios to media, processing: %s" % studio, 2)
+
+ finally: # Assign studio to item
+ query = (
+ '''
+ INSERT OR REPLACE INTO studio_link(
+ studio_id, media_id, media_type)
+
+ VALUES (?, ?, ?)
+ ''')
+ self.cursor.execute(query, (studioid, kodiid, mediatype))
+ else:
+ # Kodi Helix
+ query = ' '.join((
+
+ "SELECT idstudio",
+ "FROM studio",
+ "WHERE strstudio = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (studio,))
+ try:
+ studioid = self.cursor.fetchone()[0]
+
+ except TypeError:
+ # Studio does not exists.
+ self.cursor.execute("select coalesce(max(idstudio),0) from studio")
+ studioid = self.cursor.fetchone()[0] + 1
+
+ query = "INSERT INTO studio(idstudio, strstudio) values(?, ?)"
+ self.cursor.execute(query, (studioid, studio))
+ self.logMsg("Add Studios to media, processing: %s" % studio, 2)
+
+ finally: # Assign studio to item
+ if "movie" in mediatype:
+ query = (
+ '''
+ INSERT OR REPLACE INTO studiolinkmovie(idstudio, idMovie)
+ VALUES (?, ?)
+ ''')
+ elif "musicvideo" in mediatype:
+ query = (
+ '''
+ INSERT OR REPLACE INTO studiolinkmusicvideo(idstudio, idMVideo)
+ VALUES (?, ?)
+ ''')
+ elif "tvshow" in mediatype:
+ query = (
+ '''
+ INSERT OR REPLACE INTO studiolinktvshow(idstudio, idShow)
+ VALUES (?, ?)
+ ''')
+ elif "episode" in mediatype:
+ query = (
+ '''
+ INSERT OR REPLACE INTO studiolinkepisode(idstudio, idEpisode)
+ VALUES (?, ?)
+ ''')
+ self.cursor.execute(query, (studioid, kodiid))
+
+ def addStreams(self, fileid, streamdetails, runtime):
+
+ # First remove any existing entries
+ self.cursor.execute("DELETE FROM streamdetails WHERE idFile = ?", (fileid,))
+ if streamdetails:
+ # Video details
+ for videotrack in streamdetails['video']:
+ query = (
+ '''
+ INSERT INTO streamdetails(
+ idFile, iStreamType, strVideoCodec, fVideoAspect,
+ iVideoWidth, iVideoHeight, iVideoDuration ,strStereoMode)
+
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ '''
+ )
+ self.cursor.execute(query, (fileid, 0, videotrack['codec'],
+ videotrack['aspect'], videotrack['width'], videotrack['height'],
+ runtime ,videotrack['video3DFormat']))
+
+ # Audio details
+ for audiotrack in streamdetails['audio']:
+ query = (
+ '''
+ INSERT INTO streamdetails(
+ idFile, iStreamType, strAudioCodec, iAudioChannels, strAudioLanguage)
+
+ VALUES (?, ?, ?, ?, ?)
+ '''
+ )
+ self.cursor.execute(query, (fileid, 1, audiotrack['codec'],
+ audiotrack['channels'], audiotrack['language']))
+
+ # Subtitles details
+ for subtitletrack in streamdetails['subtitle']:
+ query = (
+ '''
+ INSERT INTO streamdetails(
+ idFile, iStreamType, strSubtitleLanguage)
+
+ VALUES (?, ?, ?)
+ '''
+ )
+ self.cursor.execute(query, (fileid, 2, subtitletrack))
+
+ def addPlaystate(self, fileid, resume_seconds, total_seconds, playcount, dateplayed):
+
+ # Delete existing resume point
+ query = ' '.join((
+
+ "DELETE FROM bookmark",
+ "WHERE idFile = ?"
+ ))
+ self.cursor.execute(query, (fileid,))
+
+ # Set watched count
+ query = ' '.join((
+
+ "UPDATE files",
+ "SET playCount = ?, lastPlayed = ?",
+ "WHERE idFile = ?"
+ ))
+ self.cursor.execute(query, (playcount, dateplayed, fileid))
+
+ # Set the resume bookmark
+ if resume_seconds:
+ self.cursor.execute("select coalesce(max(idBookmark),0) from bookmark")
+ bookmarkId = self.cursor.fetchone()[0] + 1
+ query = (
+ '''
+ INSERT INTO bookmark(
+ idBookmark, idFile, timeInSeconds, totalTimeInSeconds, player, type)
+
+ VALUES (?, ?, ?, ?, ?, ?)
+ '''
+ )
+ self.cursor.execute(query, (bookmarkId, fileid, resume_seconds, total_seconds,
+ "DVDPlayer", 1))
+
+ def addTags(self, kodiid, tags, mediatype):
+
+ # First, delete any existing tags associated to the id
+ if self.kodiversion in (15, 16, 17):
+ # Kodi Isengard, Jarvis, Krypton
+ query = ' '.join((
+
+ "DELETE FROM tag_link",
+ "WHERE media_id = ?",
+ "AND media_type = ?"
+ ))
+ self.cursor.execute(query, (kodiid, mediatype))
+ else:
+ # Kodi Helix
+ query = ' '.join((
+
+ "DELETE FROM taglinks",
+ "WHERE idMedia = ?",
+ "AND media_type = ?"
+ ))
+ self.cursor.execute(query, (kodiid, mediatype))
+
+ # Add tags
+ self.logMsg("Adding Tags: %s" % tags, 2)
+ for tag in tags:
+ self.addTag(kodiid, tag, mediatype)
+
+ def addTag(self, kodiid, tag, mediatype):
+
+ if self.kodiversion in (15, 16, 17):
+ # Kodi Isengard, Jarvis, Krypton
+ query = ' '.join((
+
+ "SELECT tag_id",
+ "FROM tag",
+ "WHERE name = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (tag,))
+ try:
+ tag_id = self.cursor.fetchone()[0]
+
+ except TypeError:
+ # Create the tag, because it does not exist
+ tag_id = self.createTag(tag)
+ self.logMsg("Adding tag: %s" % tag, 2)
+
+ finally:
+ # Assign tag to item
+ query = (
+ '''
+ INSERT OR REPLACE INTO tag_link(
+ tag_id, media_id, media_type)
+
+ VALUES (?, ?, ?)
+ '''
+ )
+ self.cursor.execute(query, (tag_id, kodiid, mediatype))
+ else:
+ # Kodi Helix
+ query = ' '.join((
+
+ "SELECT idTag",
+ "FROM tag",
+ "WHERE strTag = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (tag,))
+ try:
+ tag_id = self.cursor.fetchone()[0]
+
+ except TypeError:
+ # Create the tag
+ tag_id = self.createTag(tag)
+ self.logMsg("Adding tag: %s" % tag, 2)
+
+ finally:
+ # Assign tag to item
+ query = (
+ '''
+ INSERT OR REPLACE INTO taglinks(
+ idTag, idMedia, media_type)
+
+ VALUES (?, ?, ?)
+ '''
+ )
+ self.cursor.execute(query, (tag_id, kodiid, mediatype))
+
+ def createTag(self, name):
+
+ # This will create and return the tag_id
+ if self.kodiversion in (15, 16, 17):
+ # Kodi Isengard, Jarvis, Krypton
+ query = ' '.join((
+
+ "SELECT tag_id",
+ "FROM tag",
+ "WHERE name = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (name,))
+ try:
+ tag_id = self.cursor.fetchone()[0]
+
+ except TypeError:
+ self.cursor.execute("select coalesce(max(tag_id),0) from tag")
+ tag_id = self.cursor.fetchone()[0] + 1
+
+ query = "INSERT INTO tag(tag_id, name) values(?, ?)"
+ self.cursor.execute(query, (tag_id, name))
+ self.logMsg("Create tag_id: %s name: %s" % (tag_id, name), 2)
+ else:
+ # Kodi Helix
+ query = ' '.join((
+
+ "SELECT idTag",
+ "FROM tag",
+ "WHERE strTag = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (name,))
+ try:
+ tag_id = self.cursor.fetchone()[0]
+
+ except TypeError:
+ self.cursor.execute("select coalesce(max(idTag),0) from tag")
+ tag_id = self.cursor.fetchone()[0] + 1
+
+ query = "INSERT INTO tag(idTag, strTag) values(?, ?)"
+ self.cursor.execute(query, (tag_id, name))
+ self.logMsg("Create idTag: %s name: %s" % (tag_id, name), 2)
+
+ return tag_id
+
+ def updateTag(self, oldtag, newtag, kodiid, mediatype):
+
+ self.logMsg("Updating: %s with %s for %s: %s" % (oldtag, newtag, mediatype, kodiid), 2)
+
+ if self.kodiversion in (15, 16, 17):
+ # Kodi Isengard, Jarvis, Krypton
+ try:
+ query = ' '.join((
+
+ "UPDATE tag_link",
+ "SET tag_id = ?",
+ "WHERE media_id = ?",
+ "AND media_type = ?",
+ "AND tag_id = ?"
+ ))
+ self.cursor.execute(query, (newtag, kodiid, mediatype, oldtag,))
+ except Exception as e:
+ # The new tag we are going to apply already exists for this item
+ # delete current tag instead
+ self.logMsg("Exception: %s" % e, 1)
+ query = ' '.join((
+
+ "DELETE FROM tag_link",
+ "WHERE media_id = ?",
+ "AND media_type = ?",
+ "AND tag_id = ?"
+ ))
+ self.cursor.execute(query, (kodiid, mediatype, oldtag,))
+ else:
+ # Kodi Helix
+ try:
+ query = ' '.join((
+
+ "UPDATE taglinks",
+ "SET idTag = ?",
+ "WHERE idMedia = ?",
+ "AND media_type = ?",
+ "AND idTag = ?"
+ ))
+ self.cursor.execute(query, (newtag, kodiid, mediatype, oldtag,))
+ except Exception as e:
+ # The new tag we are going to apply already exists for this item
+ # delete current tag instead
+ self.logMsg("Exception: %s" % e, 1)
+ query = ' '.join((
+
+ "DELETE FROM taglinks",
+ "WHERE idMedia = ?",
+ "AND media_type = ?",
+ "AND idTag = ?"
+ ))
+ self.cursor.execute(query, (kodiid, mediatype, oldtag,))
+
+ def removeTag(self, kodiid, tagname, mediatype):
+
+ if self.kodiversion in (15, 16, 17):
+ # Kodi Isengard, Jarvis, Krypton
+ query = ' '.join((
+
+ "SELECT tag_id",
+ "FROM tag",
+ "WHERE name = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (tagname,))
+ try:
+ tag_id = self.cursor.fetchone()[0]
+ except TypeError:
+ return
+ else:
+ query = ' '.join((
+
+ "DELETE FROM tag_link",
+ "WHERE media_id = ?",
+ "AND media_type = ?",
+ "AND tag_id = ?"
+ ))
+ self.cursor.execute(query, (kodiid, mediatype, tag_id,))
+ else:
+ # Kodi Helix
+ query = ' '.join((
+
+ "SELECT idTag",
+ "FROM tag",
+ "WHERE strTag = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (tagname,))
+ try:
+ tag_id = self.cursor.fetchone()[0]
+ except TypeError:
+ return
+ else:
+ query = ' '.join((
+
+ "DELETE FROM taglinks",
+ "WHERE idMedia = ?",
+ "AND media_type = ?",
+ "AND idTag = ?"
+ ))
+ self.cursor.execute(query, (kodiid, mediatype, tag_id,))
+
+ def createBoxset(self, boxsetname):
+
+ self.logMsg("Adding boxset: %s" % boxsetname, 2)
+ query = ' '.join((
+
+ "SELECT idSet",
+ "FROM sets",
+ "WHERE strSet = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (boxsetname,))
+ try:
+ setid = self.cursor.fetchone()[0]
+
+ except TypeError:
+ self.cursor.execute("select coalesce(max(idSet),0) from sets")
+ setid = self.cursor.fetchone()[0] + 1
+
+ query = "INSERT INTO sets(idSet, strSet) values(?, ?)"
+ self.cursor.execute(query, (setid, boxsetname))
+
+ return setid
+
+ def assignBoxset(self, setid, movieid):
+
+ query = ' '.join((
+
+ "UPDATE movie",
+ "SET idSet = ?",
+ "WHERE idMovie = ?"
+ ))
+ self.cursor.execute(query, (setid, movieid,))
+
+ def removefromBoxset(self, movieid):
+
+ query = ' '.join((
+
+ "UPDATE movie",
+ "SET idSet = null",
+ "WHERE idMovie = ?"
+ ))
+ self.cursor.execute(query, (movieid,))
+
+ def addSeason(self, showid, seasonnumber):
+
+ query = ' '.join((
+
+ "SELECT idSeason",
+ "FROM seasons",
+ "WHERE idShow = ?",
+ "AND season = ?"
+ ))
+ self.cursor.execute(query, (showid, seasonnumber,))
+ try:
+ seasonid = self.cursor.fetchone()[0]
+ except TypeError:
+ self.cursor.execute("select coalesce(max(idSeason),0) from seasons")
+ seasonid = self.cursor.fetchone()[0] + 1
+ query = "INSERT INTO seasons(idSeason, idShow, season) values(?, ?, ?)"
+ self.cursor.execute(query, (seasonid, showid, seasonnumber))
+
+ return seasonid
+
+ def addArtist(self, name, musicbrainz):
+
+ query = ' '.join((
+
+ "SELECT idArtist, strArtist",
+ "FROM artist",
+ "WHERE strMusicBrainzArtistID = ?"
+ ))
+ self.cursor.execute(query, (musicbrainz,))
+ try:
+ result = self.cursor.fetchone()
+ artistid = result[0]
+ artistname = result[1]
+
+ except TypeError:
+
+ query = ' '.join((
+
+ "SELECT idArtist",
+ "FROM artist",
+ "WHERE strArtist = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (name,))
+ try:
+ artistid = self.cursor.fetchone()[0]
+ except TypeError:
+ self.cursor.execute("select coalesce(max(idArtist),0) from artist")
+ artistid = self.cursor.fetchone()[0] + 1
+ query = (
+ '''
+ INSERT INTO artist(idArtist, strArtist, strMusicBrainzArtistID)
+
+ VALUES (?, ?, ?)
+ '''
+ )
+ self.cursor.execute(query, (artistid, name, musicbrainz))
+ else:
+ if artistname != name:
+ query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?"
+ self.cursor.execute(query, (name, artistid,))
+
+ return artistid
+
+ def addAlbum(self, name, musicbrainz):
+
+ query = ' '.join((
+
+ "SELECT idAlbum",
+ "FROM album",
+ "WHERE strMusicBrainzAlbumID = ?"
+ ))
+ self.cursor.execute(query, (musicbrainz,))
+ try:
+ albumid = self.cursor.fetchone()[0]
+ except TypeError:
+ # Create the album
+ self.cursor.execute("select coalesce(max(idAlbum),0) from album")
+ albumid = self.cursor.fetchone()[0] + 1
+ if self.kodiversion in (15, 16, 17):
+ query = (
+ '''
+ INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType)
+
+ VALUES (?, ?, ?, ?)
+ '''
+ )
+ self.cursor.execute(query, (albumid, name, musicbrainz, "album"))
+ else: # Helix
+ query = (
+ '''
+ INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID)
+
+ VALUES (?, ?, ?)
+ '''
+ )
+ self.cursor.execute(query, (albumid, name, musicbrainz))
+
+ return albumid
+
+ def addMusicGenres(self, kodiid, genres, mediatype):
+
+ if mediatype == "album":
+
+ # Delete current genres for clean slate
+ query = ' '.join((
+
+ "DELETE FROM album_genre",
+ "WHERE idAlbum = ?"
+ ))
+ self.cursor.execute(query, (kodiid,))
+
+ for genre in genres:
+ query = ' '.join((
+
+ "SELECT idGenre",
+ "FROM genre",
+ "WHERE strGenre = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (genre,))
+ try:
+ genreid = self.cursor.fetchone()[0]
+ except TypeError:
+ # Create the genre
+ self.cursor.execute("select coalesce(max(idGenre),0) from genre")
+ genreid = self.cursor.fetchone()[0] + 1
+ query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)"
+ self.cursor.execute(query, (genreid, genre))
+
+ query = "INSERT OR REPLACE INTO album_genre(idGenre, idAlbum) values(?, ?)"
+ self.cursor.execute(query, (genreid, kodiid))
+
+ elif mediatype == "song":
+
+ # Delete current genres for clean slate
+ query = ' '.join((
+
+ "DELETE FROM song_genre",
+ "WHERE idSong = ?"
+ ))
+ self.cursor.execute(query, (kodiid,))
+
+ for genre in genres:
+ query = ' '.join((
+
+ "SELECT idGenre",
+ "FROM genre",
+ "WHERE strGenre = ?",
+ "COLLATE NOCASE"
+ ))
+ self.cursor.execute(query, (genre,))
+ try:
+ genreid = self.cursor.fetchone()[0]
+ except TypeError:
+ # Create the genre
+ self.cursor.execute("select coalesce(max(idGenre),0) from genre")
+ genreid = self.cursor.fetchone()[0] + 1
+ query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)"
+ self.cursor.execute(query, (genreid, genre))
+
+ query = "INSERT OR REPLACE INTO song_genre(idGenre, idSong) values(?, ?)"
+ self.cursor.execute(query, (genreid, kodiid))
\ No newline at end of file
diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py
index f00423c1..f2b5ae86 100644
--- a/resources/lib/kodimonitor.py
+++ b/resources/lib/kodimonitor.py
@@ -1,209 +1,209 @@
-# -*- coding: utf-8 -*-
-
-#################################################################################################
-
-import json
-
-import xbmc
-import xbmcgui
-
-import clientinfo
-import downloadutils
-import embydb_functions as embydb
-import playbackutils as pbutils
-import utils
-
-#################################################################################################
-
-
-class KodiMonitor(xbmc.Monitor):
-
-
- def __init__(self):
-
- self.clientInfo = clientinfo.ClientInfo()
- self.addonName = self.clientInfo.getAddonName()
- self.doUtils = downloadutils.DownloadUtils()
-
- self.logMsg("Kodi monitor started.", 1)
-
- def logMsg(self, msg, lvl=1):
-
- self.className = self.__class__.__name__
- utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
-
-
- def onScanStarted(self, library):
- self.logMsg("Kodi library scan %s running." % library, 2)
- if library == "video":
- utils.window('emby_kodiScan', value="true")
-
- def onScanFinished(self, library):
- self.logMsg("Kodi library scan %s finished." % library, 2)
- if library == "video":
- utils.window('emby_kodiScan', clear=True)
-
- def onSettingsChanged(self):
- # Monitor emby settings
- # Review reset setting at a later time, need to be adjusted to account for initial setup
- # changes.
- '''currentPath = utils.settings('useDirectPaths')
- if utils.window('emby_pluginpath') != currentPath:
- # Plugin path value changed. Offer to reset
- self.logMsg("Changed to playback mode detected", 1)
- utils.window('emby_pluginpath', value=currentPath)
- resp = xbmcgui.Dialog().yesno(
- heading="Playback mode change detected",
- line1=(
- "Detected the playback mode has changed. The database "
- "needs to be recreated for the change to be applied. "
- "Proceed?"))
- if resp:
- utils.reset()'''
-
- currentLog = utils.settings('logLevel')
- if utils.window('emby_logLevel') != currentLog:
- # The log level changed, set new prop
- self.logMsg("New log level: %s" % currentLog, 1)
- utils.window('emby_logLevel', value=currentLog)
-
- def onNotification(self, sender, method, data):
-
- doUtils = self.doUtils
- if method not in ("Playlist.OnAdd"):
- self.logMsg("Method: %s Data: %s" % (method, data), 1)
-
- if data:
- data = json.loads(data,'utf-8')
-
-
- if method == "Player.OnPlay":
- # Set up report progress for emby playback
- item = data.get('item')
- try:
- kodiid = item['id']
- type = item['type']
- except (KeyError, TypeError):
- self.logMsg("Item is invalid for playstate update.", 1)
- else:
- if ((utils.settings('useDirectPaths') == "1" and not type == "song") or
- (type == "song" and utils.settings('enableMusic') == "true")):
- # Set up properties for player
- embyconn = utils.kodiSQL('emby')
- embycursor = embyconn.cursor()
- emby_db = embydb.Embydb_Functions(embycursor)
- emby_dbitem = emby_db.getItem_byKodiId(kodiid, type)
- try:
- itemid = emby_dbitem[0]
- except TypeError:
- self.logMsg("No kodiid returned.", 1)
- else:
- url = "{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid
- result = doUtils.downloadUrl(url)
- self.logMsg("Item: %s" % result, 2)
-
- playurl = None
- count = 0
- while not playurl and count < 2:
- try:
- playurl = xbmc.Player().getPlayingFile()
- except RuntimeError:
- count += 1
- xbmc.sleep(200)
- else:
- listItem = xbmcgui.ListItem()
- playback = pbutils.PlaybackUtils(result)
-
- if type == "song" and utils.settings('streamMusic') == "true":
- utils.window('emby_%s.playmethod' % playurl,
- value="DirectStream")
- else:
- utils.window('emby_%s.playmethod' % playurl,
- value="DirectPlay")
- # Set properties for player.py
- playback.setProperties(playurl, listItem)
- finally:
- embycursor.close()
-
-
- elif method == "VideoLibrary.OnUpdate":
- # Manually marking as watched/unwatched
- playcount = data.get('playcount')
- item = data.get('item')
- try:
- kodiid = item['id']
- type = item['type']
- except (KeyError, TypeError):
- self.logMsg("Item is invalid for playstate update.", 1)
- else:
- # Send notification to the server.
- embyconn = utils.kodiSQL('emby')
- embycursor = embyconn.cursor()
- emby_db = embydb.Embydb_Functions(embycursor)
- emby_dbitem = emby_db.getItem_byKodiId(kodiid, type)
- try:
- itemid = emby_dbitem[0]
- except TypeError:
- self.logMsg("Could not find itemid in emby database.", 1)
- else:
- # Stop from manually marking as watched unwatched, with actual playback.
- if utils.window('emby_skipWatched%s' % itemid) == "true":
- # property is set in player.py
- utils.window('emby_skipWatched%s' % itemid, clear=True)
- else:
- # notify the server
- url = "{server}/emby/Users/{UserId}/PlayedItems/%s?format=json" % itemid
- if playcount != 0:
- doUtils.downloadUrl(url, type="POST")
- self.logMsg("Mark as watched for itemid: %s" % itemid, 1)
- else:
- doUtils.downloadUrl(url, type="DELETE")
- self.logMsg("Mark as unwatched for itemid: %s" % itemid, 1)
- finally:
- embycursor.close()
-
-
- elif method == "VideoLibrary.OnRemove":
- # Removed function, because with plugin paths + clean library, it will wipe
- # entire library if user has permissions. Instead, use the emby context menu available
- # in Isengard and higher version
- pass
- '''try:
- kodiid = data['id']
- type = data['type']
- except (KeyError, TypeError):
- self.logMsg("Item is invalid for emby deletion.", 1)
- else:
- # Send the delete action to the server.
- embyconn = utils.kodiSQL('emby')
- embycursor = embyconn.cursor()
- emby_db = embydb.Embydb_Functions(embycursor)
- emby_dbitem = emby_db.getItem_byKodiId(kodiid, type)
- try:
- itemid = emby_dbitem[0]
- except TypeError:
- self.logMsg("Could not find itemid in emby database.", 1)
- else:
- if utils.settings('skipContextMenu') != "true":
- resp = xbmcgui.Dialog().yesno(
- heading="Confirm delete",
- line1="Delete file on Emby Server?")
- if not resp:
- self.logMsg("User skipped deletion.", 1)
- embycursor.close()
- return
-
- url = "{server}/emby/Items/%s?format=json" % itemid
- self.logMsg("Deleting request: %s" % itemid)
- doUtils.downloadUrl(url, type="DELETE")
- finally:
- embycursor.close()'''
-
-
- elif method == "System.OnWake":
- # Allow network to wake up
- xbmc.sleep(10000)
- utils.window('emby_onWake', value="true")
-
- elif method == "Playlist.OnClear":
+# -*- coding: utf-8 -*-
+
+#################################################################################################
+
+import json
+
+import xbmc
+import xbmcgui
+
+import clientinfo
+import downloadutils
+import embydb_functions as embydb
+import playbackutils as pbutils
+import utils
+
+#################################################################################################
+
+
+class KodiMonitor(xbmc.Monitor):
+
+
+ def __init__(self):
+
+ self.clientInfo = clientinfo.ClientInfo()
+ self.addonName = self.clientInfo.getAddonName()
+ self.doUtils = downloadutils.DownloadUtils()
+
+ self.logMsg("Kodi monitor started.", 1)
+
+ def logMsg(self, msg, lvl=1):
+
+ self.className = self.__class__.__name__
+ utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
+
+
+ def onScanStarted(self, library):
+ self.logMsg("Kodi library scan %s running." % library, 2)
+ if library == "video":
+ utils.window('emby_kodiScan', value="true")
+
+ def onScanFinished(self, library):
+ self.logMsg("Kodi library scan %s finished." % library, 2)
+ if library == "video":
+ utils.window('emby_kodiScan', clear=True)
+
+ def onSettingsChanged(self):
+ # Monitor emby settings
+ # Review reset setting at a later time, need to be adjusted to account for initial setup
+ # changes.
+ '''currentPath = utils.settings('useDirectPaths')
+ if utils.window('emby_pluginpath') != currentPath:
+ # Plugin path value changed. Offer to reset
+ self.logMsg("Changed to playback mode detected", 1)
+ utils.window('emby_pluginpath', value=currentPath)
+ resp = xbmcgui.Dialog().yesno(
+ heading="Playback mode change detected",
+ line1=(
+ "Detected the playback mode has changed. The database "
+ "needs to be recreated for the change to be applied. "
+ "Proceed?"))
+ if resp:
+ utils.reset()'''
+
+ currentLog = utils.settings('logLevel')
+ if utils.window('emby_logLevel') != currentLog:
+ # The log level changed, set new prop
+ self.logMsg("New log level: %s" % currentLog, 1)
+ utils.window('emby_logLevel', value=currentLog)
+
+ def onNotification(self, sender, method, data):
+
+ doUtils = self.doUtils
+ if method not in ("Playlist.OnAdd"):
+ self.logMsg("Method: %s Data: %s" % (method, data), 1)
+
+ if data:
+ data = json.loads(data,'utf-8')
+
+
+ if method == "Player.OnPlay":
+ # Set up report progress for emby playback
+ item = data.get('item')
+ try:
+ kodiid = item['id']
+ item_type = item['type']
+ except (KeyError, TypeError):
+ self.logMsg("Item is invalid for playstate update.", 1)
+ else:
+ if ((utils.settings('useDirectPaths') == "1" and not item_type == "song") or
+ (item_type == "song" and utils.settings('enableMusic') == "true")):
+ # Set up properties for player
+ embyconn = utils.kodiSQL('emby')
+ embycursor = embyconn.cursor()
+ emby_db = embydb.Embydb_Functions(embycursor)
+ emby_dbitem = emby_db.getItem_byKodiId(kodiid, item_type)
+ try:
+ itemid = emby_dbitem[0]
+ except TypeError:
+ self.logMsg("No kodiid returned.", 1)
+ else:
+ url = "{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid
+ result = doUtils.downloadUrl(url)
+ self.logMsg("Item: %s" % result, 2)
+
+ playurl = None
+ count = 0
+ while not playurl and count < 2:
+ try:
+ playurl = xbmc.Player().getPlayingFile()
+ except RuntimeError:
+ count += 1
+ xbmc.sleep(200)
+ else:
+ listItem = xbmcgui.ListItem()
+ playback = pbutils.PlaybackUtils(result)
+
+ if item_type == "song" and utils.settings('streamMusic') == "true":
+ utils.window('emby_%s.playmethod' % playurl,
+ value="DirectStream")
+ else:
+ utils.window('emby_%s.playmethod' % playurl,
+ value="DirectPlay")
+ # Set properties for player.py
+ playback.setProperties(playurl, listItem)
+ finally:
+ embycursor.close()
+
+
+ elif method == "VideoLibrary.OnUpdate":
+ # Manually marking as watched/unwatched
+ playcount = data.get('playcount')
+ item = data.get('item')
+ try:
+ kodiid = item['id']
+ item_type = item['type']
+ except (KeyError, TypeError):
+ self.logMsg("Item is invalid for playstate update.", 1)
+ else:
+ # Send notification to the server.
+ embyconn = utils.kodiSQL('emby')
+ embycursor = embyconn.cursor()
+ emby_db = embydb.Embydb_Functions(embycursor)
+ emby_dbitem = emby_db.getItem_byKodiId(kodiid, item_type)
+ try:
+ itemid = emby_dbitem[0]
+ except TypeError:
+ self.logMsg("Could not find itemid in emby database.", 1)
+ else:
+ # Stop from manually marking as watched unwatched, with actual playback.
+ if utils.window('emby_skipWatched%s' % itemid) == "true":
+ # property is set in player.py
+ utils.window('emby_skipWatched%s' % itemid, clear=True)
+ else:
+ # notify the server
+ url = "{server}/emby/Users/{UserId}/PlayedItems/%s?format=json" % itemid
+ if playcount != 0:
+ doUtils.downloadUrl(url, action_type="POST")
+ self.logMsg("Mark as watched for itemid: %s" % itemid, 1)
+ else:
+ doUtils.downloadUrl(url, action_type="DELETE")
+ self.logMsg("Mark as unwatched for itemid: %s" % itemid, 1)
+ finally:
+ embycursor.close()
+
+
+ elif method == "VideoLibrary.OnRemove":
+ # Removed function, because with plugin paths + clean library, it will wipe
+ # entire library if user has permissions. Instead, use the emby context menu available
+ # in Isengard and higher version
+ pass
+ '''try:
+ kodiid = data['id']
+ type = data['type']
+ except (KeyError, TypeError):
+ self.logMsg("Item is invalid for emby deletion.", 1)
+ else:
+ # Send the delete action to the server.
+ embyconn = utils.kodiSQL('emby')
+ embycursor = embyconn.cursor()
+ emby_db = embydb.Embydb_Functions(embycursor)
+ emby_dbitem = emby_db.getItem_byKodiId(kodiid, type)
+ try:
+ itemid = emby_dbitem[0]
+ except TypeError:
+ self.logMsg("Could not find itemid in emby database.", 1)
+ else:
+ if utils.settings('skipContextMenu') != "true":
+ resp = xbmcgui.Dialog().yesno(
+ heading="Confirm delete",
+ line1="Delete file on Emby Server?")
+ if not resp:
+ self.logMsg("User skipped deletion.", 1)
+ embycursor.close()
+ return
+
+ url = "{server}/emby/Items/%s?format=json" % itemid
+ self.logMsg("Deleting request: %s" % itemid)
+ doUtils.downloadUrl(url, action_type="DELETE")
+ finally:
+ embycursor.close()'''
+
+
+ elif method == "System.OnWake":
+ # Allow network to wake up
+ xbmc.sleep(10000)
+ utils.window('emby_onWake', value="true")
+
+ elif method == "Playlist.OnClear":
pass
\ No newline at end of file
diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py
index dacebbde..d3a441dd 100644
--- a/resources/lib/librarysync.py
+++ b/resources/lib/librarysync.py
@@ -1,1549 +1,1480 @@
-# -*- coding: utf-8 -*-
-
-##################################################################################################
-
-import sqlite3
-import threading
-from datetime import datetime, timedelta, time
-
-import xbmc
-import xbmcgui
-import xbmcvfs
-
-import api
-import utils
-import clientinfo
-import downloadutils
-import itemtypes
-import embydb_functions as embydb
-import kodidb_functions as kodidb
-import read_embyserver as embyserver
-import userclient
-import videonodes
-
-##################################################################################################
-
-
-class LibrarySync(threading.Thread):
-
- _shared_state = {}
-
- stop_thread = False
- suspend_thread = False
-
- # Track websocketclient updates
- addedItems = []
- updateItems = []
- userdataItems = []
- removeItems = []
- forceLibraryUpdate = False
- refresh_views = False
-
-
- def __init__(self):
-
- self.__dict__ = self._shared_state
- self.monitor = xbmc.Monitor()
-
- self.clientInfo = clientinfo.ClientInfo()
- self.addonName = self.clientInfo.getAddonName()
- self.doUtils = downloadutils.DownloadUtils().downloadUrl
- self.user = userclient.UserClient()
- self.emby = embyserver.Read_EmbyServer()
- self.vnodes = videonodes.VideoNodes()
-
- threading.Thread.__init__(self)
-
- def logMsg(self, msg, lvl=1):
-
- className = self.__class__.__name__
- utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
-
-
- def progressDialog(self, title, forced=False):
-
- dialog = None
-
- if utils.settings('dbSyncIndicator') == "true" or forced:
- dialog = xbmcgui.DialogProgressBG()
- dialog.create("Emby for Kodi", title)
- self.logMsg("Show progress dialog: %s" % title, 2)
-
- return dialog
-
- def startSync(self):
-
- settings = utils.settings
- # Run at start up - optional to use the server plugin
- if settings('SyncInstallRunDone') == "true":
-
- # Validate views
- self.refreshViews()
- completed = False
- # Verify if server plugin is installed.
- if settings('serverSync') == "true":
- # Try to use fast start up
- url = "{server}/emby/Plugins?format=json"
- result = self.doUtils(url)
-
- for plugin in result:
- if plugin['Name'] == "Emby.Kodi Sync Queue":
- self.logMsg("Found server plugin.", 2)
- completed = self.fastSync()
-
- if not completed:
- # Fast sync failed or server plugin is not found
- completed = ManualSync().sync()
- else:
- # Install sync is not completed
- completed = self.fullSync()
-
- return completed
-
- def fastSync(self):
-
- log = self.logMsg
-
- doUtils = self.doUtils
-
- lastSync = utils.settings('LastIncrementalSync')
- if not lastSync:
- lastSync = "2010-01-01T00:00:00Z"
-
- lastSyncTime = utils.convertdate(lastSync)
- log("Last sync run: %s" % lastSyncTime, 1)
-
- # get server RetentionDateTime
- url = "{server}/emby/Emby.Kodi.SyncQueue/GetServerDateTime?format=json"
- result = doUtils(url)
- retention_time = "2010-01-01T00:00:00Z"
- if result and result.get('RetentionDateTime'):
- retention_time = result['RetentionDateTime']
-
- #Try/except equivalent
- '''
- try:
- retention_time = result['RetentionDateTime']
- except (TypeError, KeyError):
- retention_time = "2010-01-01T00:00:00Z"
- '''
-
- retention_time = utils.convertdate(retention_time)
- log("RetentionDateTime: %s" % retention_time, 1)
-
- # if last sync before retention time do a full sync
- if retention_time > lastSyncTime:
- log("Fast sync server retention insufficient, fall back to full sync", 1)
- return False
-
- url = "{server}/emby/Emby.Kodi.SyncQueue/{UserId}/GetItems?format=json"
- params = {'LastUpdateDT': lastSync}
- result = doUtils(url, parameters=params)
-
- try:
- processlist = {
-
- 'added': result['ItemsAdded'],
- 'update': result['ItemsUpdated'],
- 'userdata': result['UserDataChanged'],
- 'remove': result['ItemsRemoved']
- }
-
- except (KeyError, TypeError):
- log("Failed to retrieve latest updates using fast sync.", 1)
- return False
-
- else:
- log("Fast sync changes: %s" % result, 1)
- for action in processlist:
- self.triage_items(action, processlist[action])
-
- return True
-
- def saveLastSync(self):
-
- log = self.logMsg
- # Save last sync time
- overlap = 2
-
- url = "{server}/emby/Emby.Kodi.SyncQueue/GetServerDateTime?format=json"
- result = self.doUtils(url)
- try: # datetime fails when used more than once, TypeError
- server_time = result['ServerDateTime']
- server_time = utils.convertdate(server_time)
-
- except Exception as e:
- # If the server plugin is not installed or an error happened.
- log("An exception occurred: %s" % e, 1)
- time_now = datetime.utcnow()-timedelta(minutes=overlap)
- lastSync = time_now.strftime('%Y-%m-%dT%H:%M:%SZ')
- log("New sync time: client time -%s min: %s" % (overlap, lastSync), 1)
-
- else:
- lastSync = (server_time - timedelta(minutes=overlap)).strftime('%Y-%m-%dT%H:%M:%SZ')
- log("New sync time: server time -%s min: %s" % (overlap, lastSync), 1)
-
- finally:
- utils.settings('LastIncrementalSync', value=lastSync)
-
- def shouldStop(self):
- # Checkpoint during the syncing process
- if self.monitor.abortRequested():
- return True
- elif utils.window('emby_shouldStop') == "true":
- return True
- else: # Keep going
- return False
-
- def dbCommit(self, connection):
-
- log = self.logMsg
- window = utils.window
- # Central commit, verifies if Kodi database update is running
- kodidb_scan = window('emby_kodiScan') == "true"
-
- while kodidb_scan:
-
- log("Kodi scan is running. Waiting...", 1)
- kodidb_scan = window('emby_kodiScan') == "true"
-
- if self.shouldStop():
- log("Commit unsuccessful. Sync terminated.", 1)
- break
-
- if self.monitor.waitForAbort(1):
- # Abort was requested while waiting. We should exit
- log("Commit unsuccessful.", 1)
- break
- else:
- connection.commit()
- log("Commit successful.", 1)
-
- def fullSync(self, manualrun=False, repair=False, forceddialog=False):
-
- log = self.logMsg
- window = utils.window
- settings = utils.settings
- # Only run once when first setting up. Can be run manually.
- emby = self.emby
- music_enabled = utils.settings('enableMusic') == "true"
-
- xbmc.executebuiltin('InhibitIdleShutdown(true)')
- screensaver = utils.getScreensaver()
- utils.setScreensaver(value="")
- window('emby_dbScan', value="true")
- # Add sources
- utils.sourcesXML()
-
- embyconn = utils.kodiSQL('emby')
- embycursor = embyconn.cursor()
- # Create the tables for the emby database
- # emby, view, version
- embycursor.execute(
- """CREATE TABLE IF NOT EXISTS emby(
- emby_id TEXT UNIQUE, media_folder TEXT, emby_type TEXT, media_type TEXT, kodi_id INTEGER,
- kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, checksum INTEGER)""")
- embycursor.execute(
- """CREATE TABLE IF NOT EXISTS view(
- view_id TEXT UNIQUE, view_name TEXT, media_type TEXT, kodi_tagid INTEGER)""")
- embycursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)")
- embyconn.commit()
-
- # content sync: movies, tvshows, musicvideos, music
- kodiconn = utils.kodiSQL('video')
- kodicursor = kodiconn.cursor()
-
- if manualrun:
- message = "Manual sync"
- elif repair:
- message = "Repair sync"
- forceddialog = True
- else:
- message = "Initial sync"
- forceddialog = True
- window('emby_initialScan', value="true")
-
- pDialog = self.progressDialog("%s" % message, forced=forceddialog)
- starttotal = datetime.now()
-
- # Set views
- self.maintainViews(embycursor, kodicursor)
- embyconn.commit()
-
- # Sync video library
- process = {
-
- 'movies': self.movies,
- 'musicvideos': self.musicvideos,
- 'tvshows': self.tvshows
- }
- for itemtype in process:
- startTime = datetime.now()
- completed = process[itemtype](embycursor, kodicursor, pDialog)
- if not completed:
- xbmc.executebuiltin('InhibitIdleShutdown(false)')
- utils.setScreensaver(value=screensaver)
- window('emby_dbScan', clear=True)
- if pDialog:
- pDialog.close()
-
- embycursor.close()
- kodicursor.close()
- return False
- else:
- self.dbCommit(kodiconn)
- embyconn.commit()
- elapsedTime = datetime.now() - startTime
- log("SyncDatabase (finished %s in: %s)"
- % (itemtype, str(elapsedTime).split('.')[0]), 1)
- else:
- # Close the Kodi cursor
- kodicursor.close()
-
- # sync music
- if music_enabled:
-
- musicconn = utils.kodiSQL('music')
- musiccursor = musicconn.cursor()
-
- startTime = datetime.now()
- completed = self.music(embycursor, musiccursor, pDialog)
- if not completed:
- xbmc.executebuiltin('InhibitIdleShutdown(false)')
- utils.setScreensaver(value=screensaver)
- window('emby_dbScan', clear=True)
- if pDialog:
- pDialog.close()
-
- embycursor.close()
- musiccursor.close()
- return False
- else:
- musicconn.commit()
- embyconn.commit()
- elapsedTime = datetime.now() - startTime
- log("SyncDatabase (finished music in: %s)"
- % (str(elapsedTime).split('.')[0]), 1)
- musiccursor.close()
-
- if pDialog:
- pDialog.close()
-
- embycursor.close()
-
- settings('SyncInstallRunDone', value="true")
- settings("dbCreatedWithVersion", self.clientInfo.getVersion())
- self.saveLastSync()
- xbmc.executebuiltin('UpdateLibrary(video)')
- elapsedtotal = datetime.now() - starttotal
-
- xbmc.executebuiltin('InhibitIdleShutdown(false)')
- utils.setScreensaver(value=screensaver)
- window('emby_dbScan', clear=True)
- window('emby_initialScan', clear=True)
- if forceddialog:
- xbmcgui.Dialog().notification(
- heading="Emby for Kodi",
- message="%s %s %s" %
- (message, utils.language(33025), str(elapsedtotal).split('.')[0]),
- icon="special://home/addons/plugin.video.emby/icon.png",
- sound=False)
- return True
-
-
- def refreshViews(self):
-
- embyconn = utils.kodiSQL('emby')
- embycursor = embyconn.cursor()
- kodiconn = utils.kodiSQL('video')
- kodicursor = kodiconn.cursor()
-
- # Compare views, assign correct tags to items
- self.maintainViews(embycursor, kodicursor)
-
- self.dbCommit(kodiconn)
- kodicursor.close()
-
- embyconn.commit()
- embycursor.close()
-
- def maintainViews(self, embycursor, kodicursor):
-
- log = self.logMsg
- # Compare the views to emby
- emby = self.emby
- emby_db = embydb.Embydb_Functions(embycursor)
- kodi_db = kodidb.Kodidb_Functions(kodicursor)
- doUtils = self.doUtils
- vnodes = self.vnodes
-
- # Get views
- url = "{server}/emby/Users/{UserId}/Views?format=json"
- result = doUtils(url)
- grouped_views = result['Items']
- ordered_views = emby.getViews(sortedlist=True)
- all_views = []
- sorted_views = []
- for view in ordered_views:
- all_views.append(view['name'])
- if view['type'] == "music":
- continue
-
- if view['type'] == "mixed":
- sorted_views.append(view['name'])
- sorted_views.append(view['name'])
- log("Sorted views: %s" % sorted_views, 1)
-
- # total nodes for window properties
- vnodes.clearProperties()
- totalnodes = len(sorted_views) + 0
-
- current_views = emby_db.getViews()
- # Set views for supported media type
- emby_mediatypes = {
-
- 'movies': "Movie",
- 'tvshows': "Series",
- 'musicvideos': "MusicVideo",
- 'homevideos': "Video",
- 'music': "Audio",
- 'photos': "Photo"
- }
- mediatypes = ['movies', 'tvshows', 'musicvideos', 'homevideos', 'music', 'photos']
- for mediatype in mediatypes:
-
- nodes = [] # Prevent duplicate for nodes of the same type
- playlists = [] # Prevent duplicate for playlists of the same type
- # Get media folders from server
- folders = emby.getViews(mediatype, root=True)
- for folder in folders:
-
- folderid = folder['id']
- foldername = folder['name']
- viewtype = folder['type']
-
- if foldername not in all_views:
- # Media folders are grouped into userview
- url = "{server}/emby/Users/{UserId}/Items?format=json"
- params = {
- 'ParentId': folderid,
- 'Recursive': True,
- 'Limit': 1,
- 'IncludeItemTypes': emby_mediatypes[mediatype]
- } # Get one item from server using the folderid
- result = doUtils(url, parameters=params)
- try:
- verifyitem = result['Items'][0]['Id']
- except (TypeError, IndexError):
- # Something is wrong. Keep the same folder name.
- # Could be the view is empty or the connection
- pass
- else:
- for grouped_view in grouped_views:
- # This is only reserved for the detection of grouped views
- if (grouped_view['Type'] == "UserView" and
- grouped_view.get('CollectionType') == mediatype):
- # Take the userview, and validate the item belong to the view
- if emby.verifyView(grouped_view['Id'], verifyitem):
- # Take the name of the userview
- log("Found corresponding view: %s %s"
- % (grouped_view['Name'], grouped_view['Id']), 1)
- foldername = grouped_view['Name']
- break
- else:
- # Unable to find a match, add the name to our sorted_view list
- sorted_views.append(foldername)
- log("Couldn't find corresponding grouped view: %s" % sorted_views, 1)
-
- # Failsafe
- try:
- sorted_views.index(foldername)
- except ValueError:
- sorted_views.append(foldername)
-
- # Get current media folders from emby database
- view = emby_db.getView_byId(folderid)
- try:
- current_viewname = view[0]
- current_viewtype = view[1]
- current_tagid = view[2]
-
- except TypeError:
- log("Creating viewid: %s in Emby database." % folderid, 1)
- tagid = kodi_db.createTag(foldername)
- # Create playlist for the video library
- if (foldername not in playlists and
- mediatype in ('movies', 'tvshows', 'musicvideos')):
- utils.playlistXSP(mediatype, foldername, folderid, viewtype)
- playlists.append(foldername)
- # Create the video node
- if foldername not in nodes and mediatype not in ("musicvideos", "music"):
- vnodes.viewNode(sorted_views.index(foldername), foldername, mediatype,
- viewtype, folderid)
- if viewtype == "mixed": # Change the value
- sorted_views[sorted_views.index(foldername)] = "%ss" % foldername
- nodes.append(foldername)
- totalnodes += 1
- # Add view to emby database
- emby_db.addView(folderid, foldername, viewtype, tagid)
-
- else:
- log(' '.join((
-
- "Found viewid: %s" % folderid,
- "viewname: %s" % current_viewname,
- "viewtype: %s" % current_viewtype,
- "tagid: %s" % current_tagid)), 2)
-
- # View is still valid
- try:
- current_views.remove(folderid)
- except ValueError:
- # View was just created, nothing to remove
- pass
-
- # View was modified, update with latest info
- if current_viewname != foldername:
- log("viewid: %s new viewname: %s" % (folderid, foldername), 1)
- tagid = kodi_db.createTag(foldername)
-
- # Update view with new info
- emby_db.updateView(foldername, tagid, folderid)
-
- if mediatype != "music":
- if emby_db.getView_byName(current_viewname) is None:
- # The tag could be a combined view. Ensure there's no other tags
- # with the same name before deleting playlist.
- utils.playlistXSP(
- mediatype, current_viewname, folderid, current_viewtype, True)
- # Delete video node
- if mediatype != "musicvideos":
- vnodes.viewNode(
- indexnumber=None,
- tagname=current_viewname,
- mediatype=mediatype,
- viewtype=current_viewtype,
- viewid=folderid,
- delete=True)
- # Added new playlist
- if (foldername not in playlists and
- mediatype in ('movies', 'tvshows', 'musicvideos')):
- utils.playlistXSP(mediatype, foldername, folderid, viewtype)
- playlists.append(foldername)
- # Add new video node
- if foldername not in nodes and mediatype != "musicvideos":
- vnodes.viewNode(sorted_views.index(foldername), foldername,
- mediatype, viewtype, folderid)
- if viewtype == "mixed": # Change the value
- sorted_views[sorted_views.index(foldername)] = "%ss" % foldername
- nodes.append(foldername)
- totalnodes += 1
-
- # Update items with new tag
- items = emby_db.getItem_byView(folderid)
- for item in items:
- # Remove the "s" from viewtype for tags
- kodi_db.updateTag(
- current_tagid, tagid, item[0], current_viewtype[:-1])
- else:
- # Validate the playlist exists or recreate it
- if mediatype != "music":
- if (foldername not in playlists and
- mediatype in ('movies', 'tvshows', 'musicvideos')):
- utils.playlistXSP(mediatype, foldername, folderid, viewtype)
- playlists.append(foldername)
- # Create the video node if not already exists
- if foldername not in nodes and mediatype != "musicvideos":
- vnodes.viewNode(sorted_views.index(foldername), foldername,
- mediatype, viewtype, folderid)
- if viewtype == "mixed": # Change the value
- sorted_views[sorted_views.index(foldername)] = "%ss" % foldername
- nodes.append(foldername)
- totalnodes += 1
- else:
- # Add video nodes listings
- vnodes.singleNode(totalnodes, "Favorite movies", "movies", "favourites")
- totalnodes += 1
- vnodes.singleNode(totalnodes, "Favorite tvshows", "tvshows", "favourites")
- totalnodes += 1
- vnodes.singleNode(totalnodes, "channels", "movies", "channels")
- totalnodes += 1
- # Save total
- utils.window('Emby.nodes.total', str(totalnodes))
-
- # Remove any old referenced views
- log("Removing views: %s" % current_views, 1)
- for view in current_views:
- emby_db.removeView(view)
-
- def movies(self, embycursor, kodicursor, pdialog):
-
- log = self.logMsg
- lang = utils.language
- # Get movies from emby
- emby = self.emby
- emby_db = embydb.Embydb_Functions(embycursor)
- movies = itemtypes.Movies(embycursor, kodicursor)
-
- views = emby_db.getView_byType('movies')
- views += emby_db.getView_byType('mixed')
- log("Media folders: %s" % views, 1)
-
- ##### PROCESS MOVIES #####
- for view in views:
-
- if self.shouldStop():
- return False
-
- # Get items per view
- viewId = view['id']
- viewName = view['name']
-
- if pdialog:
- pdialog.update(
- heading="Emby for Kodi",
- message="%s %s..." % (lang(33017), viewName))
-
- # Initial or repair sync
- all_embymovies = emby.getMovies(viewId, dialog=pdialog)
- total = all_embymovies['TotalRecordCount']
- embymovies = all_embymovies['Items']
-
- if pdialog:
- pdialog.update(heading="Processing %s / %s items" % (viewName, total))
-
- count = 0
- for embymovie in embymovies:
- # Process individual movies
- if self.shouldStop():
- return False
-
- title = embymovie['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
- movies.add_update(embymovie, viewName, viewId)
- else:
- log("Movies finished.", 2)
-
-
- ##### PROCESS BOXSETS #####
- if pdialog:
- pdialog.update(heading="Emby for Kodi", message=lang(33018))
-
- boxsets = emby.getBoxset(dialog=pdialog)
- total = boxsets['TotalRecordCount']
- embyboxsets = boxsets['Items']
-
- if pdialog:
- pdialog.update(heading="Processing Boxsets / %s items" % total)
-
- count = 0
- for boxset in embyboxsets:
- # Process individual boxset
- if self.shouldStop():
- return False
-
- title = boxset['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
- movies.add_updateBoxset(boxset)
- else:
- log("Boxsets finished.", 2)
-
- return True
-
- def musicvideos(self, embycursor, kodicursor, pdialog):
-
- log = self.logMsg
- # Get musicvideos from emby
- emby = self.emby
- emby_db = embydb.Embydb_Functions(embycursor)
- mvideos = itemtypes.MusicVideos(embycursor, kodicursor)
-
- views = emby_db.getView_byType('musicvideos')
- log("Media folders: %s" % views, 1)
-
- for view in views:
-
- if self.shouldStop():
- return False
-
- # Get items per view
- viewId = view['id']
- viewName = view['name']
-
- if pdialog:
- pdialog.update(
- heading="Emby for Kodi",
- message="%s %s..." % (utils.language(33019), viewName))
-
- # Initial or repair sync
- all_embymvideos = emby.getMusicVideos(viewId, dialog=pdialog)
- total = all_embymvideos['TotalRecordCount']
- embymvideos = all_embymvideos['Items']
-
- if pdialog:
- pdialog.update(heading="Processing %s / %s items" % (viewName, total))
-
- count = 0
- for embymvideo in embymvideos:
- # Process individual musicvideo
- if self.shouldStop():
- return False
-
- title = embymvideo['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
- mvideos.add_update(embymvideo, viewName, viewId)
- else:
- log("MusicVideos finished.", 2)
-
- return True
-
- def tvshows(self, embycursor, kodicursor, pdialog):
-
- log = self.logMsg
- # Get shows from emby
- emby = self.emby
- emby_db = embydb.Embydb_Functions(embycursor)
- tvshows = itemtypes.TVShows(embycursor, kodicursor)
-
- views = emby_db.getView_byType('tvshows')
- views += emby_db.getView_byType('mixed')
- log("Media folders: %s" % views, 1)
-
- for view in views:
-
- if self.shouldStop():
- return False
-
- # Get items per view
- viewId = view['id']
- viewName = view['name']
-
- if pdialog:
- pdialog.update(
- heading="Emby for Kodi",
- message="%s %s..." % (utils.language(33020), viewName))
-
- all_embytvshows = emby.getShows(viewId, dialog=pdialog)
- total = all_embytvshows['TotalRecordCount']
- embytvshows = all_embytvshows['Items']
-
- if pdialog:
- pdialog.update(heading="Processing %s / %s items" % (viewName, total))
-
- count = 0
- for embytvshow in embytvshows:
- # Process individual show
- if self.shouldStop():
- return False
-
- itemid = embytvshow['Id']
- title = embytvshow['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
- tvshows.add_update(embytvshow, viewName, viewId)
-
- # Process episodes
- all_episodes = emby.getEpisodesbyShow(itemid)
- for episode in all_episodes['Items']:
-
- # Process individual show
- if self.shouldStop():
- return False
-
- episodetitle = episode['Name']
- if pdialog:
- pdialog.update(percentage, message="%s - %s" % (title, episodetitle))
- tvshows.add_updateEpisode(episode)
- else:
- log("TVShows finished.", 2)
-
- return True
-
- def music(self, embycursor, kodicursor, pdialog):
- # Get music from emby
- emby = self.emby
- emby_db = embydb.Embydb_Functions(embycursor)
- music = itemtypes.Music(embycursor, kodicursor)
-
- process = {
-
- 'artists': [emby.getArtists, music.add_updateArtist],
- 'albums': [emby.getAlbums, music.add_updateAlbum],
- 'songs': [emby.getSongs, music.add_updateSong]
- }
- types = ['artists', 'albums', 'songs']
- for itemtype in types:
-
- if pdialog:
- pdialog.update(
- heading="Emby for Kodi",
- message="%s %s..." % (utils.language(33021), itemtype))
-
- all_embyitems = process[itemtype][0](dialog=pdialog)
- total = all_embyitems['TotalRecordCount']
- embyitems = all_embyitems['Items']
-
- if pdialog:
- pdialog.update(heading="Processing %s / %s items" % (itemtype, total))
-
- count = 0
- for embyitem in embyitems:
- # Process individual item
- if self.shouldStop():
- return False
-
- title = embyitem['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
-
- process[itemtype][1](embyitem)
- else:
- self.logMsg("%s finished." % itemtype, 2)
-
- return True
-
- # Reserved for websocket_client.py and fast start
- def triage_items(self, process, items):
-
- processlist = {
-
- 'added': self.addedItems,
- 'update': self.updateItems,
- 'userdata': self.userdataItems,
- 'remove': self.removeItems
- }
- if items:
- if process == "userdata":
- itemids = []
- for item in items:
- itemids.append(item['ItemId'])
- items = itemids
-
- self.logMsg("Queue %s: %s" % (process, items), 1)
- processlist[process].extend(items)
-
- def incrementalSync(self):
-
- log = self.logMsg
-
- embyconn = utils.kodiSQL('emby')
- embycursor = embyconn.cursor()
- kodiconn = utils.kodiSQL('video')
- kodicursor = kodiconn.cursor()
- emby = self.emby
- emby_db = embydb.Embydb_Functions(embycursor)
- pDialog = None
- update_embydb = False
-
- if self.refresh_views:
- # Received userconfig update
- self.refresh_views = False
- self.maintainViews(embycursor, kodicursor)
- self.forceLibraryUpdate = True
- update_embydb = True
-
- if self.addedItems or self.updateItems or self.userdataItems or self.removeItems:
- # Only present dialog if we are going to process items
- pDialog = self.progressDialog('Incremental sync')
-
-
- process = {
-
- 'added': self.addedItems,
- 'update': self.updateItems,
- 'userdata': self.userdataItems,
- 'remove': self.removeItems
- }
- types = ['added', 'update', 'userdata', 'remove']
- for type in types:
-
- if process[type] and utils.window('emby_kodiScan') != "true":
-
- listItems = list(process[type])
- del process[type][:] # Reset class list
-
- items_process = itemtypes.Items(embycursor, kodicursor)
- update = False
-
- # Prepare items according to process type
- if type == "added":
- items = emby.sortby_mediatype(listItems)
-
- elif type in ("userdata", "remove"):
- items = emby_db.sortby_mediaType(listItems, unsorted=False)
-
- else:
- items = emby_db.sortby_mediaType(listItems)
- if items.get('Unsorted'):
- sorted_items = emby.sortby_mediatype(items['Unsorted'])
- doupdate = items_process.itemsbyId(sorted_items, "added", pDialog)
- if doupdate:
- embyupdate, kodiupdate_video = doupdate
- if embyupdate:
- update_embydb = True
- if kodiupdate_video:
- self.forceLibraryUpdate = True
- del items['Unsorted']
-
- doupdate = items_process.itemsbyId(items, type, pDialog)
- if doupdate:
- embyupdate, kodiupdate_video = doupdate
- if embyupdate:
- update_embydb = True
- if kodiupdate_video:
- self.forceLibraryUpdate = True
-
- if update_embydb:
- update_embydb = False
- log("Updating emby database.", 1)
- embyconn.commit()
- self.saveLastSync()
-
- if self.forceLibraryUpdate:
- # Force update the Kodi library
- self.forceLibraryUpdate = False
- self.dbCommit(kodiconn)
-
- log("Updating video library.", 1)
- utils.window('emby_kodiScan', value="true")
- xbmc.executebuiltin('UpdateLibrary(video)')
-
- if pDialog:
- pDialog.close()
-
- kodicursor.close()
- embycursor.close()
-
-
- def compareDBVersion(self, current, minimum):
- # It returns True is database is up to date. False otherwise.
- self.logMsg("current: %s minimum: %s" % (current, minimum), 1)
- currMajor, currMinor, currPatch = current.split(".")
- minMajor, minMinor, minPatch = minimum.split(".")
-
- if currMajor > minMajor:
- return True
- elif currMajor == minMajor and (currMinor > minMinor or
- (currMinor == minMinor and currPatch >= minPatch)):
- return True
- else:
- # Database out of date.
- return False
-
- def run(self):
-
- try:
- self.run_internal()
- except Exception as e:
- utils.window('emby_dbScan', clear=True)
- xbmcgui.Dialog().ok(
- heading="Emby for Kodi",
- line1=(
- "Library sync thread has exited! "
- "You should restart Kodi now. "
- "Please report this on the forum."))
- raise
-
- def run_internal(self):
-
- log = self.logMsg
- lang = utils.language
- window = utils.window
- settings = utils.settings
- dialog = xbmcgui.Dialog()
-
- startupComplete = False
- monitor = self.monitor
-
- log("---===### Starting LibrarySync ###===---", 0)
-
- while not monitor.abortRequested():
-
- # In the event the server goes offline
- while self.suspend_thread:
- # Set in service.py
- if monitor.waitForAbort(5):
- # Abort was requested while waiting. We should exit
- break
-
- if (window('emby_dbCheck') != "true" and settings('SyncInstallRunDone') == "true"):
- # Verify the validity of the database
- currentVersion = settings('dbCreatedWithVersion')
- minVersion = window('emby_minDBVersion')
- uptoDate = self.compareDBVersion(currentVersion, minVersion)
-
- if not uptoDate:
- log("Database version out of date: %s minimum version required: %s"
- % (currentVersion, minVersion), 0)
-
- resp = dialog.yesno("Emby for Kodi", lang(33022))
- if not resp:
- log("Database version is out of date! USER IGNORED!", 0)
- dialog.ok("Emby for Kodi", lang(33023))
- else:
- utils.reset()
-
- break
-
- window('emby_dbCheck', value="true")
-
-
- if not startupComplete:
- # Verify the video database can be found
- videoDb = utils.getKodiVideoDBPath()
- if not xbmcvfs.exists(videoDb):
- # Database does not exists
- log(
- "The current Kodi version is incompatible "
- "with the Emby for Kodi add-on. Please visit "
- "https://github.com/MediaBrowser/Emby.Kodi/wiki "
- "to know which Kodi versions are supported.", 0)
-
- dialog.ok(
- heading="Emby for Kodi",
- line1=lang(33024))
- break
-
- # Run start up sync
- log("Database version: %s" % settings('dbCreatedWithVersion'), 0)
- log("SyncDatabase (started)", 1)
- startTime = datetime.now()
- librarySync = self.startSync()
- elapsedTime = datetime.now() - startTime
- log("SyncDatabase (finished in: %s) %s"
- % (str(elapsedTime).split('.')[0], librarySync), 1)
- # Only try the initial sync once per kodi session regardless
- # This will prevent an infinite loop in case something goes wrong.
- startupComplete = True
-
- # Process updates
- if window('emby_dbScan') != "true":
- self.incrementalSync()
-
- if window('emby_onWake') == "true" and window('emby_online') == "true":
- # Kodi is waking up
- # Set in kodimonitor.py
- window('emby_onWake', clear=True)
- if window('emby_syncRunning') != "true":
- log("SyncDatabase onWake (started)", 0)
- librarySync = self.startSync()
- log("SyncDatabase onWake (finished) %s" % librarySync, 0)
-
- if self.stop_thread:
- # Set in service.py
- log("Service terminated thread.", 2)
- break
-
- if monitor.waitForAbort(1):
- # Abort was requested while waiting. We should exit
- break
-
- log("###===--- LibrarySync Stopped ---===###", 0)
-
- def stopThread(self):
- self.stop_thread = True
- self.logMsg("Ending thread...", 2)
-
- def suspendThread(self):
- self.suspend_thread = True
- self.logMsg("Pausing thread...", 0)
-
- def resumeThread(self):
- self.suspend_thread = False
- self.logMsg("Resuming thread...", 0)
-
-
-class ManualSync(LibrarySync):
-
-
- def __init__(self):
-
- LibrarySync.__init__(self)
-
- def sync(self, dialog=False):
-
- return self.fullSync(manualrun=True, forceddialog=dialog)
-
-
- def movies(self, embycursor, kodicursor, pdialog):
-
- log = self.logMsg
- lang = utils.language
- # Get movies from emby
- emby = self.emby
- emby_db = embydb.Embydb_Functions(embycursor)
- movies = itemtypes.Movies(embycursor, kodicursor)
-
- views = emby_db.getView_byType('movies')
- views += emby_db.getView_byType('mixed')
- log("Media folders: %s" % views, 1)
-
- # Pull the list of movies and boxsets in Kodi
- try:
- all_kodimovies = dict(emby_db.getChecksum('Movie'))
- except ValueError:
- all_kodimovies = {}
-
- try:
- all_kodisets = dict(emby_db.getChecksum('BoxSet'))
- except ValueError:
- all_kodisets = {}
-
- all_embymoviesIds = set()
- all_embyboxsetsIds = set()
- updatelist = []
-
- ##### PROCESS MOVIES #####
- for view in views:
-
- if self.shouldStop():
- return False
-
- # Get items per view
- viewId = view['id']
- viewName = view['name']
-
- if pdialog:
- pdialog.update(
- heading="Emby for Kodi",
- message="%s %s..." % (lang(33026), viewName))
-
- all_embymovies = emby.getMovies(viewId, basic=True, dialog=pdialog)
- for embymovie in all_embymovies['Items']:
-
- if self.shouldStop():
- return False
-
- API = api.API(embymovie)
- itemid = embymovie['Id']
- all_embymoviesIds.add(itemid)
-
-
- if all_kodimovies.get(itemid) != API.getChecksum():
- # Only update if movie is not in Kodi or checksum is different
- updatelist.append(itemid)
-
- log("Movies to update for %s: %s" % (viewName, updatelist), 1)
- embymovies = emby.getFullItems(updatelist)
- total = len(updatelist)
- del updatelist[:]
-
- if pdialog:
- pdialog.update(heading="Processing %s / %s items" % (viewName, total))
-
- count = 0
- for embymovie in embymovies:
- # Process individual movies
- if self.shouldStop():
- return False
-
- title = embymovie['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
- movies.add_update(embymovie, viewName, viewId)
-
- ##### PROCESS BOXSETS #####
-
- boxsets = emby.getBoxset(dialog=pdialog)
- embyboxsets = []
-
- if pdialog:
- pdialog.update(heading="Emby for Kodi", message=lang(33027))
-
- for boxset in boxsets['Items']:
-
- if self.shouldStop():
- return False
-
- # Boxset has no real userdata, so using etag to compare
- checksum = boxset['Etag']
- itemid = boxset['Id']
- all_embyboxsetsIds.add(itemid)
-
- if all_kodisets.get(itemid) != checksum:
- # Only update if boxset is not in Kodi or checksum is different
- updatelist.append(itemid)
- embyboxsets.append(boxset)
-
- log("Boxsets to update: %s" % updatelist, 1)
- total = len(updatelist)
-
- if pdialog:
- pdialog.update(heading="Processing Boxsets / %s items" % total)
-
- count = 0
- for boxset in embyboxsets:
- # Process individual boxset
- if self.shouldStop():
- return False
-
- title = boxset['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
- movies.add_updateBoxset(boxset)
-
- ##### PROCESS DELETES #####
-
- for kodimovie in all_kodimovies:
- if kodimovie not in all_embymoviesIds:
- movies.remove(kodimovie)
- else:
- log("Movies compare finished.", 1)
-
- for boxset in all_kodisets:
- if boxset not in all_embyboxsetsIds:
- movies.remove(boxset)
- else:
- log("Boxsets compare finished.", 1)
-
- return True
-
- def musicvideos(self, embycursor, kodicursor, pdialog):
-
- log = self.logMsg
- # Get musicvideos from emby
- emby = self.emby
- emby_db = embydb.Embydb_Functions(embycursor)
- mvideos = itemtypes.MusicVideos(embycursor, kodicursor)
-
- views = emby_db.getView_byType('musicvideos')
- log("Media folders: %s" % views, 1)
-
- # Pull the list of musicvideos in Kodi
- try:
- all_kodimvideos = dict(emby_db.getChecksum('MusicVideo'))
- except ValueError:
- all_kodimvideos = {}
-
- all_embymvideosIds = set()
- updatelist = []
-
- for view in views:
-
- if self.shouldStop():
- return False
-
- # Get items per view
- viewId = view['id']
- viewName = view['name']
-
- if pdialog:
- pdialog.update(
- heading="Emby for Kodi",
- message="%s %s..." % (utils.language(33028), viewName))
-
- all_embymvideos = emby.getMusicVideos(viewId, basic=True, dialog=pdialog)
- for embymvideo in all_embymvideos['Items']:
-
- if self.shouldStop():
- return False
-
- API = api.API(embymvideo)
- itemid = embymvideo['Id']
- all_embymvideosIds.add(itemid)
-
-
- if all_kodimvideos.get(itemid) != API.getChecksum():
- # Only update if musicvideo is not in Kodi or checksum is different
- updatelist.append(itemid)
-
- log("MusicVideos to update for %s: %s" % (viewName, updatelist), 1)
- embymvideos = emby.getFullItems(updatelist)
- total = len(updatelist)
- del updatelist[:]
-
-
- if pdialog:
- pdialog.update(heading="Processing %s / %s items" % (viewName, total))
-
- count = 0
- for embymvideo in embymvideos:
- # Process individual musicvideo
- if self.shouldStop():
- return False
-
- title = embymvideo['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
- mvideos.add_update(embymvideo, viewName, viewId)
-
- ##### PROCESS DELETES #####
-
- for kodimvideo in all_kodimvideos:
- if kodimvideo not in all_embymvideosIds:
- mvideos.remove(kodimvideo)
- else:
- log("MusicVideos compare finished.", 1)
-
- return True
-
- def tvshows(self, embycursor, kodicursor, pdialog):
-
- log = self.logMsg
- lang = utils.language
- # Get shows from emby
- emby = self.emby
- emby_db = embydb.Embydb_Functions(embycursor)
- tvshows = itemtypes.TVShows(embycursor, kodicursor)
-
- views = emby_db.getView_byType('tvshows')
- views += emby_db.getView_byType('mixed')
- log("Media folders: %s" % views, 1)
-
- # Pull the list of tvshows and episodes in Kodi
- try:
- all_koditvshows = dict(emby_db.getChecksum('Series'))
- except ValueError:
- all_koditvshows = {}
-
- try:
- all_kodiepisodes = dict(emby_db.getChecksum('Episode'))
- except ValueError:
- all_kodiepisodes = {}
-
- all_embytvshowsIds = set()
- all_embyepisodesIds = set()
- updatelist = []
-
-
- for view in views:
-
- if self.shouldStop():
- return False
-
- # Get items per view
- viewId = view['id']
- viewName = view['name']
-
- if pdialog:
- pdialog.update(
- heading="Emby for Kodi",
- message="%s %s..." % (lang(33029), viewName))
-
- all_embytvshows = emby.getShows(viewId, basic=True, dialog=pdialog)
- for embytvshow in all_embytvshows['Items']:
-
- if self.shouldStop():
- return False
-
- API = api.API(embytvshow)
- itemid = embytvshow['Id']
- all_embytvshowsIds.add(itemid)
-
-
- if all_koditvshows.get(itemid) != API.getChecksum():
- # Only update if movie is not in Kodi or checksum is different
- updatelist.append(itemid)
-
- log("TVShows to update for %s: %s" % (viewName, updatelist), 1)
- embytvshows = emby.getFullItems(updatelist)
- total = len(updatelist)
- del updatelist[:]
-
-
- if pdialog:
- pdialog.update(heading="Processing %s / %s items" % (viewName, total))
-
- count = 0
- for embytvshow in embytvshows:
- # Process individual show
- if self.shouldStop():
- return False
-
- itemid = embytvshow['Id']
- title = embytvshow['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
- tvshows.add_update(embytvshow, viewName, viewId)
-
- else:
- # Get all episodes in view
- if pdialog:
- pdialog.update(
- heading="Emby for Kodi",
- message="%s %s..." % (lang(33030), viewName))
-
- all_embyepisodes = emby.getEpisodes(viewId, basic=True, dialog=pdialog)
- for embyepisode in all_embyepisodes['Items']:
-
- if self.shouldStop():
- return False
-
- API = api.API(embyepisode)
- itemid = embyepisode['Id']
- all_embyepisodesIds.add(itemid)
-
- if all_kodiepisodes.get(itemid) != API.getChecksum():
- # Only update if movie is not in Kodi or checksum is different
- updatelist.append(itemid)
-
- log("Episodes to update for %s: %s" % (viewName, updatelist), 1)
- embyepisodes = emby.getFullItems(updatelist)
- total = len(updatelist)
- del updatelist[:]
-
- count = 0
- for episode in embyepisodes:
-
- # Process individual episode
- if self.shouldStop():
- return False
-
- title = episode['SeriesName']
- episodetitle = episode['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message="%s - %s" % (title, episodetitle))
- count += 1
- tvshows.add_updateEpisode(episode)
-
- ##### PROCESS DELETES #####
-
- for koditvshow in all_koditvshows:
- if koditvshow not in all_embytvshowsIds:
- tvshows.remove(koditvshow)
- else:
- log("TVShows compare finished.", 1)
-
- for kodiepisode in all_kodiepisodes:
- if kodiepisode not in all_embyepisodesIds:
- tvshows.remove(kodiepisode)
- else:
- log("Episodes compare finished.", 1)
-
- return True
-
- def music(self, embycursor, kodicursor, pdialog):
-
- log = self.logMsg
- # Get music from emby
- emby = self.emby
- emby_db = embydb.Embydb_Functions(embycursor)
- music = itemtypes.Music(embycursor, kodicursor)
-
- # Pull the list of artists, albums, songs
- try:
- all_kodiartists = dict(emby_db.getChecksum('MusicArtist'))
- except ValueError:
- all_kodiartists = {}
-
- try:
- all_kodialbums = dict(emby_db.getChecksum('MusicAlbum'))
- except ValueError:
- all_kodialbums = {}
-
- try:
- all_kodisongs = dict(emby_db.getChecksum('Audio'))
- except ValueError:
- all_kodisongs = {}
-
- all_embyartistsIds = set()
- all_embyalbumsIds = set()
- all_embysongsIds = set()
- updatelist = []
-
- process = {
-
- 'artists': [emby.getArtists, music.add_updateArtist],
- 'albums': [emby.getAlbums, music.add_updateAlbum],
- 'songs': [emby.getSongs, music.add_updateSong]
- }
- types = ['artists', 'albums', 'songs']
- for type in types:
-
- if pdialog:
- pdialog.update(
- heading="Emby for Kodi",
- message="%s %s..." % (utils.language(33031), type))
-
- if type != "artists":
- all_embyitems = process[type][0](basic=True, dialog=pdialog)
- else:
- all_embyitems = process[type][0](dialog=pdialog)
- for embyitem in all_embyitems['Items']:
-
- if self.shouldStop():
- return False
-
- API = api.API(embyitem)
- itemid = embyitem['Id']
- if type == "artists":
- all_embyartistsIds.add(itemid)
- if all_kodiartists.get(itemid) != API.getChecksum():
- # Only update if artist is not in Kodi or checksum is different
- updatelist.append(itemid)
- elif type == "albums":
- all_embyalbumsIds.add(itemid)
- if all_kodialbums.get(itemid) != API.getChecksum():
- # Only update if album is not in Kodi or checksum is different
- updatelist.append(itemid)
- else:
- all_embysongsIds.add(itemid)
- if all_kodisongs.get(itemid) != API.getChecksum():
- # Only update if songs is not in Kodi or checksum is different
- updatelist.append(itemid)
-
- log("%s to update: %s" % (type, updatelist), 1)
- embyitems = emby.getFullItems(updatelist)
- total = len(updatelist)
- del updatelist[:]
-
- if pdialog:
- pdialog.update(heading="Processing %s / %s items" % (type, total))
-
- count = 0
- for embyitem in embyitems:
- # Process individual item
- if self.shouldStop():
- return False
-
- title = embyitem['Name']
- if pdialog:
- percentage = int((float(count) / float(total))*100)
- pdialog.update(percentage, message=title)
- count += 1
-
- process[type][1](embyitem)
-
- ##### PROCESS DELETES #####
-
- for kodiartist in all_kodiartists:
- if kodiartist not in all_embyartistsIds and all_kodiartists[kodiartist] is not None:
- music.remove(kodiartist)
- else:
- log("Artist compare finished.", 1)
-
- for kodialbum in all_kodialbums:
- if kodialbum not in all_embyalbumsIds:
- music.remove(kodialbum)
- else:
- log("Albums compare finished.", 1)
-
- for kodisong in all_kodisongs:
- if kodisong not in all_embysongsIds:
- music.remove(kodisong)
- else:
- log("Songs compare finished.", 1)
-
- return True
\ No newline at end of file
+# -*- coding: utf-8 -*-
+
+##################################################################################################
+
+import sqlite3
+import threading
+from datetime import datetime, timedelta, time
+
+import xbmc
+import xbmcgui
+import xbmcvfs
+
+import api
+import utils
+import clientinfo
+import downloadutils
+import itemtypes
+import embydb_functions as embydb
+import kodidb_functions as kodidb
+import read_embyserver as embyserver
+import userclient
+import videonodes
+
+##################################################################################################
+
+
+class LibrarySync(threading.Thread):
+
+ _shared_state = {}
+
+ stop_thread = False
+ suspend_thread = False
+
+ # Track websocketclient updates
+ addedItems = []
+ updateItems = []
+ userdataItems = []
+ removeItems = []
+ forceLibraryUpdate = False
+ refresh_views = False
+
+
+ def __init__(self):
+
+ self.__dict__ = self._shared_state
+ self.monitor = xbmc.Monitor()
+
+ self.clientInfo = clientinfo.ClientInfo()
+ self.addonName = self.clientInfo.getAddonName()
+ self.doUtils = downloadutils.DownloadUtils().downloadUrl
+ self.user = userclient.UserClient()
+ self.emby = embyserver.Read_EmbyServer()
+ self.vnodes = videonodes.VideoNodes()
+
+ threading.Thread.__init__(self)
+
+ def logMsg(self, msg, lvl=1):
+
+ className = self.__class__.__name__
+ utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
+
+
+ def progressDialog(self, title, forced=False):
+
+ dialog = None
+
+ if utils.settings('dbSyncIndicator') == "true" or forced:
+ dialog = xbmcgui.DialogProgressBG()
+ dialog.create("Emby for Kodi", title)
+ self.logMsg("Show progress dialog: %s" % title, 2)
+
+ return dialog
+
+ def startSync(self):
+
+ settings = utils.settings
+ # Run at start up - optional to use the server plugin
+ if settings('SyncInstallRunDone') == "true":
+
+ # Validate views
+ self.refreshViews()
+ completed = False
+ # Verify if server plugin is installed.
+ if settings('serverSync') == "true":
+ # Try to use fast start up
+ url = "{server}/emby/Plugins?format=json"
+ result = self.doUtils(url)
+
+ for plugin in result:
+ if plugin['Name'] == "Emby.Kodi Sync Queue":
+ self.logMsg("Found server plugin.", 2)
+ completed = self.fastSync()
+ break
+
+ if not completed:
+ # Fast sync failed or server plugin is not found
+ completed = ManualSync().sync()
+ else:
+ # Install sync is not completed
+ completed = self.fullSync()
+
+ return completed
+
+ def fastSync(self):
+
+ lastSync = utils.settings('LastIncrementalSync')
+ if not lastSync:
+ lastSync = "2010-01-01T00:00:00Z"
+
+ lastSyncTime = utils.convertdate(lastSync)
+ self.logMsg("Last sync run: %s" % lastSyncTime, 1)
+
+ # get server RetentionDateTime
+ result = self.doUtils("{server}/emby/Emby.Kodi.SyncQueue/GetServerDateTime?format=json")
+ retention_time = "2010-01-01T00:00:00Z"
+ if result and result.get('RetentionDateTime'):
+ retention_time = result['RetentionDateTime']
+
+ #Try/except equivalent
+ '''
+ try:
+ retention_time = result['RetentionDateTime']
+ except (TypeError, KeyError):
+ retention_time = "2010-01-01T00:00:00Z"
+ '''
+
+ retention_time = utils.convertdate(retention_time)
+ self.logMsg("RetentionDateTime: %s" % retention_time, 1)
+
+ # if last sync before retention time do a full sync
+ if retention_time > lastSyncTime:
+ self.logMsg("Fast sync server retention insufficient, fall back to full sync", 1)
+ return False
+
+ params = {'LastUpdateDT': lastSync}
+ result = self.doUtils("{server}/emby/Emby.Kodi.SyncQueue/{UserId}/GetItems?format=json", parameters=params)
+
+ try:
+ processlist = {
+
+ 'added': result['ItemsAdded'],
+ 'update': result['ItemsUpdated'],
+ 'userdata': result['UserDataChanged'],
+ 'remove': result['ItemsRemoved']
+ }
+
+ except (KeyError, TypeError):
+ self.logMsg("Failed to retrieve latest updates using fast sync.", 1)
+ return False
+
+ else:
+ self.logMsg("Fast sync changes: %s" % result, 1)
+ for action in processlist:
+ self.triage_items(action, processlist[action])
+
+ return True
+
+ def saveLastSync(self):
+
+ # Save last sync time
+ overlap = 2
+
+ result = self.doUtils("{server}/emby/Emby.Kodi.SyncQueue/GetServerDateTime?format=json")
+ try: # datetime fails when used more than once, TypeError
+ server_time = result['ServerDateTime']
+ server_time = utils.convertdate(server_time)
+
+ except Exception as e:
+ # If the server plugin is not installed or an error happened.
+ self.logMsg("An exception occurred: %s" % e, 1)
+ time_now = datetime.utcnow()-timedelta(minutes=overlap)
+ lastSync = time_now.strftime('%Y-%m-%dT%H:%M:%SZ')
+ self.logMsg("New sync time: client time -%s min: %s" % (overlap, lastSync), 1)
+
+ else:
+ lastSync = (server_time - timedelta(minutes=overlap)).strftime('%Y-%m-%dT%H:%M:%SZ')
+ self.logMsg("New sync time: server time -%s min: %s" % (overlap, lastSync), 1)
+
+ finally:
+ utils.settings('LastIncrementalSync', value=lastSync)
+
+ def shouldStop(self):
+ # Checkpoint during the syncing process
+ if self.monitor.abortRequested():
+ return True
+ elif utils.window('emby_shouldStop') == "true":
+ return True
+ else: # Keep going
+ return False
+
+ def dbCommit(self, connection):
+
+ window = utils.window
+ # Central commit, verifies if Kodi database update is running
+ kodidb_scan = window('emby_kodiScan') == "true"
+
+ while kodidb_scan:
+
+ self.logMsg("Kodi scan is running. Waiting...", 1)
+ kodidb_scan = window('emby_kodiScan') == "true"
+
+ if self.shouldStop():
+ self.logMsg("Commit unsuccessful. Sync terminated.", 1)
+ break
+
+ if self.monitor.waitForAbort(1):
+ # Abort was requested while waiting. We should exit
+ self.logMsg("Commit unsuccessful.", 1)
+ break
+ else:
+ connection.commit()
+ self.logMsg("Commit successful.", 1)
+
+ def fullSync(self, manualrun=False, repair=False, forceddialog=False):
+
+ window = utils.window
+ settings = utils.settings
+ # Only run once when first setting up. Can be run manually.
+ music_enabled = utils.settings('enableMusic') == "true"
+
+ xbmc.executebuiltin('InhibitIdleShutdown(true)')
+ screensaver = utils.getScreensaver()
+ utils.setScreensaver(value="")
+ window('emby_dbScan', value="true")
+ # Add sources
+ utils.sourcesXML()
+
+ embyconn = utils.kodiSQL('emby')
+ embycursor = embyconn.cursor()
+ # Create the tables for the emby database
+ # emby, view, version
+ embycursor.execute(
+ """CREATE TABLE IF NOT EXISTS emby(
+ emby_id TEXT UNIQUE, media_folder TEXT, emby_type TEXT, media_type TEXT, kodi_id INTEGER,
+ kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, checksum INTEGER)""")
+ embycursor.execute(
+ """CREATE TABLE IF NOT EXISTS view(
+ view_id TEXT UNIQUE, view_name TEXT, media_type TEXT, kodi_tagid INTEGER)""")
+ embycursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)")
+ embyconn.commit()
+
+ # content sync: movies, tvshows, musicvideos, music
+ kodiconn = utils.kodiSQL('video')
+ kodicursor = kodiconn.cursor()
+
+ if manualrun:
+ message = "Manual sync"
+ elif repair:
+ message = "Repair sync"
+ forceddialog = True
+ else:
+ message = "Initial sync"
+ forceddialog = True
+ window('emby_initialScan', value="true")
+
+ pDialog = self.progressDialog("%s" % message, forced=forceddialog)
+ starttotal = datetime.now()
+
+ # Set views
+ self.maintainViews(embycursor, kodicursor)
+ embyconn.commit()
+
+ # Sync video library
+ process = {
+
+ 'movies': self.movies,
+ 'musicvideos': self.musicvideos,
+ 'tvshows': self.tvshows
+ }
+ for itemtype in process:
+ startTime = datetime.now()
+ completed = process[itemtype](embycursor, kodicursor, pDialog)
+ if not completed:
+ xbmc.executebuiltin('InhibitIdleShutdown(false)')
+ utils.setScreensaver(value=screensaver)
+ window('emby_dbScan', clear=True)
+ if pDialog:
+ pDialog.close()
+
+ embycursor.close()
+ kodicursor.close()
+ return False
+ else:
+ self.dbCommit(kodiconn)
+ embyconn.commit()
+ elapsedTime = datetime.now() - startTime
+ self.logMsg("SyncDatabase (finished %s in: %s)"
+ % (itemtype, str(elapsedTime).split('.')[0]), 1)
+ else:
+ # Close the Kodi cursor
+ kodicursor.close()
+
+ # sync music
+ if music_enabled:
+
+ musicconn = utils.kodiSQL('music')
+ musiccursor = musicconn.cursor()
+
+ startTime = datetime.now()
+ completed = self.music(embycursor, musiccursor, pDialog)
+ if not completed:
+ xbmc.executebuiltin('InhibitIdleShutdown(false)')
+ utils.setScreensaver(value=screensaver)
+ window('emby_dbScan', clear=True)
+ if pDialog:
+ pDialog.close()
+
+ embycursor.close()
+ musiccursor.close()
+ return False
+ else:
+ musicconn.commit()
+ embyconn.commit()
+ elapsedTime = datetime.now() - startTime
+ self.logMsg("SyncDatabase (finished music in: %s)"
+ % (str(elapsedTime).split('.')[0]), 1)
+ musiccursor.close()
+
+ if pDialog:
+ pDialog.close()
+
+ embycursor.close()
+
+ settings('SyncInstallRunDone', value="true")
+ settings("dbCreatedWithVersion", self.clientInfo.getVersion())
+ self.saveLastSync()
+ xbmc.executebuiltin('UpdateLibrary(video)')
+ elapsedtotal = datetime.now() - starttotal
+
+ xbmc.executebuiltin('InhibitIdleShutdown(false)')
+ utils.setScreensaver(value=screensaver)
+ window('emby_dbScan', clear=True)
+ window('emby_initialScan', clear=True)
+ if forceddialog:
+ xbmcgui.Dialog().notification(
+ heading="Emby for Kodi",
+ message="%s %s %s" %
+ (message, utils.language(33025), str(elapsedtotal).split('.')[0]),
+ icon="special://home/addons/plugin.video.emby/icon.png",
+ sound=False)
+ return True
+
+
+ def refreshViews(self):
+
+ embyconn = utils.kodiSQL('emby')
+ embycursor = embyconn.cursor()
+ kodiconn = utils.kodiSQL('video')
+ kodicursor = kodiconn.cursor()
+
+ # Compare views, assign correct tags to items
+ self.maintainViews(embycursor, kodicursor)
+
+ self.dbCommit(kodiconn)
+ kodicursor.close()
+
+ embyconn.commit()
+ embycursor.close()
+
+ def maintainViews(self, embycursor, kodicursor):
+
+ # Compare the views to emby
+ emby = self.emby
+ emby_db = embydb.Embydb_Functions(embycursor)
+ kodi_db = kodidb.Kodidb_Functions(kodicursor)
+
+ # Get views
+ result = self.doUtils("{server}/emby/Users/{UserId}/Views?format=json")
+ grouped_views = result['Items']
+ ordered_views = self.emby.getViews(sortedlist=True)
+ all_views = []
+ sorted_views = []
+ for view in ordered_views:
+ all_views.append(view['name'])
+ if view['type'] == "music":
+ continue
+
+ if view['type'] == "mixed":
+ sorted_views.append(view['name'])
+ sorted_views.append(view['name'])
+ self.logMsg("Sorted views: %s" % sorted_views, 1)
+
+ # total nodes for window properties
+ self.vnodes.clearProperties()
+ totalnodes = len(sorted_views) + 0
+
+ current_views = emby_db.getViews()
+ # Set views for supported media type
+ emby_mediatypes = {
+
+ 'movies': "Movie",
+ 'tvshows': "Series",
+ 'musicvideos': "MusicVideo",
+ 'homevideos': "Video",
+ 'music': "Audio",
+ 'photos': "Photo"
+ }
+ for mediatype in ['movies', 'tvshows', 'musicvideos', 'homevideos', 'music', 'photos']:
+
+ nodes = [] # Prevent duplicate for nodes of the same type
+ playlists = [] # Prevent duplicate for playlists of the same type
+ # Get media folders from server
+ folders = self.emby.getViews(mediatype, root=True)
+ for folder in folders:
+
+ folderid = folder['id']
+ foldername = folder['name']
+ viewtype = folder['type']
+
+ if foldername not in all_views:
+ # Media folders are grouped into userview
+ params = {
+ 'ParentId': folderid,
+ 'Recursive': True,
+ 'Limit': 1,
+ 'IncludeItemTypes': emby_mediatypes[mediatype]
+ } # Get one item from server using the folderid
+ result = self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params)
+ try:
+ verifyitem = result['Items'][0]['Id']
+ except (TypeError, IndexError):
+ # Something is wrong. Keep the same folder name.
+ # Could be the view is empty or the connection
+ pass
+ else:
+ for grouped_view in grouped_views:
+ # This is only reserved for the detection of grouped views
+ if (grouped_view['Type'] == "UserView" and
+ grouped_view.get('CollectionType') == mediatype):
+ # Take the userview, and validate the item belong to the view
+ if self.emby.verifyView(grouped_view['Id'], verifyitem):
+ # Take the name of the userview
+ self.logMsg("Found corresponding view: %s %s"
+ % (grouped_view['Name'], grouped_view['Id']), 1)
+ foldername = grouped_view['Name']
+ break
+ else:
+ # Unable to find a match, add the name to our sorted_view list
+ sorted_views.append(foldername)
+ self.logMsg("Couldn't find corresponding grouped view: %s" % sorted_views, 1)
+
+ # Failsafe
+ try:
+ sorted_views.index(foldername)
+ except ValueError:
+ sorted_views.append(foldername)
+
+ # Get current media folders from emby database
+ view = emby_db.getView_byId(folderid)
+ try:
+ current_viewname = view[0]
+ current_viewtype = view[1]
+ current_tagid = view[2]
+
+ except TypeError:
+ self.logMsg("Creating viewid: %s in Emby database." % folderid, 1)
+ tagid = kodi_db.createTag(foldername)
+ # Create playlist for the video library
+ if (foldername not in playlists and
+ mediatype in ('movies', 'tvshows', 'musicvideos')):
+ utils.playlistXSP(mediatype, foldername, folderid, viewtype)
+ playlists.append(foldername)
+ # Create the video node
+ if foldername not in nodes and mediatype not in ("musicvideos", "music"):
+ self.vnodes.viewNode(sorted_views.index(foldername), foldername, mediatype,
+ viewtype, folderid)
+ if viewtype == "mixed": # Change the value
+ sorted_views[sorted_views.index(foldername)] = "%ss" % foldername
+ nodes.append(foldername)
+ totalnodes += 1
+ # Add view to emby database
+ emby_db.addView(folderid, foldername, viewtype, tagid)
+
+ else:
+ self.logMsg(' '.join((
+
+ "Found viewid: %s" % folderid,
+ "viewname: %s" % current_viewname,
+ "viewtype: %s" % current_viewtype,
+ "tagid: %s" % current_tagid)), 2)
+
+ # View is still valid
+ try:
+ current_views.remove(folderid)
+ except ValueError:
+ # View was just created, nothing to remove
+ pass
+
+ # View was modified, update with latest info
+ if current_viewname != foldername:
+ self.logMsg("viewid: %s new viewname: %s" % (folderid, foldername), 1)
+ tagid = kodi_db.createTag(foldername)
+
+ # Update view with new info
+ emby_db.updateView(foldername, tagid, folderid)
+
+ if mediatype != "music":
+ if emby_db.getView_byName(current_viewname) is None:
+ # The tag could be a combined view. Ensure there's no other tags
+ # with the same name before deleting playlist.
+ utils.playlistXSP(
+ mediatype, current_viewname, folderid, current_viewtype, True)
+ # Delete video node
+ if mediatype != "musicvideos":
+ self.vnodes.viewNode(
+ indexnumber=None,
+ tagname=current_viewname,
+ mediatype=mediatype,
+ viewtype=current_viewtype,
+ viewid=folderid,
+ delete=True)
+ # Added new playlist
+ if (foldername not in playlists and
+ mediatype in ('movies', 'tvshows', 'musicvideos')):
+ utils.playlistXSP(mediatype, foldername, folderid, viewtype)
+ playlists.append(foldername)
+ # Add new video node
+ if foldername not in nodes and mediatype != "musicvideos":
+ self.vnodes.viewNode(sorted_views.index(foldername), foldername,
+ mediatype, viewtype, folderid)
+ if viewtype == "mixed": # Change the value
+ sorted_views[sorted_views.index(foldername)] = "%ss" % foldername
+ nodes.append(foldername)
+ totalnodes += 1
+
+ # Update items with new tag
+ items = emby_db.getItem_byView(folderid)
+ for item in items:
+ # Remove the "s" from viewtype for tags
+ kodi_db.updateTag(
+ current_tagid, tagid, item[0], current_viewtype[:-1])
+ else:
+ # Validate the playlist exists or recreate it
+ if mediatype != "music":
+ if (foldername not in playlists and
+ mediatype in ('movies', 'tvshows', 'musicvideos')):
+ utils.playlistXSP(mediatype, foldername, folderid, viewtype)
+ playlists.append(foldername)
+ # Create the video node if not already exists
+ if foldername not in nodes and mediatype != "musicvideos":
+ self.vnodes.viewNode(sorted_views.index(foldername), foldername,
+ mediatype, viewtype, folderid)
+ if viewtype == "mixed": # Change the value
+ sorted_views[sorted_views.index(foldername)] = "%ss" % foldername
+ nodes.append(foldername)
+ totalnodes += 1
+ else:
+ # Add video nodes listings
+ self.vnodes.singleNode(totalnodes, "Favorite movies", "movies", "favourites")
+ totalnodes += 1
+ self.vnodes.singleNode(totalnodes, "Favorite tvshows", "tvshows", "favourites")
+ totalnodes += 1
+ self.vnodes.singleNode(totalnodes, "channels", "movies", "channels")
+ totalnodes += 1
+ # Save total
+ utils.window('Emby.nodes.total', str(totalnodes))
+
+ # Remove any old referenced views
+ self.logMsg("Removing views: %s" % current_views, 1)
+ for view in current_views:
+ emby_db.removeView(view)
+
+ def movies(self, embycursor, kodicursor, pdialog):
+
+ lang = utils.language
+ # Get movies from emby
+ emby_db = embydb.Embydb_Functions(embycursor)
+ movies = itemtypes.Movies(embycursor, kodicursor)
+
+ views = emby_db.getView_byType('movies')
+ views += emby_db.getView_byType('mixed')
+ self.logMsg("Media folders: %s" % views, 1)
+
+ ##### PROCESS MOVIES #####
+ for view in views:
+
+ if self.shouldStop():
+ return False
+
+ # Get items per view
+ if pdialog:
+ pdialog.update(
+ heading="Emby for Kodi",
+ message="%s %s..." % (lang(33017), view['name']))
+
+ # Initial or repair sync
+ all_embymovies = self.emby.getMovies(view['id'], dialog=pdialog)
+ total = all_embymovies['TotalRecordCount']
+ embymovies = all_embymovies['Items']
+
+ if pdialog:
+ pdialog.update(heading="Processing %s / %s items" % (view['name'], total))
+
+ count = 0
+ for embymovie in embymovies:
+ # Process individual movies
+ if self.shouldStop():
+ return False
+
+ title = embymovie['Name']
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=title)
+ count += 1
+ movies.add_update(embymovie, view['name'], view['id'])
+ else:
+ self.logMsg("Movies finished.", 2)
+
+
+ ##### PROCESS BOXSETS #####
+ if pdialog:
+ pdialog.update(heading="Emby for Kodi", message=lang(33018))
+
+ boxsets = self.emby.getBoxset(dialog=pdialog)
+ total = boxsets['TotalRecordCount']
+ embyboxsets = boxsets['Items']
+
+ if pdialog:
+ pdialog.update(heading="Processing Boxsets / %s items" % total)
+
+ count = 0
+ for boxset in embyboxsets:
+ # Process individual boxset
+ if self.shouldStop():
+ return False
+
+ title = boxset['Name']
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=title)
+ count += 1
+ movies.add_updateBoxset(boxset)
+ else:
+ self.logMsg("Boxsets finished.", 2)
+
+ return True
+
+ def musicvideos(self, embycursor, kodicursor, pdialog):
+
+ # Get musicvideos from emby
+ emby_db = embydb.Embydb_Functions(embycursor)
+ mvideos = itemtypes.MusicVideos(embycursor, kodicursor)
+
+ views = emby_db.getView_byType('musicvideos')
+ self.logMsg("Media folders: %s" % views, 1)
+
+ for view in views:
+
+ if self.shouldStop():
+ return False
+
+ # Get items per view
+ viewId = view['id']
+ viewName = view['name']
+
+ if pdialog:
+ pdialog.update(
+ heading="Emby for Kodi",
+ message="%s %s..." % (utils.language(33019), viewName))
+
+ # Initial or repair sync
+ all_embymvideos = self.emby.getMusicVideos(viewId, dialog=pdialog)
+ total = all_embymvideos['TotalRecordCount']
+ embymvideos = all_embymvideos['Items']
+
+ if pdialog:
+ pdialog.update(heading="Processing %s / %s items" % (viewName, total))
+
+ count = 0
+ for embymvideo in embymvideos:
+ # Process individual musicvideo
+ if self.shouldStop():
+ return False
+
+ title = embymvideo['Name']
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=title)
+ count += 1
+ mvideos.add_update(embymvideo, viewName, viewId)
+ else:
+ self.logMsg("MusicVideos finished.", 2)
+
+ return True
+
+ def tvshows(self, embycursor, kodicursor, pdialog):
+
+ # Get shows from emby
+ emby_db = embydb.Embydb_Functions(embycursor)
+ tvshows = itemtypes.TVShows(embycursor, kodicursor)
+
+ views = emby_db.getView_byType('tvshows')
+ views += emby_db.getView_byType('mixed')
+ self.logMsg("Media folders: %s" % views, 1)
+
+ for view in views:
+
+ if self.shouldStop():
+ return False
+
+ # Get items per view
+ if pdialog:
+ pdialog.update(
+ heading="Emby for Kodi",
+ message="%s %s..." % (utils.language(33020), view['name']))
+
+ all_embytvshows = self.emby.getShows(view['id'], dialog=pdialog)
+ total = all_embytvshows['TotalRecordCount']
+ embytvshows = all_embytvshows['Items']
+
+ if pdialog:
+ pdialog.update(heading="Processing %s / %s items" % (view['name'], total))
+
+ count = 0
+ for embytvshow in embytvshows:
+ # Process individual show
+ if self.shouldStop():
+ return False
+
+ title = embytvshow['Name']
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=title)
+ count += 1
+ tvshows.add_update(embytvshow, view['name'], view['id'])
+
+ # Process episodes
+ all_episodes = self.emby.getEpisodesbyShow(embytvshow['Id'])
+ for episode in all_episodes['Items']:
+
+ # Process individual show
+ if self.shouldStop():
+ return False
+
+ episodetitle = episode['Name']
+ if pdialog:
+ pdialog.update(percentage, message="%s - %s" % (title, episodetitle))
+ tvshows.add_updateEpisode(episode)
+ else:
+ self.logMsg("TVShows finished.", 2)
+
+ return True
+
+ def music(self, embycursor, kodicursor, pdialog):
+ # Get music from emby
+ emby_db = embydb.Embydb_Functions(embycursor)
+ music = itemtypes.Music(embycursor, kodicursor)
+
+ process = {
+
+ 'artists': [self.emby.getArtists, music.add_updateArtist],
+ 'albums': [self.emby.getAlbums, music.add_updateAlbum],
+ 'songs': [self.emby.getSongs, music.add_updateSong]
+ }
+ for itemtype in ['artists', 'albums', 'songs']:
+
+ if pdialog:
+ pdialog.update(
+ heading="Emby for Kodi",
+ message="%s %s..." % (utils.language(33021), itemtype))
+
+ all_embyitems = process[itemtype][0](dialog=pdialog)
+ total = all_embyitems['TotalRecordCount']
+ embyitems = all_embyitems['Items']
+
+ if pdialog:
+ pdialog.update(heading="Processing %s / %s items" % (itemtype, total))
+
+ count = 0
+ for embyitem in embyitems:
+ # Process individual item
+ if self.shouldStop():
+ return False
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=embyitem['Name'])
+ count += 1
+
+ process[itemtype][1](embyitem)
+ else:
+ self.logMsg("%s finished." % itemtype, 2)
+
+ return True
+
+ # Reserved for websocket_client.py and fast start
+ def triage_items(self, process, items):
+
+ processlist = {
+
+ 'added': self.addedItems,
+ 'update': self.updateItems,
+ 'userdata': self.userdataItems,
+ 'remove': self.removeItems
+ }
+ if items:
+ if process == "userdata":
+ itemids = []
+ for item in items:
+ itemids.append(item['ItemId'])
+ items = itemids
+
+ self.logMsg("Queue %s: %s" % (process, items), 1)
+ processlist[process].extend(items)
+
+ def incrementalSync(self):
+
+ embyconn = utils.kodiSQL('emby')
+ embycursor = embyconn.cursor()
+ kodiconn = utils.kodiSQL('video')
+ kodicursor = kodiconn.cursor()
+ emby_db = embydb.Embydb_Functions(embycursor)
+ pDialog = None
+ update_embydb = False
+
+ if self.refresh_views:
+ # Received userconfig update
+ self.refresh_views = False
+ self.maintainViews(embycursor, kodicursor)
+ self.forceLibraryUpdate = True
+ update_embydb = True
+
+ if self.addedItems or self.updateItems or self.userdataItems or self.removeItems:
+ # Only present dialog if we are going to process items
+ pDialog = self.progressDialog('Incremental sync')
+
+
+ process = {
+
+ 'added': self.addedItems,
+ 'update': self.updateItems,
+ 'userdata': self.userdataItems,
+ 'remove': self.removeItems
+ }
+ for process_type in ['added', 'update', 'userdata', 'remove']:
+
+ if process[process_type] and utils.window('emby_kodiScan') != "true":
+
+ listItems = list(process[process_type])
+ del process[process_type][:] # Reset class list
+
+ items_process = itemtypes.Items(embycursor, kodicursor)
+ update = False
+
+ # Prepare items according to process process_type
+ if process_type == "added":
+ items = self.emby.sortby_mediatype(listItems)
+
+ elif process_type in ("userdata", "remove"):
+ items = emby_db.sortby_mediaType(listItems, unsorted=False)
+
+ else:
+ items = emby_db.sortby_mediaType(listItems)
+ if items.get('Unsorted'):
+ sorted_items = self.emby.sortby_mediatype(items['Unsorted'])
+ doupdate = items_process.itemsbyId(sorted_items, "added", pDialog)
+ if doupdate:
+ embyupdate, kodiupdate_video = doupdate
+ if embyupdate:
+ update_embydb = True
+ if kodiupdate_video:
+ self.forceLibraryUpdate = True
+ del items['Unsorted']
+
+ doupdate = items_process.itemsbyId(items, process_type, pDialog)
+ if doupdate:
+ embyupdate, kodiupdate_video = doupdate
+ if embyupdate:
+ update_embydb = True
+ if kodiupdate_video:
+ self.forceLibraryUpdate = True
+
+ if update_embydb:
+ update_embydb = False
+ self.logMsg("Updating emby database.", 1)
+ embyconn.commit()
+ self.saveLastSync()
+
+ if self.forceLibraryUpdate:
+ # Force update the Kodi library
+ self.forceLibraryUpdate = False
+ self.dbCommit(kodiconn)
+
+ self.logMsg("Updating video library.", 1)
+ utils.window('emby_kodiScan', value="true")
+ xbmc.executebuiltin('UpdateLibrary(video)')
+
+ if pDialog:
+ pDialog.close()
+
+ kodicursor.close()
+ embycursor.close()
+
+
+ def compareDBVersion(self, current, minimum):
+ # It returns True is database is up to date. False otherwise.
+ self.logMsg("current: %s minimum: %s" % (current, minimum), 1)
+ currMajor, currMinor, currPatch = current.split(".")
+ minMajor, minMinor, minPatch = minimum.split(".")
+
+ if currMajor > minMajor:
+ return True
+ elif currMajor == minMajor and (currMinor > minMinor or
+ (currMinor == minMinor and currPatch >= minPatch)):
+ return True
+ else:
+ # Database out of date.
+ return False
+
+ def run(self):
+
+ try:
+ self.run_internal()
+ except Exception as e:
+ utils.window('emby_dbScan', clear=True)
+ xbmcgui.Dialog().ok(
+ heading="Emby for Kodi",
+ line1=(
+ "Library sync thread has exited! "
+ "You should restart Kodi now. "
+ "Please report this on the forum."))
+ raise
+
+ def run_internal(self):
+
+ lang = utils.language
+ window = utils.window
+ settings = utils.settings
+ dialog = xbmcgui.Dialog()
+
+ startupComplete = False
+
+ self.logMsg("---===### Starting LibrarySync ###===---", 0)
+
+ while not self.monitor.abortRequested():
+
+ # In the event the server goes offline
+ while self.suspend_thread:
+ # Set in service.py
+ if self.monitor.waitForAbort(5):
+ # Abort was requested while waiting. We should exit
+ break
+
+ if (window('emby_dbCheck') != "true" and settings('SyncInstallRunDone') == "true"):
+ # Verify the validity of the database
+ currentVersion = settings('dbCreatedWithVersion')
+ minVersion = window('emby_minDBVersion')
+ uptoDate = self.compareDBVersion(currentVersion, minVersion)
+
+ if not uptoDate:
+ self.logMsg("Database version out of date: %s minimum version required: %s"
+ % (currentVersion, minVersion), 0)
+
+ resp = dialog.yesno("Emby for Kodi", lang(33022))
+ if not resp:
+ self.logMsg("Database version is out of date! USER IGNORED!", 0)
+ dialog.ok("Emby for Kodi", lang(33023))
+ else:
+ utils.reset()
+
+ break
+
+ window('emby_dbCheck', value="true")
+
+
+ if not startupComplete:
+ # Verify the video database can be found
+ videoDb = utils.getKodiVideoDBPath()
+ if not xbmcvfs.exists(videoDb):
+ # Database does not exists
+ self.logMsg(
+ "The current Kodi version is incompatible "
+ "with the Emby for Kodi add-on. Please visit "
+ "https://github.com/MediaBrowser/Emby.Kodi/wiki "
+ "to know which Kodi versions are supported.", 0)
+
+ dialog.ok(
+ heading="Emby for Kodi",
+ line1=lang(33024))
+ break
+
+ # Run start up sync
+ self.logMsg("Database version: %s" % settings('dbCreatedWithVersion'), 0)
+ self.logMsg("SyncDatabase (started)", 1)
+ startTime = datetime.now()
+ librarySync = self.startSync()
+ elapsedTime = datetime.now() - startTime
+ self.logMsg("SyncDatabase (finished in: %s) %s"
+ % (str(elapsedTime).split('.')[0], librarySync), 1)
+ # Only try the initial sync once per kodi session regardless
+ # This will prevent an infinite loop in case something goes wrong.
+ startupComplete = True
+
+ # Process updates
+ if window('emby_dbScan') != "true":
+ self.incrementalSync()
+
+ if window('emby_onWake') == "true" and window('emby_online') == "true":
+ # Kodi is waking up
+ # Set in kodimonitor.py
+ window('emby_onWake', clear=True)
+ if window('emby_syncRunning') != "true":
+ self.logMsg("SyncDatabase onWake (started)", 0)
+ librarySync = self.startSync()
+ self.logMsg("SyncDatabase onWake (finished) %s" % librarySync, 0)
+
+ if self.stop_thread:
+ # Set in service.py
+ self.logMsg("Service terminated thread.", 2)
+ break
+
+ if self.monitor.waitForAbort(1):
+ # Abort was requested while waiting. We should exit
+ break
+
+ self.logMsg("###===--- LibrarySync Stopped ---===###", 0)
+
+ def stopThread(self):
+ self.stop_thread = True
+ self.logMsg("Ending thread...", 2)
+
+ def suspendThread(self):
+ self.suspend_thread = True
+ self.logMsg("Pausing thread...", 0)
+
+ def resumeThread(self):
+ self.suspend_thread = False
+ self.logMsg("Resuming thread...", 0)
+
+
+class ManualSync(LibrarySync):
+
+
+ def __init__(self):
+
+ LibrarySync.__init__(self)
+
+ def sync(self, dialog=False):
+
+ return self.fullSync(manualrun=True, forceddialog=dialog)
+
+
+ def movies(self, embycursor, kodicursor, pdialog):
+
+ lang = utils.language
+ # Get movies from emby
+ emby_db = embydb.Embydb_Functions(embycursor)
+ movies = itemtypes.Movies(embycursor, kodicursor)
+
+ views = emby_db.getView_byType('movies')
+ views += emby_db.getView_byType('mixed')
+ self.logMsg("Media folders: %s" % views, 1)
+
+ # Pull the list of movies and boxsets in Kodi
+ try:
+ all_kodimovies = dict(emby_db.getChecksum('Movie'))
+ except ValueError:
+ all_kodimovies = {}
+
+ try:
+ all_kodisets = dict(emby_db.getChecksum('BoxSet'))
+ except ValueError:
+ all_kodisets = {}
+
+ all_embymoviesIds = set()
+ all_embyboxsetsIds = set()
+ updatelist = []
+
+ ##### PROCESS MOVIES #####
+ for view in views:
+
+ if self.shouldStop():
+ return False
+
+ # Get items per view
+ viewId = view['id']
+ viewName = view['name']
+
+ if pdialog:
+ pdialog.update(
+ heading="Emby for Kodi",
+ message="%s %s..." % (lang(33026), viewName))
+
+ all_embymovies = self.emby.getMovies(viewId, basic=True, dialog=pdialog)
+ for embymovie in all_embymovies['Items']:
+
+ if self.shouldStop():
+ return False
+
+ API = api.API(embymovie)
+ itemid = embymovie['Id']
+ all_embymoviesIds.add(itemid)
+
+
+ if all_kodimovies.get(itemid) != API.getChecksum():
+ # Only update if movie is not in Kodi or checksum is different
+ updatelist.append(itemid)
+
+ self.logMsg("Movies to update for %s: %s" % (viewName, updatelist), 1)
+ embymovies = self.emby.getFullItems(updatelist)
+ total = len(updatelist)
+ del updatelist[:]
+
+ if pdialog:
+ pdialog.update(heading="Processing %s / %s items" % (viewName, total))
+
+ count = 0
+ for embymovie in embymovies:
+ # Process individual movies
+ if self.shouldStop():
+ return False
+
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=embymovie['Name'])
+ count += 1
+ movies.add_update(embymovie, viewName, viewId)
+
+ ##### PROCESS BOXSETS #####
+
+ boxsets = self.emby.getBoxset(dialog=pdialog)
+ embyboxsets = []
+
+ if pdialog:
+ pdialog.update(heading="Emby for Kodi", message=lang(33027))
+
+ for boxset in boxsets['Items']:
+
+ if self.shouldStop():
+ return False
+
+ # Boxset has no real userdata, so using etag to compare
+ itemid = boxset['Id']
+ all_embyboxsetsIds.add(itemid)
+
+ if all_kodisets.get(itemid) != boxset['Etag']:
+ # Only update if boxset is not in Kodi or boxset['Etag'] is different
+ updatelist.append(itemid)
+ embyboxsets.append(boxset)
+
+ self.logMsg("Boxsets to update: %s" % updatelist, 1)
+ total = len(updatelist)
+
+ if pdialog:
+ pdialog.update(heading="Processing Boxsets / %s items" % total)
+
+ count = 0
+ for boxset in embyboxsets:
+ # Process individual boxset
+ if self.shouldStop():
+ return False
+
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=boxset['Name'])
+ count += 1
+ movies.add_updateBoxset(boxset)
+
+ ##### PROCESS DELETES #####
+
+ for kodimovie in all_kodimovies:
+ if kodimovie not in all_embymoviesIds:
+ movies.remove(kodimovie)
+ else:
+ self.logMsg("Movies compare finished.", 1)
+
+ for boxset in all_kodisets:
+ if boxset not in all_embyboxsetsIds:
+ movies.remove(boxset)
+ else:
+ self.logMsg("Boxsets compare finished.", 1)
+
+ return True
+
+ def musicvideos(self, embycursor, kodicursor, pdialog):
+
+ # Get musicvideos from emby
+ emby_db = embydb.Embydb_Functions(embycursor)
+ mvideos = itemtypes.MusicVideos(embycursor, kodicursor)
+
+ views = emby_db.getView_byType('musicvideos')
+ self.logMsg("Media folders: %s" % views, 1)
+
+ # Pull the list of musicvideos in Kodi
+ try:
+ all_kodimvideos = dict(emby_db.getChecksum('MusicVideo'))
+ except ValueError:
+ all_kodimvideos = {}
+
+ all_embymvideosIds = set()
+ updatelist = []
+
+ for view in views:
+
+ if self.shouldStop():
+ return False
+
+ # Get items per view
+ viewId = view['id']
+ viewName = view['name']
+
+ if pdialog:
+ pdialog.update(
+ heading="Emby for Kodi",
+ message="%s %s..." % (utils.language(33028), viewName))
+
+ all_embymvideos = self.emby.getMusicVideos(viewId, basic=True, dialog=pdialog)
+ for embymvideo in all_embymvideos['Items']:
+
+ if self.shouldStop():
+ return False
+
+ API = api.API(embymvideo)
+ itemid = embymvideo['Id']
+ all_embymvideosIds.add(itemid)
+
+
+ if all_kodimvideos.get(itemid) != API.getChecksum():
+ # Only update if musicvideo is not in Kodi or checksum is different
+ updatelist.append(itemid)
+
+ self.logMsg("MusicVideos to update for %s: %s" % (viewName, updatelist), 1)
+ embymvideos = self.emby.getFullItems(updatelist)
+ total = len(updatelist)
+ del updatelist[:]
+
+
+ if pdialog:
+ pdialog.update(heading="Processing %s / %s items" % (viewName, total))
+
+ count = 0
+ for embymvideo in embymvideos:
+ # Process individual musicvideo
+ if self.shouldStop():
+ return False
+
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=embymvideo['Name'])
+ count += 1
+ mvideos.add_update(embymvideo, viewName, viewId)
+
+ ##### PROCESS DELETES #####
+
+ for kodimvideo in all_kodimvideos:
+ if kodimvideo not in all_embymvideosIds:
+ mvideos.remove(kodimvideo)
+ else:
+ self.logMsg("MusicVideos compare finished.", 1)
+
+ return True
+
+ def tvshows(self, embycursor, kodicursor, pdialog):
+
+ lang = utils.language
+ # Get shows from emby
+ emby_db = embydb.Embydb_Functions(embycursor)
+ tvshows = itemtypes.TVShows(embycursor, kodicursor)
+
+ views = emby_db.getView_byType('tvshows')
+ views += emby_db.getView_byType('mixed')
+ self.logMsg("Media folders: %s" % views, 1)
+
+ # Pull the list of tvshows and episodes in Kodi
+ try:
+ all_koditvshows = dict(emby_db.getChecksum('Series'))
+ except ValueError:
+ all_koditvshows = {}
+
+ try:
+ all_kodiepisodes = dict(emby_db.getChecksum('Episode'))
+ except ValueError:
+ all_kodiepisodes = {}
+
+ all_embytvshowsIds = set()
+ all_embyepisodesIds = set()
+ updatelist = []
+
+
+ for view in views:
+
+ if self.shouldStop():
+ return False
+
+ # Get items per view
+ viewId = view['id']
+ viewName = view['name']
+
+ if pdialog:
+ pdialog.update(
+ heading="Emby for Kodi",
+ message="%s %s..." % (lang(33029), viewName))
+
+ all_embytvshows = self.emby.getShows(viewId, basic=True, dialog=pdialog)
+ for embytvshow in all_embytvshows['Items']:
+
+ if self.shouldStop():
+ return False
+
+ API = api.API(embytvshow)
+ itemid = embytvshow['Id']
+ all_embytvshowsIds.add(itemid)
+
+
+ if all_koditvshows.get(itemid) != API.getChecksum():
+ # Only update if movie is not in Kodi or checksum is different
+ updatelist.append(itemid)
+
+ self.logMsg("TVShows to update for %s: %s" % (viewName, updatelist), 1)
+ embytvshows = self.emby.getFullItems(updatelist)
+ total = len(updatelist)
+ del updatelist[:]
+
+
+ if pdialog:
+ pdialog.update(heading="Processing %s / %s items" % (viewName, total))
+
+ count = 0
+ for embytvshow in embytvshows:
+ # Process individual show
+ if self.shouldStop():
+ return False
+
+ itemid = embytvshow['Id']
+ title = embytvshow['Name']
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=title)
+ count += 1
+ tvshows.add_update(embytvshow, viewName, viewId)
+
+ else:
+ # Get all episodes in view
+ if pdialog:
+ pdialog.update(
+ heading="Emby for Kodi",
+ message="%s %s..." % (lang(33030), viewName))
+
+ all_embyepisodes = self.emby.getEpisodes(viewId, basic=True, dialog=pdialog)
+ for embyepisode in all_embyepisodes['Items']:
+
+ if self.shouldStop():
+ return False
+
+ API = api.API(embyepisode)
+ itemid = embyepisode['Id']
+ all_embyepisodesIds.add(itemid)
+
+ if all_kodiepisodes.get(itemid) != API.getChecksum():
+ # Only update if movie is not in Kodi or checksum is different
+ updatelist.append(itemid)
+
+ self.logMsg("Episodes to update for %s: %s" % (viewName, updatelist), 1)
+ embyepisodes = self.emby.getFullItems(updatelist)
+ total = len(updatelist)
+ del updatelist[:]
+
+ count = 0
+ for episode in embyepisodes:
+
+ # Process individual episode
+ if self.shouldStop():
+ return False
+
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message="%s - %s" % (episode['SeriesName'], episode['Name']))
+ count += 1
+ tvshows.add_updateEpisode(episode)
+
+ ##### PROCESS DELETES #####
+
+ for koditvshow in all_koditvshows:
+ if koditvshow not in all_embytvshowsIds:
+ tvshows.remove(koditvshow)
+ else:
+ self.logMsg("TVShows compare finished.", 1)
+
+ for kodiepisode in all_kodiepisodes:
+ if kodiepisode not in all_embyepisodesIds:
+ tvshows.remove(kodiepisode)
+ else:
+ self.logMsg("Episodes compare finished.", 1)
+
+ return True
+
+ def music(self, embycursor, kodicursor, pdialog):
+
+ # Get music from emby
+ emby_db = embydb.Embydb_Functions(embycursor)
+ music = itemtypes.Music(embycursor, kodicursor)
+
+ # Pull the list of artists, albums, songs
+ try:
+ all_kodiartists = dict(emby_db.getChecksum('MusicArtist'))
+ except ValueError:
+ all_kodiartists = {}
+
+ try:
+ all_kodialbums = dict(emby_db.getChecksum('MusicAlbum'))
+ except ValueError:
+ all_kodialbums = {}
+
+ try:
+ all_kodisongs = dict(emby_db.getChecksum('Audio'))
+ except ValueError:
+ all_kodisongs = {}
+
+ all_embyartistsIds = set()
+ all_embyalbumsIds = set()
+ all_embysongsIds = set()
+ updatelist = []
+
+ process = {
+
+ 'artists': [self.emby.getArtists, music.add_updateArtist],
+ 'albums': [self.emby.getAlbums, music.add_updateAlbum],
+ 'songs': [self.emby.getSongs, music.add_updateSong]
+ }
+ for data_type in ['artists', 'albums', 'songs']:
+ if pdialog:
+ pdialog.update(
+ heading="Emby for Kodi",
+ message="%s %s..." % (utils.language(33031), data_type))
+ if data_type != "artists":
+ all_embyitems = process[data_type][0](basic=True, dialog=pdialog)
+ else:
+ all_embyitems = process[data_type][0](dialog=pdialog)
+ for embyitem in all_embyitems['Items']:
+ if self.shouldStop():
+ return False
+ API = api.API(embyitem)
+ itemid = embyitem['Id']
+ if data_type == "artists":
+ all_embyartistsIds.add(itemid)
+ if all_kodiartists.get(itemid) != API.getChecksum():
+ # Only update if artist is not in Kodi or checksum is different
+ updatelist.append(itemid)
+ elif data_type == "albums":
+ all_embyalbumsIds.add(itemid)
+ if all_kodialbums.get(itemid) != API.getChecksum():
+ # Only update if album is not in Kodi or checksum is different
+ updatelist.append(itemid)
+ else:
+ all_embysongsIds.add(itemid)
+ if all_kodisongs.get(itemid) != API.getChecksum():
+ # Only update if songs is not in Kodi or checksum is different
+ updatelist.append(itemid)
+ self.logMsg("%s to update: %s" % (data_type, updatelist), 1)
+ embyitems = self.emby.getFullItems(updatelist)
+ total = len(updatelist)
+ del updatelist[:]
+ if pdialog:
+ pdialog.update(heading="Processing %s / %s items" % (data_type, total))
+ count = 0
+ for embyitem in embyitems:
+ # Process individual item
+ if self.shouldStop():
+ return False
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=embyitem['Name'])
+ count += 1
+ process[data_type][1](embyitem)
+ ##### PROCESS DELETES #####
+ for kodiartist in all_kodiartists:
+ if kodiartist not in all_embyartistsIds and all_kodiartists[kodiartist] is not None:
+ music.remove(kodiartist)
+ else:
+ self.logMsg("Artist compare finished.", 1)
+ for kodialbum in all_kodialbums:
+ if kodialbum not in all_embyalbumsIds:
+ music.remove(kodialbum)
+ else:
+ self.logMsg("Albums compare finished.", 1)
+ for kodisong in all_kodisongs:
+ if kodisong not in all_embysongsIds:
+ music.remove(kodisong)
+ else:
+ self.logMsg("Songs compare finished.", 1)
+ return True
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
diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py
index a0c3743c..e77777a3 100644
--- a/resources/lib/playbackutils.py
+++ b/resources/lib/playbackutils.py
@@ -1,358 +1,346 @@
-# -*- coding: utf-8 -*-
-
-#################################################################################################
-
-import json
-import sys
-
-import xbmc
-import xbmcgui
-import xbmcplugin
-
-import api
-import artwork
-import clientinfo
-import downloadutils
-import playutils as putils
-import playlist
-import read_embyserver as embyserver
-import utils
-
-#################################################################################################
-
-
-class PlaybackUtils():
-
-
- def __init__(self, item):
-
- self.item = item
- self.API = api.API(self.item)
-
- self.clientInfo = clientinfo.ClientInfo()
- self.addonName = self.clientInfo.getAddonName()
- self.doUtils = downloadutils.DownloadUtils().downloadUrl
-
- self.userid = utils.window('emby_currUser')
- self.server = utils.window('emby_server%s' % self.userid)
-
- self.artwork = artwork.Artwork()
- self.emby = embyserver.Read_EmbyServer()
- self.pl = playlist.Playlist()
-
- def logMsg(self, msg, lvl=1):
-
- self.className = self.__class__.__name__
- utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
-
-
- def play(self, itemid, dbid=None):
-
- log = self.logMsg
- window = utils.window
- settings = utils.settings
-
- doUtils = self.doUtils
- item = self.item
- API = self.API
- listitem = xbmcgui.ListItem()
- playutils = putils.PlayUtils(item)
-
- log("Play called.", 1)
- playurl = playutils.getPlayUrl()
- if not playurl:
- return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
-
- if dbid is None:
- # Item is not in Kodi database
- listitem.setPath(playurl)
- self.setProperties(playurl, listitem)
- return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
-
- ############### ORGANIZE CURRENT PLAYLIST ################
-
- homeScreen = xbmc.getCondVisibility('Window.IsActive(home)')
- playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
- startPos = max(playlist.getposition(), 0) # Can return -1
- sizePlaylist = playlist.size()
- currentPosition = startPos
-
- propertiesPlayback = window('emby_playbackProps') == "true"
- introsPlaylist = False
- dummyPlaylist = False
-
- log("Playlist start position: %s" % startPos, 2)
- log("Playlist plugin position: %s" % currentPosition, 2)
- log("Playlist size: %s" % sizePlaylist, 2)
-
- ############### RESUME POINT ################
-
- userdata = API.getUserData()
- seektime = API.adjustResume(userdata['Resume'])
-
- # We need to ensure we add the intro and additional parts only once.
- # Otherwise we get a loop.
- if not propertiesPlayback:
-
- window('emby_playbackProps', value="true")
- log("Setting up properties in playlist.", 1)
-
- if (not homeScreen and not seektime and
- window('emby_customPlaylist') != "true"):
-
- log("Adding dummy file to playlist.", 2)
- dummyPlaylist = True
- playlist.add(playurl, listitem, index=startPos)
- # Remove the original item from playlist
- self.pl.removefromPlaylist(startPos+1)
- # Readd the original item to playlist - via jsonrpc so we have full metadata
- self.pl.insertintoPlaylist(currentPosition+1, dbid, item['Type'].lower())
- currentPosition += 1
-
- ############### -- CHECK FOR INTROS ################
-
- if settings('enableCinema') == "true" and not seektime:
- # if we have any play them when the movie/show is not being resumed
- url = "{server}/emby/Users/{UserId}/Items/%s/Intros?format=json" % itemid
- intros = doUtils(url)
-
- if intros['TotalRecordCount'] != 0:
- getTrailers = True
-
- if settings('askCinema') == "true":
- resp = xbmcgui.Dialog().yesno("Emby for Kodi", utils.language(33016))
- if not resp:
- # User selected to not play trailers
- getTrailers = False
- log("Skip trailers.", 1)
-
- if getTrailers:
- for intro in intros['Items']:
- # The server randomly returns intros, process them.
- introListItem = xbmcgui.ListItem()
- introPlayurl = putils.PlayUtils(intro).getPlayUrl()
- log("Adding Intro: %s" % introPlayurl, 1)
-
- # Set listitem and properties for intros
- pbutils = PlaybackUtils(intro)
- pbutils.setProperties(introPlayurl, introListItem)
-
- self.pl.insertintoPlaylist(currentPosition, url=introPlayurl)
- introsPlaylist = True
- currentPosition += 1
-
-
- ############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ###############
-
- if homeScreen and not seektime and not sizePlaylist:
- # Extend our current playlist with the actual item to play
- # only if there's no playlist first
- log("Adding main item to playlist.", 1)
- self.pl.addtoPlaylist(dbid, item['Type'].lower())
-
- # Ensure that additional parts are played after the main item
- currentPosition += 1
-
- ############### -- CHECK FOR ADDITIONAL PARTS ################
-
- if item.get('PartCount'):
- # Only add to the playlist after intros have played
- partcount = item['PartCount']
- url = "{server}/emby/Videos/%s/AdditionalParts?format=json" % itemid
- parts = doUtils(url)
- for part in parts['Items']:
-
- additionalListItem = xbmcgui.ListItem()
- additionalPlayurl = putils.PlayUtils(part).getPlayUrl()
- log("Adding additional part: %s" % partcount, 1)
-
- # Set listitem and properties for each additional parts
- pbutils = PlaybackUtils(part)
- pbutils.setProperties(additionalPlayurl, additionalListItem)
- pbutils.setArtwork(additionalListItem)
-
- playlist.add(additionalPlayurl, additionalListItem, index=currentPosition)
- self.pl.verifyPlaylist()
- currentPosition += 1
-
- if dummyPlaylist:
- # Added a dummy file to the playlist,
- # because the first item is going to fail automatically.
- log("Processed as a playlist. First item is skipped.", 1)
- return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
-
-
- # We just skipped adding properties. Reset flag for next time.
- elif propertiesPlayback:
- log("Resetting properties playback flag.", 2)
- window('emby_playbackProps', clear=True)
-
- #self.pl.verifyPlaylist()
- ########## SETUP MAIN ITEM ##########
-
- # For transcoding only, ask for audio/subs pref
- if window('emby_%s.playmethod' % playurl) == "Transcode":
- playurl = playutils.audioSubsPref(playurl, listitem)
- window('emby_%s.playmethod' % playurl, value="Transcode")
-
- listitem.setPath(playurl)
- self.setProperties(playurl, listitem)
-
- ############### PLAYBACK ################
-
- if homeScreen and seektime and window('emby_customPlaylist') != "true":
- log("Play as a widget item.", 1)
- self.setListItem(listitem)
- xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
-
- elif ((introsPlaylist and window('emby_customPlaylist') == "true") or
- (homeScreen and not sizePlaylist)):
- # Playlist was created just now, play it.
- log("Play playlist.", 1)
- xbmc.Player().play(playlist, startpos=startPos)
-
- else:
- log("Play as a regular item.", 1)
- xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
-
- def setProperties(self, playurl, listitem):
-
- window = utils.window
- # Set all properties necessary for plugin path playback
- item = self.item
- itemid = item['Id']
- itemtype = item['Type']
-
- embyitem = "emby_%s" % playurl
- window('%s.runtime' % embyitem, value=str(item.get('RunTimeTicks')))
- window('%s.type' % embyitem, value=itemtype)
- window('%s.itemid' % embyitem, value=itemid)
-
- if itemtype == "Episode":
- window('%s.refreshid' % embyitem, value=item.get('SeriesId'))
- else:
- window('%s.refreshid' % embyitem, value=itemid)
-
- # Append external subtitles to stream
- playmethod = utils.window('%s.playmethod' % embyitem)
- # Only for direct stream
- if playmethod in ("DirectStream"):
- # Direct play automatically appends external
- subtitles = self.externalSubs(playurl)
- listitem.setSubtitles(subtitles)
-
- self.setArtwork(listitem)
-
- def externalSubs(self, playurl):
-
- externalsubs = []
- mapping = {}
-
- item = self.item
- itemid = item['Id']
- try:
- mediastreams = item['MediaSources'][0]['MediaStreams']
- except (TypeError, KeyError, IndexError):
- return
-
- kodiindex = 0
- for stream in mediastreams:
-
- index = stream['Index']
- # Since Emby returns all possible tracks together, have to pull only external subtitles.
- # IsTextSubtitleStream if true, is available to download from emby.
- if (stream['Type'] == "Subtitle" and
- stream['IsExternal'] and stream['IsTextSubtitleStream']):
-
- # Direct stream
- url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
- % (self.server, itemid, itemid, index))
-
- # map external subtitles for mapping
- mapping[kodiindex] = index
- externalsubs.append(url)
- kodiindex += 1
-
- mapping = json.dumps(mapping)
- utils.window('emby_%s.indexMapping' % playurl, value=mapping)
-
- return externalsubs
-
- def setArtwork(self, listItem):
- # Set up item and item info
- item = self.item
- artwork = self.artwork
-
- allartwork = artwork.getAllArtwork(item, parentInfo=True)
- # Set artwork for listitem
- arttypes = {
-
- 'poster': "Primary",
- 'tvshow.poster': "Primary",
- 'clearart': "Art",
- 'tvshow.clearart': "Art",
- 'clearlogo': "Logo",
- 'tvshow.clearlogo': "Logo",
- 'discart': "Disc",
- 'fanart_image': "Backdrop",
- 'landscape': "Thumb"
- }
- for arttype in arttypes:
-
- art = arttypes[arttype]
- if art == "Backdrop":
- try: # Backdrop is a list, grab the first backdrop
- self.setArtProp(listItem, arttype, allartwork[art][0])
- except: pass
- else:
- self.setArtProp(listItem, arttype, allartwork[art])
-
- def setArtProp(self, listItem, arttype, path):
-
- if arttype in (
- 'thumb', 'fanart_image', 'small_poster', 'tiny_poster',
- 'medium_landscape', 'medium_poster', 'small_fanartimage',
- 'medium_fanartimage', 'fanart_noindicators'):
-
- listItem.setProperty(arttype, path)
- else:
- listItem.setArt({arttype: path})
-
- def setListItem(self, listItem):
-
- item = self.item
- itemtype = item['Type']
- API = self.API
- people = API.getPeople()
- studios = API.getStudios()
-
- metadata = {
-
- 'title': item.get('Name', "Missing name"),
- 'year': item.get('ProductionYear'),
- 'plot': API.getOverview(),
- 'director': people.get('Director'),
- 'writer': people.get('Writer'),
- 'mpaa': API.getMpaa(),
- 'genre': " / ".join(item['Genres']),
- 'studio': " / ".join(studios),
- 'aired': API.getPremiereDate(),
- 'rating': item.get('CommunityRating'),
- 'votes': item.get('VoteCount')
- }
-
- if "Episode" in itemtype:
- # Only for tv shows
- thumbId = item.get('SeriesId')
- season = item.get('ParentIndexNumber', -1)
- episode = item.get('IndexNumber', -1)
- show = item.get('SeriesName', "")
-
- metadata['TVShowTitle'] = show
- metadata['season'] = season
- metadata['episode'] = episode
-
- listItem.setProperty('IsPlayable', 'true')
- listItem.setProperty('IsFolder', 'false')
- listItem.setLabel(metadata['title'])
+# -*- coding: utf-8 -*-
+
+#################################################################################################
+
+import json
+import sys
+
+import xbmc
+import xbmcgui
+import xbmcplugin
+
+import api
+import artwork
+import clientinfo
+import downloadutils
+import playutils as putils
+import playlist
+import read_embyserver as embyserver
+import utils
+
+#################################################################################################
+
+
+class PlaybackUtils():
+
+
+ def __init__(self, item):
+
+ self.item = item
+ self.API = api.API(self.item)
+
+ self.clientInfo = clientinfo.ClientInfo()
+ self.addonName = self.clientInfo.getAddonName()
+ self.doUtils = downloadutils.DownloadUtils().downloadUrl
+
+ self.userid = utils.window('emby_currUser')
+ self.server = utils.window('emby_server%s' % self.userid)
+
+ self.artwork = artwork.Artwork()
+ self.emby = embyserver.Read_EmbyServer()
+ self.pl = playlist.Playlist()
+
+ def logMsg(self, msg, lvl=1):
+
+ self.className = self.__class__.__name__
+ utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
+
+
+ def play(self, itemid, dbid=None):
+
+ window = utils.window
+ settings = utils.settings
+
+ listitem = xbmcgui.ListItem()
+ playutils = putils.PlayUtils(self.item)
+
+ self.logMsg("Play called.", 1)
+ playurl = playutils.getPlayUrl()
+ if not playurl:
+ return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
+
+ if dbid is None:
+ # Item is not in Kodi database
+ listitem.setPath(playurl)
+ self.setProperties(playurl, listitem)
+ return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
+
+ ############### ORGANIZE CURRENT PLAYLIST ################
+
+ homeScreen = xbmc.getCondVisibility('Window.IsActive(home)')
+ playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
+ startPos = max(playlist.getposition(), 0) # Can return -1
+ sizePlaylist = playlist.size()
+ currentPosition = startPos
+
+ propertiesPlayback = window('emby_playbackProps') == "true"
+ introsPlaylist = False
+ dummyPlaylist = False
+
+ self.logMsg("Playlist start position: %s" % startPos, 2)
+ self.logMsg("Playlist plugin position: %s" % currentPosition, 2)
+ self.logMsg("Playlist size: %s" % sizePlaylist, 2)
+
+ ############### RESUME POINT ################
+
+ userdata = self.API.getUserData()
+ seektime = self.API.adjustResume(userdata['Resume'])
+
+ # We need to ensure we add the intro and additional parts only once.
+ # Otherwise we get a loop.
+ if not propertiesPlayback:
+
+ window('emby_playbackProps', value="true")
+ self.logMsg("Setting up properties in playlist.", 1)
+
+ if (not homeScreen and not seektime and
+ window('emby_customPlaylist') != "true"):
+
+ self.logMsg("Adding dummy file to playlist.", 2)
+ dummyPlaylist = True
+ playlist.add(playurl, listitem, index=startPos)
+ # Remove the original item from playlist
+ self.pl.removefromPlaylist(startPos+1)
+ # Readd the original item to playlist - via jsonrpc so we have full metadata
+ self.pl.insertintoPlaylist(currentPosition+1, dbid, self.item['Type'].lower())
+ currentPosition += 1
+
+ ############### -- CHECK FOR INTROS ################
+
+ if settings('enableCinema') == "true" and not seektime:
+ # if we have any play them when the movie/show is not being resumed
+ url = "{server}/emby/Users/{UserId}/Items/%s/Intros?format=json" % itemid
+ intros = self.doUtils(url)
+
+ if intros['TotalRecordCount'] != 0:
+ getTrailers = True
+
+ if settings('askCinema') == "true":
+ resp = xbmcgui.Dialog().yesno("Emby for Kodi", utils.language(33016))
+ if not resp:
+ # User selected to not play trailers
+ getTrailers = False
+ self.logMsg("Skip trailers.", 1)
+
+ if getTrailers:
+ for intro in intros['Items']:
+ # The server randomly returns intros, process them.
+ introListItem = xbmcgui.ListItem()
+ introPlayurl = putils.PlayUtils(intro).getPlayUrl()
+ self.logMsg("Adding Intro: %s" % introPlayurl, 1)
+
+ # Set listitem and properties for intros
+ pbutils = PlaybackUtils(intro)
+ pbutils.setProperties(introPlayurl, introListItem)
+
+ self.pl.insertintoPlaylist(currentPosition, url=introPlayurl)
+ introsPlaylist = True
+ currentPosition += 1
+
+
+ ############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ###############
+
+ if homeScreen and not seektime and not sizePlaylist:
+ # Extend our current playlist with the actual item to play
+ # only if there's no playlist first
+ self.logMsg("Adding main item to playlist.", 1)
+ self.pl.addtoPlaylist(dbid, self.item['Type'].lower())
+
+ # Ensure that additional parts are played after the main item
+ currentPosition += 1
+
+ ############### -- CHECK FOR ADDITIONAL PARTS ################
+
+ if self.item.get('PartCount'):
+ # Only add to the playlist after intros have played
+ partcount = self.item['PartCount']
+ url = "{server}/emby/Videos/%s/AdditionalParts?format=json" % itemid
+ parts = self.doUtils(url)
+ for part in parts['Items']:
+
+ additionalListItem = xbmcgui.ListItem()
+ additionalPlayurl = putils.PlayUtils(part).getPlayUrl()
+ self.logMsg("Adding additional part: %s" % partcount, 1)
+
+ # Set listitem and properties for each additional parts
+ pbutils = PlaybackUtils(part)
+ pbutils.setProperties(additionalPlayurl, additionalListItem)
+ pbutils.setArtwork(additionalListItem)
+
+ playlist.add(additionalPlayurl, additionalListItem, index=currentPosition)
+ self.pl.verifyPlaylist()
+ currentPosition += 1
+
+ if dummyPlaylist:
+ # Added a dummy file to the playlist,
+ # because the first item is going to fail automatically.
+ self.logMsg("Processed as a playlist. First item is skipped.", 1)
+ return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
+
+
+ # We just skipped adding properties. Reset flag for next time.
+ elif propertiesPlayback:
+ self.logMsg("Resetting properties playback flag.", 2)
+ window('emby_playbackProps', clear=True)
+
+ #self.pl.verifyPlaylist()
+ ########## SETUP MAIN ITEM ##########
+
+ # For transcoding only, ask for audio/subs pref
+ if window('emby_%s.playmethod' % playurl) == "Transcode":
+ playurl = playutils.audioSubsPref(playurl, listitem)
+ window('emby_%s.playmethod' % playurl, value="Transcode")
+
+ listitem.setPath(playurl)
+ self.setProperties(playurl, listitem)
+
+ ############### PLAYBACK ################
+
+ if homeScreen and seektime and window('emby_customPlaylist') != "true":
+ self.logMsg("Play as a widget item.", 1)
+ self.setListItem(listitem)
+ xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
+
+ elif ((introsPlaylist and window('emby_customPlaylist') == "true") or
+ (homeScreen and not sizePlaylist)):
+ # Playlist was created just now, play it.
+ self.logMsg("Play playlist.", 1)
+ xbmc.Player().play(playlist, startpos=startPos)
+
+ else:
+ self.logMsg("Play as a regular item.", 1)
+ xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
+
+ def setProperties(self, playurl, listitem):
+
+ window = utils.window
+ # Set all properties necessary for plugin path playback
+ itemid = self.item['Id']
+ itemtype = self.item['Type']
+
+ embyitem = "emby_%s" % playurl
+ window('%s.runtime' % embyitem, value=str(self.item.get('RunTimeTicks')))
+ window('%s.type' % embyitem, value=itemtype)
+ window('%s.itemid' % embyitem, value=itemid)
+
+ if itemtype == "Episode":
+ window('%s.refreshid' % embyitem, value=self.item.get('SeriesId'))
+ else:
+ window('%s.refreshid' % embyitem, value=itemid)
+
+ # Append external subtitles to stream
+ playmethod = utils.window('%s.playmethod' % embyitem)
+ # Only for direct stream
+ if playmethod in ("DirectStream"):
+ # Direct play automatically appends external
+ subtitles = self.externalSubs(playurl)
+ listitem.setSubtitles(subtitles)
+
+ self.setArtwork(listitem)
+
+ def externalSubs(self, playurl):
+
+ externalsubs = []
+ mapping = {}
+
+ itemid = self.item['Id']
+ try:
+ mediastreams = self.item['MediaSources'][0]['MediaStreams']
+ except (TypeError, KeyError, IndexError):
+ return
+
+ kodiindex = 0
+ for stream in mediastreams:
+
+ index = stream['Index']
+ # Since Emby returns all possible tracks together, have to pull only external subtitles.
+ # IsTextSubtitleStream if true, is available to download from emby.
+ if (stream['Type'] == "Subtitle" and
+ stream['IsExternal'] and stream['IsTextSubtitleStream']):
+
+ # Direct stream
+ url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
+ % (self.server, itemid, itemid, index))
+
+ # map external subtitles for mapping
+ mapping[kodiindex] = index
+ externalsubs.append(url)
+ kodiindex += 1
+
+ mapping = json.dumps(mapping)
+ utils.window('emby_%s.indexMapping' % playurl, value=mapping)
+
+ return externalsubs
+
+ def setArtwork(self, listItem):
+ # Set up item and item info
+ allartwork = self.artwork.getAllArtwork(self.item, parentInfo=True)
+ # Set artwork for listitem
+ arttypes = {
+
+ 'poster': "Primary",
+ 'tvshow.poster': "Primary",
+ 'clearart': "Art",
+ 'tvshow.clearart': "Art",
+ 'clearlogo': "Logo",
+ 'tvshow.clearlogo': "Logo",
+ 'discart': "Disc",
+ 'fanart_image': "Backdrop",
+ 'landscape': "Thumb"
+ }
+ for arttype in arttypes:
+
+ art = arttypes[arttype]
+ if art == "Backdrop":
+ try: # Backdrop is a list, grab the first backdrop
+ self.setArtProp(listItem, arttype, allartwork[art][0])
+ except: pass
+ else:
+ self.setArtProp(listItem, arttype, allartwork[art])
+
+ def setArtProp(self, listItem, arttype, path):
+
+ if arttype in (
+ 'thumb', 'fanart_image', 'small_poster', 'tiny_poster',
+ 'medium_landscape', 'medium_poster', 'small_fanartimage',
+ 'medium_fanartimage', 'fanart_noindicators'):
+
+ listItem.setProperty(arttype, path)
+ else:
+ listItem.setArt({arttype: path})
+
+ def setListItem(self, listItem):
+
+ people = self.API.getPeople()
+ studios = self.API.getStudios()
+
+ metadata = {
+
+ 'title': self.item.get('Name', "Missing name"),
+ 'year': self.item.get('ProductionYear'),
+ 'plot': self.API.getOverview(),
+ 'director': people.get('Director'),
+ 'writer': people.get('Writer'),
+ 'mpaa': self.API.getMpaa(),
+ 'genre': " / ".join(self.item['Genres']),
+ 'studio': " / ".join(studios),
+ 'aired': self.API.getPremiereDate(),
+ 'rating': self.item.get('CommunityRating'),
+ 'votes': self.item.get('VoteCount')
+ }
+
+ if "Episode" in self.item['Type']:
+ # Only for tv shows
+ thumbId = self.item.get('SeriesId')
+ season = self.item.get('ParentIndexNumber', -1)
+ episode = self.item.get('IndexNumber', -1)
+ show = self.item.get('SeriesName', "")
+
+ metadata['TVShowTitle'] = show
+ metadata['season'] = season
+ metadata['episode'] = episode
+
+ listItem.setProperty('IsPlayable', 'true')
+ listItem.setProperty('IsFolder', 'false')
+ listItem.setLabel(metadata['title'])
listItem.setInfo('video', infoLabels=metadata)
\ No newline at end of file
diff --git a/resources/lib/player.py b/resources/lib/player.py
index 2e648180..7f323460 100644
--- a/resources/lib/player.py
+++ b/resources/lib/player.py
@@ -1,520 +1,511 @@
-# -*- coding: utf-8 -*-
-
-#################################################################################################
-
-import json
-
-import xbmc
-import xbmcgui
-
-import utils
-import clientinfo
-import downloadutils
-import kodidb_functions as kodidb
-import websocket_client as wsc
-
-#################################################################################################
-
-
-class Player(xbmc.Player):
-
- # Borg - multiple instances, shared state
- _shared_state = {}
-
- played_info = {}
- playStats = {}
- currentFile = None
-
-
- def __init__(self):
-
- self.__dict__ = self._shared_state
-
- self.clientInfo = clientinfo.ClientInfo()
- self.addonName = self.clientInfo.getAddonName()
- self.doUtils = downloadutils.DownloadUtils().downloadUrl
- self.ws = wsc.WebSocket_Client()
- self.xbmcplayer = xbmc.Player()
-
- self.logMsg("Starting playback monitor.", 2)
-
- def logMsg(self, msg, lvl=1):
-
- self.className = self.__class__.__name__
- utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
-
-
- def GetPlayStats(self):
- return self.playStats
-
- def onPlayBackStarted(self):
-
- log = self.logMsg
- window = utils.window
- # Will be called when xbmc starts playing a file
- xbmcplayer = self.xbmcplayer
- self.stopAll()
-
- # Get current file
- try:
- currentFile = xbmcplayer.getPlayingFile()
- xbmc.sleep(300)
- except:
- currentFile = ""
- count = 0
- while not currentFile:
- xbmc.sleep(100)
- try:
- currentFile = xbmcplayer.getPlayingFile()
- except: pass
-
- if count == 5: # try 5 times
- log("Cancelling playback report...", 1)
- break
- else: count += 1
-
-
- if currentFile:
-
- self.currentFile = currentFile
-
- # We may need to wait for info to be set in kodi monitor
- itemId = window("emby_%s.itemid" % currentFile)
- tryCount = 0
- while not itemId:
-
- xbmc.sleep(200)
- itemId = window("emby_%s.itemid" % currentFile)
- if tryCount == 20: # try 20 times or about 10 seconds
- log("Could not find itemId, cancelling playback report...", 1)
- break
- else: tryCount += 1
-
- else:
- log("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId), 0)
-
- # Only proceed if an itemId was found.
- embyitem = "emby_%s" % currentFile
- runtime = window("%s.runtime" % embyitem)
- refresh_id = window("%s.refreshid" % embyitem)
- playMethod = window("%s.playmethod" % embyitem)
- itemType = window("%s.type" % embyitem)
- window('emby_skipWatched%s' % itemId, value="true")
-
- customseek = window('emby_customPlaylist.seektime')
- if window('emby_customPlaylist') == "true" and customseek:
- # Start at, when using custom playlist (play to Kodi from webclient)
- log("Seeking to: %s" % customseek, 1)
- xbmcplayer.seekTime(int(customseek)/10000000.0)
- window('emby_customPlaylist.seektime', clear=True)
-
- seekTime = xbmcplayer.getTime()
-
- # Get playback volume
- volume_query = {
-
- "jsonrpc": "2.0",
- "id": 1,
- "method": "Application.GetProperties",
- "params": {
-
- "properties": ["volume", "muted"]
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(volume_query))
- result = json.loads(result)
- result = result.get('result')
-
- volume = result.get('volume')
- muted = result.get('muted')
-
- # Postdata structure to send to Emby server
- url = "{server}/emby/Sessions/Playing"
- postdata = {
-
- 'QueueableMediaTypes': "Video",
- 'CanSeek': True,
- 'ItemId': itemId,
- 'MediaSourceId': itemId,
- 'PlayMethod': playMethod,
- 'VolumeLevel': volume,
- 'PositionTicks': int(seekTime * 10000000),
- 'IsMuted': muted
- }
-
- # Get the current audio track and subtitles
- if playMethod == "Transcode":
- # property set in PlayUtils.py
- postdata['AudioStreamIndex'] = window("%sAudioStreamIndex" % currentFile)
- postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex" % currentFile)
- else:
- # Get the current kodi audio and subtitles and convert to Emby equivalent
- tracks_query = {
-
- "jsonrpc": "2.0",
- "id": 1,
- "method": "Player.GetProperties",
- "params": {
-
- "playerid": 1,
- "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(tracks_query))
- result = json.loads(result)
- result = result.get('result')
-
- try: # Audio tracks
- indexAudio = result['currentaudiostream']['index']
- except (KeyError, TypeError):
- indexAudio = 0
-
- try: # Subtitles tracks
- indexSubs = result['currentsubtitle']['index']
- except (KeyError, TypeError):
- indexSubs = 0
-
- try: # If subtitles are enabled
- subsEnabled = result['subtitleenabled']
- except (KeyError, TypeError):
- subsEnabled = ""
-
- # Postdata for the audio
- postdata['AudioStreamIndex'] = indexAudio + 1
-
- # Postdata for the subtitles
- if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0:
-
- # Number of audiotracks to help get Emby Index
- audioTracks = len(xbmc.Player().getAvailableAudioStreams())
- mapping = window("%s.indexMapping" % embyitem)
-
- if mapping: # Set in playbackutils.py
-
- log("Mapping for external subtitles index: %s" % mapping, 2)
- externalIndex = json.loads(mapping)
-
- if externalIndex.get(str(indexSubs)):
- # If the current subtitle is in the mapping
- postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)]
- else:
- # Internal subtitle currently selected
- subindex = indexSubs - len(externalIndex) + audioTracks + 1
- postdata['SubtitleStreamIndex'] = subindex
-
- else: # Direct paths enabled scenario or no external subtitles set
- postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1
- else:
- postdata['SubtitleStreamIndex'] = ""
-
-
- # Post playback to server
- log("Sending POST play started: %s." % postdata, 2)
- self.doUtils(url, postBody=postdata, type="POST")
-
- # Ensure we do have a runtime
- try:
- runtime = int(runtime)
- except ValueError:
- runtime = xbmcplayer.getTotalTime()
- log("Runtime is missing, Kodi runtime: %s" % runtime, 1)
-
- # Save data map for updates and position calls
- data = {
-
- 'runtime': runtime,
- 'item_id': itemId,
- 'refresh_id': refresh_id,
- 'currentfile': currentFile,
- 'AudioStreamIndex': postdata['AudioStreamIndex'],
- 'SubtitleStreamIndex': postdata['SubtitleStreamIndex'],
- 'playmethod': playMethod,
- 'Type': itemType,
- 'currentPosition': int(seekTime)
- }
-
- self.played_info[currentFile] = data
- log("ADDING_FILE: %s" % self.played_info, 1)
-
- # log some playback stats
- '''if(itemType != None):
- if(self.playStats.get(itemType) != None):
- count = self.playStats.get(itemType) + 1
- self.playStats[itemType] = count
- else:
- self.playStats[itemType] = 1
-
- if(playMethod != None):
- if(self.playStats.get(playMethod) != None):
- count = self.playStats.get(playMethod) + 1
- self.playStats[playMethod] = count
- else:
- self.playStats[playMethod] = 1'''
-
- def reportPlayback(self):
-
- log = self.logMsg
-
- log("reportPlayback Called", 2)
- xbmcplayer = self.xbmcplayer
-
- # Get current file
- currentFile = self.currentFile
- data = self.played_info.get(currentFile)
-
- # only report playback if emby has initiated the playback (item_id has value)
- if data:
- # Get playback information
- itemId = data['item_id']
- audioindex = data['AudioStreamIndex']
- subtitleindex = data['SubtitleStreamIndex']
- playTime = data['currentPosition']
- playMethod = data['playmethod']
- paused = data.get('paused', False)
-
-
- # Get playback volume
- volume_query = {
-
- "jsonrpc": "2.0",
- "id": 1,
- "method": "Application.GetProperties",
- "params": {
-
- "properties": ["volume", "muted"]
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(volume_query))
- result = json.loads(result)
- result = result.get('result')
-
- volume = result.get('volume')
- muted = result.get('muted')
-
- # Postdata for the websocketclient report
- postdata = {
-
- 'QueueableMediaTypes': "Video",
- 'CanSeek': True,
- 'ItemId': itemId,
- 'MediaSourceId': itemId,
- 'PlayMethod': playMethod,
- 'PositionTicks': int(playTime * 10000000),
- 'IsPaused': paused,
- 'VolumeLevel': volume,
- 'IsMuted': muted
- }
-
- if playMethod == "Transcode":
- # Track can't be changed, keep reporting the same index
- postdata['AudioStreamIndex'] = audioindex
- postdata['AudioStreamIndex'] = subtitleindex
-
- else:
- # Get current audio and subtitles track
- tracks_query = {
-
- "jsonrpc": "2.0",
- "id": 1,
- "method": "Player.GetProperties",
- "params": {
-
- "playerid": 1,
- "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(tracks_query))
- result = json.loads(result)
- result = result.get('result')
-
- try: # Audio tracks
- indexAudio = result['currentaudiostream']['index']
- except (KeyError, TypeError):
- indexAudio = 0
-
- try: # Subtitles tracks
- indexSubs = result['currentsubtitle']['index']
- except (KeyError, TypeError):
- indexSubs = 0
-
- try: # If subtitles are enabled
- subsEnabled = result['subtitleenabled']
- except (KeyError, TypeError):
- subsEnabled = ""
-
- # Postdata for the audio
- data['AudioStreamIndex'], postdata['AudioStreamIndex'] = [indexAudio + 1] * 2
-
- # Postdata for the subtitles
- if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0:
-
- # Number of audiotracks to help get Emby Index
- audioTracks = len(xbmc.Player().getAvailableAudioStreams())
- mapping = utils.window("emby_%s.indexMapping" % currentFile)
-
- if mapping: # Set in PlaybackUtils.py
-
- log("Mapping for external subtitles index: %s" % mapping, 2)
- externalIndex = json.loads(mapping)
-
- if externalIndex.get(str(indexSubs)):
- # If the current subtitle is in the mapping
- subindex = [externalIndex[str(indexSubs)]] * 2
- data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
- else:
- # Internal subtitle currently selected
- subindex = [indexSubs - len(externalIndex) + audioTracks + 1] * 2
- data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
-
- else: # Direct paths enabled scenario or no external subtitles set
- subindex = [indexSubs + audioTracks + 1] * 2
- data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
- else:
- data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [""] * 2
-
- # Report progress via websocketclient
- postdata = json.dumps(postdata)
- log("Report: %s" % postdata, 2)
- self.ws.sendProgressUpdate(postdata)
-
- def onPlayBackPaused(self):
-
- currentFile = self.currentFile
- self.logMsg("PLAYBACK_PAUSED: %s" % currentFile, 2)
-
- if self.played_info.get(currentFile):
- self.played_info[currentFile]['paused'] = True
-
- self.reportPlayback()
-
- def onPlayBackResumed(self):
-
- currentFile = self.currentFile
- self.logMsg("PLAYBACK_RESUMED: %s" % currentFile, 2)
-
- if self.played_info.get(currentFile):
- self.played_info[currentFile]['paused'] = False
-
- self.reportPlayback()
-
- def onPlayBackSeek(self, time, seekOffset):
- # Make position when seeking a bit more accurate
- currentFile = self.currentFile
- self.logMsg("PLAYBACK_SEEK: %s" % currentFile, 2)
-
- if self.played_info.get(currentFile):
- position = self.xbmcplayer.getTime()
- self.played_info[currentFile]['currentPosition'] = position
-
- self.reportPlayback()
-
- def onPlayBackStopped(self):
-
- log = self.logMsg
- window = utils.window
- # Will be called when user stops xbmc playing a file
- log("ONPLAYBACK_STOPPED", 2)
- window('emby_customPlaylist', clear=True)
- window('emby_customPlaylist.seektime', clear=True)
- window('emby_playbackProps', clear=True)
- log("Clear playlist properties.", 1)
- self.stopAll()
-
- def onPlayBackEnded(self):
- # Will be called when xbmc stops playing a file
- self.logMsg("ONPLAYBACK_ENDED", 2)
- utils.window('emby_customPlaylist.seektime', clear=True)
- self.stopAll()
-
- def stopAll(self):
-
- log = self.logMsg
- lang = utils.language
- settings = utils.settings
-
- doUtils = self.doUtils
-
- if not self.played_info:
- return
-
- log("Played_information: %s" % self.played_info, 1)
- # Process each items
- for item in self.played_info:
-
- data = self.played_info.get(item)
- if data:
-
- log("Item path: %s" % item, 2)
- log("Item data: %s" % data, 2)
-
- runtime = data['runtime']
- currentPosition = data['currentPosition']
- itemid = data['item_id']
- refresh_id = data['refresh_id']
- currentFile = data['currentfile']
- type = data['Type']
- playMethod = data['playmethod']
-
- # Prevent manually mark as watched in Kodi monitor
- utils.window('emby_skipWatched%s' % itemid, value="true")
-
- if currentPosition and runtime:
- try:
- percentComplete = (currentPosition * 10000000) / int(runtime)
- except ZeroDivisionError:
- # Runtime is 0.
- percentComplete = 0
-
- markPlayedAt = float(settings('markPlayed')) / 100
- log("Percent complete: %s Mark played at: %s"
- % (percentComplete, markPlayedAt), 1)
-
- # Send the delete action to the server.
- offerDelete = False
-
- if type == "Episode" and settings('deleteTV') == "true":
- offerDelete = True
- elif type == "Movie" and settings('deleteMovies') == "true":
- offerDelete = True
-
- if settings('offerDelete') != "true":
- # Delete could be disabled, even if the subsetting is enabled.
- offerDelete = False
-
- if percentComplete >= markPlayedAt and offerDelete:
- resp = xbmcgui.Dialog().yesno(lang(30091), lang(33015), autoclose=120000)
- if not resp:
- log("User skipped deletion.", 1)
- continue
-
- url = "{server}/emby/Items/%s?format=json" % itemid
- log("Deleting request: %s" % itemid, 1)
- doUtils(url, type="DELETE")
-
- self.stopPlayback(data)
-
- # Stop transcoding
- if playMethod == "Transcode":
- log("Transcoding for %s terminated." % itemid, 1)
- deviceId = self.clientInfo.getDeviceId()
- url = "{server}/emby/Videos/ActiveEncodings?DeviceId=%s" % deviceId
- doUtils(url, type="DELETE")
-
- self.played_info.clear()
-
- def stopPlayback(self, data):
-
- self.logMsg("stopPlayback called", 2)
-
- itemId = data['item_id']
- currentPosition = data['currentPosition']
- positionTicks = int(currentPosition * 10000000)
-
- url = "{server}/emby/Sessions/Playing/Stopped"
- postdata = {
-
- 'ItemId': itemId,
- 'MediaSourceId': itemId,
- 'PositionTicks': positionTicks
- }
- self.doUtils(url, postBody=postdata, type="POST")
\ No newline at end of file
+# -*- coding: utf-8 -*-
+
+#################################################################################################
+
+import json
+
+import xbmc
+import xbmcgui
+
+import utils
+import clientinfo
+import downloadutils
+import kodidb_functions as kodidb
+import websocket_client as wsc
+
+#################################################################################################
+
+
+class Player(xbmc.Player):
+
+ # Borg - multiple instances, shared state
+ _shared_state = {}
+
+ played_info = {}
+ playStats = {}
+ currentFile = None
+
+
+ def __init__(self):
+
+ self.__dict__ = self._shared_state
+
+ self.clientInfo = clientinfo.ClientInfo()
+ self.addonName = self.clientInfo.getAddonName()
+ self.doUtils = downloadutils.DownloadUtils().downloadUrl
+ self.ws = wsc.WebSocket_Client()
+ self.xbmcplayer = xbmc.Player()
+
+ self.logMsg("Starting playback monitor.", 2)
+
+ def logMsg(self, msg, lvl=1):
+
+ self.className = self.__class__.__name__
+ utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
+
+
+ def GetPlayStats(self):
+ return self.playStats
+
+ def onPlayBackStarted(self):
+
+ window = utils.window
+ # Will be called when xbmc starts playing a file
+ self.stopAll()
+
+ # Get current file
+ try:
+ currentFile = self.xbmcplayer.getPlayingFile()
+ xbmc.sleep(300)
+ except:
+ currentFile = ""
+ count = 0
+ while not currentFile:
+ xbmc.sleep(100)
+ try:
+ currentFile = self.xbmcplayer.getPlayingFile()
+ except: pass
+
+ if count == 5: # try 5 times
+ self.logMsg("Cancelling playback report...", 1)
+ break
+ else: count += 1
+
+
+ if currentFile:
+
+ self.currentFile = currentFile
+
+ # We may need to wait for info to be set in kodi monitor
+ itemId = window("emby_%s.itemid" % currentFile)
+ tryCount = 0
+ while not itemId:
+
+ xbmc.sleep(200)
+ itemId = window("emby_%s.itemid" % currentFile)
+ if tryCount == 20: # try 20 times or about 10 seconds
+ self.logMsg("Could not find itemId, cancelling playback report...", 1)
+ break
+ else: tryCount += 1
+
+ else:
+ self.logMsg("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId), 0)
+
+ # Only proceed if an itemId was found.
+ embyitem = "emby_%s" % currentFile
+ runtime = window("%s.runtime" % embyitem)
+ refresh_id = window("%s.refreshid" % embyitem)
+ playMethod = window("%s.playmethod" % embyitem)
+ itemType = window("%s.type" % embyitem)
+ window('emby_skipWatched%s' % itemId, value="true")
+
+ customseek = window('emby_customPlaylist.seektime')
+ if window('emby_customPlaylist') == "true" and customseek:
+ # Start at, when using custom playlist (play to Kodi from webclient)
+ self.logMsg("Seeking to: %s" % customseek, 1)
+ self.xbmcplayer.seekTime(int(customseek)/10000000.0)
+ window('emby_customPlaylist.seektime', clear=True)
+
+ seekTime = self.xbmcplayer.getTime()
+
+ # Get playback volume
+ volume_query = {
+
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "Application.GetProperties",
+ "params": {
+
+ "properties": ["volume", "muted"]
+ }
+ }
+ result = xbmc.executeJSONRPC(json.dumps(volume_query))
+ result = json.loads(result)
+ result = result.get('result')
+
+ volume = result.get('volume')
+ muted = result.get('muted')
+
+ # Postdata structure to send to Emby server
+ url = "{server}/emby/Sessions/Playing"
+ postdata = {
+
+ 'QueueableMediaTypes': "Video",
+ 'CanSeek': True,
+ 'ItemId': itemId,
+ 'MediaSourceId': itemId,
+ 'PlayMethod': playMethod,
+ 'VolumeLevel': volume,
+ 'PositionTicks': int(seekTime * 10000000),
+ 'IsMuted': muted
+ }
+
+ # Get the current audio track and subtitles
+ if playMethod == "Transcode":
+ # property set in PlayUtils.py
+ postdata['AudioStreamIndex'] = window("%sAudioStreamIndex" % currentFile)
+ postdata['SubtitleStreamIndex'] = window("%sSubtitleStreamIndex" % currentFile)
+ else:
+ # Get the current kodi audio and subtitles and convert to Emby equivalent
+ tracks_query = {
+
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "Player.GetProperties",
+ "params": {
+
+ "playerid": 1,
+ "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]
+ }
+ }
+ result = xbmc.executeJSONRPC(json.dumps(tracks_query))
+ result = json.loads(result)
+ result = result.get('result')
+
+ try: # Audio tracks
+ indexAudio = result['currentaudiostream']['index']
+ except (KeyError, TypeError):
+ indexAudio = 0
+
+ try: # Subtitles tracks
+ indexSubs = result['currentsubtitle']['index']
+ except (KeyError, TypeError):
+ indexSubs = 0
+
+ try: # If subtitles are enabled
+ subsEnabled = result['subtitleenabled']
+ except (KeyError, TypeError):
+ subsEnabled = ""
+
+ # Postdata for the audio
+ postdata['AudioStreamIndex'] = indexAudio + 1
+
+ # Postdata for the subtitles
+ if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0:
+
+ # Number of audiotracks to help get Emby Index
+ audioTracks = len(xbmc.Player().getAvailableAudioStreams())
+ mapping = window("%s.indexMapping" % embyitem)
+
+ if mapping: # Set in playbackutils.py
+
+ self.logMsg("Mapping for external subtitles index: %s" % mapping, 2)
+ externalIndex = json.loads(mapping)
+
+ if externalIndex.get(str(indexSubs)):
+ # If the current subtitle is in the mapping
+ postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)]
+ else:
+ # Internal subtitle currently selected
+ subindex = indexSubs - len(externalIndex) + audioTracks + 1
+ postdata['SubtitleStreamIndex'] = subindex
+
+ else: # Direct paths enabled scenario or no external subtitles set
+ postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1
+ else:
+ postdata['SubtitleStreamIndex'] = ""
+
+
+ # Post playback to server
+ self.logMsg("Sending POST play started: %s." % postdata, 2)
+ self.doUtils(url, postBody=postdata, action_type="POST")
+
+ # Ensure we do have a runtime
+ try:
+ runtime = int(runtime)
+ except ValueError:
+ runtime = self.xbmcplayer.getTotalTime()
+ self.logMsg("Runtime is missing, Kodi runtime: %s" % runtime, 1)
+
+ # Save data map for updates and position calls
+ data = {
+
+ 'runtime': runtime,
+ 'item_id': itemId,
+ 'refresh_id': refresh_id,
+ 'currentfile': currentFile,
+ 'AudioStreamIndex': postdata['AudioStreamIndex'],
+ 'SubtitleStreamIndex': postdata['SubtitleStreamIndex'],
+ 'playmethod': playMethod,
+ 'Type': itemType,
+ 'currentPosition': int(seekTime)
+ }
+
+ self.played_info[currentFile] = data
+ self.logMsg("ADDING_FILE: %s" % self.played_info, 1)
+
+ # log some playback stats
+ '''if(itemType != None):
+ if(self.playStats.get(itemType) != None):
+ count = self.playStats.get(itemType) + 1
+ self.playStats[itemType] = count
+ else:
+ self.playStats[itemType] = 1
+
+ if(playMethod != None):
+ if(self.playStats.get(playMethod) != None):
+ count = self.playStats.get(playMethod) + 1
+ self.playStats[playMethod] = count
+ else:
+ self.playStats[playMethod] = 1'''
+
+ def reportPlayback(self):
+
+ self.logMsg("reportPlayback Called", 2)
+
+ # Get current file
+ currentFile = self.currentFile
+ data = self.played_info.get(currentFile)
+
+ # only report playback if emby has initiated the playback (item_id has value)
+ if data:
+ # Get playback information
+ itemId = data['item_id']
+ audioindex = data['AudioStreamIndex']
+ subtitleindex = data['SubtitleStreamIndex']
+ playTime = data['currentPosition']
+ playMethod = data['playmethod']
+ paused = data.get('paused', False)
+
+
+ # Get playback volume
+ volume_query = {
+
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "Application.GetProperties",
+ "params": {
+
+ "properties": ["volume", "muted"]
+ }
+ }
+ result = xbmc.executeJSONRPC(json.dumps(volume_query))
+ result = json.loads(result)
+ result = result.get('result')
+
+ volume = result.get('volume')
+ muted = result.get('muted')
+
+ # Postdata for the websocketclient report
+ postdata = {
+
+ 'QueueableMediaTypes': "Video",
+ 'CanSeek': True,
+ 'ItemId': itemId,
+ 'MediaSourceId': itemId,
+ 'PlayMethod': playMethod,
+ 'PositionTicks': int(playTime * 10000000),
+ 'IsPaused': paused,
+ 'VolumeLevel': volume,
+ 'IsMuted': muted
+ }
+
+ if playMethod == "Transcode":
+ # Track can't be changed, keep reporting the same index
+ postdata['AudioStreamIndex'] = audioindex
+ postdata['AudioStreamIndex'] = subtitleindex
+
+ else:
+ # Get current audio and subtitles track
+ tracks_query = {
+
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "Player.GetProperties",
+ "params": {
+
+ "playerid": 1,
+ "properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]
+ }
+ }
+ result = xbmc.executeJSONRPC(json.dumps(tracks_query))
+ result = json.loads(result)
+ result = result.get('result')
+
+ try: # Audio tracks
+ indexAudio = result['currentaudiostream']['index']
+ except (KeyError, TypeError):
+ indexAudio = 0
+
+ try: # Subtitles tracks
+ indexSubs = result['currentsubtitle']['index']
+ except (KeyError, TypeError):
+ indexSubs = 0
+
+ try: # If subtitles are enabled
+ subsEnabled = result['subtitleenabled']
+ except (KeyError, TypeError):
+ subsEnabled = ""
+
+ # Postdata for the audio
+ data['AudioStreamIndex'], postdata['AudioStreamIndex'] = [indexAudio + 1] * 2
+
+ # Postdata for the subtitles
+ if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0:
+
+ # Number of audiotracks to help get Emby Index
+ audioTracks = len(xbmc.Player().getAvailableAudioStreams())
+ mapping = utils.window("emby_%s.indexMapping" % currentFile)
+
+ if mapping: # Set in PlaybackUtils.py
+
+ self.logMsg("Mapping for external subtitles index: %s" % mapping, 2)
+ externalIndex = json.loads(mapping)
+
+ if externalIndex.get(str(indexSubs)):
+ # If the current subtitle is in the mapping
+ subindex = [externalIndex[str(indexSubs)]] * 2
+ data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
+ else:
+ # Internal subtitle currently selected
+ subindex = [indexSubs - len(externalIndex) + audioTracks + 1] * 2
+ data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
+
+ else: # Direct paths enabled scenario or no external subtitles set
+ subindex = [indexSubs + audioTracks + 1] * 2
+ data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
+ else:
+ data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [""] * 2
+
+ # Report progress via websocketclient
+ postdata = json.dumps(postdata)
+ self.logMsg("Report: %s" % postdata, 2)
+ self.ws.sendProgressUpdate(postdata)
+
+ def onPlayBackPaused(self):
+
+ currentFile = self.currentFile
+ self.logMsg("PLAYBACK_PAUSED: %s" % currentFile, 2)
+
+ if self.played_info.get(currentFile):
+ self.played_info[currentFile]['paused'] = True
+
+ self.reportPlayback()
+
+ def onPlayBackResumed(self):
+
+ currentFile = self.currentFile
+ self.logMsg("PLAYBACK_RESUMED: %s" % currentFile, 2)
+
+ if self.played_info.get(currentFile):
+ self.played_info[currentFile]['paused'] = False
+
+ self.reportPlayback()
+
+ def onPlayBackSeek(self, time, seekOffset):
+ # Make position when seeking a bit more accurate
+ currentFile = self.currentFile
+ self.logMsg("PLAYBACK_SEEK: %s" % currentFile, 2)
+
+ if self.played_info.get(currentFile):
+ position = self.xbmcplayer.getTime()
+ self.played_info[currentFile]['currentPosition'] = position
+
+ self.reportPlayback()
+
+ def onPlayBackStopped(self):
+
+ window = utils.window
+ # Will be called when user stops xbmc playing a file
+ self.logMsg("ONPLAYBACK_STOPPED", 2)
+ window('emby_customPlaylist', clear=True)
+ window('emby_customPlaylist.seektime', clear=True)
+ window('emby_playbackProps', clear=True)
+ self.logMsg("Clear playlist properties.", 1)
+ self.stopAll()
+
+ def onPlayBackEnded(self):
+ # Will be called when xbmc stops playing a file
+ self.logMsg("ONPLAYBACK_ENDED", 2)
+ utils.window('emby_customPlaylist.seektime', clear=True)
+ self.stopAll()
+
+ def stopAll(self):
+
+ lang = utils.language
+ settings = utils.settings
+
+ if not self.played_info:
+ return
+
+ self.logMsg("Played_information: %s" % self.played_info, 1)
+ # Process each items
+ for item in self.played_info:
+
+ data = self.played_info.get(item)
+ if data:
+
+ self.logMsg("Item path: %s" % item, 2)
+ self.logMsg("Item data: %s" % data, 2)
+
+ runtime = data['runtime']
+ currentPosition = data['currentPosition']
+ itemid = data['item_id']
+ refresh_id = data['refresh_id']
+ currentFile = data['currentfile']
+ media_type = data['Type']
+ playMethod = data['playmethod']
+
+ # Prevent manually mark as watched in Kodi monitor
+ utils.window('emby_skipWatched%s' % itemid, value="true")
+
+ if currentPosition and runtime:
+ try:
+ percentComplete = (currentPosition * 10000000) / int(runtime)
+ except ZeroDivisionError:
+ # Runtime is 0.
+ percentComplete = 0
+
+ markPlayedAt = float(settings('markPlayed')) / 100
+ self.logMsg("Percent complete: %s Mark played at: %s"
+ % (percentComplete, markPlayedAt), 1)
+
+ # Send the delete action to the server.
+ offerDelete = False
+
+ if media_type == "Episode" and settings('deleteTV') == "true":
+ offerDelete = True
+ elif media_type == "Movie" and settings('deleteMovies') == "true":
+ offerDelete = True
+
+ if settings('offerDelete') != "true":
+ # Delete could be disabled, even if the subsetting is enabled.
+ offerDelete = False
+
+ if percentComplete >= markPlayedAt and offerDelete:
+ resp = xbmcgui.Dialog().yesno(lang(30091), lang(33015), autoclose=120000)
+ if not resp:
+ self.logMsg("User skipped deletion.", 1)
+ continue
+
+ url = "{server}/emby/Items/%s?format=json" % itemid
+ self.logMsg("Deleting request: %s" % itemid, 1)
+ self.doUtils(url, action_type="DELETE")
+
+ self.stopPlayback(data)
+
+ # Stop transcoding
+ if playMethod == "Transcode":
+ self.logMsg("Transcoding for %s terminated." % itemid, 1)
+ deviceId = self.clientInfo.getDeviceId()
+ url = "{server}/emby/Videos/ActiveEncodings?DeviceId=%s" % deviceId
+ self.doUtils(url, action_type="DELETE")
+
+ self.played_info.clear()
+
+ def stopPlayback(self, data):
+
+ self.logMsg("stopPlayback called", 2)
+
+ itemId = data['item_id']
+ currentPosition = data['currentPosition']
+ positionTicks = int(currentPosition * 10000000)
+
+ url = "{server}/emby/Sessions/Playing/Stopped"
+ postdata = {
+
+ 'ItemId': itemId,
+ 'MediaSourceId': itemId,
+ 'PositionTicks': positionTicks
+ }
+ self.doUtils(url, postBody=postdata, action_type="POST")
\ No newline at end of file
diff --git a/resources/lib/playlist.py b/resources/lib/playlist.py
index 383d34a8..bcd34a46 100644
--- a/resources/lib/playlist.py
+++ b/resources/lib/playlist.py
@@ -1,204 +1,196 @@
-# -*- coding: utf-8 -*-
-
-#################################################################################################
-
-import json
-
-import xbmc
-import xbmcgui
-import xbmcplugin
-
-import clientinfo
-import playutils
-import playbackutils
-import embydb_functions as embydb
-import read_embyserver as embyserver
-import utils
-
-#################################################################################################
-
-
-class Playlist():
-
-
- def __init__(self):
-
- self.clientInfo = clientinfo.ClientInfo()
- self.addonName = self.clientInfo.getAddonName()
-
- self.userid = utils.window('emby_currUser')
- self.server = utils.window('emby_server%s' % self.userid)
-
- self.emby = embyserver.Read_EmbyServer()
-
- def logMsg(self, msg, lvl=1):
-
- self.className = self.__class__.__name__
- utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
-
-
- def playAll(self, itemids, startat):
-
- log = self.logMsg
- window = utils.window
-
- embyconn = utils.kodiSQL('emby')
- embycursor = embyconn.cursor()
- emby_db = embydb.Embydb_Functions(embycursor)
-
- player = xbmc.Player()
- playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
- playlist.clear()
-
- log("---*** PLAY ALL ***---", 1)
- log("Items: %s and start at: %s" % (itemids, startat), 1)
-
- started = False
- window('emby_customplaylist', value="true")
-
- if startat != 0:
- # Seek to the starting position
- window('emby_customplaylist.seektime', str(startat))
-
- for itemid in itemids:
- embydb_item = emby_db.getItem_byId(itemid)
- try:
- dbid = embydb_item[0]
- mediatype = embydb_item[4]
- except TypeError:
- # Item is not found in our database, add item manually
- log("Item was not found in the database, manually adding item.", 1)
- item = self.emby.getItem(itemid)
- self.addtoPlaylist_xbmc(playlist, item)
- else:
- # Add to playlist
- self.addtoPlaylist(dbid, mediatype)
-
- log("Adding %s to playlist." % itemid, 1)
-
- if not started:
- started = True
- player.play(playlist)
-
- self.verifyPlaylist()
- embycursor.close()
-
- def modifyPlaylist(self, itemids):
-
- log = self.logMsg
-
- embyconn = utils.kodiSQL('emby')
- embycursor = embyconn.cursor()
- emby_db = embydb.Embydb_Functions(embycursor)
-
- log("---*** ADD TO PLAYLIST ***---", 1)
- log("Items: %s" % itemids, 1)
-
- player = xbmc.Player()
- playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
-
- for itemid in itemids:
- embydb_item = emby_db.getItem_byId(itemid)
- try:
- dbid = embydb_item[0]
- mediatype = embydb_item[4]
- except TypeError:
- # Item is not found in our database, add item manually
- item = self.emby.getItem(itemid)
- self.addtoPlaylist_xbmc(playlist, item)
- else:
- # Add to playlist
- self.addtoPlaylist(dbid, mediatype)
-
- log("Adding %s to playlist." % itemid, 1)
-
- self.verifyPlaylist()
- embycursor.close()
- return playlist
-
- def addtoPlaylist(self, dbid=None, mediatype=None, url=None):
-
- pl = {
-
- 'jsonrpc': "2.0",
- 'id': 1,
- 'method': "Playlist.Add",
- 'params': {
-
- 'playlistid': 1
- }
- }
- if dbid is not None:
- pl['params']['item'] = {'%sid' % mediatype: int(dbid)}
- else:
- pl['params']['item'] = {'file': url}
-
- result = xbmc.executeJSONRPC(json.dumps(pl))
- self.logMsg(result, 2)
-
- def addtoPlaylist_xbmc(self, playlist, item):
-
- itemid = item['Id']
- playurl = playutils.PlayUtils(item).getPlayUrl()
- if not playurl:
- # Playurl failed
- self.logMsg("Failed to retrieve playurl.", 1)
- return
-
- self.logMsg("Playurl: %s" % playurl)
- listitem = xbmcgui.ListItem()
- playbackutils.PlaybackUtils(item).setProperties(playurl, listitem)
-
- playlist.add(playurl, listitem)
-
- def insertintoPlaylist(self, position, dbid=None, mediatype=None, url=None):
-
- pl = {
-
- 'jsonrpc': "2.0",
- 'id': 1,
- 'method': "Playlist.Insert",
- 'params': {
-
- 'playlistid': 1,
- 'position': position
- }
- }
- if dbid is not None:
- pl['params']['item'] = {'%sid' % mediatype: int(dbid)}
- else:
- pl['params']['item'] = {'file': url}
-
- result = xbmc.executeJSONRPC(json.dumps(pl))
- self.logMsg(result, 2)
-
- def verifyPlaylist(self):
-
- pl = {
-
- 'jsonrpc': "2.0",
- 'id': 1,
- 'method': "Playlist.GetItems",
- 'params': {
-
- 'playlistid': 1
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(pl))
- self.logMsg(result, 2)
-
- def removefromPlaylist(self, position):
-
- pl = {
-
- 'jsonrpc': "2.0",
- 'id': 1,
- 'method': "Playlist.Remove",
- 'params': {
-
- 'playlistid': 1,
- 'position': position
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(pl))
- self.logMsg(result, 2)
\ No newline at end of file
+# -*- coding: utf-8 -*-
+
+#################################################################################################
+
+import json
+
+import xbmc
+import xbmcgui
+import xbmcplugin
+
+import clientinfo
+import playutils
+import playbackutils
+import embydb_functions as embydb
+import read_embyserver as embyserver
+import utils
+
+#################################################################################################
+
+
+class Playlist():
+
+
+ def __init__(self):
+
+ self.clientInfo = clientinfo.ClientInfo()
+ self.addonName = self.clientInfo.getAddonName()
+
+ self.userid = utils.window('emby_currUser')
+ self.server = utils.window('emby_server%s' % self.userid)
+
+ self.emby = embyserver.Read_EmbyServer()
+
+ def logMsg(self, msg, lvl=1):
+
+ self.className = self.__class__.__name__
+ utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
+
+
+ def playAll(self, itemids, startat):
+
+ window = utils.window
+
+ embyconn = utils.kodiSQL('emby')
+ embycursor = embyconn.cursor()
+ emby_db = embydb.Embydb_Functions(embycursor)
+
+ player = xbmc.Player()
+ playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
+ playlist.clear()
+
+ self.logMsg("---*** PLAY ALL ***---", 1)
+ self.logMsg("Items: %s and start at: %s" % (itemids, startat), 1)
+
+ started = False
+ window('emby_customplaylist', value="true")
+
+ if startat != 0:
+ # Seek to the starting position
+ window('emby_customplaylist.seektime', str(startat))
+
+ for itemid in itemids:
+ embydb_item = emby_db.getItem_byId(itemid)
+ try:
+ dbid = embydb_item[0]
+ mediatype = embydb_item[4]
+ except TypeError:
+ # Item is not found in our database, add item manually
+ self.logMsg("Item was not found in the database, manually adding item.", 1)
+ item = self.emby.getItem(itemid)
+ self.addtoPlaylist_xbmc(playlist, item)
+ else:
+ # Add to playlist
+ self.addtoPlaylist(dbid, mediatype)
+
+ self.logMsg("Adding %s to playlist." % itemid, 1)
+
+ if not started:
+ started = True
+ player.play(playlist)
+
+ self.verifyPlaylist()
+ embycursor.close()
+
+ def modifyPlaylist(self, itemids):
+
+ embyconn = utils.kodiSQL('emby')
+ embycursor = embyconn.cursor()
+ emby_db = embydb.Embydb_Functions(embycursor)
+
+ self.logMsg("---*** ADD TO PLAYLIST ***---", 1)
+ self.logMsg("Items: %s" % itemids, 1)
+
+ player = xbmc.Player()
+ playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
+
+ for itemid in itemids:
+ embydb_item = emby_db.getItem_byId(itemid)
+ try:
+ dbid = embydb_item[0]
+ mediatype = embydb_item[4]
+ except TypeError:
+ # Item is not found in our database, add item manually
+ item = self.emby.getItem(itemid)
+ self.addtoPlaylist_xbmc(playlist, item)
+ else:
+ # Add to playlist
+ self.addtoPlaylist(dbid, mediatype)
+
+ self.logMsg("Adding %s to playlist." % itemid, 1)
+
+ self.verifyPlaylist()
+ embycursor.close()
+ return playlist
+
+ def addtoPlaylist(self, dbid=None, mediatype=None, url=None):
+
+ pl = {
+
+ 'jsonrpc': "2.0",
+ 'id': 1,
+ 'method': "Playlist.Add",
+ 'params': {
+
+ 'playlistid': 1
+ }
+ }
+ if dbid is not None:
+ pl['params']['item'] = {'%sid' % mediatype: int(dbid)}
+ else:
+ pl['params']['item'] = {'file': url}
+
+ self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2)
+
+ def addtoPlaylist_xbmc(self, playlist, item):
+
+ playurl = playutils.PlayUtils(item).getPlayUrl()
+ if not playurl:
+ # Playurl failed
+ self.logMsg("Failed to retrieve playurl.", 1)
+ return
+
+ self.logMsg("Playurl: %s" % playurl)
+ listitem = xbmcgui.ListItem()
+ playbackutils.PlaybackUtils(item).setProperties(playurl, listitem)
+
+ playlist.add(playurl, listitem)
+
+ def insertintoPlaylist(self, position, dbid=None, mediatype=None, url=None):
+
+ pl = {
+
+ 'jsonrpc': "2.0",
+ 'id': 1,
+ 'method': "Playlist.Insert",
+ 'params': {
+
+ 'playlistid': 1,
+ 'position': position
+ }
+ }
+ if dbid is not None:
+ pl['params']['item'] = {'%sid' % mediatype: int(dbid)}
+ else:
+ pl['params']['item'] = {'file': url}
+
+ self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2)
+
+ def verifyPlaylist(self):
+
+ pl = {
+
+ 'jsonrpc': "2.0",
+ 'id': 1,
+ 'method': "Playlist.GetItems",
+ 'params': {
+
+ 'playlistid': 1
+ }
+ }
+ self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2)
+
+ def removefromPlaylist(self, position):
+
+ pl = {
+
+ 'jsonrpc': "2.0",
+ 'id': 1,
+ 'method': "Playlist.Remove",
+ 'params': {
+
+ 'playlistid': 1,
+ 'position': position
+ }
+ }
+ self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2)
diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py
index 37332b16..622781d2 100644
--- a/resources/lib/playutils.py
+++ b/resources/lib/playutils.py
@@ -1,458 +1,426 @@
-# -*- coding: utf-8 -*-
-
-#################################################################################################
-
-import xbmc
-import xbmcgui
-import xbmcvfs
-
-import clientinfo
-import utils
-
-#################################################################################################
-
-
-class PlayUtils():
-
-
- def __init__(self, item):
-
- self.item = item
-
- self.clientInfo = clientinfo.ClientInfo()
- self.addonName = self.clientInfo.getAddonName()
-
- self.userid = utils.window('emby_currUser')
- self.server = utils.window('emby_server%s' % self.userid)
-
- def logMsg(self, msg, lvl=1):
-
- self.className = self.__class__.__name__
- utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
-
-
- def getPlayUrl(self):
-
- log = self.logMsg
- window = utils.window
-
- item = self.item
- playurl = None
-
- if (item.get('Type') in ("Recording", "TvChannel") and
- item.get('MediaSources') and item['MediaSources'][0]['Protocol'] == "Http"):
- # Play LiveTV or recordings
- log("File protocol is http (livetv).", 1)
- playurl = "%s/emby/Videos/%s/live.m3u8?static=true" % (self.server, item['Id'])
- window('emby_%s.playmethod' % playurl, value="Transcode")
-
- elif item.get('MediaSources') and item['MediaSources'][0]['Protocol'] == "Http":
- # Only play as http, used for channels, or online hosting of content
- log("File protocol is http.", 1)
- playurl = self.httpPlay()
- window('emby_%s.playmethod' % playurl, value="DirectStream")
-
- elif self.isDirectPlay():
-
- log("File is direct playing.", 1)
- playurl = self.directPlay()
- playurl = playurl.encode('utf-8')
- # Set playmethod property
- window('emby_%s.playmethod' % playurl, value="DirectPlay")
-
- elif self.isDirectStream():
-
- log("File is direct streaming.", 1)
- playurl = self.directStream()
- # Set playmethod property
- window('emby_%s.playmethod' % playurl, value="DirectStream")
-
- elif self.isTranscoding():
-
- log("File is transcoding.", 1)
- playurl = self.transcoding()
- # Set playmethod property
- window('emby_%s.playmethod' % playurl, value="Transcode")
-
- return playurl
-
- def httpPlay(self):
- # Audio, Video, Photo
- item = self.item
- server = self.server
-
- itemid = item['Id']
- mediatype = item['MediaType']
-
- if mediatype == "Audio":
- playurl = "%s/emby/Audio/%s/stream" % (server, itemid)
- else:
- playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid)
-
- return playurl
-
- def isDirectPlay(self):
-
- log = self.logMsg
- lang = utils.language
- settings = utils.settings
- dialog = xbmcgui.Dialog()
-
- item = self.item
-
- # Requirement: Filesystem, Accessible path
- if settings('playFromStream') == "true":
- # User forcing to play via HTTP
- log("Can't direct play, play from HTTP enabled.", 1)
- return False
-
- videotrack = item['MediaSources'][0]['Name']
- transcodeH265 = settings('transcodeH265')
-
- if transcodeH265 in ("1", "2", "3") and ("HEVC" in videotrack or "H265" in videotrack):
- # Avoid H265/HEVC depending on the resolution
- resolution = int(videotrack.split("P", 1)[0])
- res = {
-
- '1': 480,
- '2': 720,
- '3': 1080
- }
- log("Resolution is: %sP, transcode for resolution: %sP+"
- % (resolution, res[transcodeH265]), 1)
- if res[transcodeH265] <= resolution:
- return False
-
- canDirectPlay = item['MediaSources'][0]['SupportsDirectPlay']
- # Make sure direct play is supported by the server
- if not canDirectPlay:
- log("Can't direct play, server doesn't allow/support it.", 1)
- return False
-
- location = item['LocationType']
- if location == "FileSystem":
- # Verify the path
- if not self.fileExists():
- log("Unable to direct play.")
- try:
- count = int(settings('failCount'))
- except ValueError:
- count = 0
- log("Direct play failed: %s times." % count, 1)
-
- if count < 2:
- # Let the user know that direct play failed
- settings('failCount', value=str(count+1))
- dialog.notification(
- heading="Emby for Kodi",
- message=lang(33011),
- icon="special://home/addons/plugin.video.emby/icon.png",
- sound=False)
- elif settings('playFromStream') != "true":
- # Permanently set direct stream as true
- settings('playFromStream', value="true")
- settings('failCount', value="0")
- dialog.notification(
- heading="Emby for Kodi",
- message=lang(33012),
- icon="special://home/addons/plugin.video.emby/icon.png",
- sound=False)
- return False
-
- return True
-
- def directPlay(self):
-
- item = self.item
-
- try:
- playurl = item['MediaSources'][0]['Path']
- except (IndexError, KeyError):
- playurl = item['Path']
-
- if item.get('VideoType'):
- # Specific format modification
- type = item['VideoType']
-
- if type == "Dvd":
- playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl
- elif type == "BluRay":
- playurl = "%s/BDMV/index.bdmv" % playurl
-
- # Assign network protocol
- if playurl.startswith('\\\\'):
- playurl = playurl.replace("\\\\", "smb://")
- playurl = playurl.replace("\\", "/")
-
- if "apple.com" in playurl:
- USER_AGENT = "QuickTime/7.7.4"
- playurl += "?|User-Agent=%s" % USER_AGENT
-
- return playurl
-
- def fileExists(self):
-
- log = self.logMsg
-
- if 'Path' not in self.item:
- # File has no path defined in server
- return False
-
- # Convert path to direct play
- path = self.directPlay()
- log("Verifying path: %s" % path, 1)
-
- if xbmcvfs.exists(path):
- log("Path exists.", 1)
- return True
-
- elif ":" not in path:
- log("Can't verify path, assumed linux. Still try to direct play.", 1)
- return True
-
- else:
- log("Failed to find file.", 1)
- return False
-
- def isDirectStream(self):
-
- log = self.logMsg
-
- item = self.item
-
- videotrack = item['MediaSources'][0]['Name']
- transcodeH265 = utils.settings('transcodeH265')
-
- if transcodeH265 in ("1", "2", "3") and ("HEVC" in videotrack or "H265" in videotrack):
- # Avoid H265/HEVC depending on the resolution
- resolution = int(videotrack.split("P", 1)[0])
- res = {
-
- '1': 480,
- '2': 720,
- '3': 1080
- }
- log("Resolution is: %sP, transcode for resolution: %sP+"
- % (resolution, res[transcodeH265]), 1)
- if res[transcodeH265] <= resolution:
- return False
-
- # Requirement: BitRate, supported encoding
- canDirectStream = item['MediaSources'][0]['SupportsDirectStream']
- # Make sure the server supports it
- if not canDirectStream:
- return False
-
- # Verify the bitrate
- if not self.isNetworkSufficient():
- log("The network speed is insufficient to direct stream file.", 1)
- return False
-
- return True
-
- def directStream(self):
-
- item = self.item
- server = self.server
-
- itemid = item['Id']
- itemtype = item['Type']
-
- if 'Path' in item and item['Path'].endswith('.strm'):
- # Allow strm loading when direct streaming
- playurl = self.directPlay()
- elif itemtype == "Audio":
- playurl = "%s/emby/Audio/%s/stream.mp3" % (server, itemid)
- else:
- playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid)
-
- return playurl
-
- def isNetworkSufficient(self):
-
- log = self.logMsg
-
- settings = self.getBitrate()*1000
-
- try:
- sourceBitrate = int(self.item['MediaSources'][0]['Bitrate'])
- except (KeyError, TypeError):
- log("Bitrate value is missing.", 1)
- else:
- log("The add-on settings bitrate is: %s, the video bitrate required is: %s"
- % (settings, sourceBitrate), 1)
- if settings < sourceBitrate:
- return False
-
- return True
-
- def isTranscoding(self):
-
- item = self.item
-
- canTranscode = item['MediaSources'][0]['SupportsTranscoding']
- # Make sure the server supports it
- if not canTranscode:
- return False
-
- return True
-
- def transcoding(self):
-
- item = self.item
-
- if 'Path' in item and item['Path'].endswith('.strm'):
- # Allow strm loading when transcoding
- playurl = self.directPlay()
- else:
- itemid = item['Id']
- deviceId = self.clientInfo.getDeviceId()
- playurl = (
- "%s/emby/Videos/%s/master.m3u8?MediaSourceId=%s"
- % (self.server, itemid, itemid)
- )
- playurl = (
- "%s&VideoCodec=h264&AudioCodec=ac3&MaxAudioChannels=6&deviceId=%s&VideoBitrate=%s"
- % (playurl, deviceId, self.getBitrate()*1000))
-
- return playurl
-
- def getBitrate(self):
-
- # get the addon video quality
- videoQuality = utils.settings('videoBitrate')
- bitrate = {
-
- '0': 664,
- '1': 996,
- '2': 1320,
- '3': 2000,
- '4': 3200,
- '5': 4700,
- '6': 6200,
- '7': 7700,
- '8': 9200,
- '9': 10700,
- '10': 12200,
- '11': 13700,
- '12': 15200,
- '13': 16700,
- '14': 18200,
- '15': 20000,
- '16': 40000,
- '17': 100000,
- '18': 1000000
- }
-
- # max bit rate supported by server (max signed 32bit integer)
- return bitrate.get(videoQuality, 2147483)
-
- def audioSubsPref(self, url, listitem):
-
- log = self.logMsg
- lang = utils.language
- dialog = xbmcgui.Dialog()
- # For transcoding only
- # Present the list of audio to select from
- audioStreamsList = {}
- audioStreams = []
- audioStreamsChannelsList = {}
- subtitleStreamsList = {}
- subtitleStreams = ['No subtitles']
- downloadableStreams = []
- selectAudioIndex = ""
- selectSubsIndex = ""
- playurlprefs = "%s" % url
-
- item = self.item
- try:
- mediasources = item['MediaSources'][0]
- mediastreams = mediasources['MediaStreams']
- except (TypeError, KeyError, IndexError):
- return
-
- for stream in mediastreams:
- # Since Emby returns all possible tracks together, have to sort them.
- index = stream['Index']
- type = stream['Type']
-
- if 'Audio' in type:
- codec = stream['Codec']
- channelLayout = stream.get('ChannelLayout', "")
-
- try:
- track = "%s - %s - %s %s" % (index, stream['Language'], codec, channelLayout)
- except:
- track = "%s - %s %s" % (index, codec, channelLayout)
-
- audioStreamsChannelsList[index] = stream['Channels']
- audioStreamsList[track] = index
- audioStreams.append(track)
-
- elif 'Subtitle' in type:
- try:
- track = "%s - %s" % (index, stream['Language'])
- except:
- track = "%s - %s" % (index, stream['Codec'])
-
- default = stream['IsDefault']
- forced = stream['IsForced']
- downloadable = stream['IsTextSubtitleStream']
-
- if default:
- track = "%s - Default" % track
- if forced:
- track = "%s - Forced" % track
- if downloadable:
- downloadableStreams.append(index)
-
- subtitleStreamsList[track] = index
- subtitleStreams.append(track)
-
-
- if len(audioStreams) > 1:
- resp = dialog.select(lang(33013), audioStreams)
- if resp > -1:
- # User selected audio
- selected = audioStreams[resp]
- selectAudioIndex = audioStreamsList[selected]
- playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
- else: # User backed out of selection
- playurlprefs += "&AudioStreamIndex=%s" % mediasources['DefaultAudioStreamIndex']
- else: # There's only one audiotrack.
- selectAudioIndex = audioStreamsList[audioStreams[0]]
- playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
-
- if len(subtitleStreams) > 1:
- resp = dialog.select(lang(33014), subtitleStreams)
- if resp == 0:
- # User selected no subtitles
- pass
- elif resp > -1:
- # User selected subtitles
- selected = subtitleStreams[resp]
- selectSubsIndex = subtitleStreamsList[selected]
-
- # Load subtitles in the listitem if downloadable
- if selectSubsIndex in downloadableStreams:
-
- itemid = item['Id']
- url = [("%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
- % (self.server, itemid, itemid, selectSubsIndex))]
- log("Set up subtitles: %s %s" % (selectSubsIndex, url), 1)
- listitem.setSubtitles(url)
- else:
- # Burn subtitles
- playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex
-
- else: # User backed out of selection
- playurlprefs += "&SubtitleStreamIndex=%s" % mediasources.get('DefaultSubtitleStreamIndex', "")
-
- # Get number of channels for selected audio track
- audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0)
- if audioChannels > 2:
- playurlprefs += "&AudioBitrate=384000"
- else:
- playurlprefs += "&AudioBitrate=192000"
-
+# -*- coding: utf-8 -*-
+
+#################################################################################################
+
+import xbmc
+import xbmcgui
+import xbmcvfs
+
+import clientinfo
+import utils
+
+#################################################################################################
+
+
+class PlayUtils():
+
+
+ def __init__(self, item):
+
+ self.item = item
+
+ self.clientInfo = clientinfo.ClientInfo()
+ self.addonName = self.clientInfo.getAddonName()
+
+ self.userid = utils.window('emby_currUser')
+ self.server = utils.window('emby_server%s' % self.userid)
+
+ def logMsg(self, msg, lvl=1):
+
+ self.className = self.__class__.__name__
+ utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
+
+
+ def getPlayUrl(self):
+
+ window = utils.window
+
+ playurl = None
+
+ if (self.item.get('Type') in ("Recording", "TvChannel") and
+ self.item.get('MediaSources') and self.item['MediaSources'][0]['Protocol'] == "Http"):
+ # Play LiveTV or recordings
+ self.logMsg("File protocol is http (livetv).", 1)
+ playurl = "%s/emby/Videos/%s/live.m3u8?static=true" % (self.server, self.item['Id'])
+ window('emby_%s.playmethod' % playurl, value="Transcode")
+
+ elif self.item.get('MediaSources') and self.item['MediaSources'][0]['Protocol'] == "Http":
+ # Only play as http, used for channels, or online hosting of content
+ self.logMsg("File protocol is http.", 1)
+ playurl = self.httpPlay()
+ window('emby_%s.playmethod' % playurl, value="DirectStream")
+
+ elif self.isDirectPlay():
+
+ self.logMsg("File is direct playing.", 1)
+ playurl = self.directPlay()
+ playurl = playurl.encode('utf-8')
+ # Set playmethod property
+ window('emby_%s.playmethod' % playurl, value="DirectPlay")
+
+ elif self.isDirectStream():
+
+ self.logMsg("File is direct streaming.", 1)
+ playurl = self.directStream()
+ # Set playmethod property
+ window('emby_%s.playmethod' % playurl, value="DirectStream")
+
+ elif self.isTranscoding():
+
+ self.logMsg("File is transcoding.", 1)
+ playurl = self.transcoding()
+ # Set playmethod property
+ window('emby_%s.playmethod' % playurl, value="Transcode")
+
+ return playurl
+
+ def httpPlay(self):
+ # Audio, Video, Photo
+
+ itemid = self.item['Id']
+ mediatype = self.item['MediaType']
+
+ if mediatype == "Audio":
+ playurl = "%s/emby/Audio/%s/stream" % (self.server, itemid)
+ else:
+ playurl = "%s/emby/Videos/%s/stream?static=true" % (self.server, itemid)
+
+ return playurl
+
+ def isDirectPlay(self):
+
+ lang = utils.language
+ settings = utils.settings
+ dialog = xbmcgui.Dialog()
+
+
+ # Requirement: Filesystem, Accessible path
+ if settings('playFromStream') == "true":
+ # User forcing to play via HTTP
+ self.logMsg("Can't direct play, play from HTTP enabled.", 1)
+ return False
+
+ videotrack = self.item['MediaSources'][0]['Name']
+ transcodeH265 = settings('transcodeH265')
+
+ if transcodeH265 in ("1", "2", "3") and ("HEVC" in videotrack or "H265" in videotrack):
+ # Avoid H265/HEVC depending on the resolution
+ resolution = int(videotrack.split("P", 1)[0])
+ res = {
+
+ '1': 480,
+ '2': 720,
+ '3': 1080
+ }
+ self.logMsg("Resolution is: %sP, transcode for resolution: %sP+"
+ % (resolution, res[transcodeH265]), 1)
+ if res[transcodeH265] <= resolution:
+ return False
+
+ canDirectPlay = self.item['MediaSources'][0]['SupportsDirectPlay']
+ # Make sure direct play is supported by the server
+ if not canDirectPlay:
+ self.logMsg("Can't direct play, server doesn't allow/support it.", 1)
+ return False
+
+ location = self.item['LocationType']
+ if location == "FileSystem":
+ # Verify the path
+ if not self.fileExists():
+ self.logMsg("Unable to direct play.")
+ try:
+ count = int(settings('failCount'))
+ except ValueError:
+ count = 0
+ self.logMsg("Direct play failed: %s times." % count, 1)
+
+ if count < 2:
+ # Let the user know that direct play failed
+ settings('failCount', value=str(count+1))
+ dialog.notification(
+ heading="Emby for Kodi",
+ message=lang(33011),
+ icon="special://home/addons/plugin.video.emby/icon.png",
+ sound=False)
+ elif settings('playFromStream') != "true":
+ # Permanently set direct stream as true
+ settings('playFromStream', value="true")
+ settings('failCount', value="0")
+ dialog.notification(
+ heading="Emby for Kodi",
+ message=lang(33012),
+ icon="special://home/addons/plugin.video.emby/icon.png",
+ sound=False)
+ return False
+
+ return True
+
+ def directPlay(self):
+
+ try:
+ playurl = self.item['MediaSources'][0]['Path']
+ except (IndexError, KeyError):
+ playurl = self.item['Path']
+
+ if self.item.get('VideoType'):
+ # Specific format modification
+ if self.item['VideoType'] == "Dvd":
+ playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl
+ elif self.item['VideoType'] == "BluRay":
+ playurl = "%s/BDMV/index.bdmv" % playurl
+
+ # Assign network protocol
+ if playurl.startswith('\\\\'):
+ playurl = playurl.replace("\\\\", "smb://")
+ playurl = playurl.replace("\\", "/")
+
+ if "apple.com" in playurl:
+ USER_AGENT = "QuickTime/7.7.4"
+ playurl += "?|User-Agent=%s" % USER_AGENT
+
+ return playurl
+
+ def fileExists(self):
+
+ if 'Path' not in self.item:
+ # File has no path defined in server
+ return False
+
+ # Convert path to direct play
+ path = self.directPlay()
+ self.logMsg("Verifying path: %s" % path, 1)
+
+ if xbmcvfs.exists(path):
+ self.logMsg("Path exists.", 1)
+ return True
+
+ elif ":" not in path:
+ self.logMsg("Can't verify path, assumed linux. Still try to direct play.", 1)
+ return True
+
+ else:
+ self.logMsg("Failed to find file.", 1)
+ return False
+
+ def isDirectStream(self):
+
+
+ videotrack = self.item['MediaSources'][0]['Name']
+ transcodeH265 = utils.settings('transcodeH265')
+
+ if transcodeH265 in ("1", "2", "3") and ("HEVC" in videotrack or "H265" in videotrack):
+ # Avoid H265/HEVC depending on the resolution
+ resolution = int(videotrack.split("P", 1)[0])
+ res = {
+
+ '1': 480,
+ '2': 720,
+ '3': 1080
+ }
+ self.logMsg("Resolution is: %sP, transcode for resolution: %sP+"
+ % (resolution, res[transcodeH265]), 1)
+ if res[transcodeH265] <= resolution:
+ return False
+
+ # Requirement: BitRate, supported encoding
+ canDirectStream = self.item['MediaSources'][0]['SupportsDirectStream']
+ # Make sure the server supports it
+ if not canDirectStream:
+ return False
+
+ # Verify the bitrate
+ if not self.isNetworkSufficient():
+ self.logMsg("The network speed is insufficient to direct stream file.", 1)
+ return False
+
+ return True
+
+ def directStream(self):
+
+ if 'Path' in self.item and self.item['Path'].endswith('.strm'):
+ # Allow strm loading when direct streaming
+ playurl = self.directPlay()
+ elif self.item['Type'] == "Audio":
+ playurl = "%s/emby/Audio/%s/stream.mp3" % (self.server, self.item['Id'])
+ else:
+ playurl = "%s/emby/Videos/%s/stream?static=true" % (self.server, self.item['Id'])
+
+ return playurl
+
+ def isNetworkSufficient(self):
+
+
+ settings = self.getBitrate()*1000
+
+ try:
+ sourceBitrate = int(self.item['MediaSources'][0]['Bitrate'])
+ except (KeyError, TypeError):
+ self.logMsg("Bitrate value is missing.", 1)
+ else:
+ self.logMsg("The add-on settings bitrate is: %s, the video bitrate required is: %s"
+ % (settings, sourceBitrate), 1)
+ if settings < sourceBitrate:
+ return False
+
+ return True
+
+ def isTranscoding(self):
+ # Make sure the server supports it
+ if not self.item['MediaSources'][0]['SupportsTranscoding']:
+ return False
+
+ return True
+
+ def transcoding(self):
+
+ if 'Path' in self.item and self.item['Path'].endswith('.strm'):
+ # Allow strm loading when transcoding
+ playurl = self.directPlay()
+ else:
+ itemid = self.item['Id']
+ deviceId = self.clientInfo.getDeviceId()
+ playurl = (
+ "%s/emby/Videos/%s/master.m3u8?MediaSourceId=%s"
+ % (self.server, itemid, itemid)
+ )
+ playurl = (
+ "%s&VideoCodec=h264&AudioCodec=ac3&MaxAudioChannels=6&deviceId=%s&VideoBitrate=%s"
+ % (playurl, deviceId, self.getBitrate()*1000))
+
+ return playurl
+
+ def getBitrate(self):
+
+ # get the addon video quality
+ bitrate = {
+
+ '0': 664,
+ '1': 996,
+ '2': 1320,
+ '3': 2000,
+ '4': 3200,
+ '5': 4700,
+ '6': 6200,
+ '7': 7700,
+ '8': 9200,
+ '9': 10700,
+ '10': 12200,
+ '11': 13700,
+ '12': 15200,
+ '13': 16700,
+ '14': 18200,
+ '15': 20000,
+ '16': 40000,
+ '17': 100000,
+ '18': 1000000
+ }
+
+ # max bit rate supported by server (max signed 32bit integer)
+ return bitrate.get(utils.settings('videoBitrate'), 2147483)
+
+ def audioSubsPref(self, url, listitem):
+
+ lang = utils.language
+ dialog = xbmcgui.Dialog()
+ # For transcoding only
+ # Present the list of audio to select from
+ audioStreamsList = {}
+ audioStreams = []
+ audioStreamsChannelsList = {}
+ subtitleStreamsList = {}
+ subtitleStreams = ['No subtitles']
+ downloadableStreams = []
+ selectAudioIndex = ""
+ selectSubsIndex = ""
+ playurlprefs = "%s" % url
+
+ try:
+ mediasources = self.item['MediaSources'][0]
+ mediastreams = mediasources['MediaStreams']
+ except (TypeError, KeyError, IndexError):
+ return
+
+ for stream in mediastreams:
+ # Since Emby returns all possible tracks together, have to sort them.
+ index = stream['Index']
+
+ if 'Audio' in stream['Type']:
+ codec = stream['Codec']
+ channelLayout = stream.get('ChannelLayout', "")
+
+ try:
+ track = "%s - %s - %s %s" % (index, stream['Language'], codec, channelLayout)
+ except:
+ track = "%s - %s %s" % (index, codec, channelLayout)
+
+ audioStreamsChannelsList[index] = stream['Channels']
+ audioStreamsList[track] = index
+ audioStreams.append(track)
+
+ elif 'Subtitle' in stream['Type']:
+ try:
+ track = "%s - %s" % (index, stream['Language'])
+ except:
+ track = "%s - %s" % (index, stream['Codec'])
+
+ default = stream['IsDefault']
+ forced = stream['IsForced']
+ downloadable = stream['IsTextSubtitleStream']
+
+ if default:
+ track = "%s - Default" % track
+ if forced:
+ track = "%s - Forced" % track
+ if downloadable:
+ downloadableStreams.append(index)
+
+ subtitleStreamsList[track] = index
+ subtitleStreams.append(track)
+
+
+ if len(audioStreams) > 1:
+ resp = dialog.select(lang(33013), audioStreams)
+ if resp > -1:
+ # User selected audio
+ selected = audioStreams[resp]
+ selectAudioIndex = audioStreamsList[selected]
+ playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
+ else: # User backed out of selection
+ playurlprefs += "&AudioStreamIndex=%s" % mediasources['DefaultAudioStreamIndex']
+ else: # There's only one audiotrack.
+ selectAudioIndex = audioStreamsList[audioStreams[0]]
+ playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
+
+ if len(subtitleStreams) > 1:
+ resp = dialog.select(lang(33014), subtitleStreams)
+ if resp == 0:
+ # User selected no subtitles
+ pass
+ elif resp > -1:
+ # User selected subtitles
+ selected = subtitleStreams[resp]
+ selectSubsIndex = subtitleStreamsList[selected]
+
+ # Load subtitles in the listitem if downloadable
+ if selectSubsIndex in downloadableStreams:
+
+ itemid = self.item['Id']
+ url = [("%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
+ % (self.server, itemid, itemid, selectSubsIndex))]
+ self.logMsg("Set up subtitles: %s %s" % (selectSubsIndex, url), 1)
+ listitem.setSubtitles(url)
+ else:
+ # Burn subtitles
+ playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex
+
+ else: # User backed out of selection
+ playurlprefs += "&SubtitleStreamIndex=%s" % mediasources.get('DefaultSubtitleStreamIndex', "")
+
+ # Get number of channels for selected audio track
+ audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0)
+ if audioChannels > 2:
+ playurlprefs += "&AudioBitrate=384000"
+ else:
+ playurlprefs += "&AudioBitrate=192000"
+
return playurlprefs
\ No newline at end of file
diff --git a/resources/lib/read_embyserver.py b/resources/lib/read_embyserver.py
index 13c976a1..6eeb5fb6 100644
--- a/resources/lib/read_embyserver.py
+++ b/resources/lib/read_embyserver.py
@@ -1,601 +1,538 @@
-# -*- coding: utf-8 -*-
-
-#################################################################################################
-
-import xbmc
-
-import utils
-import clientinfo
-import downloadutils
-
-#################################################################################################
-
-
-class Read_EmbyServer():
-
- limitIndex = int(utils.settings('limitindex'))
-
-
- def __init__(self):
-
- window = utils.window
-
- self.clientInfo = clientinfo.ClientInfo()
- self.addonName = self.clientInfo.getAddonName()
- self.doUtils = downloadutils.DownloadUtils().downloadUrl
-
- self.userId = window('emby_currUser')
- self.server = window('emby_server%s' % self.userId)
-
- def logMsg(self, msg, lvl=1):
-
- className = self.__class__.__name__
- utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
-
-
- def split_list(self, itemlist, size):
- # Split up list in pieces of size. Will generate a list of lists
- return [itemlist[i:i+size] for i in range(0, len(itemlist), size)]
-
-
- def getItem(self, itemid):
- # This will return the full item
- item = {}
-
- url = "{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid
- result = self.doUtils(url)
- if result:
- item = result
-
- return item
-
- def getItems(self, itemlist):
-
- items = []
-
- itemlists = self.split_list(itemlist, 50)
- for itemlist in itemlists:
- # Will return basic information
- url = "{server}/emby/Users/{UserId}/Items?&format=json"
- params = {
-
- 'Ids': ",".join(itemlist),
- 'Fields': "Etag"
- }
- result = self.doUtils(url, parameters=params)
- if result:
- items.extend(result['Items'])
-
- return items
-
- def getFullItems(self, itemlist):
-
- items = []
-
- itemlists = self.split_list(itemlist, 50)
- for itemlist in itemlists:
-
- url = "{server}/emby/Users/{UserId}/Items?format=json"
- params = {
-
- "Ids": ",".join(itemlist),
- "Fields": (
-
- "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines,"
- "CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
- "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
- "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
- "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,"
- "MediaSources"
- )
- }
- result = self.doUtils(url, parameters=params)
- if result:
- items.extend(result['Items'])
-
- return items
-
- def getView_embyId(self, itemid):
- # Returns ancestors using embyId
- viewId = None
- url = "{server}/emby/Items/%s/Ancestors?UserId={UserId}&format=json" % itemid
- result = self.doUtils(url)
-
- for view in result:
-
- viewtype = view['Type']
- if viewtype == "CollectionFolder":
- # Found view
- viewId = view['Id']
-
- # Compare to view table in emby database
- emby = utils.kodiSQL('emby')
- cursor_emby = emby.cursor()
- query = ' '.join((
-
- "SELECT view_name, media_type",
- "FROM view",
- "WHERE view_id = ?"
- ))
- cursor_emby.execute(query, (viewId,))
- result = cursor_emby.fetchone()
- try:
- viewName = result[0]
- mediatype = result[1]
- except TypeError:
- viewName = None
- mediatype = None
-
- cursor_emby.close()
-
- return [viewName, viewId, mediatype]
-
- def getFilteredSection(self, parentid, itemtype=None, sortby="SortName", recursive=True, limit=None, sortorder="Ascending", filter=""):
- doUtils = self.doUtils
- url = "{server}/emby/Users/{UserId}/Items?format=json"
- params = {
-
- 'ParentId': parentid,
- 'IncludeItemTypes': itemtype,
- 'CollapseBoxSetItems': False,
- 'IsVirtualUnaired': False,
- 'IsMissing': False,
- 'Recursive': recursive,
- 'Limit': limit,
- 'SortBy': sortby,
- 'SortOrder': sortorder,
- 'Filters': filter,
- 'Fields': ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines,"
- "CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
- "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
- "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
- "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers")
- }
- return doUtils(url, parameters=params)
-
- def getTvChannels(self):
- doUtils = self.doUtils
- url = "{server}/emby/LiveTv/Channels/?userid={UserId}&format=json"
- params = {
-
- 'EnableImages': True,
- 'Fields': ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines,"
- "CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
- "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
- "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
- "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers")
- }
- return doUtils(url, parameters=params)
-
- def getTvRecordings(self, groupid):
- doUtils = self.doUtils
- url = "{server}/emby/LiveTv/Recordings/?userid={UserId}&format=json"
- if groupid == "root": groupid = ""
- params = {
-
- 'GroupId': groupid,
- 'EnableImages': True,
- 'Fields': ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines,"
- "CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
- "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
- "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
- "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers")
- }
- return doUtils(url, parameters=params)
-
- def getSection(self, parentid, itemtype=None, sortby="SortName", basic=False, dialog=None):
-
- log = self.logMsg
-
- doUtils = self.doUtils
- items = {
-
- 'Items': [],
- 'TotalRecordCount': 0
- }
-
- # Get total number of items
- url = "{server}/emby/Users/{UserId}/Items?format=json"
- params = {
-
- 'ParentId': parentid,
- 'IncludeItemTypes': itemtype,
- 'CollapseBoxSetItems': False,
- 'IsVirtualUnaired': False,
- 'IsMissing': False,
- 'Recursive': True,
- 'Limit': 1
- }
- result = doUtils(url, parameters=params)
- try:
- total = result['TotalRecordCount']
- items['TotalRecordCount'] = total
-
- except TypeError: # Failed to retrieve
- log("%s:%s Failed to retrieve the server response." % (url, params), 2)
-
- else:
- index = 0
- jump = self.limitIndex
- throttled = False
- highestjump = 0
-
- while index < total:
- # Get items by chunk to increase retrieval speed at scale
- params = {
-
- 'ParentId': parentid,
- 'IncludeItemTypes': itemtype,
- 'CollapseBoxSetItems': False,
- 'IsVirtualUnaired': False,
- 'IsMissing': False,
- 'Recursive': True,
- 'StartIndex': index,
- 'Limit': jump,
- 'SortBy': sortby,
- 'SortOrder': "Ascending",
- }
- if basic:
- params['Fields'] = "Etag"
- else:
- params['Fields'] = (
-
- "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines,"
- "CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
- "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
- "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
- "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,"
- "MediaSources"
- )
- result = doUtils(url, parameters=params)
- try:
- items['Items'].extend(result['Items'])
- except TypeError:
- # Something happened to the connection
- if not throttled:
- throttled = True
- log("Throttle activated.", 1)
-
- if jump == highestjump:
- # We already tried with the highestjump, but it failed. Reset value.
- log("Reset highest value.", 1)
- highestjump = 0
-
- # Lower the number by half
- if highestjump:
- throttled = False
- jump = highestjump
- log("Throttle deactivated.", 1)
- else:
- jump = int(jump/4)
- log("Set jump limit to recover: %s" % jump, 2)
-
- retry = 0
- while utils.window('emby_online') != "true":
- # Wait server to come back online
- if retry == 5:
- log("Unable to reconnect to server. Abort process.", 1)
- return items
-
- retry += 1
- if xbmc.Monitor().waitForAbort(1):
- # Abort was requested while waiting.
- return items
- else:
- # Request succeeded
- index += jump
-
- if dialog:
- percentage = int((float(index) / float(total))*100)
- dialog.update(percentage)
-
- if jump > highestjump:
- # Adjust with the latest number, if it's greater
- highestjump = jump
-
- if throttled:
- # We needed to adjust the number of item requested.
- # keep increasing until the connection times out again
- # to find the highest value
- increment = int(jump*0.33)
- if not increment: # Incase the increment is 0
- increment = 10
-
- jump += increment
- log("Increase jump limit to: %s" % jump, 1)
- return items
-
- def getViews(self, mediatype="", root=False, sortedlist=False):
- # Build a list of user views
- doUtils = self.doUtils
- views = []
- mediatype = mediatype.lower()
-
- if not root:
- url = "{server}/emby/Users/{UserId}/Views?format=json"
- else: # Views ungrouped
- url = "{server}/emby/Users/{UserId}/Items?Sortby=SortName&format=json"
-
- result = doUtils(url)
- try:
- items = result['Items']
- except TypeError:
- self.logMsg("Error retrieving views for type: %s" % mediatype, 2)
- else:
- for item in items:
-
- name = item['Name']
- itemId = item['Id']
- viewtype = item['Type']
-
- if viewtype == "Channel":
- # Filter view types
- continue
-
- # 3/4/2016 OriginalCollectionType is added
- itemtype = item.get('OriginalCollectionType', item.get('CollectionType', "mixed"))
-
- # 11/29/2015 Remove this once OriginalCollectionType is added to stable server.
- # Assumed missing is mixed then.
- '''if itemtype is None:
- url = "{server}/emby/Library/MediaFolders?format=json"
- result = doUtils(url)
-
- for folder in result['Items']:
- if itemId == folder['Id']:
- itemtype = folder.get('CollectionType', "mixed")'''
-
- if name not in ('Collections', 'Trailers'):
-
- if sortedlist:
- views.append({
-
- 'name': name,
- 'type': itemtype,
- 'id': itemId
- })
-
- elif (itemtype == mediatype or
- (itemtype == "mixed" and mediatype in ("movies", "tvshows"))):
-
- views.append({
-
- 'name': name,
- 'type': itemtype,
- 'id': itemId
- })
-
- return views
-
- def verifyView(self, parentid, itemid):
-
- belongs = False
-
- url = "{server}/emby/Users/{UserId}/Items?format=json"
- params = {
-
- 'ParentId': parentid,
- 'CollapseBoxSetItems': False,
- 'IsVirtualUnaired': False,
- 'IsMissing': False,
- 'Recursive': True,
- 'Ids': itemid
- }
- result = self.doUtils(url, parameters=params)
- try:
- total = result['TotalRecordCount']
- except TypeError:
- # Something happened to the connection
- pass
- else:
- if total:
- belongs = True
-
- return belongs
-
- def getMovies(self, parentId, basic=False, dialog=None):
-
- items = self.getSection(parentId, "Movie", basic=basic, dialog=dialog)
-
- return items
-
- def getBoxset(self, dialog=None):
-
- items = self.getSection(None, "BoxSet", dialog=dialog)
-
- return items
-
- def getMovies_byBoxset(self, boxsetid):
-
- items = self.getSection(boxsetid, "Movie")
-
- return items
-
- def getMusicVideos(self, parentId, basic=False, dialog=None):
-
- items = self.getSection(parentId, "MusicVideo", basic=basic, dialog=dialog)
-
- return items
-
- def getHomeVideos(self, parentId):
-
- items = self.getSection(parentId, "Video")
-
- return items
-
- def getShows(self, parentId, basic=False, dialog=None):
-
- items = self.getSection(parentId, "Series", basic=basic, dialog=dialog)
-
- return items
-
- def getSeasons(self, showId):
-
- items = {
-
- 'Items': [],
- 'TotalRecordCount': 0
- }
-
- url = "{server}/emby/Shows/%s/Seasons?UserId={UserId}&format=json" % showId
- params = {
-
- 'IsVirtualUnaired': False,
- 'Fields': "Etag"
- }
- result = self.doUtils(url, parameters=params)
- if result:
- items = result
-
- return items
-
- def getEpisodes(self, parentId, basic=False, dialog=None):
-
- items = self.getSection(parentId, "Episode", basic=basic, dialog=dialog)
-
- return items
-
- def getEpisodesbyShow(self, showId):
-
- items = self.getSection(showId, "Episode")
-
- return items
-
- def getEpisodesbySeason(self, seasonId):
-
- items = self.getSection(seasonId, "Episode")
-
- return items
-
- def getArtists(self, dialog=None):
-
- doUtils = self.doUtils
- items = {
-
- 'Items': [],
- 'TotalRecordCount': 0
- }
-
- # Get total number of items
- url = "{server}/emby/Artists?UserId={UserId}&format=json"
- params = {
-
- 'Recursive': True,
- 'Limit': 1
- }
- result = doUtils(url, parameters=params)
- try:
- total = result['TotalRecordCount']
- items['TotalRecordCount'] = total
-
- except TypeError: # Failed to retrieve
- self.logMsg("%s:%s Failed to retrieve the server response." % (url, params), 2)
-
- else:
- index = 1
- jump = self.limitIndex
-
- while index < total:
- # Get items by chunk to increase retrieval speed at scale
- params = {
-
- 'Recursive': True,
- 'IsVirtualUnaired': False,
- 'IsMissing': False,
- 'StartIndex': index,
- 'Limit': jump,
- 'SortBy': "SortName",
- 'SortOrder': "Ascending",
- 'Fields': (
-
- "Etag,Genres,SortName,Studios,Writer,ProductionYear,"
- "CommunityRating,OfficialRating,CumulativeRunTimeTicks,Metascore,"
- "AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview"
- )
- }
- result = doUtils(url, parameters=params)
- items['Items'].extend(result['Items'])
-
- index += jump
- if dialog:
- percentage = int((float(index) / float(total))*100)
- dialog.update(percentage)
- return items
-
- def getAlbums(self, basic=False, dialog=None):
-
- items = self.getSection(None, "MusicAlbum", sortby="DateCreated", basic=basic, dialog=dialog)
-
- return items
-
- def getAlbumsbyArtist(self, artistId):
-
- items = self.getSection(artistId, "MusicAlbum", sortby="DateCreated")
-
- return items
-
- def getSongs(self, basic=False, dialog=None):
-
- items = self.getSection(None, "Audio", basic=basic, dialog=dialog)
-
- return items
-
- def getSongsbyAlbum(self, albumId):
-
- items = self.getSection(albumId, "Audio")
-
- return items
-
- def getAdditionalParts(self, itemId):
-
- items = {
-
- 'Items': [],
- 'TotalRecordCount': 0
- }
-
- url = "{server}/emby/Videos/%s/AdditionalParts?UserId={UserId}&format=json" % itemId
- result = self.doUtils(url)
- if result:
- items = result
-
- return items
-
- def sortby_mediatype(self, itemids):
-
- sorted_items = {}
-
- # Sort items
- items = self.getFullItems(itemids)
- for item in items:
-
- mediatype = item.get('Type')
- if mediatype:
- sorted_items.setdefault(mediatype, []).append(item)
-
- return sorted_items
-
- def updateUserRating(self, itemid, like=None, favourite=None, deletelike=False):
- # Updates the user rating to Emby
- doUtils = self.doUtils
-
- if favourite:
- url = "{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid
- doUtils(url, type="POST")
- elif favourite == False:
- url = "{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid
- doUtils(url, type="DELETE")
-
- if not deletelike and like:
- url = "{server}/emby/Users/{UserId}/Items/%s/Rating?Likes=true&format=json" % itemid
- doUtils(url, type="POST")
- elif not deletelike and like == False:
- url = "{server}/emby/Users/{UserId}/Items/%s/Rating?Likes=false&format=json" % itemid
- doUtil(url, type="POST")
- elif deletelike:
- url = "{server}/emby/Users/{UserId}/Items/%s/Rating?format=json" % itemid
- doUtils(url, type="DELETE")
-
- self.logMsg("Update user rating to emby for itemid: %s "
- "| like: %s | favourite: %s | deletelike: %s"
+# -*- coding: utf-8 -*-
+
+#################################################################################################
+
+import xbmc
+
+import utils
+import clientinfo
+import downloadutils
+
+#################################################################################################
+
+
+class Read_EmbyServer():
+
+ limitIndex = int(utils.settings('limitindex'))
+
+
+ def __init__(self):
+
+ window = utils.window
+
+ self.clientInfo = clientinfo.ClientInfo()
+ self.addonName = self.clientInfo.getAddonName()
+ self.doUtils = downloadutils.DownloadUtils().downloadUrl
+
+ self.userId = window('emby_currUser')
+ self.server = window('emby_server%s' % self.userId)
+
+ def logMsg(self, msg, lvl=1):
+
+ className = self.__class__.__name__
+ utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
+
+
+ def split_list(self, itemlist, size):
+ # Split up list in pieces of size. Will generate a list of lists
+ return [itemlist[i:i+size] for i in range(0, len(itemlist), size)]
+
+
+ def getItem(self, itemid):
+ # This will return the full item
+ item = {}
+
+ result = self.doUtils("{server}/metaman/Users/{UserId}/Items/%s?format=json" % itemid)
+ if result:
+ item = result
+
+ return item
+
+ def getItems(self, itemlist):
+
+ items = []
+
+ itemlists = self.split_list(itemlist, 50)
+ for itemlist in itemlists:
+ # Will return basic information
+ params = {
+
+ 'Ids': ",".join(itemlist),
+ 'Fields': "Etag"
+ }
+ result = self.doUtils("{server}/emby/Users/{UserId}/Items?&format=json", parameters=params)
+ if result:
+ items.extend(result['Items'])
+
+ return items
+
+ def getFullItems(self, itemlist):
+
+ items = []
+
+ itemlists = self.split_list(itemlist, 50)
+ for itemlist in itemlists:
+
+ params = {
+
+ "Ids": ",".join(itemlist),
+ "Fields": (
+
+ "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines,"
+ "CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
+ "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
+ "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
+ "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,"
+ "MediaSources"
+ )
+ }
+ result = self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params)
+ if result:
+ items.extend(result['Items'])
+
+ return items
+
+ def getView_embyId(self, itemid):
+ # Returns ancestors using embyId
+ viewId = None
+
+ for view in self.doUtils("{server}/emby/Items/%s/Ancestors?UserId={UserId}&format=json" % itemid):
+
+ if view['Type'] == "CollectionFolder":
+ # Found view
+ viewId = view['Id']
+
+ # Compare to view table in emby database
+ emby = utils.kodiSQL('emby')
+ cursor_emby = emby.cursor()
+ query = ' '.join((
+
+ "SELECT view_name, media_type",
+ "FROM view",
+ "WHERE view_id = ?"
+ ))
+ cursor_emby.execute(query, (viewId,))
+ result = cursor_emby.fetchone()
+ try:
+ viewName = result[0]
+ mediatype = result[1]
+ except TypeError:
+ viewName = None
+ mediatype = None
+
+ cursor_emby.close()
+
+ return [viewName, viewId, mediatype]
+
+ def getFilteredSection(self, parentid, itemtype=None, sortby="SortName", recursive=True, limit=None, sortorder="Ascending", filter=""):
+ params = {
+
+ 'ParentId': parentid,
+ 'IncludeItemTypes': itemtype,
+ 'CollapseBoxSetItems': False,
+ 'IsVirtualUnaired': False,
+ 'IsMissing': False,
+ 'Recursive': recursive,
+ 'Limit': limit,
+ 'SortBy': sortby,
+ 'SortOrder': sortorder,
+ 'Filters': filter,
+ 'Fields': ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines,"
+ "CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
+ "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
+ "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
+ "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers")
+ }
+ return self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params)
+
+ def getTvChannels(self):
+ params = {
+
+ 'EnableImages': True,
+ 'Fields': ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines,"
+ "CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
+ "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
+ "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
+ "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers")
+ }
+ return self.doUtils("{server}/emby/LiveTv/Channels/?userid={UserId}&format=json", parameters=params)
+
+ def getTvRecordings(self, groupid):
+ if groupid == "root": groupid = ""
+ params = {
+
+ 'GroupId': groupid,
+ 'EnableImages': True,
+ 'Fields': ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines,"
+ "CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
+ "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
+ "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
+ "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers")
+ }
+ return self.doUtils("{server}/emby/LiveTv/Recordings/?userid={UserId}&format=json", parameters=params)
+
+ def getSection(self, parentid, itemtype=None, sortby="SortName", basic=False, dialog=None):
+
+ items = {
+
+ 'Items': [],
+ 'TotalRecordCount': 0
+ }
+
+ # Get total number of items
+ url = "{server}/emby/Users/{UserId}/Items?format=json"
+ params = {
+
+ 'ParentId': parentid,
+ 'IncludeItemTypes': itemtype,
+ 'CollapseBoxSetItems': False,
+ 'IsVirtualUnaired': False,
+ 'IsMissing': False,
+ 'Recursive': True,
+ 'Limit': 1
+ }
+ result = self.doUtils(url, parameters=params)
+ try:
+ total = result['TotalRecordCount']
+ items['TotalRecordCount'] = total
+
+ except TypeError: # Failed to retrieve
+ self.logMsg("%s:%s Failed to retrieve the server response." % (url, params), 2)
+
+ else:
+ index = 0
+ jump = self.limitIndex
+ throttled = False
+ highestjump = 0
+
+ while index < total:
+ # Get items by chunk to increase retrieval speed at scale
+ params = {
+
+ 'ParentId': parentid,
+ 'IncludeItemTypes': itemtype,
+ 'CollapseBoxSetItems': False,
+ 'IsVirtualUnaired': False,
+ 'IsMissing': False,
+ 'Recursive': True,
+ 'StartIndex': index,
+ 'Limit': jump,
+ 'SortBy': sortby,
+ 'SortOrder': "Ascending",
+ }
+ if basic:
+ params['Fields'] = "Etag"
+ else:
+ params['Fields'] = (
+
+ "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines,"
+ "CommunityRating,OfficialRating,CumulativeRunTimeTicks,"
+ "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
+ "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
+ "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,"
+ "MediaSources"
+ )
+ result = self.doUtils(url, parameters=params)
+ try:
+ items['Items'].extend(result['Items'])
+ except TypeError:
+ # Something happened to the connection
+ if not throttled:
+ throttled = True
+ self.logMsg("Throttle activated.", 1)
+
+ if jump == highestjump:
+ # We already tried with the highestjump, but it failed. Reset value.
+ self.logMsg("Reset highest value.", 1)
+ highestjump = 0
+
+ # Lower the number by half
+ if highestjump:
+ throttled = False
+ jump = highestjump
+ self.logMsg("Throttle deactivated.", 1)
+ else:
+ jump = int(jump/4)
+ self.logMsg("Set jump limit to recover: %s" % jump, 2)
+
+ retry = 0
+ while utils.window('emby_online') != "true":
+ # Wait server to come back online
+ if retry == 5:
+ self.logMsg("Unable to reconnect to server. Abort process.", 1)
+ return items
+
+ retry += 1
+ if xbmc.Monitor().waitForAbort(1):
+ # Abort was requested while waiting.
+ return items
+ else:
+ # Request succeeded
+ index += jump
+
+ if dialog:
+ percentage = int((float(index) / float(total))*100)
+ dialog.update(percentage)
+
+ if jump > highestjump:
+ # Adjust with the latest number, if it's greater
+ highestjump = jump
+
+ if throttled:
+ # We needed to adjust the number of item requested.
+ # keep increasing until the connection times out again
+ # to find the highest value
+ increment = int(jump*0.33)
+ if not increment: # Incase the increment is 0
+ increment = 10
+
+ jump += increment
+ self.logMsg("Increase jump limit to: %s" % jump, 1)
+ return items
+
+ def getViews(self, mediatype="", root=False, sortedlist=False):
+ # Build a list of user views
+ views = []
+ mediatype = mediatype.lower()
+
+ if not root:
+ url = "{server}/emby/Users/{UserId}/Views?format=json"
+ else: # Views ungrouped
+ url = "{server}/emby/Users/{UserId}/Items?Sortby=SortName&format=json"
+
+ result = self.doUtils(url)
+ try:
+ items = result['Items']
+ except TypeError:
+ self.logMsg("Error retrieving views for type: %s" % mediatype, 2)
+ else:
+ for item in items:
+
+ item['Name'] = item['Name']
+ if item['Type'] == "Channel":
+ # Filter view types
+ continue
+
+ # 3/4/2016 OriginalCollectionType is added
+ itemtype = item.get('OriginalCollectionType', item.get('CollectionType', "mixed"))
+
+ # 11/29/2015 Remove this once OriginalCollectionType is added to stable server.
+ # Assumed missing is mixed then.
+ '''if itemtype is None:
+ url = "{server}/emby/Library/MediaFolders?format=json"
+ result = self.doUtils(url)
+
+ for folder in result['Items']:
+ if item['Id'] == folder['Id']:
+ itemtype = folder.get('CollectionType', "mixed")'''
+
+ if item['Name'] not in ('Collections', 'Trailers'):
+
+ if sortedlist:
+ views.append({
+
+ 'name': item['Name'],
+ 'type': itemtype,
+ 'id': item['Id']
+ })
+
+ elif (itemtype == mediatype or
+ (itemtype == "mixed" and mediatype in ("movies", "tvshows"))):
+
+ views.append({
+
+ 'name': item['Name'],
+ 'type': itemtype,
+ 'id': item['Id']
+ })
+
+ return views
+
+ def verifyView(self, parentid, itemid):
+
+ belongs = False
+ params = {
+
+ 'ParentId': parentid,
+ 'CollapseBoxSetItems': False,
+ 'IsVirtualUnaired': False,
+ 'IsMissing': False,
+ 'Recursive': True,
+ 'Ids': itemid
+ }
+ result = self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params)
+ try:
+ total = result['TotalRecordCount']
+ except TypeError:
+ # Something happened to the connection
+ pass
+ else:
+ if total:
+ belongs = True
+
+ return belongs
+
+ def getMovies(self, parentId, basic=False, dialog=None):
+ return self.getSection(parentId, "Movie", basic=basic, dialog=dialog)
+
+ def getBoxset(self, dialog=None):
+ return self.getSection(None, "BoxSet", dialog=dialog)
+
+ def getMovies_byBoxset(self, boxsetid):
+ return self.getSection(boxsetid, "Movie")
+
+ def getMusicVideos(self, parentId, basic=False, dialog=None):
+ return self.getSection(parentId, "MusicVideo", basic=basic, dialog=dialog)
+
+ def getHomeVideos(self, parentId):
+
+ return self.getSection(parentId, "Video")
+
+ def getShows(self, parentId, basic=False, dialog=None):
+ return self.getSection(parentId, "Series", basic=basic, dialog=dialog)
+
+ def getSeasons(self, showId):
+
+ items = {
+
+ 'Items': [],
+ 'TotalRecordCount': 0
+ }
+
+ params = {
+
+ 'IsVirtualUnaired': False,
+ 'Fields': "Etag"
+ }
+ result = self.doUtils("{server}/emby/Shows/%s/Seasons?UserId={UserId}&format=json" % showId, parameters=params)
+ if result:
+ items = result
+
+ return items
+
+ def getEpisodes(self, parentId, basic=False, dialog=None):
+
+ return self.getSection(parentId, "Episode", basic=basic, dialog=dialog)
+
+ def getEpisodesbyShow(self, showId):
+
+ return self.getSection(showId, "Episode")
+
+ def getEpisodesbySeason(self, seasonId):
+
+ return self.getSection(seasonId, "Episode")
+
+
+ def getArtists(self, dialog=None):
+
+ items = {
+
+ 'Items': [],
+ 'TotalRecordCount': 0
+ }
+
+ # Get total number of items
+ url = "{server}/emby/Artists?UserId={UserId}&format=json"
+ params = {
+
+ 'Recursive': True,
+ 'Limit': 1
+ }
+ result = self.doUtils(url, parameters=params)
+ try:
+ total = result['TotalRecordCount']
+ items['TotalRecordCount'] = total
+
+ except TypeError: # Failed to retrieve
+ self.logMsg("%s:%s Failed to retrieve the server response." % (url, params), 2)
+
+ else:
+ index = 1
+ jump = self.limitIndex
+
+ while index < total:
+ # Get items by chunk to increase retrieval speed at scale
+ params = {
+
+ 'Recursive': True,
+ 'IsVirtualUnaired': False,
+ 'IsMissing': False,
+ 'StartIndex': index,
+ 'Limit': jump,
+ 'SortBy': "SortName",
+ 'SortOrder': "Ascending",
+ 'Fields': (
+
+ "Etag,Genres,SortName,Studios,Writer,ProductionYear,"
+ "CommunityRating,OfficialRating,CumulativeRunTimeTicks,Metascore,"
+ "AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview"
+ )
+ }
+ result = self.doUtils(url, parameters=params)
+ items['Items'].extend(result['Items'])
+
+ index += jump
+ if dialog:
+ percentage = int((float(index) / float(total))*100)
+ dialog.update(percentage)
+ return items
+
+ def getAlbums(self, basic=False, dialog=None):
+ return self.getSection(None, "MusicAlbum", sortby="DateCreated", basic=basic, dialog=dialog)
+
+ def getAlbumsbyArtist(self, artistId):
+ return self.getSection(artistId, "MusicAlbum", sortby="DateCreated")
+
+ def getSongs(self, basic=False, dialog=None):
+ return self.getSection(None, "Audio", basic=basic, dialog=dialog)
+
+ def getSongsbyAlbum(self, albumId):
+ return self.getSection(albumId, "Audio")
+
+
+ def getAdditionalParts(self, itemId):
+
+ items = {
+
+ 'Items': [],
+ 'TotalRecordCount': 0
+ }
+
+ result = self.doUtils("{server}/emby/Videos/%s/AdditionalParts?UserId={UserId}&format=json" % itemId)
+ if result:
+ items = result
+
+ return items
+
+ def sortby_mediatype(self, itemids):
+
+ sorted_items = {}
+
+ # Sort items
+ items = self.getFullItems(itemids)
+ for item in items:
+
+ mediatype = item.get('Type')
+ if mediatype:
+ sorted_items.setdefault(mediatype, []).append(item)
+
+ return sorted_items
+
+ def updateUserRating(self, itemid, like=None, favourite=None, deletelike=False):
+ # Updates the user rating to Emby
+
+ if favourite:
+ self.doUtils("{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid, action_type="POST")
+ elif favourite == False:
+ self.doUtils("{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid, action_type="DELETE")
+
+ if not deletelike and like:
+ self.doUtils("{server}/emby/Users/{UserId}/Items/%s/Rating?Likes=true&format=json" % itemid, action_type="POST")
+ elif not deletelike and like is False:
+ self.doUtils("{server}/emby/Users/{UserId}/Items/%s/Rating?Likes=false&format=json" % itemid, action_type="POST")
+ elif deletelike:
+ self.doUtils("{server}/emby/Users/{UserId}/Items/%s/Rating?format=json" % itemid, action_type="DELETE")
+
+ self.logMsg("Update user rating to emby for itemid: %s "
+ "| like: %s | favourite: %s | deletelike: %s"
% (itemid, like, favourite, deletelike), 1)
\ No newline at end of file
diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py
index ecda0a9c..f068b772 100644
--- a/resources/lib/userclient.py
+++ b/resources/lib/userclient.py
@@ -1,487 +1,468 @@
-# -*- coding: utf-8 -*-
-
-##################################################################################################
-
-import hashlib
-import threading
-
-import xbmc
-import xbmcgui
-import xbmcaddon
-import xbmcvfs
-
-import artwork
-import utils
-import clientinfo
-import downloadutils
-
-##################################################################################################
-
-
-class UserClient(threading.Thread):
-
- # Borg - multiple instances, shared state
- _shared_state = {}
-
- stopClient = False
- auth = True
- retry = 0
-
- currUser = None
- currUserId = None
- currServer = None
- currToken = None
- HasAccess = True
- AdditionalUser = []
-
- userSettings = None
-
-
- def __init__(self):
-
- self.__dict__ = self._shared_state
- self.addon = xbmcaddon.Addon()
-
- self.addonName = clientinfo.ClientInfo().getAddonName()
- self.doUtils = downloadutils.DownloadUtils()
-
- threading.Thread.__init__(self)
-
- def logMsg(self, msg, lvl=1):
-
- className = self.__class__.__name__
- utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
-
-
- def getAdditionalUsers(self):
-
- additionalUsers = utils.settings('additionalUsers')
-
- if additionalUsers:
- self.AdditionalUser = additionalUsers.split(',')
-
- def getUsername(self):
-
- username = utils.settings('username')
-
- if not username:
- self.logMsg("No username saved.", 2)
- return ""
-
- return username
-
- def getLogLevel(self):
-
- try:
- logLevel = int(utils.settings('logLevel'))
- except ValueError:
- logLevel = 0
-
- return logLevel
-
- def getUserId(self):
-
- log = self.logMsg
- window = utils.window
- settings = utils.settings
-
- username = self.getUsername()
- w_userId = window('emby_currUser')
- s_userId = settings('userId%s' % username)
-
- # Verify the window property
- if w_userId:
- if not s_userId:
- # Save access token if it's missing from settings
- settings('userId%s' % username, value=w_userId)
- log("Returning userId from WINDOW for username: %s UserId: %s"
- % (username, w_userId), 2)
- return w_userId
- # Verify the settings
- elif s_userId:
- log("Returning userId from SETTINGS for username: %s userId: %s"
- % (username, s_userId), 2)
- return s_userId
- # No userId found
- else:
- log("No userId saved for username: %s." % username, 1)
-
- def getServer(self, prefix=True):
-
- settings = utils.settings
-
- alternate = settings('altip') == "true"
- if alternate:
- # Alternate host
- HTTPS = settings('secondhttps') == "true"
- host = settings('secondipaddress')
- port = settings('secondport')
- else:
- # Original host
- HTTPS = settings('https') == "true"
- host = settings('ipaddress')
- port = settings('port')
-
- server = host + ":" + port
-
- if not host:
- self.logMsg("No server information saved.", 2)
- return False
-
- # If https is true
- if prefix and HTTPS:
- server = "https://%s" % server
- return server
- # If https is false
- elif prefix and not HTTPS:
- server = "http://%s" % server
- return server
- # If only the host:port is required
- elif not prefix:
- return server
-
- def getToken(self):
-
- log = self.logMsg
- window = utils.window
- settings = utils.settings
-
- username = self.getUsername()
- userId = self.getUserId()
- w_token = window('emby_accessToken%s' % userId)
- s_token = settings('accessToken')
-
- # Verify the window property
- if w_token:
- if not s_token:
- # Save access token if it's missing from settings
- settings('accessToken', value=w_token)
- log("Returning accessToken from WINDOW for username: %s accessToken: %s"
- % (username, w_token), 2)
- return w_token
- # Verify the settings
- elif s_token:
- log("Returning accessToken from SETTINGS for username: %s accessToken: %s"
- % (username, s_token), 2)
- window('emby_accessToken%s' % username, value=s_token)
- return s_token
- else:
- log("No token found.", 1)
- return ""
-
- def getSSLverify(self):
- # Verify host certificate
- settings = utils.settings
-
- s_sslverify = settings('sslverify')
- if settings('altip') == "true":
- s_sslverify = settings('secondsslverify')
-
- if s_sslverify == "true":
- return True
- else:
- return False
-
- def getSSL(self):
- # Client side certificate
- settings = utils.settings
-
- s_cert = settings('sslcert')
- if settings('altip') == "true":
- s_cert = settings('secondsslcert')
-
- if s_cert == "None":
- return None
- else:
- return s_cert
-
- def setUserPref(self):
-
- doUtils = self.doUtils.downloadUrl
- art = artwork.Artwork()
-
- url = "{server}/emby/Users/{UserId}?format=json"
- result = doUtils(url)
- self.userSettings = result
- # Set user image for skin display
- if result.get('PrimaryImageTag'):
- utils.window('EmbyUserImage', value=art.getUserArtwork(result['Id'], 'Primary'))
-
- # Set resume point max
- url = "{server}/emby/System/Configuration?format=json"
- result = doUtils(url)
-
- utils.settings('markPlayed', value=str(result['MaxResumePct']))
-
- def getPublicUsers(self):
-
- server = self.getServer()
-
- # Get public Users
- url = "%s/emby/Users/Public?format=json" % server
- result = self.doUtils.downloadUrl(url, authenticate=False)
-
- if result != "":
- return result
- else:
- # Server connection failed
- return False
-
- def hasAccess(self):
- # hasAccess is verified in service.py
- log = self.logMsg
- window = utils.window
-
- url = "{server}/emby/Users?format=json"
- result = self.doUtils.downloadUrl(url)
-
- if result == False:
- # Access is restricted, set in downloadutils.py via exception
- log("Access is restricted.", 1)
- self.HasAccess = False
-
- elif window('emby_online') != "true":
- # Server connection failed
- pass
-
- elif window('emby_serverStatus') == "restricted":
- log("Access is granted.", 1)
- self.HasAccess = True
- window('emby_serverStatus', clear=True)
- xbmcgui.Dialog().notification("Emby for Kodi", utils.language(33007))
-
- def loadCurrUser(self, authenticated=False):
-
- window = utils.window
-
- doUtils = self.doUtils
- username = self.getUsername()
- userId = self.getUserId()
-
- # Only to be used if token exists
- self.currUserId = userId
- self.currServer = self.getServer()
- self.currToken = self.getToken()
- self.ssl = self.getSSLverify()
- self.sslcert = self.getSSL()
-
- # Test the validity of current token
- if authenticated == False:
- url = "%s/emby/Users/%s?format=json" % (self.currServer, userId)
- window('emby_currUser', value=userId)
- window('emby_accessToken%s' % userId, value=self.currToken)
- result = doUtils.downloadUrl(url)
-
- if result == 401:
- # Token is no longer valid
- self.resetClient()
- return False
-
- # Set to windows property
- window('emby_currUser', value=userId)
- window('emby_accessToken%s' % userId, value=self.currToken)
- window('emby_server%s' % userId, value=self.currServer)
- window('emby_server_%s' % userId, value=self.getServer(prefix=False))
-
- # Set DownloadUtils values
- doUtils.setUsername(username)
- doUtils.setUserId(self.currUserId)
- doUtils.setServer(self.currServer)
- doUtils.setToken(self.currToken)
- doUtils.setSSL(self.ssl, self.sslcert)
- # parental control - let's verify if access is restricted
- self.hasAccess()
- # Start DownloadUtils session
- doUtils.startSession()
- self.getAdditionalUsers()
- # Set user preferences in settings
- self.currUser = username
- self.setUserPref()
-
-
- def authenticate(self):
-
- log = self.logMsg
- lang = utils.language
- window = utils.window
- settings = utils.settings
- dialog = xbmcgui.Dialog()
-
- # Get /profile/addon_data
- addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8')
- hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir)
-
- username = self.getUsername()
- server = self.getServer()
-
- # If there's no settings.xml
- if not hasSettings:
- log("No settings.xml found.", 1)
- self.auth = False
- return
- # If no user information
- elif not server or not username:
- log("Missing server information.", 1)
- self.auth = False
- return
- # If there's a token, load the user
- elif self.getToken():
- result = self.loadCurrUser()
-
- if result == False:
- pass
- else:
- log("Current user: %s" % self.currUser, 1)
- log("Current userId: %s" % self.currUserId, 1)
- log("Current accessToken: %s" % self.currToken, 2)
- return
-
- ##### AUTHENTICATE USER #####
-
- users = self.getPublicUsers()
- password = ""
-
- # Find user in list
- for user in users:
- name = user['Name']
-
- if username.decode('utf-8') in name:
- # If user has password
- if user['HasPassword'] == True:
- password = dialog.input(
- heading="%s %s" % (lang(33008), username.decode('utf-8')),
- option=xbmcgui.ALPHANUM_HIDE_INPUT)
- # If password dialog is cancelled
- if not password:
- log("No password entered.", 0)
- window('emby_serverStatus', value="Stop")
- self.auth = False
- return
- break
- else:
- # Manual login, user is hidden
- password = dialog.input(
- heading="%s %s" % (lang(33008), username),
- option=xbmcgui.ALPHANUM_HIDE_INPUT)
- sha1 = hashlib.sha1(password)
- sha1 = sha1.hexdigest()
-
- # Authenticate username and password
- url = "%s/emby/Users/AuthenticateByName?format=json" % server
- data = {'username': username, 'password': sha1}
- log(data, 2)
-
- result = self.doUtils.downloadUrl(url, postBody=data, type="POST", authenticate=False)
-
- try:
- log("Auth response: %s" % result, 1)
- accessToken = result['AccessToken']
-
- except (KeyError, TypeError):
- log("Failed to retrieve the api key.", 1)
- accessToken = None
-
- if accessToken is not None:
- self.currUser = username
- dialog.notification("Emby for Kodi",
- "%s %s!" % (lang(33000), self.currUser.decode('utf-8')))
- userId = result['User']['Id']
- settings('accessToken', value=accessToken)
- settings('userId%s' % username, value=userId)
- log("User Authenticated: %s" % accessToken, 1)
- self.loadCurrUser(authenticated=True)
- window('emby_serverStatus', clear=True)
- self.retry = 0
- else:
- log("User authentication failed.", 1)
- settings('accessToken', value="")
- settings('userId%s' % username, value="")
- dialog.ok(lang(33001), lang(33009))
-
- # Give two attempts at entering password
- if self.retry == 2:
- log("Too many retries. "
- "You can retry by resetting attempts in the addon settings.", 1)
- window('emby_serverStatus', value="Stop")
- dialog.ok(lang(33001), lang(33010))
-
- self.retry += 1
- self.auth = False
-
- def resetClient(self):
-
- log = self.logMsg
-
- log("Reset UserClient authentication.", 1)
- userId = self.getUserId()
-
- if self.currToken is not None:
- # In case of 401, removed saved token
- utils.settings('accessToken', value="")
- utils.window('emby_accessToken%s' % userId, clear=True)
- self.currToken = None
- log("User token has been removed.", 1)
-
- self.auth = True
- self.currUser = None
-
- def run(self):
-
- log = self.logMsg
- window = utils.window
-
- monitor = xbmc.Monitor()
- log("----===## Starting UserClient ##===----", 0)
-
- while not monitor.abortRequested():
-
- status = window('emby_serverStatus')
- if status:
- # Verify the connection status to server
- if status == "restricted":
- # Parental control is restricting access
- self.HasAccess = False
-
- elif status == "401":
- # Unauthorized access, revoke token
- window('emby_serverStatus', value="Auth")
- self.resetClient()
-
- if self.auth and (self.currUser is None):
- # Try to authenticate user
- status = window('emby_serverStatus')
- if not status or status == "Auth":
- # Set auth flag because we no longer need
- # to authenticate the user
- self.auth = False
- self.authenticate()
-
-
- if not self.auth and (self.currUser is None):
- # If authenticate failed.
- server = self.getServer()
- username = self.getUsername()
- status = window('emby_serverStatus')
-
- # The status Stop is for when user cancelled password dialog.
- if server and username and status != "Stop":
- # Only if there's information found to login
- log("Server found: %s" % server, 2)
- log("Username found: %s" % username, 2)
- self.auth = True
-
-
- if self.stopClient == True:
- # If stopping the client didn't work
- break
-
- if monitor.waitForAbort(1):
- # Abort was requested while waiting. We should exit
- break
-
- self.doUtils.stopSession()
- log("##===---- UserClient Stopped ----===##", 0)
-
- def stopClient(self):
- # When emby for kodi terminates
+# -*- coding: utf-8 -*-
+
+##################################################################################################
+
+import hashlib
+import threading
+
+import xbmc
+import xbmcgui
+import xbmcaddon
+import xbmcvfs
+
+import artwork
+import utils
+import clientinfo
+import downloadutils
+
+##################################################################################################
+
+
+class UserClient(threading.Thread):
+
+ # Borg - multiple instances, shared state
+ _shared_state = {}
+
+ stopClient = False
+ auth = True
+ retry = 0
+
+ currUser = None
+ currUserId = None
+ currServer = None
+ currToken = None
+ HasAccess = True
+ AdditionalUser = []
+
+ userSettings = None
+
+
+ def __init__(self):
+
+ self.__dict__ = self._shared_state
+ self.addon = xbmcaddon.Addon()
+
+ self.addonName = clientinfo.ClientInfo().getAddonName()
+ self.doUtils = downloadutils.DownloadUtils()
+
+ threading.Thread.__init__(self)
+
+ def logMsg(self, msg, lvl=1):
+
+ className = self.__class__.__name__
+ utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
+
+
+ def getAdditionalUsers(self):
+
+ additionalUsers = utils.settings('additionalUsers')
+
+ if additionalUsers:
+ self.AdditionalUser = additionalUsers.split(',')
+
+ def getUsername(self):
+
+ username = utils.settings('username')
+
+ if not username:
+ self.logMsg("No username saved.", 2)
+ return ""
+
+ return username
+
+ def getLogLevel(self):
+
+ try:
+ logLevel = int(utils.settings('logLevel'))
+ except ValueError:
+ logLevel = 0
+
+ return logLevel
+
+ def getUserId(self):
+
+ window = utils.window
+ settings = utils.settings
+
+ username = self.getUsername()
+ w_userId = window('emby_currUser')
+ s_userId = settings('userId%s' % username)
+
+ # Verify the window property
+ if w_userId:
+ if not s_userId:
+ # Save access token if it's missing from settings
+ settings('userId%s' % username, value=w_userId)
+ self.logMsg("Returning userId from WINDOW for username: %s UserId: %s"
+ % (username, w_userId), 2)
+ return w_userId
+ # Verify the settings
+ elif s_userId:
+ self.logMsg("Returning userId from SETTINGS for username: %s userId: %s"
+ % (username, s_userId), 2)
+ return s_userId
+ # No userId found
+ else:
+ self.logMsg("No userId saved for username: %s." % username, 1)
+
+ def getServer(self, prefix=True):
+
+ settings = utils.settings
+
+ alternate = settings('altip') == "true"
+ if alternate:
+ # Alternate host
+ HTTPS = settings('secondhttps') == "true"
+ host = settings('secondipaddress')
+ port = settings('secondport')
+ else:
+ # Original host
+ HTTPS = settings('https') == "true"
+ host = settings('ipaddress')
+ port = settings('port')
+
+ server = host + ":" + port
+
+ if not host:
+ self.logMsg("No server information saved.", 2)
+ return False
+
+ # If https is true
+ if prefix and HTTPS:
+ server = "https://%s" % server
+ return server
+ # If https is false
+ elif prefix and not HTTPS:
+ server = "http://%s" % server
+ return server
+ # If only the host:port is required
+ elif not prefix:
+ return server
+
+ def getToken(self):
+
+ window = utils.window
+ settings = utils.settings
+
+ username = self.getUsername()
+ userId = self.getUserId()
+ w_token = window('emby_accessToken%s' % userId)
+ s_token = settings('accessToken')
+
+ # Verify the window property
+ if w_token:
+ if not s_token:
+ # Save access token if it's missing from settings
+ settings('accessToken', value=w_token)
+ self.logMsg("Returning accessToken from WINDOW for username: %s accessToken: %s"
+ % (username, w_token), 2)
+ return w_token
+ # Verify the settings
+ elif s_token:
+ self.logMsg("Returning accessToken from SETTINGS for username: %s accessToken: %s"
+ % (username, s_token), 2)
+ window('emby_accessToken%s' % username, value=s_token)
+ return s_token
+ else:
+ self.logMsg("No token found.", 1)
+ return ""
+
+ def getSSLverify(self):
+ # Verify host certificate
+ settings = utils.settings
+
+ s_sslverify = settings('sslverify')
+ if settings('altip') == "true":
+ s_sslverify = settings('secondsslverify')
+
+ if s_sslverify == "true":
+ return True
+ else:
+ return False
+
+ def getSSL(self):
+ # Client side certificate
+ settings = utils.settings
+
+ s_cert = settings('sslcert')
+ if settings('altip') == "true":
+ s_cert = settings('secondsslcert')
+
+ if s_cert == "None":
+ return None
+ else:
+ return s_cert
+
+ def setUserPref(self):
+
+ doUtils = self.doUtils.downloadUrl
+
+ result = doUtils("{server}/emby/Users/{UserId}?format=json")
+ self.userSettings = result
+ # Set user image for skin display
+ if result.get('PrimaryImageTag'):
+ utils.window('EmbyUserImage', value=artwork.Artwork().getUserArtwork(result['Id'], 'Primary'))
+
+ # Set resume point max
+ result = doUtils("{server}/emby/System/Configuration?format=json")
+
+ utils.settings('markPlayed', value=str(result['MaxResumePct']))
+
+ def getPublicUsers(self):
+ # Get public Users
+ result = self.doUtils.downloadUrl("%s/emby/Users/Public?format=json" % self.getServer(), authenticate=False)
+ if result != "":
+ return result
+ else:
+ # Server connection failed
+ return False
+
+
+ def hasAccess(self):
+ # hasAccess is verified in service.py
+ window = utils.window
+
+ result = self.doUtils.downloadUrl("{server}/emby/Users?format=json")
+
+ if result == False:
+ # Access is restricted, set in downloadutils.py via exception
+ self.logMsg("Access is restricted.", 1)
+ self.HasAccess = False
+
+ elif window('emby_online') != "true":
+ # Server connection failed
+ pass
+
+ elif window('emby_serverStatus') == "restricted":
+ self.logMsg("Access is granted.", 1)
+ self.HasAccess = True
+ window('emby_serverStatus', clear=True)
+ xbmcgui.Dialog().notification("Emby for Kodi", utils.language(33007))
+
+ def loadCurrUser(self, authenticated=False):
+
+ window = utils.window
+
+ doUtils = self.doUtils
+ username = self.getUsername()
+ userId = self.getUserId()
+
+ # Only to be used if token exists
+ self.currUserId = userId
+ self.currServer = self.getServer()
+ self.currToken = self.getToken()
+ self.ssl = self.getSSLverify()
+ self.sslcert = self.getSSL()
+
+ # Test the validity of current token
+ if authenticated == False:
+ url = "%s/emby/Users/%s?format=json" % (self.currServer, userId)
+ window('emby_currUser', value=userId)
+ window('emby_accessToken%s' % userId, value=self.currToken)
+ result = doUtils.downloadUrl(url)
+
+ if result == 401:
+ # Token is no longer valid
+ self.resetClient()
+ return False
+
+ # Set to windows property
+ window('emby_currUser', value=userId)
+ window('emby_accessToken%s' % userId, value=self.currToken)
+ window('emby_server%s' % userId, value=self.currServer)
+ window('emby_server_%s' % userId, value=self.getServer(prefix=False))
+
+ # Set DownloadUtils values
+ doUtils.setUsername(username)
+ doUtils.setUserId(self.currUserId)
+ doUtils.setServer(self.currServer)
+ doUtils.setToken(self.currToken)
+ doUtils.setSSL(self.ssl, self.sslcert)
+ # parental control - let's verify if access is restricted
+ self.hasAccess()
+ # Start DownloadUtils session
+ doUtils.startSession()
+ self.getAdditionalUsers()
+ # Set user preferences in settings
+ self.currUser = username
+ self.setUserPref()
+
+
+ def authenticate(self):
+
+ lang = utils.language
+ window = utils.window
+ settings = utils.settings
+ dialog = xbmcgui.Dialog()
+
+ # Get /profile/addon_data
+ addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8')
+ hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir)
+
+ username = self.getUsername()
+ server = self.getServer()
+
+ # If there's no settings.xml
+ if not hasSettings:
+ self.logMsg("No settings.xml found.", 1)
+ self.auth = False
+ return
+ # If no user information
+ elif not server or not username:
+ self.logMsg("Missing server information.", 1)
+ self.auth = False
+ return
+ # If there's a token, load the user
+ elif self.getToken():
+ result = self.loadCurrUser()
+
+ if result is False:
+ pass
+ else:
+ self.logMsg("Current user: %s" % self.currUser, 1)
+ self.logMsg("Current userId: %s" % self.currUserId, 1)
+ self.logMsg("Current accessToken: %s" % self.currToken, 2)
+ return
+
+ ##### AUTHENTICATE USER #####
+
+ users = self.getPublicUsers()
+ password = ""
+
+ # Find user in list
+ for user in users:
+ name = user['Name']
+
+ if username.decode('utf-8') in name:
+ # If user has password
+ if user['HasPassword'] == True:
+ password = dialog.input(
+ heading="%s %s" % (lang(33008), username.decode('utf-8')),
+ option=xbmcgui.ALPHANUM_HIDE_INPUT)
+ # If password dialog is cancelled
+ if not password:
+ self.logMsg("No password entered.", 0)
+ window('emby_serverStatus', value="Stop")
+ self.auth = False
+ return
+ break
+ else:
+ # Manual login, user is hidden
+ password = dialog.input(
+ heading="%s %s" % (lang(33008), username),
+ option=xbmcgui.ALPHANUM_HIDE_INPUT)
+ sha1 = hashlib.sha1(password)
+ sha1 = sha1.hexdigest()
+
+ # Authenticate username and password
+ data = {'username': username, 'password': sha1}
+ self.logMsg(data, 2)
+
+ result = self.doUtils.downloadUrl("%s/emby/Users/AuthenticateByName?format=json" % server, postBody=data, action_type="POST", authenticate=False)
+
+ try:
+ self.logMsg("Auth response: %s" % result, 1)
+ accessToken = result['AccessToken']
+
+ except (KeyError, TypeError):
+ self.logMsg("Failed to retrieve the api key.", 1)
+ accessToken = None
+
+ if accessToken is not None:
+ self.currUser = username
+ dialog.notification("Emby for Kodi",
+ "%s %s!" % (lang(33000), self.currUser.decode('utf-8')))
+ settings('accessToken', value=accessToken)
+ settings('userId%s' % username, value=result['User']['Id'])
+ self.logMsg("User Authenticated: %s" % accessToken, 1)
+ self.loadCurrUser(authenticated=True)
+ window('emby_serverStatus', clear=True)
+ self.retry = 0
+ else:
+ self.logMsg("User authentication failed.", 1)
+ settings('accessToken', value="")
+ settings('userId%s' % username, value="")
+ dialog.ok(lang(33001), lang(33009))
+
+ # Give two attempts at entering password
+ if self.retry == 2:
+ self.logMsg("Too many retries. "
+ "You can retry by resetting attempts in the addon settings.", 1)
+ window('emby_serverStatus', value="Stop")
+ dialog.ok(lang(33001), lang(33010))
+
+ self.retry += 1
+ self.auth = False
+
+ def resetClient(self):
+
+ self.logMsg("Reset UserClient authentication.", 1)
+ if self.currToken is not None:
+ # In case of 401, removed saved token
+ utils.settings('accessToken', value="")
+ utils.window('emby_accessToken%s' % self.getUserId(), clear=True)
+ self.currToken = None
+ self.logMsg("User token has been removed.", 1)
+
+ self.auth = True
+ self.currUser = None
+
+ def run(self):
+
+ window = utils.window
+
+ monitor = xbmc.Monitor()
+ self.logMsg("----===## Starting UserClient ##===----", 0)
+
+ while not monitor.abortRequested():
+
+ status = window('emby_serverStatus')
+ if status:
+ # Verify the connection status to server
+ if status == "restricted":
+ # Parental control is restricting access
+ self.HasAccess = False
+
+ elif status == "401":
+ # Unauthorized access, revoke token
+ window('emby_serverStatus', value="Auth")
+ self.resetClient()
+
+ if self.auth and (self.currUser is None):
+ # Try to authenticate user
+ status = window('emby_serverStatus')
+ if not status or status == "Auth":
+ # Set auth flag because we no longer need
+ # to authenticate the user
+ self.auth = False
+ self.authenticate()
+
+
+ if not self.auth and (self.currUser is None):
+ # If authenticate failed.
+ server = self.getServer()
+ username = self.getUsername()
+ status = window('emby_serverStatus')
+
+ # The status Stop is for when user cancelled password dialog.
+ if server and username and status != "Stop":
+ # Only if there's information found to login
+ self.logMsg("Server found: %s" % server, 2)
+ self.logMsg("Username found: %s" % username, 2)
+ self.auth = True
+
+
+ if self.stopClient == True:
+ # If stopping the client didn't work
+ break
+
+ if monitor.waitForAbort(1):
+ # Abort was requested while waiting. We should exit
+ break
+
+ self.doUtils.stopSession()
+ self.logMsg("##===---- UserClient Stopped ----===##", 0)
+
+ def stopClient(self):
+ # When emby for kodi terminates
self.stopClient = True
\ No newline at end of file
diff --git a/resources/lib/utils.py b/resources/lib/utils.py
index c9b3474f..a8b97c0b 100644
--- a/resources/lib/utils.py
+++ b/resources/lib/utils.py
@@ -1,577 +1,564 @@
-# -*- coding: utf-8 -*-
-
-#################################################################################################
-
-import cProfile
-import inspect
-import json
-import pstats
-import sqlite3
-import StringIO
-import os
-from datetime import datetime, time
-import time
-import unicodedata
-import xml.etree.ElementTree as etree
-
-import xbmc
-import xbmcaddon
-import xbmcgui
-import xbmcvfs
-
-#################################################################################################
-
-
-def logMsg(title, msg, level=1):
-
- # Get the logLevel set in UserClient
- try:
- logLevel = int(window('emby_logLevel'))
- except ValueError:
- logLevel = 0
-
- if logLevel >= level:
-
- if logLevel == 2: # inspect.stack() is expensive
- try:
- xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg))
- except UnicodeEncodeError:
- xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg.encode('utf-8')))
- else:
- try:
- xbmc.log("%s -> %s" % (title, msg))
- except UnicodeEncodeError:
- xbmc.log("%s -> %s" % (title, msg.encode('utf-8')))
-
-def window(property, value=None, clear=False, windowid=10000):
- # Get or set window property
- WINDOW = xbmcgui.Window(windowid)
-
- #setproperty accepts both string and unicode but utf-8 strings are adviced by kodi devs because some unicode can give issues
- '''if isinstance(property, unicode):
- property = property.encode("utf-8")
- if isinstance(value, unicode):
- value = value.encode("utf-8")'''
-
- if clear:
- WINDOW.clearProperty(property)
- elif value is not None:
- WINDOW.setProperty(property, value)
- else: #getproperty returns string so convert to unicode
- return WINDOW.getProperty(property)#.decode("utf-8")
-
-def settings(setting, value=None):
- # Get or add addon setting
- addon = xbmcaddon.Addon(id='plugin.video.emby')
-
- if value is not None:
- addon.setSetting(setting, value)
- else:
- return addon.getSetting(setting) #returns unicode object
-
-def language(stringid):
- # Central string retrieval
- addon = xbmcaddon.Addon(id='plugin.video.emby')
- string = addon.getLocalizedString(stringid) #returns unicode object
- return string
-
-def kodiSQL(type="video"):
-
- if type == "emby":
- dbPath = xbmc.translatePath("special://database/emby.db").decode('utf-8')
- elif type == "music":
- dbPath = getKodiMusicDBPath()
- elif type == "texture":
- dbPath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8')
- else:
- dbPath = getKodiVideoDBPath()
-
- connection = sqlite3.connect(dbPath)
- return connection
-
-def getKodiVideoDBPath():
-
- kodibuild = xbmc.getInfoLabel('System.BuildVersion')[:2]
- dbVersion = {
-
- "13": 78, # Gotham
- "14": 90, # Helix
- "15": 93, # Isengard
- "16": 99 # Jarvis
- }
-
- dbPath = xbmc.translatePath(
- "special://database/MyVideos%s.db"
- % dbVersion.get(kodibuild, "")).decode('utf-8')
- return dbPath
-
-def getKodiMusicDBPath():
-
- kodibuild = xbmc.getInfoLabel('System.BuildVersion')[:2]
- dbVersion = {
-
- "13": 46, # Gotham
- "14": 48, # Helix
- "15": 52, # Isengard
- "16": 56 # Jarvis
- }
-
- dbPath = xbmc.translatePath(
- "special://database/MyMusic%s.db"
- % dbVersion.get(kodibuild, "")).decode('utf-8')
- return dbPath
-
-def getScreensaver():
- # Get the current screensaver value
- query = {
-
- 'jsonrpc': "2.0",
- 'id': 0,
- 'method': "Settings.getSettingValue",
- 'params': {
-
- 'setting': "screensaver.mode"
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(query))
- result = json.loads(result)
- screensaver = result['result']['value']
-
- return screensaver
-
-def setScreensaver(value):
- # Toggle the screensaver
- query = {
-
- 'jsonrpc': "2.0",
- 'id': 0,
- 'method': "Settings.setSettingValue",
- 'params': {
-
- 'setting': "screensaver.mode",
- 'value': value
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(query))
- logMsg("EMBY", "Toggling screensaver: %s %s" % (value, result), 1)
-
-def reset():
-
- dialog = xbmcgui.Dialog()
-
- resp = dialog.yesno("Warning", "Are you sure you want to reset your local Kodi database?")
- if resp == 0:
- return
-
- # first stop any db sync
- window('emby_shouldStop', value="true")
- count = 10
- while window('emby_dbScan') == "true":
- logMsg("EMBY", "Sync is running, will retry: %s..." % count)
- count -= 1
- if count == 0:
- dialog.ok("Warning", "Could not stop the database from running. Try again.")
- return
- xbmc.sleep(1000)
-
- # Clean up the playlists
- deletePlaylists()
-
- # Clean up the video nodes
- deleteNodes()
-
- # Wipe the kodi databases
- logMsg("EMBY", "Resetting the Kodi video database.", 0)
- connection = kodiSQL('video')
- cursor = connection.cursor()
- cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
- rows = cursor.fetchall()
- for row in rows:
- tablename = row[0]
- if tablename != "version":
- cursor.execute("DELETE FROM " + tablename)
- connection.commit()
- cursor.close()
-
- if settings('enableMusic') == "true":
- logMsg("EMBY", "Resetting the Kodi music database.")
- connection = kodiSQL('music')
- cursor = connection.cursor()
- cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
- rows = cursor.fetchall()
- for row in rows:
- tablename = row[0]
- if tablename != "version":
- cursor.execute("DELETE FROM " + tablename)
- connection.commit()
- cursor.close()
-
- # Wipe the emby database
- logMsg("EMBY", "Resetting the Emby database.", 0)
- connection = kodiSQL('emby')
- cursor = connection.cursor()
- cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
- rows = cursor.fetchall()
- for row in rows:
- tablename = row[0]
- if tablename != "version":
- cursor.execute("DELETE FROM " + tablename)
- cursor.execute('DROP table IF EXISTS emby')
- cursor.execute('DROP table IF EXISTS view')
- connection.commit()
- cursor.close()
-
- # Offer to wipe cached thumbnails
- resp = dialog.yesno("Warning", "Removed all cached artwork?")
- if resp:
- logMsg("EMBY", "Resetting all cached artwork.", 0)
- # Remove all existing textures first
- path = xbmc.translatePath("special://thumbnails/").decode('utf-8')
- if xbmcvfs.exists(path):
- allDirs, allFiles = xbmcvfs.listdir(path)
- for dir in allDirs:
- allDirs, allFiles = xbmcvfs.listdir(path+dir)
- for file in allFiles:
- if os.path.supports_unicode_filenames:
- xbmcvfs.delete(os.path.join(path+dir.decode('utf-8'),file.decode('utf-8')))
- else:
- xbmcvfs.delete(os.path.join(path.encode('utf-8')+dir,file))
-
- # remove all existing data from texture DB
- connection = kodiSQL('texture')
- cursor = connection.cursor()
- cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
- rows = cursor.fetchall()
- for row in rows:
- tableName = row[0]
- if(tableName != "version"):
- cursor.execute("DELETE FROM " + tableName)
- connection.commit()
- cursor.close()
-
- # reset the install run flag
- settings('SyncInstallRunDone', value="false")
-
- # Remove emby info
- resp = dialog.yesno("Warning", "Reset all Emby Addon settings?")
- if resp:
- # Delete the settings
- addon = xbmcaddon.Addon()
- addondir = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8')
- dataPath = "%ssettings.xml" % addondir
- xbmcvfs.delete(dataPath)
- logMsg("EMBY", "Deleting: settings.xml", 1)
-
- dialog.ok(
- heading="Emby for Kodi",
- line1="Database reset has completed, Kodi will now restart to apply the changes.")
- xbmc.executebuiltin('RestartApp')
-
-def profiling(sortby="cumulative"):
- # Will print results to Kodi log
- def decorator(func):
- def wrapper(*args, **kwargs):
-
- pr = cProfile.Profile()
-
- pr.enable()
- result = func(*args, **kwargs)
- pr.disable()
-
- s = StringIO.StringIO()
- ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
- ps.print_stats()
- logMsg("EMBY Profiling", s.getvalue(), 1)
-
- return result
-
- return wrapper
- return decorator
-
-def convertdate(date):
- try:
- date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ")
- except TypeError:
- # TypeError: attribute of type 'NoneType' is not callable
- # Known Kodi/python error
- date = datetime(*(time.strptime(date, "%Y-%m-%dT%H:%M:%SZ")[0:6]))
-
- return date
-
-def normalize_nodes(text):
- # For video nodes
- text = text.replace(":", "")
- text = text.replace("/", "-")
- text = text.replace("\\", "-")
- text = text.replace("<", "")
- text = text.replace(">", "")
- text = text.replace("*", "")
- text = text.replace("?", "")
- text = text.replace('|', "")
- text = text.replace('(', "")
- text = text.replace(')', "")
- text = text.strip()
- # Remove dots from the last character as windows can not have directories
- # with dots at the end
- text = text.rstrip('.')
- text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore')
-
- return text
-
-def normalize_string(text):
- # For theme media, do not modify unless
- # modified in TV Tunes
- text = text.replace(":", "")
- text = text.replace("/", "-")
- text = text.replace("\\", "-")
- text = text.replace("<", "")
- text = text.replace(">", "")
- text = text.replace("*", "")
- text = text.replace("?", "")
- text = text.replace('|', "")
- text = text.strip()
- # Remove dots from the last character as windows can not have directories
- # with dots at the end
- text = text.rstrip('.')
- text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore')
-
- return text
-
-def indent(elem, level=0):
- # Prettify xml trees
- i = "\n" + level*" "
- if len(elem):
- if not elem.text or not elem.text.strip():
- elem.text = i + " "
- if not elem.tail or not elem.tail.strip():
- elem.tail = i
- for elem in elem:
- indent(elem, level+1)
- if not elem.tail or not elem.tail.strip():
- elem.tail = i
- else:
- if level and (not elem.tail or not elem.tail.strip()):
- elem.tail = i
-
-def sourcesXML():
- # To make Master lock compatible
- path = xbmc.translatePath("special://profile/").decode('utf-8')
- xmlpath = "%ssources.xml" % path
-
- try:
- xmlparse = etree.parse(xmlpath)
- except: # Document is blank or missing
- root = etree.Element('sources')
- else:
- root = xmlparse.getroot()
-
-
- video = root.find('video')
- if video is None:
- video = etree.SubElement(root, 'video')
- etree.SubElement(video, 'default', attrib={'pathversion': "1"})
-
- # Add elements
- count = 2
- for source in root.findall('.//path'):
- if source.text == "smb://":
- count -= 1
-
- if count == 0:
- # sources already set
- break
- else:
- # Missing smb:// occurences, re-add.
- for i in range(0, count):
- source = etree.SubElement(video, 'source')
- etree.SubElement(source, 'name').text = "Emby"
- etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = "smb://"
- etree.SubElement(source, 'allowsharing').text = "true"
- # Prettify and write to file
- try:
- indent(root)
- except: pass
- etree.ElementTree(root).write(xmlpath)
-
-def passwordsXML():
-
- # To add network credentials
- path = xbmc.translatePath("special://userdata/").decode('utf-8')
- xmlpath = "%spasswords.xml" % path
-
- try:
- xmlparse = etree.parse(xmlpath)
- except: # Document is blank or missing
- root = etree.Element('passwords')
- else:
- root = xmlparse.getroot()
-
- dialog = xbmcgui.Dialog()
- credentials = settings('networkCreds')
- if credentials:
- # Present user with options
- option = dialog.select("Modify/Remove network credentials", ["Modify", "Remove"])
-
- if option < 0:
- # User cancelled dialog
- return
-
- elif option == 1:
- # User selected remove
- iterator = root.getiterator('passwords')
-
- for paths in iterator:
- for path in paths:
- if path.find('.//from').text == "smb://%s/" % credentials:
- paths.remove(path)
- logMsg("EMBY", "Successfully removed credentials for: %s"
- % credentials, 1)
- etree.ElementTree(root).write(xmlpath)
- break
- else:
- logMsg("EMBY", "Failed to find saved server: %s in passwords.xml" % credentials, 1)
-
- settings('networkCreds', value="")
- xbmcgui.Dialog().notification(
- heading="Emby for Kodi",
- message="%s removed from passwords.xml" % credentials,
- icon="special://home/addons/plugin.video.emby/icon.png",
- time=1000,
- sound=False)
- return
-
- elif option == 0:
- # User selected to modify
- server = dialog.input("Modify the computer name or ip address", credentials)
- if not server:
- return
- else:
- # No credentials added
- dialog.ok(
- heading="Network credentials",
- line1= (
- "Input the server name or IP address as indicated in your emby library paths. "
- 'For example, the server name: \\\\SERVER-PC\\path\\ is "SERVER-PC".'))
- server = dialog.input("Enter the server name or IP address")
- if not server:
- return
-
- # Network username
- user = dialog.input("Enter the network username")
- if not user:
- return
- # Network password
- password = dialog.input(
- heading="Enter the network password",
- option=xbmcgui.ALPHANUM_HIDE_INPUT)
- if not password:
- return
-
- # Add elements
- for path in root.findall('.//path'):
- if path.find('.//from').text.lower() == "smb://%s/" % server.lower():
- # Found the server, rewrite credentials
- path.find('.//to').text = "smb://%s:%s@%s/" % (user, password, server)
- break
- else:
- # Server not found, add it.
- path = etree.SubElement(root, 'path')
- etree.SubElement(path, 'from', attrib={'pathversion': "1"}).text = "smb://%s/" % server
- topath = "smb://%s:%s@%s/" % (user, password, server)
- etree.SubElement(path, 'to', attrib={'pathversion': "1"}).text = topath
- # Force Kodi to see the credentials without restarting
- xbmcvfs.exists(topath)
-
- # Add credentials
- settings('networkCreds', value="%s" % server)
- logMsg("EMBY", "Added server: %s to passwords.xml" % server, 1)
- # Prettify and write to file
- try:
- indent(root)
- except: pass
- etree.ElementTree(root).write(xmlpath)
-
- dialog.notification(
- heading="Emby for Kodi",
- message="%s added to passwords.xml" % server,
- icon="special://home/addons/plugin.video.emby/icon.png",
- time=1000,
- sound=False)
-
-def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False):
- # Tagname is in unicode - actions: add or delete
- tagname = tagname.encode('utf-8')
-
- path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
- if viewtype == "mixed":
- plname = "%s - %s" % (tagname, mediatype)
- xsppath = "%sEmby %s - %s.xsp" % (path, viewid, mediatype)
- else:
- plname = tagname
- xsppath = "%sEmby %s.xsp" % (path, viewid)
-
- # Create the playlist directory
- if not xbmcvfs.exists(path):
- logMsg("EMBY", "Creating directory: %s" % path, 1)
- xbmcvfs.mkdirs(path)
-
- # Only add the playlist if it doesn't already exists
- if xbmcvfs.exists(xsppath):
-
- if delete:
- xbmcvfs.delete(xsppath)
- logMsg("EMBY", "Successfully removed playlist: %s." % tagname, 1)
-
- return
-
- # Using write process since there's no guarantee the xml declaration works with etree
- itemtypes = {
- 'homevideos': "movies"
- }
- logMsg("EMBY", "Writing playlist file to: %s" % xsppath, 1)
- try:
- f = xbmcvfs.File(xsppath, 'w')
- except:
- logMsg("EMBY", "Failed to create playlist: %s" % xsppath, 1)
- return
- else:
- f.write(
- '\n'
- '\n\t'
- 'Emby %s\n\t'
- 'all\n\t'
- '\n\t\t'
- '%s\n\t'
- ''
- ''
- % (itemtypes.get(mediatype, mediatype), plname, tagname))
- f.close()
- logMsg("EMBY", "Successfully added playlist: %s" % tagname)
-
-def deletePlaylists():
-
- # Clean up the playlists
- path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
- dirs, files = xbmcvfs.listdir(path)
- for file in files:
- if file.decode('utf-8').startswith('Emby'):
- xbmcvfs.delete("%s%s" % (path, file))
-
-def deleteNodes():
-
- # Clean up video nodes
- import shutil
- path = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
- dirs, files = xbmcvfs.listdir(path)
- for dir in dirs:
- if dir.decode('utf-8').startswith('Emby'):
- try:
- shutil.rmtree("%s%s" % (path, dir.decode('utf-8')))
- except:
- logMsg("EMBY", "Failed to delete directory: %s" % dir.decode('utf-8'))
- for file in files:
- if file.decode('utf-8').startswith('emby'):
- try:
- xbmcvfs.delete("%s%s" % (path, file.decode('utf-8')))
- except:
+# -*- coding: utf-8 -*-
+
+#################################################################################################
+
+import cProfile
+import inspect
+import json
+import pstats
+import sqlite3
+import StringIO
+import os
+from datetime import datetime, time
+import time
+import unicodedata
+import xml.etree.ElementTree as etree
+
+import xbmc
+import xbmcaddon
+import xbmcgui
+import xbmcvfs
+
+#################################################################################################
+
+
+def logMsg(title, msg, level=1):
+
+ # Get the logLevel set in UserClient
+ try:
+ logLevel = int(window('emby_logLevel'))
+ except ValueError:
+ logLevel = 0
+
+ if logLevel >= level:
+
+ if logLevel == 2: # inspect.stack() is expensive
+ try:
+ xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg))
+ except UnicodeEncodeError:
+ xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg.encode('utf-8')))
+ else:
+ try:
+ xbmc.log("%s -> %s" % (title, msg))
+ except UnicodeEncodeError:
+ xbmc.log("%s -> %s" % (title, msg.encode('utf-8')))
+
+def window(property, value=None, clear=False, windowid=10000):
+ # Get or set window property
+ WINDOW = xbmcgui.Window(windowid)
+
+ #setproperty accepts both string and unicode but utf-8 strings are adviced by kodi devs because some unicode can give issues
+ '''if isinstance(property, unicode):
+ property = property.encode("utf-8")
+ if isinstance(value, unicode):
+ value = value.encode("utf-8")'''
+
+ if clear:
+ WINDOW.clearProperty(property)
+ elif value is not None:
+ WINDOW.setProperty(property, value)
+ else: #getproperty returns string so convert to unicode
+ return WINDOW.getProperty(property)#.decode("utf-8")
+
+def settings(setting, value=None):
+ # Get or add addon setting
+ if value is not None:
+ xbmcaddon.Addon(id='plugin.video.metaman').setSetting(setting, value)
+ else:
+ return xbmcaddon.Addon(id='plugin.video.metaman').getSetting(setting) #returns unicode object
+
+def language(stringid):
+ # Central string retrieval
+ string = xbmcaddon.Addon(id='plugin.video.emby').getLocalizedString(stringid) #returns unicode object
+ return string
+
+def kodiSQL(media_type="video"):
+
+ if media_type == "emby":
+ dbPath = xbmc.translatePath("special://database/emby.db").decode('utf-8')
+ elif media_type == "music":
+ dbPath = getKodiMusicDBPath()
+ elif media_type == "texture":
+ dbPath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8')
+ else:
+ dbPath = getKodiVideoDBPath()
+
+ connection = sqlite3.connect(dbPath)
+ return connection
+
+def getKodiVideoDBPath():
+
+ dbVersion = {
+
+ "13": 78, # Gotham
+ "14": 90, # Helix
+ "15": 93, # Isengard
+ "16": 99 # Jarvis
+ }
+
+ dbPath = xbmc.translatePath(
+ "special://database/MyVideos%s.db"
+ % dbVersion.get(xbmc.getInfoLabel('System.BuildVersion')[:2], "")).decode('utf-8')
+ return dbPath
+
+def getKodiMusicDBPath():
+
+ dbVersion = {
+
+ "13": 46, # Gotham
+ "14": 48, # Helix
+ "15": 52, # Isengard
+ "16": 56 # Jarvis
+ }
+
+ dbPath = xbmc.translatePath(
+ "special://database/MyMusic%s.db"
+ % dbVersion.get(xbmc.getInfoLabel('System.BuildVersion')[:2], "")).decode('utf-8')
+ return dbPath
+
+def getScreensaver():
+ # Get the current screensaver value
+ query = {
+
+ 'jsonrpc': "2.0",
+ 'id': 0,
+ 'method': "Settings.getSettingValue",
+ 'params': {
+
+ 'setting': "screensaver.mode"
+ }
+ }
+ return json.loads(xbmc.executeJSONRPC(json.dumps(query)))['result']['value']
+
+def setScreensaver(value):
+ # Toggle the screensaver
+ query = {
+
+ 'jsonrpc': "2.0",
+ 'id': 0,
+ 'method': "Settings.setSettingValue",
+ 'params': {
+
+ 'setting': "screensaver.mode",
+ 'value': value
+ }
+ }
+ logMsg("EMBY", "Toggling screensaver: %s %s" % (value, xbmc.executeJSONRPC(json.dumps(query))), 1)
+
+def reset():
+
+ dialog = xbmcgui.Dialog()
+
+ if dialog.yesno("Warning", "Are you sure you want to reset your local Kodi database?") == 0:
+ return
+
+ # first stop any db sync
+ window('emby_shouldStop', value="true")
+ count = 10
+ while window('emby_dbScan') == "true":
+ logMsg("EMBY", "Sync is running, will retry: %s..." % count)
+ count -= 1
+ if count == 0:
+ dialog.ok("Warning", "Could not stop the database from running. Try again.")
+ return
+ xbmc.sleep(1000)
+
+ # Clean up the playlists
+ deletePlaylists()
+
+ # Clean up the video nodes
+ deleteNodes()
+
+ # Wipe the kodi databases
+ logMsg("EMBY", "Resetting the Kodi video database.", 0)
+ connection = kodiSQL('video')
+ cursor = connection.cursor()
+ cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
+ rows = cursor.fetchall()
+ for row in rows:
+ tablename = row[0]
+ if tablename != "version":
+ cursor.execute("DELETE FROM " + tablename)
+ connection.commit()
+ cursor.close()
+
+ if settings('enableMusic') == "true":
+ logMsg("EMBY", "Resetting the Kodi music database.")
+ connection = kodiSQL('music')
+ cursor = connection.cursor()
+ cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
+ rows = cursor.fetchall()
+ for row in rows:
+ tablename = row[0]
+ if tablename != "version":
+ cursor.execute("DELETE FROM " + tablename)
+ connection.commit()
+ cursor.close()
+
+ # Wipe the emby database
+ logMsg("EMBY", "Resetting the Emby database.", 0)
+ connection = kodiSQL('emby')
+ cursor = connection.cursor()
+ cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
+ rows = cursor.fetchall()
+ for row in rows:
+ tablename = row[0]
+ if tablename != "version":
+ cursor.execute("DELETE FROM " + tablename)
+ cursor.execute('DROP table IF EXISTS emby')
+ cursor.execute('DROP table IF EXISTS view')
+ connection.commit()
+ cursor.close()
+
+ # Offer to wipe cached thumbnails
+ resp = dialog.yesno("Warning", "Remove all cached artwork?")
+ if resp:
+ logMsg("EMBY", "Resetting all cached artwork.", 0)
+ # Remove all existing textures first
+ path = xbmc.translatePath("special://thumbnails/").decode('utf-8')
+ if xbmcvfs.exists(path):
+ allDirs, allFiles = xbmcvfs.listdir(path)
+ for dir in allDirs:
+ allDirs, allFiles = xbmcvfs.listdir(path+dir)
+ for file in allFiles:
+ if os.path.supports_unicode_filenames:
+ xbmcvfs.delete(os.path.join(path+dir.decode('utf-8'),file.decode('utf-8')))
+ else:
+ xbmcvfs.delete(os.path.join(path.encode('utf-8')+dir,file))
+
+ # remove all existing data from texture DB
+ connection = kodiSQL('texture')
+ cursor = connection.cursor()
+ cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
+ rows = cursor.fetchall()
+ for row in rows:
+ tableName = row[0]
+ if(tableName != "version"):
+ cursor.execute("DELETE FROM " + tableName)
+ connection.commit()
+ cursor.close()
+
+ # reset the install run flag
+ settings('SyncInstallRunDone', value="false")
+
+ # Remove emby info
+ resp = dialog.yesno("Warning", "Reset all Emby Addon settings?")
+ if resp:
+ # Delete the settings
+ addon = xbmcaddon.Addon()
+ addondir = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8')
+ dataPath = "%ssettings.xml" % addondir
+ xbmcvfs.delete(dataPath)
+ logMsg("EMBY", "Deleting: settings.xml", 1)
+
+ dialog.ok(
+ heading="Emby for Kodi",
+ line1="Database reset has completed, Kodi will now restart to apply the changes.")
+ xbmc.executebuiltin('RestartApp')
+
+def profiling(sortby="cumulative"):
+ # Will print results to Kodi log
+ def decorator(func):
+ def wrapper(*args, **kwargs):
+
+ pr = cProfile.Profile()
+
+ pr.enable()
+ result = func(*args, **kwargs)
+ pr.disable()
+
+ s = StringIO.StringIO()
+ ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
+ ps.print_stats()
+ logMsg("EMBY Profiling", s.getvalue(), 1)
+
+ return result
+
+ return wrapper
+ return decorator
+
+def convertdate(date):
+ try:
+ date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ")
+ except TypeError:
+ # TypeError: attribute of type 'NoneType' is not callable
+ # Known Kodi/python error
+ date = datetime(*(time.strptime(date, "%Y-%m-%dT%H:%M:%SZ")[0:6]))
+
+ return date
+
+def normalize_nodes(text):
+ # For video nodes
+ text = text.replace(":", "")
+ text = text.replace("/", "-")
+ text = text.replace("\\", "-")
+ text = text.replace("<", "")
+ text = text.replace(">", "")
+ text = text.replace("*", "")
+ text = text.replace("?", "")
+ text = text.replace('|', "")
+ text = text.replace('(', "")
+ text = text.replace(')', "")
+ text = text.strip()
+ # Remove dots from the last character as windows can not have directories
+ # with dots at the end
+ text = text.rstrip('.')
+ text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore')
+
+ return text
+
+def normalize_string(text):
+ # For theme media, do not modify unless
+ # modified in TV Tunes
+ text = text.replace(":", "")
+ text = text.replace("/", "-")
+ text = text.replace("\\", "-")
+ text = text.replace("<", "")
+ text = text.replace(">", "")
+ text = text.replace("*", "")
+ text = text.replace("?", "")
+ text = text.replace('|', "")
+ text = text.strip()
+ # Remove dots from the last character as windows can not have directories
+ # with dots at the end
+ text = text.rstrip('.')
+ text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore')
+
+ return text
+
+def indent(elem, level=0):
+ # Prettify xml trees
+ i = "\n" + level*" "
+ if len(elem):
+ if not elem.text or not elem.text.strip():
+ elem.text = i + " "
+ if not elem.tail or not elem.tail.strip():
+ elem.tail = i
+ for elem in elem:
+ indent(elem, level+1)
+ if not elem.tail or not elem.tail.strip():
+ elem.tail = i
+ else:
+ if level and (not elem.tail or not elem.tail.strip()):
+ elem.tail = i
+
+def sourcesXML():
+ # To make Master lock compatible
+ path = xbmc.translatePath("special://profile/").decode('utf-8')
+ xmlpath = "%ssources.xml" % path
+
+ try:
+ xmlparse = etree.parse(xmlpath)
+ except: # Document is blank or missing
+ root = etree.Element('sources')
+ else:
+ root = xmlparse.getroot()
+
+
+ video = root.find('video')
+ if video is None:
+ video = etree.SubElement(root, 'video')
+ etree.SubElement(video, 'default', attrib={'pathversion': "1"})
+
+ # Add elements
+ count = 2
+ for source in root.findall('.//path'):
+ if source.text == "smb://":
+ count -= 1
+
+ if count == 0:
+ # sources already set
+ break
+ else:
+ # Missing smb:// occurences, re-add.
+ for i in range(0, count):
+ source = etree.SubElement(video, 'source')
+ etree.SubElement(source, 'name').text = "Emby"
+ etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = "smb://"
+ etree.SubElement(source, 'allowsharing').text = "true"
+ # Prettify and write to file
+ try:
+ indent(root)
+ except: pass
+ etree.ElementTree(root).write(xmlpath)
+
+def passwordsXML():
+
+ # To add network credentials
+ path = xbmc.translatePath("special://userdata/").decode('utf-8')
+ xmlpath = "%spasswords.xml" % path
+
+ try:
+ xmlparse = etree.parse(xmlpath)
+ except: # Document is blank or missing
+ root = etree.Element('passwords')
+ else:
+ root = xmlparse.getroot()
+
+ dialog = xbmcgui.Dialog()
+ credentials = settings('networkCreds')
+ if credentials:
+ # Present user with options
+ option = dialog.select("Modify/Remove network credentials", ["Modify", "Remove"])
+
+ if option < 0:
+ # User cancelled dialog
+ return
+
+ elif option == 1:
+ # User selected remove
+ for paths in root.getiterator('passwords'):
+ for path in paths:
+ if path.find('.//from').text == "smb://%s/" % credentials:
+ paths.remove(path)
+ logMsg("EMBY", "Successfully removed credentials for: %s"
+ % credentials, 1)
+ etree.ElementTree(root).write(xmlpath)
+ break
+ else:
+ logMsg("EMBY", "Failed to find saved server: %s in passwords.xml" % credentials, 1)
+
+ settings('networkCreds', value="")
+ xbmcgui.Dialog().notification(
+ heading="Emby for Kodi",
+ message="%s removed from passwords.xml" % credentials,
+ icon="special://home/addons/plugin.video.emby/icon.png",
+ time=1000,
+ sound=False)
+ return
+
+ elif option == 0:
+ # User selected to modify
+ server = dialog.input("Modify the computer name or ip address", credentials)
+ if not server:
+ return
+ else:
+ # No credentials added
+ dialog.ok(
+ heading="Network credentials",
+ line1= (
+ "Input the server name or IP address as indicated in your emby library paths. "
+ 'For example, the server name: \\\\SERVER-PC\\path\\ is "SERVER-PC".'))
+ server = dialog.input("Enter the server name or IP address")
+ if not server:
+ return
+
+ # Network username
+ user = dialog.input("Enter the network username")
+ if not user:
+ return
+ # Network password
+ password = dialog.input(
+ heading="Enter the network password",
+ option=xbmcgui.ALPHANUM_HIDE_INPUT)
+ if not password:
+ return
+
+ # Add elements
+ for path in root.findall('.//path'):
+ if path.find('.//from').text.lower() == "smb://%s/" % server.lower():
+ # Found the server, rewrite credentials
+ path.find('.//to').text = "smb://%s:%s@%s/" % (user, password, server)
+ break
+ else:
+ # Server not found, add it.
+ path = etree.SubElement(root, 'path')
+ etree.SubElement(path, 'from', attrib={'pathversion': "1"}).text = "smb://%s/" % server
+ topath = "smb://%s:%s@%s/" % (user, password, server)
+ etree.SubElement(path, 'to', attrib={'pathversion': "1"}).text = topath
+ # Force Kodi to see the credentials without restarting
+ xbmcvfs.exists(topath)
+
+ # Add credentials
+ settings('networkCreds', value="%s" % server)
+ logMsg("EMBY", "Added server: %s to passwords.xml" % server, 1)
+ # Prettify and write to file
+ try:
+ indent(root)
+ except: pass
+ etree.ElementTree(root).write(xmlpath)
+
+ dialog.notification(
+ heading="Emby for Kodi",
+ message="%s added to passwords.xml" % server,
+ icon="special://home/addons/plugin.video.emby/icon.png",
+ time=1000,
+ sound=False)
+
+def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False):
+ # Tagname is in unicode - actions: add or delete
+ tagname = tagname.encode('utf-8')
+
+ path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
+ if viewtype == "mixed":
+ plname = "%s - %s" % (tagname, mediatype)
+ xsppath = "%sEmby %s - %s.xsp" % (path, viewid, mediatype)
+ else:
+ plname = tagname
+ xsppath = "%sEmby %s.xsp" % (path, viewid)
+
+ # Create the playlist directory
+ if not xbmcvfs.exists(path):
+ logMsg("EMBY", "Creating directory: %s" % path, 1)
+ xbmcvfs.mkdirs(path)
+
+ # Only add the playlist if it doesn't already exists
+ if xbmcvfs.exists(xsppath):
+
+ if delete:
+ xbmcvfs.delete(xsppath)
+ logMsg("EMBY", "Successfully removed playlist: %s." % tagname, 1)
+
+ return
+
+ # Using write process since there's no guarantee the xml declaration works with etree
+ itemtypes = {
+ 'homevideos': "movies"
+ }
+ logMsg("EMBY", "Writing playlist file to: %s" % xsppath, 1)
+ try:
+ f = xbmcvfs.File(xsppath, 'w')
+ except:
+ logMsg("EMBY", "Failed to create playlist: %s" % xsppath, 1)
+ return
+ else:
+ f.write(
+ '\n'
+ '\n\t'
+ 'Emby %s\n\t'
+ 'all\n\t'
+ '\n\t\t'
+ '%s\n\t'
+ ''
+ ''
+ % (itemtypes.get(mediatype, mediatype), plname, tagname))
+ f.close()
+ logMsg("EMBY", "Successfully added playlist: %s" % tagname)
+
+def deletePlaylists():
+
+ # Clean up the playlists
+ path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
+ dirs, files = xbmcvfs.listdir(path)
+ for file in files:
+ if file.decode('utf-8').startswith('Emby'):
+ xbmcvfs.delete("%s%s" % (path, file))
+
+def deleteNodes():
+
+ # Clean up video nodes
+ import shutil
+ path = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
+ dirs, files = xbmcvfs.listdir(path)
+ for dir in dirs:
+ if dir.decode('utf-8').startswith('Emby'):
+ try:
+ shutil.rmtree("%s%s" % (path, dir.decode('utf-8')))
+ except:
+ logMsg("EMBY", "Failed to delete directory: %s" % dir.decode('utf-8'))
+ for file in files:
+ if file.decode('utf-8').startswith('emby'):
+ try:
+ xbmcvfs.delete("%s%s" % (path, file.decode('utf-8')))
+ except:
logMsg("EMBY", "Failed to file: %s" % file.decode('utf-8'))
\ No newline at end of file
diff --git a/resources/lib/videonodes.py b/resources/lib/videonodes.py
index 53c18385..f7f63c3c 100644
--- a/resources/lib/videonodes.py
+++ b/resources/lib/videonodes.py
@@ -1,395 +1,394 @@
-# -*- coding: utf-8 -*-
-
-#################################################################################################
-
-import shutil
-import xml.etree.ElementTree as etree
-
-import xbmc
-import xbmcaddon
-import xbmcvfs
-
-import clientinfo
-import utils
-
-#################################################################################################
-
-
-class VideoNodes(object):
-
-
- def __init__(self):
-
- clientInfo = clientinfo.ClientInfo()
- self.addonName = clientInfo.getAddonName()
-
- self.kodiversion = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
-
- def logMsg(self, msg, lvl=1):
-
- className = self.__class__.__name__
- utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
-
-
- def commonRoot(self, order, label, tagname, roottype=1):
-
- if roottype == 0:
- # Index
- root = etree.Element('node', attrib={'order': "%s" % order})
- elif roottype == 1:
- # Filter
- root = etree.Element('node', attrib={'order': "%s" % order, 'type': "filter"})
- etree.SubElement(root, 'match').text = "all"
- # Add tag rule
- rule = etree.SubElement(root, 'rule', attrib={'field': "tag", 'operator': "is"})
- etree.SubElement(rule, 'value').text = tagname
- else:
- # Folder
- root = etree.Element('node', attrib={'order': "%s" % order, 'type': "folder"})
-
- etree.SubElement(root, 'label').text = label
- etree.SubElement(root, 'icon').text = "special://home/addons/plugin.video.emby/icon.png"
-
- return root
-
- def viewNode(self, indexnumber, tagname, mediatype, viewtype, viewid, delete=False):
-
- window = utils.window
- kodiversion = self.kodiversion
-
- if viewtype == "mixed":
- dirname = "%s - %s" % (viewid, mediatype)
- else:
- dirname = viewid
-
- path = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
- nodepath = xbmc.translatePath(
- "special://profile/library/video/Emby - %s/" % dirname).decode('utf-8')
-
- # Verify the video directory
- if not xbmcvfs.exists(path):
- shutil.copytree(
- src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'),
- dst=xbmc.translatePath("special://profile/library/video").decode('utf-8'))
- xbmcvfs.exists(path)
-
- # Create the node directory
- if not xbmcvfs.exists(nodepath) and not mediatype == "photos":
- # We need to copy over the default items
- xbmcvfs.mkdirs(nodepath)
- else:
- if delete:
- dirs, files = xbmcvfs.listdir(nodepath)
- for file in files:
- xbmcvfs.delete(nodepath + file)
-
- self.logMsg("Sucessfully removed videonode: %s." % tagname, 1)
- return
-
- # Create index entry
- nodeXML = "%sindex.xml" % nodepath
- # Set windows property
- path = "library://video/Emby - %s/" % dirname
- for i in range(1, indexnumber):
- # Verify to make sure we don't create duplicates
- if window('Emby.nodes.%s.index' % i) == path:
- return
-
- if mediatype == "photos":
- path = "plugin://plugin.video.emby/?id=%s&mode=getsubfolders" % indexnumber
-
- window('Emby.nodes.%s.index' % indexnumber, value=path)
-
- # Root
- if not mediatype == "photos":
- if viewtype == "mixed":
- specialtag = "%s - %s" % (tagname, mediatype)
- root = self.commonRoot(order=0, label=specialtag, tagname=tagname, roottype=0)
- else:
- root = self.commonRoot(order=0, label=tagname, tagname=tagname, roottype=0)
- try:
- utils.indent(root)
- except: pass
- etree.ElementTree(root).write(nodeXML)
-
- nodetypes = {
-
- '1': "all",
- '2': "recent",
- '3': "recentepisodes",
- '4': "inprogress",
- '5': "inprogressepisodes",
- '6': "unwatched",
- '7': "nextepisodes",
- '8': "sets",
- '9': "genres",
- '10': "random",
- '11': "recommended",
- }
- mediatypes = {
- # label according to nodetype per mediatype
- 'movies':
- {
- '1': tagname,
- '2': 30174,
- '4': 30177,
- '6': 30189,
- '8': 20434,
- '9': 135,
- '10': 30229,
- '11': 30230
- },
-
- 'tvshows':
- {
- '1': tagname,
- '2': 30170,
- '3': 30175,
- '4': 30171,
- '5': 30178,
- '7': 30179,
- '9': 135,
- '10': 30229,
- '11': 30230
- },
-
- 'homevideos':
- {
- '1': tagname,
- '2': 30251,
- '11': 30253
- },
-
- 'photos':
- {
- '1': tagname,
- '2': 30252,
- '8': 30255,
- '11': 30254
- },
-
- 'musicvideos':
- {
- '1': tagname,
- '2': 30256,
- '4': 30257,
- '6': 30258
- }
- }
-
- nodes = mediatypes[mediatype]
- for node in nodes:
-
- nodetype = nodetypes[node]
- nodeXML = "%s%s_%s.xml" % (nodepath, viewid, nodetype)
- # Get label
- stringid = nodes[node]
- if node != "1":
- label = utils.language(stringid)
- if not label:
- label = xbmc.getLocalizedString(stringid)
- else:
- label = stringid
-
- # Set window properties
- if (mediatype == "homevideos" or mediatype == "photos") and nodetype == "all":
- # Custom query
- path = ("plugin://plugin.video.emby/?id=%s&mode=browsecontent&type=%s"
- % (tagname, mediatype))
- elif (mediatype == "homevideos" or mediatype == "photos"):
- # Custom query
- path = ("plugin://plugin.video.emby/?id=%s&mode=browsecontent&type=%s&folderid=%s"
- % (tagname, mediatype, nodetype))
- elif nodetype == "nextepisodes":
- # Custom query
- path = "plugin://plugin.video.emby/?id=%s&mode=nextup&limit=25" % tagname
- elif kodiversion == 14 and nodetype == "recentepisodes":
- # Custom query
- path = "plugin://plugin.video.emby/?id=%s&mode=recentepisodes&limit=25" % tagname
- elif kodiversion == 14 and nodetype == "inprogressepisodes":
- # Custom query
- path = "plugin://plugin.video.emby/?id=%s&mode=inprogressepisodes&limit=25"% tagname
- else:
- path = "library://video/Emby - %s/%s_%s.xml" % (dirname, viewid, nodetype)
-
- if mediatype == "photos":
- windowpath = "ActivateWindow(Pictures,%s,return)" % path
- else:
- windowpath = "ActivateWindow(Video,%s,return)" % path
-
- if nodetype == "all":
-
- if viewtype == "mixed":
- templabel = "%s - %s" % (tagname, mediatype)
- else:
- templabel = label
-
- embynode = "Emby.nodes.%s" % indexnumber
- window('%s.title' % embynode, value=templabel)
- window('%s.path' % embynode, value=windowpath)
- window('%s.content' % embynode, value=path)
- window('%s.type' % embynode, value=mediatype)
- else:
- embynode = "Emby.nodes.%s.%s" % (indexnumber, nodetype)
- window('%s.title' % embynode, value=label)
- window('%s.path' % embynode, value=windowpath)
- window('%s.content' % embynode, value=path)
-
- if mediatype == "photos":
- # For photos, we do not create a node in videos but we do want the window props
- # to be created.
- # To do: add our photos nodes to kodi picture sources somehow
- continue
-
- if xbmcvfs.exists(nodeXML):
- # Don't recreate xml if already exists
- continue
-
- # Create the root
- if (nodetype == "nextepisodes" or mediatype == "homevideos" or
- (kodiversion == 14 and nodetype in ('recentepisodes', 'inprogressepisodes'))):
- # Folder type with plugin path
- root = self.commonRoot(order=node, label=label, tagname=tagname, roottype=2)
- etree.SubElement(root, 'path').text = path
- etree.SubElement(root, 'content').text = "episodes"
- else:
- root = self.commonRoot(order=node, label=label, tagname=tagname)
- if nodetype in ('recentepisodes', 'inprogressepisodes'):
- etree.SubElement(root, 'content').text = "episodes"
- else:
- etree.SubElement(root, 'content').text = mediatype
-
- limit = "25"
- # Elements per nodetype
- if nodetype == "all":
- etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
-
- elif nodetype == "recent":
- etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded"
- etree.SubElement(root, 'limit').text = limit
- rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
- etree.SubElement(rule, 'value').text = "0"
-
- elif nodetype == "inprogress":
- etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"})
- etree.SubElement(root, 'limit').text = limit
-
- elif nodetype == "genres":
- etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
- etree.SubElement(root, 'group').text = "genres"
-
- elif nodetype == "unwatched":
- etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
- rule = etree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"})
- etree.SubElement(rule, 'value').text = "0"
-
- elif nodetype == "sets":
- etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
- etree.SubElement(root, 'group').text = "sets"
-
- elif nodetype == "random":
- etree.SubElement(root, 'order', {'direction': "ascending"}).text = "random"
- etree.SubElement(root, 'limit').text = limit
-
- elif nodetype == "recommended":
- etree.SubElement(root, 'order', {'direction': "descending"}).text = "rating"
- etree.SubElement(root, 'limit').text = limit
- rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
- etree.SubElement(rule, 'value').text = "0"
- rule2 = etree.SubElement(root, 'rule',
- attrib={'field': "rating", 'operator': "greaterthan"})
- etree.SubElement(rule2, 'value').text = "7"
-
- elif nodetype == "recentepisodes":
- # Kodi Isengard, Jarvis
- etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded"
- etree.SubElement(root, 'limit').text = limit
- rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
- etree.SubElement(rule, 'value').text = "0"
-
- elif nodetype == "inprogressepisodes":
- # Kodi Isengard, Jarvis
- etree.SubElement(root, 'limit').text = "25"
- rule = etree.SubElement(root, 'rule',
- attrib={'field': "inprogress", 'operator':"true"})
-
- try:
- utils.indent(root)
- except: pass
- etree.ElementTree(root).write(nodeXML)
-
- def singleNode(self, indexnumber, tagname, mediatype, itemtype):
-
- window = utils.window
-
- tagname = tagname.encode('utf-8')
- cleantagname = utils.normalize_nodes(tagname)
- nodepath = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
- nodeXML = "%semby_%s.xml" % (nodepath, cleantagname)
- path = "library://video/emby_%s.xml" % cleantagname
- windowpath = "ActivateWindow(Video,%s,return)" % path
-
- # Create the video node directory
- if not xbmcvfs.exists(nodepath):
- # We need to copy over the default items
- shutil.copytree(
- src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'),
- dst=xbmc.translatePath("special://profile/library/video").decode('utf-8'))
- xbmcvfs.exists(path)
-
- labels = {
-
- 'Favorite movies': 30180,
- 'Favorite tvshows': 30181,
- 'channels': 30173
- }
- label = utils.language(labels[tagname])
- embynode = "Emby.nodes.%s" % indexnumber
- window('%s.title' % embynode, value=label)
- window('%s.path' % embynode, value=windowpath)
- window('%s.content' % embynode, value=path)
- window('%s.type' % embynode, value=itemtype)
-
- if xbmcvfs.exists(nodeXML):
- # Don't recreate xml if already exists
- return
-
- if itemtype == "channels":
- root = self.commonRoot(order=1, label=label, tagname=tagname, roottype=2)
- etree.SubElement(root, 'path').text = "plugin://plugin.video.emby/?id=0&mode=channels"
- else:
- root = self.commonRoot(order=1, label=label, tagname=tagname)
- etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
-
- etree.SubElement(root, 'content').text = mediatype
-
- try:
- utils.indent(root)
- except: pass
- etree.ElementTree(root).write(nodeXML)
-
- def clearProperties(self):
-
- window = utils.window
-
- self.logMsg("Clearing nodes properties.", 1)
- embyprops = window('Emby.nodes.total')
- propnames = [
-
- "index","path","title","content",
- "inprogress.content","inprogress.title",
- "inprogress.content","inprogress.path",
- "nextepisodes.title","nextepisodes.content",
- "nextepisodes.path","unwatched.title",
- "unwatched.content","unwatched.path",
- "recent.title","recent.content","recent.path",
- "recentepisodes.title","recentepisodes.content",
- "recentepisodes.path","inprogressepisodes.title",
- "inprogressepisodes.content","inprogressepisodes.path"
- ]
-
- if embyprops:
- totalnodes = int(embyprops)
- for i in range(totalnodes):
- for prop in propnames:
+# -*- coding: utf-8 -*-
+
+#################################################################################################
+
+import shutil
+import xml.etree.ElementTree as etree
+
+import xbmc
+import xbmcaddon
+import xbmcvfs
+
+import clientinfo
+import utils
+
+#################################################################################################
+
+
+class VideoNodes(object):
+
+
+ def __init__(self):
+
+ clientInfo = clientinfo.ClientInfo()
+ self.addonName = clientInfo.getAddonName()
+
+ self.kodiversion = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
+
+ def logMsg(self, msg, lvl=1):
+
+ className = self.__class__.__name__
+ utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
+
+
+ def commonRoot(self, order, label, tagname, roottype=1):
+
+ if roottype == 0:
+ # Index
+ root = etree.Element('node', attrib={'order': "%s" % order})
+ elif roottype == 1:
+ # Filter
+ root = etree.Element('node', attrib={'order': "%s" % order, 'type': "filter"})
+ etree.SubElement(root, 'match').text = "all"
+ # Add tag rule
+ rule = etree.SubElement(root, 'rule', attrib={'field': "tag", 'operator': "is"})
+ etree.SubElement(rule, 'value').text = tagname
+ else:
+ # Folder
+ root = etree.Element('node', attrib={'order': "%s" % order, 'type': "folder"})
+
+ etree.SubElement(root, 'label').text = label
+ etree.SubElement(root, 'icon').text = "special://home/addons/plugin.video.emby/icon.png"
+
+ return root
+
+ def viewNode(self, indexnumber, tagname, mediatype, viewtype, viewid, delete=False):
+
+ window = utils.window
+
+ if viewtype == "mixed":
+ dirname = "%s - %s" % (viewid, mediatype)
+ else:
+ dirname = viewid
+
+ path = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
+ nodepath = xbmc.translatePath(
+ "special://profile/library/video/Emby - %s/" % dirname).decode('utf-8')
+
+ # Verify the video directory
+ if not xbmcvfs.exists(path):
+ shutil.copytree(
+ src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'),
+ dst=xbmc.translatePath("special://profile/library/video").decode('utf-8'))
+ xbmcvfs.exists(path)
+
+ # Create the node directory
+ if not xbmcvfs.exists(nodepath) and not mediatype == "photos":
+ # We need to copy over the default items
+ xbmcvfs.mkdirs(nodepath)
+ else:
+ if delete:
+ dirs, files = xbmcvfs.listdir(nodepath)
+ for file in files:
+ xbmcvfs.delete(nodepath + file)
+
+ self.logMsg("Sucessfully removed videonode: %s." % tagname, 1)
+ return
+
+ # Create index entry
+ nodeXML = "%sindex.xml" % nodepath
+ # Set windows property
+ path = "library://video/Emby - %s/" % dirname
+ for i in range(1, indexnumber):
+ # Verify to make sure we don't create duplicates
+ if window('Emby.nodes.%s.index' % i) == path:
+ return
+
+ if mediatype == "photos":
+ path = "plugin://plugin.video.emby/?id=%s&mode=getsubfolders" % indexnumber
+
+ window('Emby.nodes.%s.index' % indexnumber, value=path)
+
+ # Root
+ if not mediatype == "photos":
+ if viewtype == "mixed":
+ specialtag = "%s - %s" % (tagname, mediatype)
+ root = self.commonRoot(order=0, label=specialtag, tagname=tagname, roottype=0)
+ else:
+ root = self.commonRoot(order=0, label=tagname, tagname=tagname, roottype=0)
+ try:
+ utils.indent(root)
+ except: pass
+ etree.ElementTree(root).write(nodeXML)
+
+ nodetypes = {
+
+ '1': "all",
+ '2': "recent",
+ '3': "recentepisodes",
+ '4': "inprogress",
+ '5': "inprogressepisodes",
+ '6': "unwatched",
+ '7': "nextepisodes",
+ '8': "sets",
+ '9': "genres",
+ '10': "random",
+ '11': "recommended",
+ }
+ mediatypes = {
+ # label according to nodetype per mediatype
+ 'movies':
+ {
+ '1': tagname,
+ '2': 30174,
+ '4': 30177,
+ '6': 30189,
+ '8': 20434,
+ '9': 135,
+ '10': 30229,
+ '11': 30230
+ },
+
+ 'tvshows':
+ {
+ '1': tagname,
+ '2': 30170,
+ '3': 30175,
+ '4': 30171,
+ '5': 30178,
+ '7': 30179,
+ '9': 135,
+ '10': 30229,
+ '11': 30230
+ },
+
+ 'homevideos':
+ {
+ '1': tagname,
+ '2': 30251,
+ '11': 30253
+ },
+
+ 'photos':
+ {
+ '1': tagname,
+ '2': 30252,
+ '8': 30255,
+ '11': 30254
+ },
+
+ 'musicvideos':
+ {
+ '1': tagname,
+ '2': 30256,
+ '4': 30257,
+ '6': 30258
+ }
+ }
+
+ nodes = mediatypes[mediatype]
+ for node in nodes:
+
+ nodetype = nodetypes[node]
+ nodeXML = "%s%s_%s.xml" % (nodepath, viewid, nodetype)
+ # Get label
+ stringid = nodes[node]
+ if node != "1":
+ label = utils.language(stringid)
+ if not label:
+ label = xbmc.getLocalizedString(stringid)
+ else:
+ label = stringid
+
+ # Set window properties
+ if (mediatype == "homevideos" or mediatype == "photos") and nodetype == "all":
+ # Custom query
+ path = ("plugin://plugin.video.emby/?id=%s&mode=browsecontent&type=%s"
+ % (tagname, mediatype))
+ elif (mediatype == "homevideos" or mediatype == "photos"):
+ # Custom query
+ path = ("plugin://plugin.video.emby/?id=%s&mode=browsecontent&type=%s&folderid=%s"
+ % (tagname, mediatype, nodetype))
+ elif nodetype == "nextepisodes":
+ # Custom query
+ path = "plugin://plugin.video.emby/?id=%s&mode=nextup&limit=25" % tagname
+ elif self.kodiversion == 14 and nodetype == "recentepisodes":
+ # Custom query
+ path = "plugin://plugin.video.emby/?id=%s&mode=recentepisodes&limit=25" % tagname
+ elif self.kodiversion == 14 and nodetype == "inprogressepisodes":
+ # Custom query
+ path = "plugin://plugin.video.emby/?id=%s&mode=inprogressepisodes&limit=25"% tagname
+ else:
+ path = "library://video/Emby - %s/%s_%s.xml" % (dirname, viewid, nodetype)
+
+ if mediatype == "photos":
+ windowpath = "ActivateWindow(Pictures,%s,return)" % path
+ else:
+ windowpath = "ActivateWindow(Video,%s,return)" % path
+
+ if nodetype == "all":
+
+ if viewtype == "mixed":
+ templabel = "%s - %s" % (tagname, mediatype)
+ else:
+ templabel = label
+
+ embynode = "Emby.nodes.%s" % indexnumber
+ window('%s.title' % embynode, value=templabel)
+ window('%s.path' % embynode, value=windowpath)
+ window('%s.content' % embynode, value=path)
+ window('%s.type' % embynode, value=mediatype)
+ else:
+ embynode = "Emby.nodes.%s.%s" % (indexnumber, nodetype)
+ window('%s.title' % embynode, value=label)
+ window('%s.path' % embynode, value=windowpath)
+ window('%s.content' % embynode, value=path)
+
+ if mediatype == "photos":
+ # For photos, we do not create a node in videos but we do want the window props
+ # to be created.
+ # To do: add our photos nodes to kodi picture sources somehow
+ continue
+
+ if xbmcvfs.exists(nodeXML):
+ # Don't recreate xml if already exists
+ continue
+
+ # Create the root
+ if (nodetype == "nextepisodes" or mediatype == "homevideos" or
+ (self.kodiversion == 14 and nodetype in ('recentepisodes', 'inprogressepisodes'))):
+ # Folder type with plugin path
+ root = self.commonRoot(order=node, label=label, tagname=tagname, roottype=2)
+ etree.SubElement(root, 'path').text = path
+ etree.SubElement(root, 'content').text = "episodes"
+ else:
+ root = self.commonRoot(order=node, label=label, tagname=tagname)
+ if nodetype in ('recentepisodes', 'inprogressepisodes'):
+ etree.SubElement(root, 'content').text = "episodes"
+ else:
+ etree.SubElement(root, 'content').text = mediatype
+
+ limit = "25"
+ # Elements per nodetype
+ if nodetype == "all":
+ etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
+
+ elif nodetype == "recent":
+ etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded"
+ etree.SubElement(root, 'limit').text = limit
+ rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
+ etree.SubElement(rule, 'value').text = "0"
+
+ elif nodetype == "inprogress":
+ etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"})
+ etree.SubElement(root, 'limit').text = limit
+
+ elif nodetype == "genres":
+ etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
+ etree.SubElement(root, 'group').text = "genres"
+
+ elif nodetype == "unwatched":
+ etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
+ rule = etree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"})
+ etree.SubElement(rule, 'value').text = "0"
+
+ elif nodetype == "sets":
+ etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
+ etree.SubElement(root, 'group').text = "sets"
+
+ elif nodetype == "random":
+ etree.SubElement(root, 'order', {'direction': "ascending"}).text = "random"
+ etree.SubElement(root, 'limit').text = limit
+
+ elif nodetype == "recommended":
+ etree.SubElement(root, 'order', {'direction': "descending"}).text = "rating"
+ etree.SubElement(root, 'limit').text = limit
+ rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
+ etree.SubElement(rule, 'value').text = "0"
+ rule2 = etree.SubElement(root, 'rule',
+ attrib={'field': "rating", 'operator': "greaterthan"})
+ etree.SubElement(rule2, 'value').text = "7"
+
+ elif nodetype == "recentepisodes":
+ # Kodi Isengard, Jarvis
+ etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded"
+ etree.SubElement(root, 'limit').text = limit
+ rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
+ etree.SubElement(rule, 'value').text = "0"
+
+ elif nodetype == "inprogressepisodes":
+ # Kodi Isengard, Jarvis
+ etree.SubElement(root, 'limit').text = "25"
+ rule = etree.SubElement(root, 'rule',
+ attrib={'field': "inprogress", 'operator':"true"})
+
+ try:
+ utils.indent(root)
+ except: pass
+ etree.ElementTree(root).write(nodeXML)
+
+ def singleNode(self, indexnumber, tagname, mediatype, itemtype):
+
+ window = utils.window
+
+ tagname = tagname.encode('utf-8')
+ cleantagname = utils.normalize_nodes(tagname)
+ nodepath = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
+ nodeXML = "%semby_%s.xml" % (nodepath, cleantagname)
+ path = "library://video/emby_%s.xml" % cleantagname
+ windowpath = "ActivateWindow(Video,%s,return)" % path
+
+ # Create the video node directory
+ if not xbmcvfs.exists(nodepath):
+ # We need to copy over the default items
+ shutil.copytree(
+ src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'),
+ dst=xbmc.translatePath("special://profile/library/video").decode('utf-8'))
+ xbmcvfs.exists(path)
+
+ labels = {
+
+ 'Favorite movies': 30180,
+ 'Favorite tvshows': 30181,
+ 'channels': 30173
+ }
+ label = utils.language(labels[tagname])
+ embynode = "Emby.nodes.%s" % indexnumber
+ window('%s.title' % embynode, value=label)
+ window('%s.path' % embynode, value=windowpath)
+ window('%s.content' % embynode, value=path)
+ window('%s.type' % embynode, value=itemtype)
+
+ if xbmcvfs.exists(nodeXML):
+ # Don't recreate xml if already exists
+ return
+
+ if itemtype == "channels":
+ root = self.commonRoot(order=1, label=label, tagname=tagname, roottype=2)
+ etree.SubElement(root, 'path').text = "plugin://plugin.video.emby/?id=0&mode=channels"
+ else:
+ root = self.commonRoot(order=1, label=label, tagname=tagname)
+ etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
+
+ etree.SubElement(root, 'content').text = mediatype
+
+ try:
+ utils.indent(root)
+ except: pass
+ etree.ElementTree(root).write(nodeXML)
+
+ def clearProperties(self):
+
+ window = utils.window
+
+ self.logMsg("Clearing nodes properties.", 1)
+ embyprops = window('Emby.nodes.total')
+ propnames = [
+
+ "index","path","title","content",
+ "inprogress.content","inprogress.title",
+ "inprogress.content","inprogress.path",
+ "nextepisodes.title","nextepisodes.content",
+ "nextepisodes.path","unwatched.title",
+ "unwatched.content","unwatched.path",
+ "recent.title","recent.content","recent.path",
+ "recentepisodes.title","recentepisodes.content",
+ "recentepisodes.path","inprogressepisodes.title",
+ "inprogressepisodes.content","inprogressepisodes.path"
+ ]
+
+ if embyprops:
+ totalnodes = int(embyprops)
+ for i in range(totalnodes):
+ for prop in propnames:
window('Emby.nodes.%s.%s' % (str(i), prop), clear=True)
\ No newline at end of file
diff --git a/resources/lib/websocket.py b/resources/lib/websocket.py
index 3d777a97..e35d1966 100644
--- a/resources/lib/websocket.py
+++ b/resources/lib/websocket.py
@@ -1,912 +1,911 @@
-"""
-websocket - WebSocket client library for Python
-
-Copyright (C) 2010 Hiroki Ohtani(liris)
-
- This library is free software; you can redistribute it and/or
- modify it under the terms of the GNU Lesser General Public
- License as published by the Free Software Foundation; either
- version 2.1 of the License, or (at your option) any later version.
-
- This library is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- Lesser General Public License for more details.
-
- You should have received a copy of the GNU Lesser General Public
- License along with this library; if not, write to the Free Software
- Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-
-"""
-
-
-import socket
-
-try:
- import ssl
- from ssl import SSLError
- HAVE_SSL = True
-except ImportError:
- # dummy class of SSLError for ssl none-support environment.
- class SSLError(Exception):
- pass
-
- HAVE_SSL = False
-
-from urlparse import urlparse
-import os
-import array
-import struct
-import uuid
-import hashlib
-import base64
-import threading
-import time
-import logging
-import traceback
-import sys
-
-"""
-websocket python client.
-=========================
-
-This version support only hybi-13.
-Please see http://tools.ietf.org/html/rfc6455 for protocol.
-"""
-
-
-# websocket supported version.
-VERSION = 13
-
-# closing frame status codes.
-STATUS_NORMAL = 1000
-STATUS_GOING_AWAY = 1001
-STATUS_PROTOCOL_ERROR = 1002
-STATUS_UNSUPPORTED_DATA_TYPE = 1003
-STATUS_STATUS_NOT_AVAILABLE = 1005
-STATUS_ABNORMAL_CLOSED = 1006
-STATUS_INVALID_PAYLOAD = 1007
-STATUS_POLICY_VIOLATION = 1008
-STATUS_MESSAGE_TOO_BIG = 1009
-STATUS_INVALID_EXTENSION = 1010
-STATUS_UNEXPECTED_CONDITION = 1011
-STATUS_TLS_HANDSHAKE_ERROR = 1015
-
-logger = logging.getLogger()
-
-
-class WebSocketException(Exception):
- """
- websocket exeception class.
- """
- pass
-
-
-class WebSocketConnectionClosedException(WebSocketException):
- """
- If remote host closed the connection or some network error happened,
- this exception will be raised.
- """
- pass
-
-class WebSocketTimeoutException(WebSocketException):
- """
- WebSocketTimeoutException will be raised at socket timeout during read/write data.
- """
- pass
-
-default_timeout = None
-traceEnabled = False
-
-
-def enableTrace(tracable):
- """
- turn on/off the tracability.
-
- tracable: boolean value. if set True, tracability is enabled.
- """
- global traceEnabled
- traceEnabled = tracable
- if tracable:
- if not logger.handlers:
- logger.addHandler(logging.StreamHandler())
- logger.setLevel(logging.DEBUG)
-
-
-def setdefaulttimeout(timeout):
- """
- Set the global timeout setting to connect.
-
- timeout: default socket timeout time. This value is second.
- """
- global default_timeout
- default_timeout = timeout
-
-
-def getdefaulttimeout():
- """
- Return the global timeout setting(second) to connect.
- """
- return default_timeout
-
-
-def _parse_url(url):
- """
- parse url and the result is tuple of
- (hostname, port, resource path and the flag of secure mode)
-
- url: url string.
- """
- if ":" not in url:
- raise ValueError("url is invalid")
-
- scheme, url = url.split(":", 1)
-
- parsed = urlparse(url, scheme="http")
- if parsed.hostname:
- hostname = parsed.hostname
- else:
- raise ValueError("hostname is invalid")
- port = 0
- if parsed.port:
- port = parsed.port
-
- is_secure = False
- if scheme == "ws":
- if not port:
- port = 80
- elif scheme == "wss":
- is_secure = True
- if not port:
- port = 443
- else:
- raise ValueError("scheme %s is invalid" % scheme)
-
- if parsed.path:
- resource = parsed.path
- else:
- resource = "/"
-
- if parsed.query:
- resource += "?" + parsed.query
-
- return (hostname, port, resource, is_secure)
-
-
-def create_connection(url, timeout=None, **options):
- """
- connect to url and return websocket object.
-
- Connect to url and return the WebSocket object.
- Passing optional timeout parameter will set the timeout on the socket.
- If no timeout is supplied, the global default timeout setting returned by getdefauttimeout() is used.
- You can customize using 'options'.
- If you set "header" list object, you can set your own custom header.
-
- >>> conn = create_connection("ws://echo.websocket.org/",
- ... header=["User-Agent: MyProgram",
- ... "x-custom: header"])
-
-
- timeout: socket timeout time. This value is integer.
- if you set None for this value, it means "use default_timeout value"
-
- options: current support option is only "header".
- if you set header as dict value, the custom HTTP headers are added.
- """
- sockopt = options.get("sockopt", [])
- sslopt = options.get("sslopt", {})
- websock = WebSocket(sockopt=sockopt, sslopt=sslopt)
- websock.settimeout(timeout if timeout is not None else default_timeout)
- websock.connect(url, **options)
- return websock
-
-_MAX_INTEGER = (1 << 32) -1
-_AVAILABLE_KEY_CHARS = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1)
-_MAX_CHAR_BYTE = (1<<8) -1
-
-# ref. Websocket gets an update, and it breaks stuff.
-# http://axod.blogspot.com/2010/06/websocket-gets-update-and-it-breaks.html
-
-
-def _create_sec_websocket_key():
- uid = uuid.uuid4()
- return base64.encodestring(uid.bytes).strip()
-
-
-_HEADERS_TO_CHECK = {
- "upgrade": "websocket",
- "connection": "upgrade",
- }
-
-
-class ABNF(object):
- """
- ABNF frame class.
- see http://tools.ietf.org/html/rfc5234
- and http://tools.ietf.org/html/rfc6455#section-5.2
- """
-
- # operation code values.
- OPCODE_CONT = 0x0
- OPCODE_TEXT = 0x1
- OPCODE_BINARY = 0x2
- OPCODE_CLOSE = 0x8
- OPCODE_PING = 0x9
- OPCODE_PONG = 0xa
-
- # available operation code value tuple
- OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE,
- OPCODE_PING, OPCODE_PONG)
-
- # opcode human readable string
- OPCODE_MAP = {
- OPCODE_CONT: "cont",
- OPCODE_TEXT: "text",
- OPCODE_BINARY: "binary",
- OPCODE_CLOSE: "close",
- OPCODE_PING: "ping",
- OPCODE_PONG: "pong"
- }
-
- # data length threashold.
- LENGTH_7 = 0x7d
- LENGTH_16 = 1 << 16
- LENGTH_63 = 1 << 63
-
- def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0,
- opcode=OPCODE_TEXT, mask=1, data=""):
- """
- Constructor for ABNF.
- please check RFC for arguments.
- """
- self.fin = fin
- self.rsv1 = rsv1
- self.rsv2 = rsv2
- self.rsv3 = rsv3
- self.opcode = opcode
- self.mask = mask
- self.data = data
- self.get_mask_key = os.urandom
-
- def __str__(self):
- return "fin=" + str(self.fin) \
- + " opcode=" + str(self.opcode) \
- + " data=" + str(self.data)
-
- @staticmethod
- def create_frame(data, opcode):
- """
- create frame to send text, binary and other data.
-
- data: data to send. This is string value(byte array).
- if opcode is OPCODE_TEXT and this value is uniocde,
- data value is conveted into unicode string, automatically.
-
- opcode: operation code. please see OPCODE_XXX.
- """
- if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode):
- data = data.encode("utf-8")
- # mask must be set if send data from client
- return ABNF(1, 0, 0, 0, opcode, 1, data)
-
- def format(self):
- """
- format this object to string(byte array) to send data to server.
- """
- if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]):
- raise ValueError("not 0 or 1")
- if self.opcode not in ABNF.OPCODES:
- raise ValueError("Invalid OPCODE")
- length = len(self.data)
- if length >= ABNF.LENGTH_63:
- raise ValueError("data is too long")
-
- frame_header = chr(self.fin << 7
- | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4
- | self.opcode)
- if length < ABNF.LENGTH_7:
- frame_header += chr(self.mask << 7 | length)
- elif length < ABNF.LENGTH_16:
- frame_header += chr(self.mask << 7 | 0x7e)
- frame_header += struct.pack("!H", length)
- else:
- frame_header += chr(self.mask << 7 | 0x7f)
- frame_header += struct.pack("!Q", length)
-
- if not self.mask:
- return frame_header + self.data
- else:
- mask_key = self.get_mask_key(4)
- return frame_header + self._get_masked(mask_key)
-
- def _get_masked(self, mask_key):
- s = ABNF.mask(mask_key, self.data)
- return mask_key + "".join(s)
-
- @staticmethod
- def mask(mask_key, data):
- """
- mask or unmask data. Just do xor for each byte
-
- mask_key: 4 byte string(byte).
-
- data: data to mask/unmask.
- """
- _m = array.array("B", mask_key)
- _d = array.array("B", data)
- for i in xrange(len(_d)):
- _d[i] ^= _m[i % 4]
- return _d.tostring()
-
-
-class WebSocket(object):
- """
- Low level WebSocket interface.
- This class is based on
- The WebSocket protocol draft-hixie-thewebsocketprotocol-76
- http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
-
- We can connect to the websocket server and send/recieve data.
- The following example is a echo client.
-
- >>> import websocket
- >>> ws = websocket.WebSocket()
- >>> ws.connect("ws://echo.websocket.org")
- >>> ws.send("Hello, Server")
- >>> ws.recv()
- 'Hello, Server'
- >>> ws.close()
-
- get_mask_key: a callable to produce new mask keys, see the set_mask_key
- function's docstring for more details
- sockopt: values for socket.setsockopt.
- sockopt must be tuple and each element is argument of sock.setscokopt.
- sslopt: dict object for ssl socket option.
- """
-
- def __init__(self, get_mask_key=None, sockopt=None, sslopt=None):
- """
- Initalize WebSocket object.
- """
- if sockopt is None:
- sockopt = []
- if sslopt is None:
- sslopt = {}
- self.connected = False
- self.sock = socket.socket()
- for opts in sockopt:
- self.sock.setsockopt(*opts)
- self.sslopt = sslopt
- self.get_mask_key = get_mask_key
- # Buffers over the packets from the layer beneath until desired amount
- # bytes of bytes are received.
- self._recv_buffer = []
- # These buffer over the build-up of a single frame.
- self._frame_header = None
- self._frame_length = None
- self._frame_mask = None
- self._cont_data = None
-
- def fileno(self):
- return self.sock.fileno()
-
- def set_mask_key(self, func):
- """
- set function to create musk key. You can custumize mask key generator.
- Mainly, this is for testing purpose.
-
- func: callable object. the fuct must 1 argument as integer.
- The argument means length of mask key.
- This func must be return string(byte array),
- which length is argument specified.
- """
- self.get_mask_key = func
-
- def gettimeout(self):
- """
- Get the websocket timeout(second).
- """
- return self.sock.gettimeout()
-
- def settimeout(self, timeout):
- """
- Set the timeout to the websocket.
-
- timeout: timeout time(second).
- """
- self.sock.settimeout(timeout)
-
- timeout = property(gettimeout, settimeout)
-
- def connect(self, url, **options):
- """
- Connect to url. url is websocket url scheme. ie. ws://host:port/resource
- You can customize using 'options'.
- If you set "header" dict object, you can set your own custom header.
-
- >>> ws = WebSocket()
- >>> ws.connect("ws://echo.websocket.org/",
- ... header={"User-Agent: MyProgram",
- ... "x-custom: header"})
-
- timeout: socket timeout time. This value is integer.
- if you set None for this value,
- it means "use default_timeout value"
-
- options: current support option is only "header".
- if you set header as dict value,
- the custom HTTP headers are added.
-
- """
- hostname, port, resource, is_secure = _parse_url(url)
- # TODO: we need to support proxy
- self.sock.connect((hostname, port))
- if is_secure:
- if HAVE_SSL:
- if self.sslopt is None:
- sslopt = {}
- else:
- sslopt = self.sslopt
- self.sock = ssl.wrap_socket(self.sock, **sslopt)
- else:
- raise WebSocketException("SSL not available.")
-
- self._handshake(hostname, port, resource, **options)
-
- def _handshake(self, host, port, resource, **options):
- sock = self.sock
- headers = []
- headers.append("GET %s HTTP/1.1" % resource)
- headers.append("Upgrade: websocket")
- headers.append("Connection: Upgrade")
- if port == 80:
- hostport = host
- else:
- hostport = "%s:%d" % (host, port)
- headers.append("Host: %s" % hostport)
-
- if "origin" in options:
- headers.append("Origin: %s" % options["origin"])
- else:
- headers.append("Origin: http://%s" % hostport)
-
- key = _create_sec_websocket_key()
- headers.append("Sec-WebSocket-Key: %s" % key)
- headers.append("Sec-WebSocket-Version: %s" % VERSION)
- if "header" in options:
- headers.extend(options["header"])
-
- headers.append("")
- headers.append("")
-
- header_str = "\r\n".join(headers)
- self._send(header_str)
- if traceEnabled:
- logger.debug("--- request header ---")
- logger.debug(header_str)
- logger.debug("-----------------------")
-
- status, resp_headers = self._read_headers()
- if status != 101:
- self.close()
- raise WebSocketException("Handshake Status %d" % status)
-
- success = self._validate_header(resp_headers, key)
- if not success:
- self.close()
- raise WebSocketException("Invalid WebSocket Header")
-
- self.connected = True
-
- def _validate_header(self, headers, key):
- for k, v in _HEADERS_TO_CHECK.iteritems():
- r = headers.get(k, None)
- if not r:
- return False
- r = r.lower()
- if v != r:
- return False
-
- result = headers.get("sec-websocket-accept", None)
- if not result:
- return False
- result = result.lower()
-
- value = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
- hashed = base64.encodestring(hashlib.sha1(value).digest()).strip().lower()
- return hashed == result
-
- def _read_headers(self):
- status = None
- headers = {}
- if traceEnabled:
- logger.debug("--- response header ---")
-
- while True:
- line = self._recv_line()
- if line == "\r\n":
- break
- line = line.strip()
- if traceEnabled:
- logger.debug(line)
- if not status:
- status_info = line.split(" ", 2)
- status = int(status_info[1])
- else:
- kv = line.split(":", 1)
- if len(kv) == 2:
- key, value = kv
- headers[key.lower()] = value.strip().lower()
- else:
- raise WebSocketException("Invalid header")
-
- if traceEnabled:
- logger.debug("-----------------------")
-
- return status, headers
-
- def send(self, payload, opcode=ABNF.OPCODE_TEXT):
- """
- Send the data as string.
-
- payload: Payload must be utf-8 string or unicoce,
- if the opcode is OPCODE_TEXT.
- Otherwise, it must be string(byte array)
-
- opcode: operation code to send. Please see OPCODE_XXX.
- """
- frame = ABNF.create_frame(payload, opcode)
- if self.get_mask_key:
- frame.get_mask_key = self.get_mask_key
- data = frame.format()
- length = len(data)
- if traceEnabled:
- logger.debug("send: " + repr(data))
- while data:
- l = self._send(data)
- data = data[l:]
- return length
-
- def send_binary(self, payload):
- return self.send(payload, ABNF.OPCODE_BINARY)
-
- def ping(self, payload=""):
- """
- send ping data.
-
- payload: data payload to send server.
- """
- self.send(payload, ABNF.OPCODE_PING)
-
- def pong(self, payload):
- """
- send pong data.
-
- payload: data payload to send server.
- """
- self.send(payload, ABNF.OPCODE_PONG)
-
- def recv(self):
- """
- Receive string data(byte array) from the server.
-
- return value: string(byte array) value.
- """
- opcode, data = self.recv_data()
- return data
-
- def recv_data(self):
- """
- Recieve data with operation code.
-
- return value: tuple of operation code and string(byte array) value.
- """
- while True:
- frame = self.recv_frame()
- if not frame:
- # handle error:
- # 'NoneType' object has no attribute 'opcode'
- raise WebSocketException("Not a valid frame %s" % frame)
- elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT):
- if frame.opcode == ABNF.OPCODE_CONT and not self._cont_data:
- raise WebSocketException("Illegal frame")
- if self._cont_data:
- self._cont_data[1] += frame.data
- else:
- self._cont_data = [frame.opcode, frame.data]
-
- if frame.fin:
- data = self._cont_data
- self._cont_data = None
- return data
- elif frame.opcode == ABNF.OPCODE_CLOSE:
- self.send_close()
- return (frame.opcode, None)
- elif frame.opcode == ABNF.OPCODE_PING:
- self.pong(frame.data)
-
- def recv_frame(self):
- """
- recieve data as frame from server.
-
- return value: ABNF frame object.
- """
- # Header
- if self._frame_header is None:
- self._frame_header = self._recv_strict(2)
- b1 = ord(self._frame_header[0])
- fin = b1 >> 7 & 1
- rsv1 = b1 >> 6 & 1
- rsv2 = b1 >> 5 & 1
- rsv3 = b1 >> 4 & 1
- opcode = b1 & 0xf
- b2 = ord(self._frame_header[1])
- has_mask = b2 >> 7 & 1
- # Frame length
- if self._frame_length is None:
- length_bits = b2 & 0x7f
- if length_bits == 0x7e:
- length_data = self._recv_strict(2)
- self._frame_length = struct.unpack("!H", length_data)[0]
- elif length_bits == 0x7f:
- length_data = self._recv_strict(8)
- self._frame_length = struct.unpack("!Q", length_data)[0]
- else:
- self._frame_length = length_bits
- # Mask
- if self._frame_mask is None:
- self._frame_mask = self._recv_strict(4) if has_mask else ""
- # Payload
- payload = self._recv_strict(self._frame_length)
- if has_mask:
- payload = ABNF.mask(self._frame_mask, payload)
- # Reset for next frame
- self._frame_header = None
- self._frame_length = None
- self._frame_mask = None
- return ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload)
-
-
- def send_close(self, status=STATUS_NORMAL, reason=""):
- """
- send close data to the server.
-
- status: status code to send. see STATUS_XXX.
-
- reason: the reason to close. This must be string.
- """
- if status < 0 or status >= ABNF.LENGTH_16:
- raise ValueError("code is invalid range")
- self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
-
- def close(self, status=STATUS_NORMAL, reason=""):
- """
- Close Websocket object
-
- status: status code to send. see STATUS_XXX.
-
- reason: the reason to close. This must be string.
- """
-
- try:
- self.sock.shutdown(socket.SHUT_RDWR)
- except:
- pass
-
- '''
- if self.connected:
- if status < 0 or status >= ABNF.LENGTH_16:
- raise ValueError("code is invalid range")
-
- try:
- self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
- timeout = self.sock.gettimeout()
- self.sock.settimeout(3)
- try:
- frame = self.recv_frame()
- if logger.isEnabledFor(logging.ERROR):
- recv_status = struct.unpack("!H", frame.data)[0]
- if recv_status != STATUS_NORMAL:
- logger.error("close status: " + repr(recv_status))
- except:
- pass
- self.sock.settimeout(timeout)
- self.sock.shutdown(socket.SHUT_RDWR)
- except:
- pass
- '''
- self._closeInternal()
-
- def _closeInternal(self):
- self.connected = False
- self.sock.close()
-
- def _send(self, data):
- try:
- return self.sock.send(data)
- except socket.timeout as e:
- raise WebSocketTimeoutException(e.args[0])
- except Exception as e:
- if "timed out" in e.args[0]:
- raise WebSocketTimeoutException(e.args[0])
- else:
- raise e
-
- def _recv(self, bufsize):
- try:
- bytes = self.sock.recv(bufsize)
- except socket.timeout as e:
- raise WebSocketTimeoutException(e.args[0])
- except SSLError as e:
- if e.args[0] == "The read operation timed out":
- raise WebSocketTimeoutException(e.args[0])
- else:
- raise
- if not bytes:
- raise WebSocketConnectionClosedException()
- return bytes
-
-
- def _recv_strict(self, bufsize):
- shortage = bufsize - sum(len(x) for x in self._recv_buffer)
- while shortage > 0:
- bytes = self._recv(shortage)
- self._recv_buffer.append(bytes)
- shortage -= len(bytes)
- unified = "".join(self._recv_buffer)
- if shortage == 0:
- self._recv_buffer = []
- return unified
- else:
- self._recv_buffer = [unified[bufsize:]]
- return unified[:bufsize]
-
-
- def _recv_line(self):
- line = []
- while True:
- c = self._recv(1)
- line.append(c)
- if c == "\n":
- break
- return "".join(line)
-
-
-class WebSocketApp(object):
- """
- Higher level of APIs are provided.
- The interface is like JavaScript WebSocket object.
- """
- def __init__(self, url, header=[],
- on_open=None, on_message=None, on_error=None,
- on_close=None, keep_running=True, get_mask_key=None):
- """
- url: websocket url.
- header: custom header for websocket handshake.
- on_open: callable object which is called at opening websocket.
- this function has one argument. The arugment is this class object.
- on_message: callbale object which is called when recieved data.
- on_message has 2 arguments.
- The 1st arugment is this class object.
- The passing 2nd arugment is utf-8 string which we get from the server.
- on_error: callable object which is called when we get error.
- on_error has 2 arguments.
- The 1st arugment is this class object.
- The passing 2nd arugment is exception object.
- on_close: callable object which is called when closed the connection.
- this function has one argument. The arugment is this class object.
- keep_running: a boolean flag indicating whether the app's main loop should
- keep running, defaults to True
- get_mask_key: a callable to produce new mask keys, see the WebSocket.set_mask_key's
- docstring for more information
- """
- self.url = url
- self.header = header
- self.on_open = on_open
- self.on_message = on_message
- self.on_error = on_error
- self.on_close = on_close
- self.keep_running = keep_running
- self.get_mask_key = get_mask_key
- self.sock = None
-
- def send(self, data, opcode=ABNF.OPCODE_TEXT):
- """
- send message.
- data: message to send. If you set opcode to OPCODE_TEXT, data must be utf-8 string or unicode.
- opcode: operation code of data. default is OPCODE_TEXT.
- """
- if self.sock.send(data, opcode) == 0:
- raise WebSocketConnectionClosedException()
-
- def close(self):
- """
- close websocket connection.
- """
- self.keep_running = False
- if(self.sock != None):
- self.sock.close()
-
- def _send_ping(self, interval):
- while True:
- for i in range(interval):
- time.sleep(1)
- if not self.keep_running:
- return
- self.sock.ping()
-
- def run_forever(self, sockopt=None, sslopt=None, ping_interval=0):
- """
- run event loop for WebSocket framework.
- This loop is infinite loop and is alive during websocket is available.
- sockopt: values for socket.setsockopt.
- sockopt must be tuple and each element is argument of sock.setscokopt.
- sslopt: ssl socket optional dict.
- ping_interval: automatically send "ping" command every specified period(second)
- if set to 0, not send automatically.
- """
- if sockopt is None:
- sockopt = []
- if sslopt is None:
- sslopt = {}
- if self.sock:
- raise WebSocketException("socket is already opened")
- thread = None
- self.keep_running = True
-
- try:
- self.sock = WebSocket(self.get_mask_key, sockopt=sockopt, sslopt=sslopt)
- self.sock.settimeout(default_timeout)
- self.sock.connect(self.url, header=self.header)
- self._callback(self.on_open)
-
- if ping_interval:
- thread = threading.Thread(target=self._send_ping, args=(ping_interval,))
- thread.setDaemon(True)
- thread.start()
-
- while self.keep_running:
-
- try:
- data = self.sock.recv()
-
- if data is None or self.keep_running == False:
- break
- self._callback(self.on_message, data)
-
- except Exception, e:
- #print str(e.args[0])
- if "timed out" not in e.args[0]:
- raise e
-
- except Exception, e:
- self._callback(self.on_error, e)
- finally:
- if thread:
- self.keep_running = False
- self.sock.close()
- self._callback(self.on_close)
- self.sock = None
-
- def _callback(self, callback, *args):
- if callback:
- try:
- callback(self, *args)
- except Exception, e:
- logger.error(e)
- if True:#logger.isEnabledFor(logging.DEBUG):
- _, _, tb = sys.exc_info()
- traceback.print_tb(tb)
-
-
-if __name__ == "__main__":
- enableTrace(True)
- ws = create_connection("ws://echo.websocket.org/")
- print("Sending 'Hello, World'...")
- ws.send("Hello, World")
- print("Sent")
- print("Receiving...")
- result = ws.recv()
- print("Received '%s'" % result)
- ws.close()
+"""
+websocket - WebSocket client library for Python
+
+Copyright (C) 2010 Hiroki Ohtani(liris)
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+"""
+
+
+import socket
+
+try:
+ import ssl
+ from ssl import SSLError
+ HAVE_SSL = True
+except ImportError:
+ # dummy class of SSLError for ssl none-support environment.
+ class SSLError(Exception):
+ pass
+
+ HAVE_SSL = False
+
+from urlparse import urlparse
+import os
+import array
+import struct
+import uuid
+import hashlib
+import base64
+import threading
+import time
+import logging
+import traceback
+import sys
+
+"""
+websocket python client.
+=========================
+
+This version support only hybi-13.
+Please see http://tools.ietf.org/html/rfc6455 for protocol.
+"""
+
+
+# websocket supported version.
+VERSION = 13
+
+# closing frame status codes.
+STATUS_NORMAL = 1000
+STATUS_GOING_AWAY = 1001
+STATUS_PROTOCOL_ERROR = 1002
+STATUS_UNSUPPORTED_DATA_TYPE = 1003
+STATUS_STATUS_NOT_AVAILABLE = 1005
+STATUS_ABNORMAL_CLOSED = 1006
+STATUS_INVALID_PAYLOAD = 1007
+STATUS_POLICY_VIOLATION = 1008
+STATUS_MESSAGE_TOO_BIG = 1009
+STATUS_INVALID_EXTENSION = 1010
+STATUS_UNEXPECTED_CONDITION = 1011
+STATUS_TLS_HANDSHAKE_ERROR = 1015
+
+logger = logging.getLogger()
+
+
+class WebSocketException(Exception):
+ """
+ websocket exeception class.
+ """
+ pass
+
+
+class WebSocketConnectionClosedException(WebSocketException):
+ """
+ If remote host closed the connection or some network error happened,
+ this exception will be raised.
+ """
+ pass
+
+class WebSocketTimeoutException(WebSocketException):
+ """
+ WebSocketTimeoutException will be raised at socket timeout during read/write data.
+ """
+ pass
+
+default_timeout = None
+traceEnabled = False
+
+
+def enableTrace(tracable):
+ """
+ turn on/off the tracability.
+
+ tracable: boolean value. if set True, tracability is enabled.
+ """
+ global traceEnabled
+ traceEnabled = tracable
+ if tracable:
+ if not logger.handlers:
+ logger.addHandler(logging.StreamHandler())
+ logger.setLevel(logging.DEBUG)
+
+
+def setdefaulttimeout(timeout):
+ """
+ Set the global timeout setting to connect.
+
+ timeout: default socket timeout time. This value is second.
+ """
+ global default_timeout
+ default_timeout = timeout
+
+
+def getdefaulttimeout():
+ """
+ Return the global timeout setting(second) to connect.
+ """
+ return default_timeout
+
+
+def _parse_url(url):
+ """
+ parse url and the result is tuple of
+ (hostname, port, resource path and the flag of secure mode)
+
+ url: url string.
+ """
+ if ":" not in url:
+ raise ValueError("url is invalid")
+
+ scheme, url = url.split(":", 1)
+
+ parsed = urlparse(url, scheme="http")
+ if parsed.hostname:
+ hostname = parsed.hostname
+ else:
+ raise ValueError("hostname is invalid")
+ port = 0
+ if parsed.port:
+ port = parsed.port
+
+ is_secure = False
+ if scheme == "ws":
+ if not port:
+ port = 80
+ elif scheme == "wss":
+ is_secure = True
+ if not port:
+ port = 443
+ else:
+ raise ValueError("scheme %s is invalid" % scheme)
+
+ if parsed.path:
+ resource = parsed.path
+ else:
+ resource = "/"
+
+ if parsed.query:
+ resource += "?" + parsed.query
+
+ return (hostname, port, resource, is_secure)
+
+
+def create_connection(url, timeout=None, **options):
+ """
+ connect to url and return websocket object.
+
+ Connect to url and return the WebSocket object.
+ Passing optional timeout parameter will set the timeout on the socket.
+ If no timeout is supplied, the global default timeout setting returned by getdefauttimeout() is used.
+ You can customize using 'options'.
+ If you set "header" list object, you can set your own custom header.
+
+ >>> conn = create_connection("ws://echo.websocket.org/",
+ ... header=["User-Agent: MyProgram",
+ ... "x-custom: header"])
+
+
+ timeout: socket timeout time. This value is integer.
+ if you set None for this value, it means "use default_timeout value"
+
+ options: current support option is only "header".
+ if you set header as dict value, the custom HTTP headers are added.
+ """
+ sockopt = options.get("sockopt", [])
+ sslopt = options.get("sslopt", {})
+ websock = WebSocket(sockopt=sockopt, sslopt=sslopt)
+ websock.settimeout(timeout if timeout is not None else default_timeout)
+ websock.connect(url, **options)
+ return websock
+
+_MAX_INTEGER = (1 << 32) -1
+_AVAILABLE_KEY_CHARS = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1)
+_MAX_CHAR_BYTE = (1<<8) -1
+
+# ref. Websocket gets an update, and it breaks stuff.
+# http://axod.blogspot.com/2010/06/websocket-gets-update-and-it-breaks.html
+
+
+def _create_sec_websocket_key():
+ uid = uuid.uuid4()
+ return base64.encodestring(uid.bytes).strip()
+
+
+_HEADERS_TO_CHECK = {
+ "upgrade": "websocket",
+ "connection": "upgrade",
+ }
+
+
+class ABNF(object):
+ """
+ ABNF frame class.
+ see http://tools.ietf.org/html/rfc5234
+ and http://tools.ietf.org/html/rfc6455#section-5.2
+ """
+
+ # operation code values.
+ OPCODE_CONT = 0x0
+ OPCODE_TEXT = 0x1
+ OPCODE_BINARY = 0x2
+ OPCODE_CLOSE = 0x8
+ OPCODE_PING = 0x9
+ OPCODE_PONG = 0xa
+
+ # available operation code value tuple
+ OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE,
+ OPCODE_PING, OPCODE_PONG)
+
+ # opcode human readable string
+ OPCODE_MAP = {
+ OPCODE_CONT: "cont",
+ OPCODE_TEXT: "text",
+ OPCODE_BINARY: "binary",
+ OPCODE_CLOSE: "close",
+ OPCODE_PING: "ping",
+ OPCODE_PONG: "pong"
+ }
+
+ # data length threashold.
+ LENGTH_7 = 0x7d
+ LENGTH_16 = 1 << 16
+ LENGTH_63 = 1 << 63
+
+ def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0,
+ opcode=OPCODE_TEXT, mask=1, data=""):
+ """
+ Constructor for ABNF.
+ please check RFC for arguments.
+ """
+ self.fin = fin
+ self.rsv1 = rsv1
+ self.rsv2 = rsv2
+ self.rsv3 = rsv3
+ self.opcode = opcode
+ self.mask = mask
+ self.data = data
+ self.get_mask_key = os.urandom
+
+ def __str__(self):
+ return "fin=" + str(self.fin) \
+ + " opcode=" + str(self.opcode) \
+ + " data=" + str(self.data)
+
+ @staticmethod
+ def create_frame(data, opcode):
+ """
+ create frame to send text, binary and other data.
+
+ data: data to send. This is string value(byte array).
+ if opcode is OPCODE_TEXT and this value is uniocde,
+ data value is conveted into unicode string, automatically.
+
+ opcode: operation code. please see OPCODE_XXX.
+ """
+ if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode):
+ data = data.encode("utf-8")
+ # mask must be set if send data from client
+ return ABNF(1, 0, 0, 0, opcode, 1, data)
+
+ def format(self):
+ """
+ format this object to string(byte array) to send data to server.
+ """
+ if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]):
+ raise ValueError("not 0 or 1")
+ if self.opcode not in ABNF.OPCODES:
+ raise ValueError("Invalid OPCODE")
+ length = len(self.data)
+ if length >= ABNF.LENGTH_63:
+ raise ValueError("data is too long")
+
+ frame_header = chr(self.fin << 7
+ | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4
+ | self.opcode)
+ if length < ABNF.LENGTH_7:
+ frame_header += chr(self.mask << 7 | length)
+ elif length < ABNF.LENGTH_16:
+ frame_header += chr(self.mask << 7 | 0x7e)
+ frame_header += struct.pack("!H", length)
+ else:
+ frame_header += chr(self.mask << 7 | 0x7f)
+ frame_header += struct.pack("!Q", length)
+
+ if not self.mask:
+ return frame_header + self.data
+ else:
+ mask_key = self.get_mask_key(4)
+ return frame_header + self._get_masked(mask_key)
+
+ def _get_masked(self, mask_key):
+ s = ABNF.mask(mask_key, self.data)
+ return mask_key + "".join(s)
+
+ @staticmethod
+ def mask(mask_key, data):
+ """
+ mask or unmask data. Just do xor for each byte
+
+ mask_key: 4 byte string(byte).
+
+ data: data to mask/unmask.
+ """
+ _m = array.array("B", mask_key)
+ _d = array.array("B", data)
+ for i in xrange(len(_d)):
+ _d[i] ^= _m[i % 4]
+ return _d.tostring()
+
+
+class WebSocket(object):
+ """
+ Low level WebSocket interface.
+ This class is based on
+ The WebSocket protocol draft-hixie-thewebsocketprotocol-76
+ http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
+
+ We can connect to the websocket server and send/recieve data.
+ The following example is a echo client.
+
+ >>> import websocket
+ >>> ws = websocket.WebSocket()
+ >>> ws.connect("ws://echo.websocket.org")
+ >>> ws.send("Hello, Server")
+ >>> ws.recv()
+ 'Hello, Server'
+ >>> ws.close()
+
+ get_mask_key: a callable to produce new mask keys, see the set_mask_key
+ function's docstring for more details
+ sockopt: values for socket.setsockopt.
+ sockopt must be tuple and each element is argument of sock.setscokopt.
+ sslopt: dict object for ssl socket option.
+ """
+
+ def __init__(self, get_mask_key=None, sockopt=None, sslopt=None):
+ """
+ Initalize WebSocket object.
+ """
+ if sockopt is None:
+ sockopt = []
+ if sslopt is None:
+ sslopt = {}
+ self.connected = False
+ self.sock = socket.socket()
+ for opts in sockopt:
+ self.sock.setsockopt(*opts)
+ self.sslopt = sslopt
+ self.get_mask_key = get_mask_key
+ # Buffers over the packets from the layer beneath until desired amount
+ # bytes of bytes are received.
+ self._recv_buffer = []
+ # These buffer over the build-up of a single frame.
+ self._frame_header = None
+ self._frame_length = None
+ self._frame_mask = None
+ self._cont_data = None
+
+ def fileno(self):
+ return self.sock.fileno()
+
+ def set_mask_key(self, func):
+ """
+ set function to create musk key. You can custumize mask key generator.
+ Mainly, this is for testing purpose.
+
+ func: callable object. the fuct must 1 argument as integer.
+ The argument means length of mask key.
+ This func must be return string(byte array),
+ which length is argument specified.
+ """
+ self.get_mask_key = func
+
+ def gettimeout(self):
+ """
+ Get the websocket timeout(second).
+ """
+ return self.sock.gettimeout()
+
+ def settimeout(self, timeout):
+ """
+ Set the timeout to the websocket.
+
+ timeout: timeout time(second).
+ """
+ self.sock.settimeout(timeout)
+
+ timeout = property(gettimeout, settimeout)
+
+ def connect(self, url, **options):
+ """
+ Connect to url. url is websocket url scheme. ie. ws://host:port/resource
+ You can customize using 'options'.
+ If you set "header" dict object, you can set your own custom header.
+
+ >>> ws = WebSocket()
+ >>> ws.connect("ws://echo.websocket.org/",
+ ... header={"User-Agent: MyProgram",
+ ... "x-custom: header"})
+
+ timeout: socket timeout time. This value is integer.
+ if you set None for this value,
+ it means "use default_timeout value"
+
+ options: current support option is only "header".
+ if you set header as dict value,
+ the custom HTTP headers are added.
+
+ """
+ hostname, port, resource, is_secure = _parse_url(url)
+ # TODO: we need to support proxy
+ self.sock.connect((hostname, port))
+ if is_secure:
+ if HAVE_SSL:
+ if self.sslopt is None:
+ sslopt = {}
+ else:
+ sslopt = self.sslopt
+ self.sock = ssl.wrap_socket(self.sock, **sslopt)
+ else:
+ raise WebSocketException("SSL not available.")
+
+ self._handshake(hostname, port, resource, **options)
+
+ def _handshake(self, host, port, resource, **options):
+ headers = []
+ headers.append("GET %s HTTP/1.1" % resource)
+ headers.append("Upgrade: websocket")
+ headers.append("Connection: Upgrade")
+ if port == 80:
+ hostport = host
+ else:
+ hostport = "%s:%d" % (host, port)
+ headers.append("Host: %s" % hostport)
+
+ if "origin" in options:
+ headers.append("Origin: %s" % options["origin"])
+ else:
+ headers.append("Origin: http://%s" % hostport)
+
+ key = _create_sec_websocket_key()
+ headers.append("Sec-WebSocket-Key: %s" % key)
+ headers.append("Sec-WebSocket-Version: %s" % VERSION)
+ if "header" in options:
+ headers.extend(options["header"])
+
+ headers.append("")
+ headers.append("")
+
+ header_str = "\r\n".join(headers)
+ self._send(header_str)
+ if traceEnabled:
+ logger.debug("--- request header ---")
+ logger.debug(header_str)
+ logger.debug("-----------------------")
+
+ status, resp_headers = self._read_headers()
+ if status != 101:
+ self.close()
+ raise WebSocketException("Handshake Status %d" % status)
+
+ success = self._validate_header(resp_headers, key)
+ if not success:
+ self.close()
+ raise WebSocketException("Invalid WebSocket Header")
+
+ self.connected = True
+
+ def _validate_header(self, headers, key):
+ for k, v in _HEADERS_TO_CHECK.iteritems():
+ r = headers.get(k, None)
+ if not r:
+ return False
+ r = r.lower()
+ if v != r:
+ return False
+
+ result = headers.get("sec-websocket-accept", None)
+ if not result:
+ return False
+ result = result.lower()
+
+ value = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+ hashed = base64.encodestring(hashlib.sha1(value).digest()).strip().lower()
+ return hashed == result
+
+ def _read_headers(self):
+ status = None
+ headers = {}
+ if traceEnabled:
+ logger.debug("--- response header ---")
+
+ while True:
+ line = self._recv_line()
+ if line == "\r\n":
+ break
+ line = line.strip()
+ if traceEnabled:
+ logger.debug(line)
+ if not status:
+ status_info = line.split(" ", 2)
+ status = int(status_info[1])
+ else:
+ kv = line.split(":", 1)
+ if len(kv) == 2:
+ key, value = kv
+ headers[key.lower()] = value.strip().lower()
+ else:
+ raise WebSocketException("Invalid header")
+
+ if traceEnabled:
+ logger.debug("-----------------------")
+
+ return status, headers
+
+ def send(self, payload, opcode=ABNF.OPCODE_TEXT):
+ """
+ Send the data as string.
+
+ payload: Payload must be utf-8 string or unicoce,
+ if the opcode is OPCODE_TEXT.
+ Otherwise, it must be string(byte array)
+
+ opcode: operation code to send. Please see OPCODE_XXX.
+ """
+ frame = ABNF.create_frame(payload, opcode)
+ if self.get_mask_key:
+ frame.get_mask_key = self.get_mask_key
+ data = frame.format()
+ length = len(data)
+ if traceEnabled:
+ logger.debug("send: " + repr(data))
+ while data:
+ l = self._send(data)
+ data = data[l:]
+ return length
+
+ def send_binary(self, payload):
+ return self.send(payload, ABNF.OPCODE_BINARY)
+
+ def ping(self, payload=""):
+ """
+ send ping data.
+
+ payload: data payload to send server.
+ """
+ self.send(payload, ABNF.OPCODE_PING)
+
+ def pong(self, payload):
+ """
+ send pong data.
+
+ payload: data payload to send server.
+ """
+ self.send(payload, ABNF.OPCODE_PONG)
+
+ def recv(self):
+ """
+ Receive string data(byte array) from the server.
+
+ return value: string(byte array) value.
+ """
+ opcode, data = self.recv_data()
+ return data
+
+ def recv_data(self):
+ """
+ Recieve data with operation code.
+
+ return value: tuple of operation code and string(byte array) value.
+ """
+ while True:
+ frame = self.recv_frame()
+ if not frame:
+ # handle error:
+ # 'NoneType' object has no attribute 'opcode'
+ raise WebSocketException("Not a valid frame %s" % frame)
+ elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT):
+ if frame.opcode == ABNF.OPCODE_CONT and not self._cont_data:
+ raise WebSocketException("Illegal frame")
+ if self._cont_data:
+ self._cont_data[1] += frame.data
+ else:
+ self._cont_data = [frame.opcode, frame.data]
+
+ if frame.fin:
+ data = self._cont_data
+ self._cont_data = None
+ return data
+ elif frame.opcode == ABNF.OPCODE_CLOSE:
+ self.send_close()
+ return (frame.opcode, None)
+ elif frame.opcode == ABNF.OPCODE_PING:
+ self.pong(frame.data)
+
+ def recv_frame(self):
+ """
+ recieve data as frame from server.
+
+ return value: ABNF frame object.
+ """
+ # Header
+ if self._frame_header is None:
+ self._frame_header = self._recv_strict(2)
+ b1 = ord(self._frame_header[0])
+ fin = b1 >> 7 & 1
+ rsv1 = b1 >> 6 & 1
+ rsv2 = b1 >> 5 & 1
+ rsv3 = b1 >> 4 & 1
+ opcode = b1 & 0xf
+ b2 = ord(self._frame_header[1])
+ has_mask = b2 >> 7 & 1
+ # Frame length
+ if self._frame_length is None:
+ length_bits = b2 & 0x7f
+ if length_bits == 0x7e:
+ length_data = self._recv_strict(2)
+ self._frame_length = struct.unpack("!H", length_data)[0]
+ elif length_bits == 0x7f:
+ length_data = self._recv_strict(8)
+ self._frame_length = struct.unpack("!Q", length_data)[0]
+ else:
+ self._frame_length = length_bits
+ # Mask
+ if self._frame_mask is None:
+ self._frame_mask = self._recv_strict(4) if has_mask else ""
+ # Payload
+ payload = self._recv_strict(self._frame_length)
+ if has_mask:
+ payload = ABNF.mask(self._frame_mask, payload)
+ # Reset for next frame
+ self._frame_header = None
+ self._frame_length = None
+ self._frame_mask = None
+ return ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload)
+
+
+ def send_close(self, status=STATUS_NORMAL, reason=""):
+ """
+ send close data to the server.
+
+ status: status code to send. see STATUS_XXX.
+
+ reason: the reason to close. This must be string.
+ """
+ if status < 0 or status >= ABNF.LENGTH_16:
+ raise ValueError("code is invalid range")
+ self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
+
+ def close(self, status=STATUS_NORMAL, reason=""):
+ """
+ Close Websocket object
+
+ status: status code to send. see STATUS_XXX.
+
+ reason: the reason to close. This must be string.
+ """
+
+ try:
+ self.sock.shutdown(socket.SHUT_RDWR)
+ except:
+ pass
+
+ '''
+ if self.connected:
+ if status < 0 or status >= ABNF.LENGTH_16:
+ raise ValueError("code is invalid range")
+
+ try:
+ self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
+ timeout = self.sock.gettimeout()
+ self.sock.settimeout(3)
+ try:
+ frame = self.recv_frame()
+ if logger.isEnabledFor(logging.ERROR):
+ recv_status = struct.unpack("!H", frame.data)[0]
+ if recv_status != STATUS_NORMAL:
+ logger.error("close status: " + repr(recv_status))
+ except:
+ pass
+ self.sock.settimeout(timeout)
+ self.sock.shutdown(socket.SHUT_RDWR)
+ except:
+ pass
+ '''
+ self._closeInternal()
+
+ def _closeInternal(self):
+ self.connected = False
+ self.sock.close()
+
+ def _send(self, data):
+ try:
+ return self.sock.send(data)
+ except socket.timeout as e:
+ raise WebSocketTimeoutException(e.args[0])
+ except Exception as e:
+ if "timed out" in e.args[0]:
+ raise WebSocketTimeoutException(e.args[0])
+ else:
+ raise e
+
+ def _recv(self, bufsize):
+ try:
+ bytes = self.sock.recv(bufsize)
+ except socket.timeout as e:
+ raise WebSocketTimeoutException(e.args[0])
+ except SSLError as e:
+ if e.args[0] == "The read operation timed out":
+ raise WebSocketTimeoutException(e.args[0])
+ else:
+ raise
+ if not bytes:
+ raise WebSocketConnectionClosedException()
+ return bytes
+
+
+ def _recv_strict(self, bufsize):
+ shortage = bufsize - sum(len(x) for x in self._recv_buffer)
+ while shortage > 0:
+ bytes = self._recv(shortage)
+ self._recv_buffer.append(bytes)
+ shortage -= len(bytes)
+ unified = "".join(self._recv_buffer)
+ if shortage == 0:
+ self._recv_buffer = []
+ return unified
+ else:
+ self._recv_buffer = [unified[bufsize:]]
+ return unified[:bufsize]
+
+
+ def _recv_line(self):
+ line = []
+ while True:
+ c = self._recv(1)
+ line.append(c)
+ if c == "\n":
+ break
+ return "".join(line)
+
+
+class WebSocketApp(object):
+ """
+ Higher level of APIs are provided.
+ The interface is like JavaScript WebSocket object.
+ """
+ def __init__(self, url, header=[],
+ on_open=None, on_message=None, on_error=None,
+ on_close=None, keep_running=True, get_mask_key=None):
+ """
+ url: websocket url.
+ header: custom header for websocket handshake.
+ on_open: callable object which is called at opening websocket.
+ this function has one argument. The arugment is this class object.
+ on_message: callbale object which is called when recieved data.
+ on_message has 2 arguments.
+ The 1st arugment is this class object.
+ The passing 2nd arugment is utf-8 string which we get from the server.
+ on_error: callable object which is called when we get error.
+ on_error has 2 arguments.
+ The 1st arugment is this class object.
+ The passing 2nd arugment is exception object.
+ on_close: callable object which is called when closed the connection.
+ this function has one argument. The arugment is this class object.
+ keep_running: a boolean flag indicating whether the app's main loop should
+ keep running, defaults to True
+ get_mask_key: a callable to produce new mask keys, see the WebSocket.set_mask_key's
+ docstring for more information
+ """
+ self.url = url
+ self.header = header
+ self.on_open = on_open
+ self.on_message = on_message
+ self.on_error = on_error
+ self.on_close = on_close
+ self.keep_running = keep_running
+ self.get_mask_key = get_mask_key
+ self.sock = None
+
+ def send(self, data, opcode=ABNF.OPCODE_TEXT):
+ """
+ send message.
+ data: message to send. If you set opcode to OPCODE_TEXT, data must be utf-8 string or unicode.
+ opcode: operation code of data. default is OPCODE_TEXT.
+ """
+ if self.sock.send(data, opcode) == 0:
+ raise WebSocketConnectionClosedException()
+
+ def close(self):
+ """
+ close websocket connection.
+ """
+ self.keep_running = False
+ if(self.sock != None):
+ self.sock.close()
+
+ def _send_ping(self, interval):
+ while True:
+ for i in range(interval):
+ time.sleep(1)
+ if not self.keep_running:
+ return
+ self.sock.ping()
+
+ def run_forever(self, sockopt=None, sslopt=None, ping_interval=0):
+ """
+ run event loop for WebSocket framework.
+ This loop is infinite loop and is alive during websocket is available.
+ sockopt: values for socket.setsockopt.
+ sockopt must be tuple and each element is argument of sock.setscokopt.
+ sslopt: ssl socket optional dict.
+ ping_interval: automatically send "ping" command every specified period(second)
+ if set to 0, not send automatically.
+ """
+ if sockopt is None:
+ sockopt = []
+ if sslopt is None:
+ sslopt = {}
+ if self.sock:
+ raise WebSocketException("socket is already opened")
+ thread = None
+ self.keep_running = True
+
+ try:
+ self.sock = WebSocket(self.get_mask_key, sockopt=sockopt, sslopt=sslopt)
+ self.sock.settimeout(default_timeout)
+ self.sock.connect(self.url, header=self.header)
+ self._callback(self.on_open)
+
+ if ping_interval:
+ thread = threading.Thread(target=self._send_ping, args=(ping_interval,))
+ thread.setDaemon(True)
+ thread.start()
+
+ while self.keep_running:
+
+ try:
+ data = self.sock.recv()
+
+ if data is None or self.keep_running == False:
+ break
+ self._callback(self.on_message, data)
+
+ except Exception, e:
+ #print str(e.args[0])
+ if "timed out" not in e.args[0]:
+ raise e
+
+ except Exception, e:
+ self._callback(self.on_error, e)
+ finally:
+ if thread:
+ self.keep_running = False
+ self.sock.close()
+ self._callback(self.on_close)
+ self.sock = None
+
+ def _callback(self, callback, *args):
+ if callback:
+ try:
+ callback(self, *args)
+ except Exception, e:
+ logger.error(e)
+ if True:#logger.isEnabledFor(logging.DEBUG):
+ _, _, tb = sys.exc_info()
+ traceback.print_tb(tb)
+
+
+if __name__ == "__main__":
+ enableTrace(True)
+ ws = create_connection("ws://echo.websocket.org/")
+ print("Sending 'Hello, World'...")
+ ws.send("Hello, World")
+ print("Sent")
+ print("Receiving...")
+ result = ws.recv()
+ print("Received '%s'" % result)
+ ws.close()
diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py
index acf0df36..559cb152 100644
--- a/resources/lib/websocket_client.py
+++ b/resources/lib/websocket_client.py
@@ -1,327 +1,319 @@
-# -*- coding: utf-8 -*-
-
-#################################################################################################
-
-import json
-import threading
-import websocket
-
-import xbmc
-import xbmcgui
-
-import clientinfo
-import downloadutils
-import librarysync
-import playlist
-import userclient
-import utils
-
-import logging
-logging.basicConfig()
-
-#################################################################################################
-
-
-class WebSocket_Client(threading.Thread):
-
- _shared_state = {}
-
- client = None
- stopWebsocket = False
-
-
- def __init__(self):
-
- self.__dict__ = self._shared_state
- self.monitor = xbmc.Monitor()
-
- self.doUtils = downloadutils.DownloadUtils()
- self.clientInfo = clientinfo.ClientInfo()
- self.addonName = self.clientInfo.getAddonName()
- self.deviceId = self.clientInfo.getDeviceId()
- self.librarySync = librarysync.LibrarySync()
-
- threading.Thread.__init__(self)
-
- def logMsg(self, msg, lvl=1):
-
- self.className = self.__class__.__name__
- utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
-
-
- def sendProgressUpdate(self, data):
-
- log = self.logMsg
-
- log("sendProgressUpdate", 2)
- try:
- messageData = {
-
- 'MessageType': "ReportPlaybackProgress",
- 'Data': data
- }
- messageString = json.dumps(messageData)
- self.client.send(messageString)
- log("Message data: %s" % messageString, 2)
-
- except Exception as e:
- log("Exception: %s" % e, 1)
-
- def on_message(self, ws, message):
-
- log = self.logMsg
- window = utils.window
- lang = utils.language
-
- result = json.loads(message)
- messageType = result['MessageType']
- data = result['Data']
-
- if messageType not in ('SessionEnded'):
- # Mute certain events
- log("Message: %s" % message, 1)
-
- if messageType == "Play":
- # A remote control play command has been sent from the server.
- itemIds = data['ItemIds']
- command = data['PlayCommand']
-
- pl = playlist.Playlist()
- dialog = xbmcgui.Dialog()
-
- if command == "PlayNow":
- dialog.notification(
- heading="Emby for Kodi",
- message="%s %s" % (len(itemIds), lang(33004)),
- icon="special://home/addons/plugin.video.emby/icon.png",
- sound=False)
- startat = data.get('StartPositionTicks', 0)
- pl.playAll(itemIds, startat)
-
- elif command == "PlayNext":
- dialog.notification(
- heading="Emby for Kodi",
- message="%s %s" % (len(itemIds), lang(33005)),
- icon="special://home/addons/plugin.video.emby/icon.png",
- sound=False)
- newplaylist = pl.modifyPlaylist(itemIds)
- player = xbmc.Player()
- if not player.isPlaying():
- # Only start the playlist if nothing is playing
- player.play(newplaylist)
-
- elif messageType == "Playstate":
- # A remote control update playstate command has been sent from the server.
- command = data['Command']
- player = xbmc.Player()
-
- actions = {
-
- 'Stop': player.stop,
- 'Unpause': player.pause,
- 'Pause': player.pause,
- 'NextTrack': player.playnext,
- 'PreviousTrack': player.playprevious,
- 'Seek': player.seekTime
- }
- action = actions[command]
- if command == "Seek":
- seekto = data['SeekPositionTicks']
- seektime = seekto / 10000000.0
- action(seektime)
- log("Seek to %s." % seektime, 1)
- else:
- action()
- log("Command: %s completed." % command, 1)
-
- window('emby_command', value="true")
-
- elif messageType == "UserDataChanged":
- # A user changed their personal rating for an item, or their playstate was updated
- userdata_list = data['UserDataList']
- self.librarySync.triage_items("userdata", userdata_list)
-
- elif messageType == "LibraryChanged":
-
- librarySync = self.librarySync
- processlist = {
-
- 'added': data['ItemsAdded'],
- 'update': data['ItemsUpdated'],
- 'remove': data['ItemsRemoved']
- }
- for action in processlist:
- librarySync.triage_items(action, processlist[action])
-
- elif messageType == "GeneralCommand":
-
- command = data['Name']
- arguments = data['Arguments']
-
- if command in ('Mute', 'Unmute', 'SetVolume',
- 'SetSubtitleStreamIndex', 'SetAudioStreamIndex'):
-
- player = xbmc.Player()
- # These commands need to be reported back
- if command == "Mute":
- xbmc.executebuiltin('Mute')
- elif command == "Unmute":
- xbmc.executebuiltin('Mute')
- elif command == "SetVolume":
- volume = arguments['Volume']
- xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % volume)
- elif command == "SetAudioStreamIndex":
- index = int(arguments['Index'])
- player.setAudioStream(index - 1)
- elif command == "SetSubtitleStreamIndex":
- embyindex = int(arguments['Index'])
- currentFile = player.getPlayingFile()
-
- mapping = window('emby_%s.indexMapping' % currentFile)
- if mapping:
- externalIndex = json.loads(mapping)
- # If there's external subtitles added via playbackutils
- for index in externalIndex:
- if externalIndex[index] == embyindex:
- player.setSubtitleStream(int(index))
- break
- else:
- # User selected internal subtitles
- external = len(externalIndex)
- audioTracks = len(player.getAvailableAudioStreams())
- player.setSubtitleStream(external + embyindex - audioTracks - 1)
- else:
- # Emby merges audio and subtitle index together
- audioTracks = len(player.getAvailableAudioStreams())
- player.setSubtitleStream(index - audioTracks - 1)
-
- # Let service know
- window('emby_command', value="true")
-
- elif command == "DisplayMessage":
-
- header = arguments['Header']
- text = arguments['Text']
- xbmcgui.Dialog().notification(
- heading=header,
- message=text,
- icon="special://home/addons/plugin.video.emby/icon.png",
- time=4000)
-
- elif command == "SendString":
-
- string = arguments['String']
- text = {
-
- 'jsonrpc': "2.0",
- 'id': 0,
- 'method': "Input.SendText",
- 'params': {
-
- 'text': "%s" % string,
- 'done': False
- }
- }
- result = xbmc.executeJSONRPC(json.dumps(text))
-
- else:
- builtin = {
-
- 'ToggleFullscreen': 'Action(FullScreen)',
- 'ToggleOsdMenu': 'Action(OSD)',
- 'ToggleContextMenu': 'Action(ContextMenu)',
- 'MoveUp': 'Action(Up)',
- 'MoveDown': 'Action(Down)',
- 'MoveLeft': 'Action(Left)',
- 'MoveRight': 'Action(Right)',
- 'Select': 'Action(Select)',
- 'Back': 'Action(back)',
- 'GoHome': 'ActivateWindow(Home)',
- 'PageUp': 'Action(PageUp)',
- 'NextLetter': 'Action(NextLetter)',
- 'GoToSearch': 'VideoLibrary.Search',
- 'GoToSettings': 'ActivateWindow(Settings)',
- 'PageDown': 'Action(PageDown)',
- 'PreviousLetter': 'Action(PrevLetter)',
- 'TakeScreenshot': 'TakeScreenshot',
- 'ToggleMute': 'Mute',
- 'VolumeUp': 'Action(VolumeUp)',
- 'VolumeDown': 'Action(VolumeDown)',
- }
- action = builtin.get(command)
- if action:
- xbmc.executebuiltin(action)
-
- elif messageType == "ServerRestarting":
- if utils.settings('supressRestartMsg') == "true":
- xbmcgui.Dialog().notification(
- heading="Emby for Kodi",
- message=lang(33006),
- icon="special://home/addons/plugin.video.emby/icon.png")
-
- elif messageType == "UserConfigurationUpdated":
- # Update user data set in userclient
- userclient.UserClient().userSettings = data
- self.librarySync.refresh_views = True
-
- def on_close(self, ws):
- self.logMsg("Closed.", 2)
-
- def on_open(self, ws):
- self.doUtils.postCapabilities(self.deviceId)
-
- def on_error(self, ws, error):
- if "10061" in str(error):
- # Server is offline
- pass
- else:
- self.logMsg("Error: %s" % error, 2)
-
- def run(self):
-
- log = self.logMsg
- window = utils.window
- monitor = self.monitor
-
- loglevel = int(window('emby_logLevel'))
- # websocket.enableTrace(True)
-
- userId = window('emby_currUser')
- server = window('emby_server%s' % userId)
- token = window('emby_accessToken%s' % userId)
- deviceId = self.deviceId
-
- # Get the appropriate prefix for the websocket
- if "https" in server:
- server = server.replace('https', "wss")
- else:
- server = server.replace('http', "ws")
-
- websocket_url = "%s?api_key=%s&deviceId=%s" % (server, token, deviceId)
- log("websocket url: %s" % websocket_url, 1)
-
- self.client = websocket.WebSocketApp(websocket_url,
- on_message=self.on_message,
- on_error=self.on_error,
- on_close=self.on_close)
-
- self.client.on_open = self.on_open
- log("----===## Starting WebSocketClient ##===----", 0)
-
- while not monitor.abortRequested():
-
- self.client.run_forever(ping_interval=10)
- if self.stopWebsocket:
- break
-
- if monitor.waitForAbort(5):
- # Abort was requested, exit
- break
-
- log("##===---- WebSocketClient Stopped ----===##", 0)
-
- def stopClient(self):
-
- self.stopWebsocket = True
- self.client.close()
+# -*- coding: utf-8 -*-
+
+#################################################################################################
+
+import json
+import threading
+import websocket
+
+import xbmc
+import xbmcgui
+
+import clientinfo
+import downloadutils
+import librarysync
+import playlist
+import userclient
+import utils
+
+import logging
+logging.basicConfig()
+
+#################################################################################################
+
+
+class WebSocket_Client(threading.Thread):
+
+ _shared_state = {}
+
+ client = None
+ stopWebsocket = False
+
+
+ def __init__(self):
+
+ self.__dict__ = self._shared_state
+ self.monitor = xbmc.Monitor()
+
+ self.doUtils = downloadutils.DownloadUtils()
+ self.clientInfo = clientinfo.ClientInfo()
+ self.addonName = self.clientInfo.getAddonName()
+ self.deviceId = self.clientInfo.getDeviceId()
+ self.librarySync = librarysync.LibrarySync()
+
+ threading.Thread.__init__(self)
+
+ def logMsg(self, msg, lvl=1):
+
+ self.className = self.__class__.__name__
+ utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
+
+
+ def sendProgressUpdate(self, data):
+
+ self.logMsg("sendProgressUpdate", 2)
+ try:
+ messageData = {
+
+ 'MessageType': "ReportPlaybackProgress",
+ 'Data': data
+ }
+ messageString = json.dumps(messageData)
+ self.client.send(messageString)
+ self.logMsg("Message data: %s" % messageString, 2)
+
+ except Exception as e:
+ self.logMsg("Exception: %s" % e, 1)
+
+ def on_message(self, ws, message):
+
+ window = utils.window
+ lang = utils.language
+
+ result = json.loads(message)
+ messageType = result['MessageType']
+ data = result['Data']
+
+ if messageType not in ('SessionEnded'):
+ # Mute certain events
+ self.logMsg("Message: %s" % message, 1)
+
+ if messageType == "Play":
+ # A remote control play command has been sent from the server.
+ itemIds = data['ItemIds']
+ command = data['PlayCommand']
+
+ pl = playlist.Playlist()
+ dialog = xbmcgui.Dialog()
+
+ if command == "PlayNow":
+ dialog.notification(
+ heading="Emby for Kodi",
+ message="%s %s" % (len(itemIds), lang(33004)),
+ icon="special://home/addons/plugin.video.emby/icon.png",
+ sound=False)
+ startat = data.get('StartPositionTicks', 0)
+ pl.playAll(itemIds, startat)
+
+ elif command == "PlayNext":
+ dialog.notification(
+ heading="Emby for Kodi",
+ message="%s %s" % (len(itemIds), lang(33005)),
+ icon="special://home/addons/plugin.video.emby/icon.png",
+ sound=False)
+ newplaylist = pl.modifyPlaylist(itemIds)
+ player = xbmc.Player()
+ if not player.isPlaying():
+ # Only start the playlist if nothing is playing
+ player.play(newplaylist)
+
+ elif messageType == "Playstate":
+ # A remote control update playstate command has been sent from the server.
+ command = data['Command']
+ player = xbmc.Player()
+
+ actions = {
+
+ 'Stop': player.stop,
+ 'Unpause': player.pause,
+ 'Pause': player.pause,
+ 'NextTrack': player.playnext,
+ 'PreviousTrack': player.playprevious,
+ 'Seek': player.seekTime
+ }
+ action = actions[command]
+ if command == "Seek":
+ seekto = data['SeekPositionTicks']
+ seektime = seekto / 10000000.0
+ action(seektime)
+ self.logMsg("Seek to %s." % seektime, 1)
+ else:
+ action()
+ self.logMsg("Command: %s completed." % command, 1)
+
+ window('emby_command', value="true")
+
+ elif messageType == "UserDataChanged":
+ # A user changed their personal rating for an item, or their playstate was updated
+ userdata_list = data['UserDataList']
+ self.librarySync.triage_items("userdata", userdata_list)
+
+ elif messageType == "LibraryChanged":
+
+ librarySync = self.librarySync
+ processlist = {
+
+ 'added': data['ItemsAdded'],
+ 'update': data['ItemsUpdated'],
+ 'remove': data['ItemsRemoved']
+ }
+ for action in processlist:
+ librarySync.triage_items(action, processlist[action])
+
+ elif messageType == "GeneralCommand":
+
+ command = data['Name']
+ arguments = data['Arguments']
+
+ if command in ('Mute', 'Unmute', 'SetVolume',
+ 'SetSubtitleStreamIndex', 'SetAudioStreamIndex'):
+
+ player = xbmc.Player()
+ # These commands need to be reported back
+ if command == "Mute":
+ xbmc.executebuiltin('Mute')
+ elif command == "Unmute":
+ xbmc.executebuiltin('Mute')
+ elif command == "SetVolume":
+ volume = arguments['Volume']
+ xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % volume)
+ elif command == "SetAudioStreamIndex":
+ index = int(arguments['Index'])
+ player.setAudioStream(index - 1)
+ elif command == "SetSubtitleStreamIndex":
+ embyindex = int(arguments['Index'])
+ currentFile = player.getPlayingFile()
+
+ mapping = window('emby_%s.indexMapping' % currentFile)
+ if mapping:
+ externalIndex = json.loads(mapping)
+ # If there's external subtitles added via playbackutils
+ for index in externalIndex:
+ if externalIndex[index] == embyindex:
+ player.setSubtitleStream(int(index))
+ break
+ else:
+ # User selected internal subtitles
+ external = len(externalIndex)
+ audioTracks = len(player.getAvailableAudioStreams())
+ player.setSubtitleStream(external + embyindex - audioTracks - 1)
+ else:
+ # Emby merges audio and subtitle index together
+ audioTracks = len(player.getAvailableAudioStreams())
+ player.setSubtitleStream(index - audioTracks - 1)
+
+ # Let service know
+ window('emby_command', value="true")
+
+ elif command == "DisplayMessage":
+
+ header = arguments['Header']
+ text = arguments['Text']
+ xbmcgui.Dialog().notification(
+ heading=header,
+ message=text,
+ icon="special://home/addons/plugin.video.emby/icon.png",
+ time=4000)
+
+ elif command == "SendString":
+
+ string = arguments['String']
+ text = {
+
+ 'jsonrpc': "2.0",
+ 'id': 0,
+ 'method': "Input.SendText",
+ 'params': {
+
+ 'text': "%s" % string,
+ 'done': False
+ }
+ }
+ result = xbmc.executeJSONRPC(json.dumps(text))
+
+ else:
+ builtin = {
+
+ 'ToggleFullscreen': 'Action(FullScreen)',
+ 'ToggleOsdMenu': 'Action(OSD)',
+ 'ToggleContextMenu': 'Action(ContextMenu)',
+ 'MoveUp': 'Action(Up)',
+ 'MoveDown': 'Action(Down)',
+ 'MoveLeft': 'Action(Left)',
+ 'MoveRight': 'Action(Right)',
+ 'Select': 'Action(Select)',
+ 'Back': 'Action(back)',
+ 'GoHome': 'ActivateWindow(Home)',
+ 'PageUp': 'Action(PageUp)',
+ 'NextLetter': 'Action(NextLetter)',
+ 'GoToSearch': 'VideoLibrary.Search',
+ 'GoToSettings': 'ActivateWindow(Settings)',
+ 'PageDown': 'Action(PageDown)',
+ 'PreviousLetter': 'Action(PrevLetter)',
+ 'TakeScreenshot': 'TakeScreenshot',
+ 'ToggleMute': 'Mute',
+ 'VolumeUp': 'Action(VolumeUp)',
+ 'VolumeDown': 'Action(VolumeDown)',
+ }
+ action = builtin.get(command)
+ if action:
+ xbmc.executebuiltin(action)
+
+ elif messageType == "ServerRestarting":
+ if utils.settings('supressRestartMsg') == "true":
+ xbmcgui.Dialog().notification(
+ heading="Emby for Kodi",
+ message=lang(33006),
+ icon="special://home/addons/plugin.video.emby/icon.png")
+
+ elif messageType == "UserConfigurationUpdated":
+ # Update user data set in userclient
+ userclient.UserClient().userSettings = data
+ self.librarySync.refresh_views = True
+
+ def on_close(self, ws):
+ self.logMsg("Closed.", 2)
+
+ def on_open(self, ws):
+ self.doUtils.postCapabilities(self.deviceId)
+
+ def on_error(self, ws, error):
+ if "10061" in str(error):
+ # Server is offline
+ pass
+ else:
+ self.logMsg("Error: %s" % error, 2)
+
+ def run(self):
+
+ window = utils.window
+ loglevel = int(window('emby_logLevel'))
+ # websocket.enableTrace(True)
+
+ userId = window('emby_currUser')
+ server = window('emby_server%s' % userId)
+ token = window('emby_accessToken%s' % userId)
+ # Get the appropriate prefix for the websocket
+ if "https" in server:
+ server = server.replace('https', "wss")
+ else:
+ server = server.replace('http', "ws")
+
+ websocket_url = "%s?api_key=%s&deviceId=%s" % (server, token, self.deviceId)
+ self.logMsg("websocket url: %s" % websocket_url, 1)
+
+ self.client = websocket.WebSocketApp(websocket_url,
+ on_message=self.on_message,
+ on_error=self.on_error,
+ on_close=self.on_close)
+
+ self.client.on_open = self.on_open
+ self.logMsg("----===## Starting WebSocketClient ##===----", 0)
+
+ while not self.monitor.abortRequested():
+
+ self.client.run_forever(ping_interval=10)
+ if self.stopWebsocket:
+ break
+
+ if self.monitor.waitForAbort(5):
+ # Abort was requested, exit
+ break
+
+ self.logMsg("##===---- WebSocketClient Stopped ----===##", 0)
+
+ def stopClient(self):
+
+ self.stopWebsocket = True
+ self.client.close()
self.logMsg("Stopping thread.", 1)
\ No newline at end of file