diff --git a/resources/lib/api.py b/resources/lib/api.py
new file mode 100644
index 00000000..d15822bb
--- /dev/null
+++ b/resources/lib/api.py
@@ -0,0 +1,378 @@
+# -*- coding: utf-8 -*-
+
+##################################################################################################
+
+import clientinfo
+import utils
+
+##################################################################################################
+
+
+class API():
+
+ def __init__(self, item):
+
+ self.item = item
+ 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 getUserData(self):
+ # Default
+ favorite = False
+ playcount = None
+ played = False
+ lastPlayedDate = None
+ resume = 0
+ rating = 0
+
+ try:
+ userdata = self.item['UserData']
+
+ except KeyError: # No userdata found.
+ pass
+
+ else:
+ favorite = userdata['IsFavorite']
+ likes = userdata.get('Likes')
+ # Rating for album and songs
+ if favorite:
+ rating = 5
+ elif likes:
+ rating = 3
+ elif likes == False:
+ rating = 1
+ else:
+ rating = 0
+
+ lastPlayedDate = userdata.get('LastPlayedDate')
+ if lastPlayedDate:
+ lastPlayedDate = lastPlayedDate.split('.')[0].replace('T', " ")
+
+ if userdata['Played']:
+ # Playcount is tied to the watch status
+ played = True
+ playcount = userdata['PlayCount']
+ if playcount == 0:
+ playcount = 1
+
+ if lastPlayedDate is None:
+ lastPlayedDate = self.getDateCreated()
+
+ playbackPosition = userdata.get('PlaybackPositionTicks')
+ if playbackPosition:
+ resume = playbackPosition / 10000000.0
+
+ return {
+
+ 'Favorite': favorite,
+ 'PlayCount': playcount,
+ 'Played': played,
+ 'LastPlayedDate': lastPlayedDate,
+ 'Resume': resume,
+ 'Rating': rating
+ }
+
+ def getPeople(self):
+ # Process People
+ director = []
+ writer = []
+ cast = []
+
+ try:
+ people = self.item['People']
+
+ except KeyError:
+ pass
+
+ else:
+ for person in people:
+
+ type = person['Type']
+ name = person['Name']
+
+ if "Director" in type:
+ director.append(name)
+ elif "Actor" in type:
+ cast.append(name)
+ elif type in ("Writing", "Writer"):
+ writer.append(name)
+
+ return {
+
+ 'Director': director,
+ 'Writer': writer,
+ 'Cast': cast
+ }
+
+ def getMediaStreams(self):
+ item = self.item
+ videotracks = []
+ audiotracks = []
+ subtitlelanguages = []
+
+ try:
+ media_streams = item['MediaSources'][0]['MediaStreams']
+
+ except KeyError:
+ media_streams = item['MediaStreams']
+
+ for media_stream in media_streams:
+ # Sort through Video, Audio, Subtitle
+ stream_type = media_stream['Type']
+ codec = media_stream.get('Codec', "").lower()
+ profile = media_stream.get('Profile', "").lower()
+
+ if stream_type == "Video":
+ # Height, Width, Codec, AspectRatio, AspectFloat, 3D
+ track = {
+
+ 'videocodec': codec,
+ 'height': media_stream.get('Height'),
+ 'width': media_stream.get('Width'),
+ 'video3DFormat': item.get('Video3DFormat'),
+ 'aspectratio': 1.85
+ }
+
+ try:
+ container = item['MediaSources'][0]['Container'].lower()
+ except:
+ container = ""
+
+ # Sort codec vs container/profile
+ if "msmpeg4" in codec:
+ track['videocodec'] = "divx"
+ elif "mpeg4" in codec:
+ if "simple profile" in profile or not profile:
+ track['videocodec'] = "xvid"
+ elif "h264" in codec:
+ if container in ("mp4", "mov", "m4v"):
+ track['videocodec'] = "avc1"
+
+ # Aspect ratio
+ if item.get('AspectRatio'):
+ # Metadata AR
+ aspectratio = item['AspectRatio']
+ else: # File AR
+ aspectratio = media_stream.get('AspectRatio', "0")
+
+ try:
+ aspectwidth, aspectheight = aspectratio.split(':')
+ track['aspectratio'] = round(float(aspectwidth) / float(aspectheight), 6)
+
+ except ValueError:
+ width = track['width']
+ height = track['height']
+
+ if width and height:
+ track['aspectratio'] = round(float(width / height), 6)
+
+ videotracks.append(track)
+
+ elif stream_type == "Audio":
+ # Codec, Channels, language
+ track = {
+
+ 'audiocodec': codec,
+ 'channels': media_stream.get('Channels'),
+ 'audiolanguage': media_stream.get('Language')
+ }
+
+ if "dca" in codec and "dts-hd ma" in profile:
+ track['audiocodec'] = "dtshd_ma"
+
+ audiotracks.append(track)
+
+ elif stream_type == "Subtitle":
+ # Language
+ subtitlelanguages.append(media_stream.get('Language', "Unknown"))
+
+ return {
+
+ 'video': videotracks,
+ 'audio': audiotracks,
+ 'subtitle': subtitlelanguages
+ }
+
+ def getRuntime(self):
+ item = self.item
+ try:
+ runtime = item['RunTimeTicks'] / 10000000.0
+
+ except KeyError:
+ runtime = item.get('CumulativeRunTimeTicks', 0) / 10000000.0
+
+ return runtime
+
+ def adjustResume(self, resume_seconds):
+
+ resume = 0
+ if resume_seconds:
+ resume = round(float(resume_seconds), 6)
+ jumpback = int(utils.settings('resumeJumpBack'))
+ if resume > jumpback:
+ # To avoid negative bookmark
+ resume = resume - jumpback
+
+ return resume
+
+ def getStudios(self):
+ # Process Studios
+ item = self.item
+ studios = []
+
+ try:
+ studio = item['SeriesStudio']
+ studios.append(self.verifyStudio(studio))
+
+ except KeyError:
+ studioList = item['Studios']
+ for studio in studioList:
+
+ name = studio['Name']
+ studios.append(self.verifyStudio(name))
+
+ return studios
+
+ def verifyStudio(self, studioName):
+ # Convert studio for Kodi to properly detect them
+ studios = {
+
+ 'abc (us)': "ABC",
+ 'fox (us)': "FOX",
+ 'mtv (us)': "MTV",
+ 'showcase (ca)': "Showcase",
+ 'wgn america': "WGN"
+ }
+
+ return studios.get(studioName.lower(), studioName)
+
+ def getChecksum(self):
+ # Use the etags checksum and userdata
+ item = self.item
+ userdata = item['UserData']
+
+ checksum = "%s%s%s%s%s%s" % (
+
+ item['Etag'],
+ userdata['Played'],
+ userdata['IsFavorite'],
+ userdata['PlaybackPositionTicks'],
+ userdata.get('UnplayedItemCount', ""),
+ userdata.get('LastPlayedDate', "")
+ )
+
+ return checksum
+
+ def getGenres(self):
+ item = self.item
+ all_genres = ""
+ genres = item.get('Genres', item.get('SeriesGenres'))
+
+ if genres:
+ all_genres = " / ".join(genres)
+
+ return all_genres
+
+ def getDateCreated(self):
+
+ try:
+ dateadded = self.item['DateCreated']
+ dateadded = dateadded.split('.')[0].replace('T', " ")
+ except KeyError:
+ dateadded = None
+
+ return dateadded
+
+ def getPremiereDate(self):
+
+ try:
+ premiere = self.item['PremiereDate']
+ premiere = premiere.split('.')[0].replace('T', " ")
+ except KeyError:
+ premiere = None
+
+ return premiere
+
+ def getOverview(self):
+
+ try:
+ overview = self.item['Overview']
+ overview = overview.replace("\"", "\'")
+ overview = overview.replace("\n", " ")
+ overview = overview.replace("\r", " ")
+ except KeyError:
+ overview = ""
+
+ return overview
+
+ def getTagline(self):
+
+ try:
+ tagline = self.item['Taglines'][0]
+ except IndexError:
+ tagline = None
+
+ return tagline
+
+ def getProvider(self, providername):
+
+ try:
+ provider = self.item['ProviderIds'][providername]
+ except KeyError:
+ provider = None
+
+ return provider
+
+ 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"
+
+ return mpaa
+
+ def getCountry(self):
+
+ try:
+ country = self.item['ProductionLocations'][0]
+ except IndexError:
+ country = None
+
+ return country
+
+ def getFilePath(self):
+
+ item = self.item
+ try:
+ filepath = item['Path']
+
+ except KeyError:
+ filepath = ""
+
+ else:
+ if "\\\\" in filepath:
+ # append smb protocol
+ filepath = filepath.replace("\\\\", "smb://")
+ filepath = filepath.replace("\\", "/")
+
+ if item.get('VideoType'):
+ videotype = 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/downloadutils.py b/resources/lib/downloadutils.py
new file mode 100644
index 00000000..5c47144d
--- /dev/null
+++ b/resources/lib/downloadutils.py
@@ -0,0 +1,401 @@
+# -*- coding: utf-8 -*-
+
+##################################################################################################
+
+import json
+import requests
+import logging
+
+import xbmc
+import xbmcgui
+
+import utils
+import clientinfo
+
+##################################################################################################
+
+# Disable requests logging
+from requests.packages.urllib3.exceptions import InsecureRequestWarning
+requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
+#logging.getLogger('requests').setLevel(logging.WARNING)
+
+##################################################################################################
+
+
+class DownloadUtils():
+
+ # Borg - multiple instances, shared state
+ _shared_state = {}
+ clientInfo = clientinfo.ClientInfo()
+ addonName = clientInfo.getAddonName()
+
+ # Requests session
+ s = None
+ timeout = 30
+
+
+ def __init__(self):
+
+ self.__dict__ = self._shared_state
+
+ def logMsg(self, msg, lvl=1):
+
+ className = self.__class__.__name__
+ utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
+
+
+ def setUsername(self, username):
+ # Reserved for userclient only
+ self.username = username
+ self.logMsg("Set username: %s" % username, 2)
+
+ def setUserId(self, userId):
+ # Reserved for userclient only
+ self.userId = userId
+ self.logMsg("Set userId: %s" % userId, 2)
+
+ def setServer(self, server):
+ # Reserved for userclient only
+ self.server = server
+ self.logMsg("Set server: %s" % server, 2)
+
+ def setToken(self, token):
+ # Reserved for userclient only
+ self.token = token
+ self.logMsg("Set token: %s" % token, 2)
+
+ def setSSL(self, ssl, sslclient):
+ # Reserved for userclient only
+ self.sslverify = ssl
+ self.sslclient = sslclient
+ self.logMsg("Verify SSL host certificate: %s" % ssl, 2)
+ self.logMsg("SSL client side certificate: %s" % sslclient, 2)
+
+
+ def postCapabilities(self, deviceId):
+
+ # 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,"
+ "GoToSettings,PageDown,PreviousLetter,TakeScreenshot,"
+ "VolumeUp,VolumeDown,ToggleMute,SendString,DisplayMessage,"
+ "SetAudioStreamIndex,SetSubtitleStreamIndex,"
+
+ "Mute,Unmute,SetVolume,"
+ "Play,Playstate,PlayNext"
+ )
+ }
+
+ self.logMsg("Capabilities URL: %s" % url, 2)
+ self.logMsg("Postdata: %s" % data, 2)
+
+ self.downloadUrl(url, postBody=data, type="POST")
+ self.logMsg("Posted capabilities to %s" % self.server, 2)
+
+ # Attempt at getting sessionId
+ url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId
+ 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"
+ % additionalUsers, 1)
+
+ # Get the user list from server to get the userId
+ url = "{server}/emby/Users?format=json"
+ result = self.downloadUrl(url)
+
+ for additional in additionalUsers:
+ addUser = additional.decode('utf-8').lower()
+
+ # Compare to server users to list of permanent additional users
+ for user in result:
+ username = user['Name'].lower()
+
+ if username in addUser:
+ userId = user['Id']
+ url = (
+ "{server}/emby/Sessions/%s/Users/%s?format=json"
+ % (sessionId, userId)
+ )
+ self.downloadUrl(url, postBody={}, type="POST")
+
+
+ def startSession(self):
+
+ self.deviceId = self.clientInfo.getDeviceId()
+
+ # User is identified from this point
+ # Attach authenticated header to the session
+ verify = None
+ cert = None
+ header = self.getHeader()
+
+ # If user enabled host certificate verification
+ try:
+ verify = self.sslverify
+ cert = self.sslclient
+ except:
+ self.logMsg("Could not load SSL settings.", 1)
+
+ # Start session
+ self.s = requests.Session()
+ self.s.headers = header
+ self.s.verify = verify
+ self.s.cert = cert
+ # Retry connections to the server
+ self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
+ self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
+
+ self.logMsg("Requests session started on: %s" % self.server, 1)
+
+ def stopSession(self):
+ try:
+ self.s.close()
+ except:
+ self.logMsg("Requests session could not be terminated.", 1)
+
+ def getHeader(self, authenticate=True):
+
+ clientInfo = self.clientInfo
+
+ deviceName = clientInfo.getDeviceName()
+ deviceId = clientInfo.getDeviceId()
+ version = clientInfo.getVersion()
+
+ if not authenticate:
+ # If user is not authenticated
+ auth = (
+ 'MediaBrowser Client="Kodi", Device="%s", DeviceId="%s", Version="%s"'
+ % (deviceName, deviceId, version))
+ header = {
+
+ 'Content-type': 'application/json',
+ 'Accept-encoding': 'gzip',
+ 'Accept-Charset': 'UTF-8,*',
+ 'Authorization': auth
+ }
+ self.logMsg("Header: %s" % header, 2)
+
+ else:
+ userId = self.userId
+ token = self.token
+ # Attached to the requests session
+ auth = (
+ 'MediaBrowser UserId="%s", Client="Kodi", Device="%s", DeviceId="%s", Version="%s"'
+ % (userId, deviceName, deviceId, version))
+ header = {
+
+ 'Content-type': 'application/json',
+ 'Accept-encoding': 'gzip',
+ '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):
+
+ self.logMsg("=== ENTER downloadUrl ===", 2)
+
+ timeout = self.timeout
+ default_link = ""
+
+ try:
+ # If user is authenticated
+ if (authenticate):
+ # Get requests session
+ 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)
+
+ except AttributeError:
+ # request session does not exists
+ # Get user information
+ self.userId = utils.window('emby_currUser')
+ self.server = utils.window('emby_server%s' % self.userId)
+ self.token = utils.window('emby_accessToken%s' % self.userId)
+ header = self.getHeader()
+ verifyssl = False
+ cert = None
+
+ # IF user enables ssl verification
+ if utils.settings('sslverify') == "true":
+ verifyssl = True
+ if utils.settings('sslcert') != "None":
+ cert = utils.settings('sslcert')
+
+ # Replace for the real values
+ url = url.replace("{server}", self.server)
+ url = url.replace("{UserId}", self.userId)
+
+ # Prepare request
+ if type == "GET":
+ r = requests.get(url,
+ json=postBody,
+ params=parameters,
+ headers=header,
+ timeout=timeout,
+ cert=cert,
+ verify=verifyssl)
+
+ elif type == "POST":
+ r = requests.post(url,
+ json=postBody,
+ headers=header,
+ timeout=timeout,
+ cert=cert,
+ verify=verifyssl)
+
+ elif type == "DELETE":
+ r = requests.delete(url,
+ json=postBody,
+ headers=header,
+ timeout=timeout,
+ cert=cert,
+ verify=verifyssl)
+
+ # If user is not authenticated
+ elif not authenticate:
+
+ header = self.getHeader(authenticate=False)
+ verifyssl = False
+
+ # If user enables ssl verification
+ try:
+ verifyssl = self.sslverify
+ except AttributeError:
+ pass
+
+ # Prepare request
+ if type == "GET":
+ r = requests.get(url,
+ json=postBody,
+ params=parameters,
+ headers=header,
+ timeout=timeout,
+ verify=verifyssl)
+
+ elif type == "POST":
+ r = requests.post(url,
+ json=postBody,
+ headers=header,
+ timeout=timeout,
+ verify=verifyssl)
+
+ ##### THE RESPONSE #####
+ self.logMsg(r.url, 2)
+ if r.status_code == 204:
+ # No body in the response
+ self.logMsg("====== 204 Success ======", 2)
+
+ elif r.status_code == requests.codes.ok:
+
+ try:
+ # UTF-8 - JSON object
+ r = r.json()
+ self.logMsg("====== 200 Success ======", 2)
+ self.logMsg("Response: %s" % r, 2)
+ return r
+
+ except:
+ if r.headers.get('content-type') != "text/html":
+ self.logMsg("Unable to convert the response for: %s" % url, 1)
+ else:
+ r.raise_for_status()
+
+ ##### EXCEPTIONS #####
+
+ except requests.exceptions.ConnectionError as e:
+ # Make the addon aware of status
+ if utils.window('emby_online') != "false":
+ self.logMsg("Server unreachable at: %s" % url, 0)
+ self.logMsg(e, 2)
+ utils.window('emby_online', value="false")
+
+ except requests.exceptions.ConnectTimeout as e:
+ self.logMsg("Server timeout at: %s" % url, 0)
+ self.logMsg(e, 1)
+
+ except requests.exceptions.HTTPError as e:
+
+ if r.status_code == 401:
+ # Unauthorized
+ status = utils.window('emby_serverStatus')
+
+ if 'X-Application-Error-Code' in r.headers:
+ # Emby server errors
+ if r.headers['X-Application-Error-Code'] == "ParentalControl":
+ # Parental control - access restricted
+ utils.window('emby_serverStatus', value="restricted")
+ xbmcgui.Dialog().notification(
+ heading="Emby server",
+ message="Access restricted.",
+ 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
+
+ elif status not in ("401", "Auth"):
+ # Tell userclient token has been revoked.
+ utils.window('emby_serverStatus', value="401")
+ self.logMsg("HTTP Error: %s" % e, 0)
+ xbmcgui.Dialog().notification(
+ heading="Error connecting",
+ message="Unauthorized.",
+ icon=xbmcgui.NOTIFICATION_ERROR)
+ return 401
+
+ elif r.status_code in (301, 302):
+ # Redirects
+ pass
+ elif r.status_code == 400:
+ # Bad requests
+ pass
+
+ except requests.exceptions.SSLError as e:
+ self.logMsg("Invalid SSL certificate for: %s" % url, 0)
+ self.logMsg(e, 1)
+
+ except requests.exceptions.RequestException as e:
+ self.logMsg("Unknown error connecting to: %s" % url, 0)
+ self.logMsg(e, 1)
+
+ return default_link
\ No newline at end of file
diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py
new file mode 100644
index 00000000..588b8d3c
--- /dev/null
+++ b/resources/lib/entrypoint.py
@@ -0,0 +1,842 @@
+# -*- 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 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)
+ if path:
+ addDirectoryItem(label, path)
+
+ # some extra entries for settings and stuff. TODO --> localize the labels
+ addDirectoryItem("Network credentials", "plugin://plugin.video.emby/?mode=passwords", False)
+ addDirectoryItem("Settings", "plugin://plugin.video.emby/?mode=settings", False)
+ addDirectoryItem("Add user to session", "plugin://plugin.video.emby/?mode=adduser", False)
+ #addDirectoryItem("Cache all images to Kodi texture cache (advanced)", "plugin://plugin.video.emby/?mode=texturecache")
+ addDirectoryItem("Perform manual sync", "plugin://plugin.video.emby/?mode=manualsync", False)
+ addDirectoryItem(
+ label="Repair local database (force update all content)",
+ path="plugin://plugin.video.emby/?mode=repair",
+ folder=False)
+ addDirectoryItem(
+ label="Perform local database reset (full resync)",
+ path="plugin://plugin.video.emby/?mode=reset",
+ folder=False)
+ addDirectoryItem(
+ label="Sync Emby Theme Media to Kodi",
+ path="plugin://plugin.video.emby/?mode=thememedia",
+ folder=False)
+
+ xbmcplugin.endOfDirectory(int(sys.argv[1]))
+
+##### ADD ADDITIONAL USERS #####
+def addUser():
+
+ doUtils = downloadutils.DownloadUtils()
+ 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)
+
+ url = "{server}/emby/Sessions?DeviceId=%s" % deviceId
+ result = doUtils.downloadUrl(url)
+ additionalUsers = result[0]['AdditionalUsers']
+ count = 0
+ for additionaluser in additionalUsers:
+ url = "{server}/emby/Users/%s?format=json" % additionaluser['UserId']
+ result = doUtils.downloadUrl(url)
+ utils.window('EmbyAdditionalUserImage.%s' % count,
+ value=artwork.Artwork().getUserArtwork(result, 'Primary'))
+ utils.window('EmbyAdditionalUserPosition.%s' % additionaluser['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 = open(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 = open(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)
+
+##### 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)
+ try:
+ channels = result['Items']
+ except TypeError:
+ pass
+ else:
+ for item in channels:
+
+ API = api.API(item)
+ itemid = item['Id']
+ itemtype = item['Type']
+ title = item.get('Name', "Missing Title")
+ li = xbmcgui.ListItem(title)
+
+ if itemtype == "ChannelFolderItem":
+ isFolder = True
+ else:
+ isFolder = False
+
+ channelId = item.get('ChannelId', "")
+ channelName = item.get('ChannelName', "")
+
+ premieredate = API.getPremiereDate()
+ # Process Genres
+ genre = API.getGenres()
+ # Process UserData
+ overlay = 0
+
+ userdata = API.getUserData()
+ seektime = userdata['Resume']
+ played = userdata['Played']
+ if played:
+ overlay = 7
+ else:
+ overlay = 6
+
+ favorite = userdata['Favorite']
+ if favorite:
+ overlay = 5
+
+ playcount = userdata['PlayCount']
+ if playcount is None:
+ playcount = 0
+
+ # Populate the details list
+ details = {
+
+ 'title': title,
+ 'channelname': channelName,
+ 'plot': API.getOverview(),
+ 'Overlay': str(overlay),
+ 'playcount': str(playcount)
+ }
+
+ if itemtype == "ChannelVideoItem":
+ xbmcplugin.setContent(_addon_id, 'movies')
+ elif itemtype == "ChannelAudioItem":
+ xbmcplugin.setContent(_addon_id, 'songs')
+
+ # Populate the extradata list and artwork
+ pbutils.PlaybackUtils(item).setArtwork(li)
+ extradata = {
+
+ 'id': itemid,
+ 'rating': item.get('CommunityRating'),
+ 'year': item.get('ProductionYear'),
+ 'premieredate': premieredate,
+ 'genre': genre,
+ 'playcount': str(playcount),
+ 'itemtype': itemtype
+ }
+ li.setInfo('video', infoLabels=extradata)
+ li.setThumbnailImage(art.getAllArtwork(item)['Primary'])
+ li.setIconImage('DefaultTVShows.png')
+
+ 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': "contains", '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=item['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': "contains", '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=item['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': "contains", '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=item['file'],
+ listitem=li)
+ count += 1
+
+ if count == limit:
+ break
+
+ xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
+
+##### GET EXTRAFANART FOR LISTITEM #####
+def getExtraFanArt():
+
+ emby = embyserver.Read_EmbyServer()
+ art = artwork.Artwork()
+
+ # Get extrafanart for listitem
+ # this will only be used for skins that actually call the listitem's path + fanart dir...
+ try:
+ # Only do this if the listitem has actually changed
+ itemPath = xbmc.getInfoLabel("ListItem.FileNameAndPath")
+
+ if not itemPath:
+ itemPath = xbmc.getInfoLabel("ListItem.Path")
+
+ if any([x in itemPath for x in ['tvshows', 'musicvideos', 'movies']]):
+ params = urlparse.parse_qs(itemPath)
+ embyId = params['id'][0]
+
+ utils.logMsg("EMBY", "Requesting extrafanart for Id: %s" % embyId, 1)
+
+ # 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]
+ fanartFile = os.path.join(fanartDir, "fanart%s.jpg" % tag)
+ 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)
+ 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, 1)
+
+ # 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/kodimonitor.py b/resources/lib/kodimonitor.py
new file mode 100644
index 00000000..1b3f7862
--- /dev/null
+++ b/resources/lib/kodimonitor.py
@@ -0,0 +1,195 @@
+# -*- 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 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)
+
+
+ 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("Properties already set for item.", 1)
+ else:
+ if ((utils.settings('useDirectPaths') == "1" and not type == "song") or
+ (type == "song" and utils.settings('disableMusic') == "false")):
+ # 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":
+
+ 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.
+ offerDelete = False
+
+ if type == "episode" and utils.settings('deleteTV') == "true":
+ offerDelete = True
+ elif type == "movie" and utils.settings('deleteMovies') == "true":
+ offerDelete = True
+
+ if utils.settings('offerDelete') != "true":
+ # Delete could be disabled, even if the subsetting is enabled.
+ offerDelete = False
+
+ if offerDelete:
+ 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('skipConfirmDelete') != "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":
+ utils.window('emby_customPlaylist', clear=True, windowid=10101)
+ #xbmcgui.Window(10101).clearProperties()
+ self.logMsg("Clear playlist properties.")
\ No newline at end of file
diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py
new file mode 100644
index 00000000..d6d15d8e
--- /dev/null
+++ b/resources/lib/librarysync.py
@@ -0,0 +1,1338 @@
+# -*- 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()
+ 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):
+ # Run at start up - optional to use the server plugin
+ if utils.settings('SyncInstallRunDone') == "true":
+
+ # Validate views
+ self.refreshViews()
+ completed = False
+ # Verify if server plugin is installed.
+ if utils.settings('serverSync') == "true":
+ # Try to use fast start up
+ url = "{server}/emby/Plugins?format=json"
+ result = self.doUtils.downloadUrl(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 = self.fullSync(manualrun=True)
+ 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"
+ self.logMsg("Last sync run: %s" % lastSync, 1)
+
+ url = "{server}/emby/Emby.Kodi.SyncQueue/{UserId}/GetItems?format=json"
+ params = {'LastUpdateDT': lastSync}
+ result = self.doUtils.downloadUrl(url, 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
+
+ url = "{server}/Emby.Kodi.SyncQueue/GetServerDateTime?format=json"
+ result = self.doUtils.downloadUrl(url)
+ try: # datetime fails when used more than once, TypeError
+ server_time = result['ServerDateTime']
+ server_time = datetime.strptime(server_time, "%Y-%m-%dT%H:%M:%SZ")
+
+ 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):
+ # Central commit, verifies if Kodi database update is running
+ kodidb_scan = utils.window('emby_kodiScan') == "true"
+
+ while kodidb_scan:
+
+ self.logMsg("Kodi scan is running. Waiting...", 1)
+ kodidb_scan = utils.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):
+ # Only run once when first setting up. Can be run manually.
+ emby = self.emby
+ music_enabled = utils.settings('enableMusic') == "true"
+
+ utils.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"
+ else:
+ message = "Initial sync"
+
+ pDialog = self.progressDialog("%s" % message, forced=True)
+ starttotal = datetime.now()
+
+ # Set views
+ self.maintainViews(embycursor, kodicursor)
+ embyconn.commit()
+
+ # Sync video library
+ process = {
+
+ 'movies': self.movies,
+ 'musicvideos': self.musicvideos,
+ 'tvshows': self.tvshows,
+ 'homevideos': self.homevideos
+ }
+ for itemtype in process:
+ startTime = datetime.now()
+ completed = process[itemtype](embycursor, kodicursor, pDialog, compare=manualrun)
+ if not completed:
+
+ utils.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)
+
+ # sync music
+ if music_enabled:
+
+ musicconn = utils.kodiSQL('music')
+ musiccursor = musicconn.cursor()
+
+ startTime = datetime.now()
+ completed = self.music(embycursor, musiccursor, pDialog, compare=manualrun)
+ if not completed:
+
+ utils.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()
+ kodicursor.close()
+
+ utils.settings('SyncInstallRunDone', value="true")
+ utils.settings("dbCreatedWithVersion", self.clientInfo.getVersion())
+ self.saveLastSync()
+ # tell any widgets to refresh because the content has changed
+ utils.window('widgetreload', value=datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
+ xbmc.executebuiltin('UpdateLibrary(video)')
+ elapsedtotal = datetime.now() - starttotal
+
+ utils.window('emby_dbScan', clear=True)
+ xbmcgui.Dialog().notification(
+ heading="Emby for Kodi",
+ message="%s completed in: %s!" %
+ (message, 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_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.downloadUrl(url)
+ grouped_views = result['Items']
+
+ try:
+ groupedFolders = self.user.userSettings['Configuration']['GroupedFolders']
+ except TypeError:
+ url = "{server}/emby/Users/{UserId}?format=json"
+ result = doUtils.downloadUrl(url)
+ groupedFolders = result['Configuration']['GroupedFolders']
+
+ # total nodes for window properties
+ vnodes.clearProperties()
+ totalnodes = 0
+
+ # Set views for supported media type
+ mediatypes = ['movies', 'tvshows', 'musicvideos', 'homevideos', 'music']
+ for mediatype in mediatypes:
+
+ # 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 folderid in groupedFolders:
+ # Media folders are grouped into userview
+ for grouped_view in grouped_views:
+ if (grouped_view['Type'] == "UserView" and
+ grouped_view['CollectionType'] == mediatype):
+ # Take the name of the userview
+ foldername = grouped_view['Name']
+ break
+
+ # 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 mediatype != "music":
+ utils.playlistXSP(mediatype, foldername, viewtype)
+ # Create the video node
+ if mediatype != "musicvideos":
+ vnodes.viewNode(totalnodes, foldername, mediatype, viewtype)
+ 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 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, current_viewtype, True)
+ # Delete video node
+ if mediatype != "musicvideos":
+ vnodes.viewNode(
+ indexnumber=totalnodes,
+ tagname=current_viewname,
+ mediatype=mediatype,
+ viewtype=current_viewtype,
+ delete=True)
+ # Added new playlist
+ utils.playlistXSP(mediatype, foldername, viewtype)
+ # Add new video node
+ if mediatype != "musicvideos":
+ vnodes.viewNode(totalnodes, foldername, mediatype, viewtype)
+ 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:
+ if mediatype != "music":
+ # Validate the playlist exists or recreate it
+ utils.playlistXSP(mediatype, foldername, viewtype)
+ # Create the video node if not already exists
+ if mediatype != "musicvideos":
+ vnodes.viewNode(totalnodes, foldername, mediatype, viewtype)
+ 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))
+
+
+ def movies(self, embycursor, kodicursor, pdialog, compare=False):
+ # 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')
+ self.logMsg("Media folders: %s" % views, 1)
+
+ if compare:
+ # 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="Gathering movies from view: %s..." % viewName)
+
+ if compare:
+ # Manual sync
+ if pdialog:
+ pdialog.update(
+ heading="Emby for Kodi",
+ message="Comparing movies from view: %s..." % viewName)
+
+ all_embymovies = emby.getMovies(viewId, basic=True)
+ 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 = emby.getFullItems(updatelist)
+ total = len(updatelist)
+ del updatelist[:]
+ else:
+ # Initial or repair sync
+ all_embymovies = emby.getMovies(viewId)
+ 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:
+ self.logMsg("Movies finished.", 2)
+
+
+ ##### PROCESS BOXSETS #####
+ if pdialog:
+ pdialog.update(heading="Emby for Kodi", message="Gathering boxsets from server...")
+
+ boxsets = emby.getBoxset()
+
+ if compare:
+ # Manual sync
+ embyboxsets = []
+
+ if pdialog:
+ pdialog.update(
+ heading="Emby for Kodi",
+ message="Comparing boxsets...")
+
+ 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)
+
+ self.logMsg("Boxsets to update: %s" % updatelist, 1)
+ total = len(updatelist)
+ else:
+ 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)
+
+
+ ##### PROCESS DELETES #####
+ if compare:
+ # Manual sync, 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, compare=False):
+ # Get musicvideos from emby
+ emby = self.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)
+
+ if compare:
+ # 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="Gathering musicvideos from view: %s..." % viewName)
+
+ if compare:
+ # Manual sync
+ if pdialog:
+ pdialog.update(
+ heading="Emby for Kodi",
+ message="Comparing musicvideos from view: %s..." % viewName)
+
+ all_embymvideos = emby.getMusicVideos(viewId, basic=True)
+ 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 = emby.getFullItems(updatelist)
+ total = len(updatelist)
+ del updatelist[:]
+ else:
+ # Initial or repair sync
+ all_embymvideos = emby.getMusicVideos(viewId)
+ 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)
+
+ ##### PROCESS DELETES #####
+ if compare:
+ # Manual sync, 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 homevideos(self, embycursor, kodicursor, pdialog, compare=False):
+ # Get homevideos from emby
+ emby = self.emby
+ emby_db = embydb.Embydb_Functions(embycursor)
+ hvideos = itemtypes.HomeVideos(embycursor, kodicursor)
+
+ views = emby_db.getView_byType('homevideos')
+ self.logMsg("Media folders: %s" % views, 1)
+
+ if compare:
+ # Pull the list of homevideos in Kodi
+ try:
+ all_kodihvideos = dict(emby_db.getChecksum('Video'))
+ except ValueError:
+ all_kodihvideos = {}
+
+ all_embyhvideosIds = 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="Gathering homevideos from view: %s..." % viewName)
+
+ all_embyhvideos = emby.getHomeVideos(viewId)
+
+ if compare:
+ # Manual sync
+ if pdialog:
+ pdialog.update(
+ heading="Emby for Kodi",
+ message="Comparing homevideos from view: %s..." % viewName)
+
+ for embyhvideo in all_embyhvideos['Items']:
+
+ if self.shouldStop():
+ return False
+
+ API = api.API(embyhvideo)
+ itemid = embyhvideo['Id']
+ all_embyhvideosIds.add(itemid)
+
+
+ if all_kodihvideos.get(itemid) != API.getChecksum():
+ # Only update if homemovie is not in Kodi or checksum is different
+ updatelist.append(itemid)
+
+ self.logMsg("HomeVideos to update for %s: %s" % (viewName, updatelist), 1)
+ embyhvideos = emby.getFullItems(updatelist)
+ total = len(updatelist)
+ del updatelist[:]
+ else:
+ total = all_embyhvideos['TotalRecordCount']
+ embyhvideos = all_embyhvideos['Items']
+
+ if pdialog:
+ pdialog.update(heading="Processing %s / %s items" % (viewName, total))
+
+ count = 0
+ for embyhvideo in embyhvideos:
+ # Process individual homemovies
+ if self.shouldStop():
+ return False
+
+ title = embyhvideo['Name']
+ if pdialog:
+ percentage = int((float(count) / float(total))*100)
+ pdialog.update(percentage, message=title)
+ count += 1
+ hvideos.add_update(embyhvideo, viewName, viewId)
+ else:
+ self.logMsg("HomeVideos finished.", 2)
+
+ ##### PROCESS DELETES #####
+ if compare:
+ # Manual sync, process deletes
+ for kodihvideo in all_kodihvideos:
+ if kodihvideo not in all_embyhvideosIds:
+ hvideos.remove(kodihvideo)
+ else:
+ self.logMsg("HomeVideos compare finished.", 1)
+
+ return True
+
+ def tvshows(self, embycursor, kodicursor, pdialog, compare=False):
+ # 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')
+ self.logMsg("Media folders: %s" % views, 1)
+
+ if compare:
+ # Pull the list of movies and boxsets 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="Gathering tvshows from view: %s..." % viewName)
+
+ if compare:
+ # Manual sync
+ if pdialog:
+ pdialog.update(
+ heading="Emby for Kodi",
+ message="Comparing tvshows from view: %s..." % viewName)
+
+ all_embytvshows = emby.getShows(viewId, basic=True)
+ 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 = emby.getFullItems(updatelist)
+ total = len(updatelist)
+ del updatelist[:]
+ else:
+ all_embytvshows = emby.getShows(viewId)
+ 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)
+
+ if not compare:
+ # 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:
+ if compare:
+ # Get all episodes in view
+ if pdialog:
+ pdialog.update(
+ heading="Emby for Kodi",
+ message="Comparing episodes from view: %s..." % viewName)
+
+ all_embyepisodes = emby.getEpisodes(viewId, basic=True)
+ 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 = emby.getFullItems(updatelist)
+ total = len(updatelist)
+ del updatelist[:]
+
+ for episode in embyepisodes:
+
+ # Process individual episode
+ if self.shouldStop():
+ return False
+
+ title = episode['SeriesName']
+ episodetitle = episode['Name']
+ if pdialog:
+ pdialog.update(percentage, message="%s - %s" % (title, episodetitle))
+ tvshows.add_updateEpisode(episode)
+ else:
+ self.logMsg("TVShows finished.", 2)
+
+ ##### PROCESS DELETES #####
+ if compare:
+ # Manual sync, 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, compare=False):
+ # Get music from emby
+ emby = self.emby
+ emby_db = embydb.Embydb_Functions(embycursor)
+ music = itemtypes.Music(embycursor, kodicursor)
+
+ if compare:
+ # Pull the list of movies and boxsets in Kodi
+ 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="Gathering %s..." % type)
+
+ if compare:
+ # Manual Sync
+ if pdialog:
+ pdialog.update(
+ heading="Emby for Kodi",
+ message="Comparing %s..." % type)
+
+ if type != "artists":
+ all_embyitems = process[type][0](basic=True)
+ else:
+ all_embyitems = process[type][0]()
+ 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)
+
+ self.logMsg("%s to update: %s" % (type, updatelist), 1)
+ embyitems = emby.getFullItems(updatelist)
+ total = len(updatelist)
+ del updatelist[:]
+ else:
+ all_embyitems = process[type][0]()
+ total = all_embyitems['TotalRecordCount']
+ embyitems = all_embyitems['Items']
+
+ 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)
+ else:
+ self.logMsg("%s finished." % type, 2)
+
+ ##### PROCESS DELETES #####
+ if compare:
+ # Manual sync, 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
+
+ # 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 = self.emby
+ emby_db = embydb.Embydb_Functions(embycursor)
+ pDialog = None
+
+ if self.refresh_views:
+ # Received userconfig update
+ self.refresh_views = False
+ self.maintainViews(embycursor, kodicursor)
+ self.forceLibraryUpdate = 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:
+ update = True
+ del items['Unsorted']
+
+ doupdate = items_process.itemsbyId(items, type, pDialog)
+ if doupdate:
+ update = True
+
+ if update:
+ self.forceLibraryUpdate = True
+
+
+ if self.forceLibraryUpdate:
+ # Force update the Kodi library
+ self.forceLibraryUpdate = False
+ self.dbCommit(kodiconn)
+ embyconn.commit()
+ self.saveLastSync()
+
+ # tell any widgets to refresh because the content has changed
+ utils.window('widgetreload', value=datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
+
+ 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:
+ 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):
+
+ startupComplete = False
+ monitor = self.monitor
+
+ self.logMsg("---===### 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 (utils.window('emby_dbCheck') != "true" and
+ utils.settings('SyncInstallRunDone') == "true"):
+
+ # Verify the validity of the database
+ currentVersion = utils.settings('dbCreatedWithVersion')
+ minVersion = utils.window('emby_minDBVersion')
+ uptoDate = self.compareDBVersion(currentVersion, minVersion)
+
+ if not uptoDate:
+ self.logMsg(
+ "Db version out of date: %s minimum version required: %s"
+ % (currentVersion, minVersion), 0)
+
+ resp = xbmcgui.Dialog().yesno(
+ heading="Db Version",
+ line1=(
+ "Detected the database needs to be "
+ "recreated for this version of Emby for Kodi. "
+ "Proceed?"))
+ if not resp:
+ self.logMsg("Db version out of date! USER IGNORED!", 0)
+ xbmcgui.Dialog().ok(
+ heading="Emby for Kodi",
+ line1=(
+ "Emby for Kodi may not work correctly "
+ "until the database is reset."))
+ else:
+ utils.reset()
+
+ utils.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)
+
+ xbmcgui.Dialog().ok(
+ heading="Emby Warning",
+ line1=(
+ "Cancelling the database syncing process. "
+ "Current Kodi versoin: %s is unsupported. "
+ "Please verify your logs for more info."
+ % xbmc.getInfoLabel('System.BuildVersion')))
+ break
+
+ # Run start up sync
+ self.logMsg("Db version: %s" % utils.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 utils.window('emby_dbScan') != "true":
+ self.incrementalSync()
+
+ if (utils.window('emby_onWake') == "true" and
+ utils.window('emby_online') == "true"):
+ # Kodi is waking up
+ # Set in kodimonitor.py
+ utils.window('emby_onWake', clear=True)
+ if utils.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 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)
\ No newline at end of file
diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py
new file mode 100644
index 00000000..affa2b81
--- /dev/null
+++ b/resources/lib/playbackutils.py
@@ -0,0 +1,353 @@
+# -*- 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()
+
+ 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):
+
+ self.logMsg("Play called.", 1)
+
+ doUtils = self.doUtils
+ item = self.item
+ API = self.API
+ listitem = xbmcgui.ListItem()
+ playutils = putils.PlayUtils(item)
+
+ 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 = utils.window('emby_playbackProps', windowid=10101) == "true"
+ introsPlaylist = False
+ dummyPlaylist = False
+
+ self.logMsg("Playlist start position: %s" % startPos, 1)
+ self.logMsg("Playlist plugin position: %s" % currentPosition, 1)
+ self.logMsg("Playlist size: %s" % sizePlaylist, 1)
+
+ ############### 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:
+
+ utils.window('emby_playbackProps', value="true", windowid=10101)
+ self.logMsg("Setting up properties in playlist.", 1)
+
+ if (not homeScreen and not seektime and
+ utils.window('emby_customPlaylist', windowid=10101) != "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, item['Type'].lower())
+ currentPosition += 1
+
+ ############### -- CHECK FOR INTROS ################
+
+ if utils.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.downloadUrl(url)
+
+ if intros['TotalRecordCount'] != 0:
+ getTrailers = True
+
+ if utils.settings('askCinema') == "true":
+ resp = xbmcgui.Dialog().yesno("Emby Cinema Mode", "Play trailers?")
+ 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 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, 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.downloadUrl(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)
+ utils.window('emby_playbackProps', clear=True, windowid=10101)
+
+ #self.pl.verifyPlaylist()
+ ########## SETUP MAIN ITEM ##########
+
+ # For transcoding only, ask for audio/subs pref
+ if utils.window('emby_%s.playmethod' % playurl) == "Transcode":
+ playurl = playutils.audioSubsPref(playurl)
+ utils.window('emby_%s.playmethod' % playurl, value="Transcode")
+
+ listitem.setPath(playurl)
+ self.setProperties(playurl, listitem)
+
+ ############### PLAYBACK ################
+
+ if homeScreen and seektime:
+ self.logMsg("Play as a widget item.", 1)
+ self.setListItem(listitem)
+ xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
+
+ elif ((introsPlaylist and utils.window('emby_customPlaylist', windowid=10101) == "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):
+ # Set all properties necessary for plugin path playback
+ item = self.item
+ itemid = item['Id']
+ itemtype = item['Type']
+
+ embyitem = "emby_%s" % playurl
+ utils.window('%s.runtime' % embyitem, value=str(item.get('RunTimeTicks')))
+ utils.window('%s.type' % embyitem, value=itemtype)
+ utils.window('%s.itemid' % embyitem, value=itemid)
+
+ if itemtype == "Episode":
+ utils.window('%s.refreshid' % embyitem, value=item.get('SeriesId'))
+ else:
+ utils.window('%s.refreshid' % embyitem, value=itemid)
+
+ # Append external subtitles to stream
+ playmethod = utils.window('%s.playmethod' % embyitem)
+ # Only for direct play and direct stream
+ subtitles = self.externalSubs(playurl)
+ if playmethod in ("DirectStream", "Transcode"):
+ # Direct play automatically appends external
+ 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
+ type = 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 type:
+ # 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'])
+ listItem.setInfo('video', infoLabels=metadata)
\ No newline at end of file
diff --git a/resources/lib/player.py b/resources/lib/player.py
new file mode 100644
index 00000000..b6d25e19
--- /dev/null
+++ b/resources/lib/player.py
@@ -0,0 +1,502 @@
+# -*- 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()
+ 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 ):
+ # 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
+ 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 = utils.window("emby_%s.itemid" % currentFile)
+ tryCount = 0
+ while not itemId:
+
+ xbmc.sleep(200)
+ itemId = utils.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 = utils.window("%s.runtime" % embyitem)
+ refresh_id = utils.window("%s.refreshid" % embyitem)
+ playMethod = utils.window("%s.playmethod" % embyitem)
+ itemType = utils.window("%s.type" % embyitem)
+ utils.window('emby_skipWatched%s' % itemId, value="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'] = utils.window("%sAudioStreamIndex" % currentFile)
+ postdata['SubtitleStreamIndex'] = utils.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 = utils.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.downloadUrl(url, postBody=postdata, type="POST")
+
+ # Ensure we do have a runtime
+ try:
+ runtime = int(runtime)
+ except ValueError:
+ runtime = 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)
+ 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
+
+ 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 ):
+ # Will be called when user stops xbmc playing a file
+ self.logMsg("ONPLAYBACK_STOPPED", 2)
+ xbmcgui.Window(10101).clearProperties()
+ self.logMsg("Clear playlist properties.")
+ self.stopAll()
+
+ def onPlayBackEnded( self ):
+ # Will be called when xbmc stops playing a file
+ self.logMsg("ONPLAYBACK_ENDED", 2)
+ self.stopAll()
+
+ def stopAll(self):
+
+ doUtils = self.doUtils
+
+ 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']
+ type = data['Type']
+ playMethod = data['playmethod']
+
+ if currentPosition and runtime:
+ try:
+ percentComplete = (currentPosition * 10000000) / int(runtime)
+ except ZeroDivisionError:
+ # Runtime is 0.
+ percentComplete = 0
+
+ markPlayedAt = float(utils.settings('markPlayed')) / 100
+ self.logMsg(
+ "Percent complete: %s Mark played at: %s"
+ % (percentComplete, markPlayedAt), 1)
+
+ # Prevent manually mark as watched in Kodi monitor
+ utils.window('emby_skipWatched%s' % itemid, value="true")
+
+ 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
+ doUtils.downloadUrl(url, type="DELETE")
+
+ # Send the delete action to the server.
+ offerDelete = False
+
+ if type == "Episode" and utils.settings('deleteTV') == "true":
+ offerDelete = True
+ elif type == "Movie" and utils.settings('deleteMovies') == "true":
+ offerDelete = True
+
+ if utils.settings('offerDelete') != "true":
+ # Delete could be disabled, even if the subsetting is enabled.
+ offerDelete = False
+
+ if percentComplete >= markPlayedAt and offerDelete:
+ if utils.settings('skipConfirmDelete') != "true":
+ resp = xbmcgui.Dialog().yesno(
+ heading="Confirm delete",
+ line1="Delete file on Emby Server?")
+ if not resp:
+ self.logMsg("User skipped deletion.", 1)
+ continue
+
+ url = "{server}/emby/Items/%s?format=json" % itemid
+ self.logMsg("Deleting request: %s" % itemid)
+ doUtils.downloadUrl(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.downloadUrl(url, postBody=postdata, type="POST")
\ No newline at end of file
diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py
new file mode 100644
index 00000000..0a74690b
--- /dev/null
+++ b/resources/lib/playutils.py
@@ -0,0 +1,397 @@
+# -*- 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):
+
+ item = self.item
+ playurl = None
+
+ if item['MediaSources'][0]['Protocol'] == "Http":
+ # Only play as http
+ self.logMsg("File protocol is http.", 1)
+ playurl = self.httpPlay()
+ utils.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
+ utils.window('emby_%s.playmethod' % playurl, value="DirectPlay")
+
+ elif self.isDirectStream():
+
+ self.logMsg("File is direct streaming.", 1)
+ playurl = self.directStream()
+ # Set playmethod property
+ utils.window('emby_%s.playmethod' % playurl, value="DirectStream")
+
+ elif self.isTranscoding():
+
+ self.logMsg("File is transcoding.", 1)
+ playurl = self.transcoding()
+ # Set playmethod property
+ utils.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 type == "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):
+
+ item = self.item
+
+ # Requirement: Filesystem, Accessible path
+ if utils.settings('playFromStream') == "true":
+ # User forcing to play via HTTP
+ self.logMsg("Can't direct play, play from HTTP enabled.", 1)
+ return False
+
+ if (utils.settings('transcodeH265') == "true" and
+ result['MediaSources'][0]['Name'].startswith("1080P/H265")):
+ # Avoid H265 1080p
+ self.logMsg("Option to transcode 1080P/H265 enabled.", 1)
+ return False
+
+ canDirectPlay = 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 = item['LocationType']
+ if location == "FileSystem":
+ # Verify the path
+ if not self.fileExists():
+ self.logMsg("Unable to direct play.")
+ try:
+ count = int(utils.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
+ utils.settings('failCount', value=str(count+1))
+ xbmcgui.Dialog().notification(
+ heading="Emby server",
+ message="Unable to direct play.",
+ icon="special://home/addons/plugin.video.emby/icon.png",
+ sound=False)
+ elif utils.settings('playFromStream') != "true":
+ # Permanently set direct stream as true
+ utils.settings('playFromStream', value="true")
+ utils.settings('failCount', value="0")
+ xbmcgui.Dialog().notification(
+ heading="Emby server",
+ message=("Direct play failed 3 times. Enabled play "
+ "from HTTP in the add-on settings."),
+ 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):
+
+ 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.")
+ return False
+
+ def isDirectStream(self):
+
+ item = self.item
+
+ if (utils.settings('transcodeH265') == "true" and
+ result['MediaSources'][0]['Name'].startswith("1080P/H265")):
+ # Avoid H265 1080p
+ self.logMsg("Option to transcode 1080P/H265 enabled.", 1)
+ 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():
+ self.logMsg("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']
+ type = item['Type']
+
+ if 'Path' in item and item['Path'].endswith('.strm'):
+ # Allow strm loading when direct streaming
+ playurl = self.directPlay()
+ elif type == "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):
+
+ 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):
+
+ 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):
+ # For transcoding only
+ # Present the list of audio to select from
+ audioStreamsList = {}
+ audioStreams = []
+ audioStreamsChannelsList = {}
+ subtitleStreamsList = {}
+ subtitleStreams = ['No subtitles']
+ 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:
+ if stream['IsExternal']:
+ continue
+ try:
+ track = "%s - %s" % (index, stream['Language'])
+ except:
+ track = "%s - %s" % (index, stream['Codec'])
+
+ default = stream['IsDefault']
+ forced = stream['IsForced']
+ if default:
+ track = "%s - Default" % track
+ if forced:
+ track = "%s - Forced" % track
+
+ subtitleStreamsList[track] = index
+ subtitleStreams.append(track)
+
+
+ if len(audioStreams) > 1:
+ resp = xbmcgui.Dialog().select("Choose the audio stream", 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 = xbmcgui.Dialog().select("Choose the subtitle stream", subtitleStreams)
+ if resp == 0:
+ # User selected no subtitles
+ pass
+ elif resp > -1:
+ # User selected subtitles
+ selected = subtitleStreams[resp]
+ selectSubsIndex = subtitleStreamsList[selected]
+ 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/userclient.py b/resources/lib/userclient.py
new file mode 100644
index 00000000..a6562a53
--- /dev/null
+++ b/resources/lib/userclient.py
@@ -0,0 +1,471 @@
+# -*- 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()
+ self.logLevel = int(utils.settings('logLevel'))
+
+ 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):
+
+ username = self.getUsername()
+ w_userId = utils.window('emby_userId%s' % username)
+ s_userId = utils.settings('userId%s' % username)
+
+ # Verify the window property
+ if w_userId:
+ if not s_userId:
+ # Save access token if it's missing from settings
+ utils.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):
+
+ alternate = utils.settings('altip') == "true"
+ if alternate:
+ # Alternate host
+ HTTPS = utils.settings('secondhttps') == "true"
+ host = utils.settings('secondipaddress')
+ port = utils.settings('secondport')
+ else:
+ # Original host
+ HTTPS = utils.settings('https') == "true"
+ host = utils.settings('ipaddress')
+ port = utils.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):
+
+ username = self.getUsername()
+ w_token = utils.window('emby_accessToken%s' % username)
+ s_token = utils.settings('accessToken')
+
+ # Verify the window property
+ if w_token:
+ if not s_token:
+ # Save access token if it's missing from settings
+ utils.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)
+ utils.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
+ s_sslverify = utils.settings('sslverify')
+ if utils.settings('altip') == "true":
+ s_sslverify = utils.settings('secondsslverify')
+
+ if s_sslverify == "true":
+ return True
+ else:
+ return False
+
+ def getSSL(self):
+ # Client side certificate
+ s_cert = utils.settings('sslcert')
+ if utils.settings('altip') == "true":
+ s_cert = utils.settings('secondsslcert')
+
+ if s_cert == "None":
+ return None
+ else:
+ return s_cert
+
+ def setUserPref(self):
+
+ doUtils = self.doUtils
+
+ url = "{server}/emby/Users/{UserId}?format=json"
+ result = doUtils.downloadUrl(url)
+ self.userSettings = result
+ # Set user image for skin display
+ if result.get('PrimaryImageTag'):
+ utils.window('EmbyUserImage', value=artwork.Artwork().getUserArtwork(result, 'Primary'))
+
+ # Set resume point max
+ url = "{server}/emby/System/Configuration?format=json"
+ result = doUtils.downloadUrl(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
+ url = "{server}/emby/Users?format=json"
+ result = self.doUtils.downloadUrl(url)
+
+ if result == False:
+ # Access is restricted, set in downloadutils.py via exception
+ self.logMsg("Access is restricted.", 1)
+ self.HasAccess = False
+
+ elif utils.window('emby_online') != "true":
+ # Server connection failed
+ pass
+
+ elif utils.window('emby_serverStatus') == "restricted":
+ self.logMsg("Access is granted.", 1)
+ self.HasAccess = True
+ utils.window('emby_serverStatus', clear=True)
+ xbmcgui.Dialog().notification("Emby server", "Access is enabled.")
+
+ def loadCurrUser(self, authenticated=False):
+
+ 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)
+ utils.window('emby_currUser', value=userId)
+ utils.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
+ utils.window('emby_currUser', value=userId)
+ utils.window('emby_accessToken%s' % userId, value=self.currToken)
+ utils.window('emby_server%s' % userId, value=self.currServer)
+ utils.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):
+ # 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 == 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 = xbmcgui.Dialog().input(
+ heading="Enter password for user: %s" % username,
+ option=xbmcgui.ALPHANUM_HIDE_INPUT)
+ # If password dialog is cancelled
+ if not password:
+ self.logMsg("No password entered.", 0)
+ utils.window('emby_serverStatus', value="Stop")
+ self.auth = False
+ return
+ break
+ else:
+ # Manual login, user is hidden
+ password = xbmcgui.Dialog().input(
+ heading="Enter password for user: %s" % 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}
+ self.logMsg(data, 2)
+
+ result = self.doUtils.downloadUrl(url, postBody=data, 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
+ xbmcgui.Dialog().notification("Emby server", "Welcome %s!" % self.currUser)
+ userId = result['User']['Id']
+ utils.settings('accessToken', value=accessToken)
+ utils.settings('userId%s' % username, value=userId)
+ self.logMsg("User Authenticated: %s" % accessToken, 1)
+ self.loadCurrUser(authenticated=True)
+ utils.window('emby_serverStatus', clear=True)
+ self.retry = 0
+ else:
+ self.logMsg("User authentication failed.", 1)
+ utils.settings('accessToken', value="")
+ utils.settings('userId%s' % username, value="")
+ xbmcgui.Dialog().ok("Error connecting", "Invalid username or password.")
+
+ # 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)
+ utils.window('emby_serverStatus', value="Stop")
+ xbmcgui.Dialog().ok(
+ heading="Error connecting",
+ line1="Failed to authenticate too many times.",
+ line2="You can retry by resetting attempts in the addon settings.")
+
+ self.retry += 1
+ self.auth = False
+
+ def resetClient(self):
+
+ self.logMsg("Reset UserClient authentication.", 1)
+ username = self.getUsername()
+
+ if self.currToken is not None:
+ # In case of 401, removed saved token
+ utils.settings('accessToken', value="")
+ utils.window('emby_accessToken%s' % username, clear=True)
+ self.currToken = None
+ self.logMsg("User token has been removed.", 1)
+
+ self.auth = True
+ self.currUser = None
+
+ def run(self):
+
+ monitor = xbmc.Monitor()
+ self.logMsg("----===## Starting UserClient ##===----", 0)
+
+ while not monitor.abortRequested():
+
+ # Verify the log level
+ currLogLevel = self.getLogLevel()
+ if self.logLevel != currLogLevel:
+ # Set new log level
+ self.logLevel = currLogLevel
+ utils.window('emby_logLevel', value=str(currLogLevel))
+ self.logMsg("New Log Level: %s" % currLogLevel, 0)
+
+
+ status = utils.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
+ utils.window('emby_serverStatus', value="Auth")
+ self.resetClient()
+
+ if self.auth and (self.currUser is None):
+ # Try to authenticate user
+ status = utils.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 = utils.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
new file mode 100644
index 00000000..83e73e1d
--- /dev/null
+++ b/resources/lib/utils.py
@@ -0,0 +1,487 @@
+# -*- coding: utf-8 -*-
+
+#################################################################################################
+
+import cProfile
+import inspect
+import pstats
+import sqlite3
+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)
+
+ if clear:
+ WINDOW.clearProperty(property)
+ elif value is not None:
+ WINDOW.setProperty(property, value)
+ else:
+ return WINDOW.getProperty(property)
+
+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)
+
+def language(stringid):
+ # Central string retrieval
+ addon = xbmcaddon.Addon(id='plugin.video.emby')
+ string = addon.getLocalizedString(stringid)
+
+ 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 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
+ path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
+ dirs, files = xbmcvfs.listdir(path)
+ for file in files:
+ if file.startswith('Emby'):
+ xbmcvfs.delete("%s%s" % (path, file))
+
+ # Clean up the 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.startswith('Emby'):
+ shutil.rmtree("%s%s" % (path, dir))
+ for file in files:
+ if file.startswith('emby'):
+ xbmcvfs.delete("%s%s" % (path, file))
+
+ # Wipe the kodi databases
+ logMsg("EMBY", "Resetting the Kodi video database.")
+ 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('disableMusic') != "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.")
+ 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)
+ 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 == 1:
+ # 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 startProfiling():
+
+ pr = cProfile.Profile()
+ pr.enable()
+
+ return pr
+
+def stopProfiling(pr, profileName):
+
+ pr.disable()
+ ps = pstats.Stats(pr)
+
+ profiles = xbmc.translatePath("%sprofiles/"
+ % xbmcaddon.Addon().getAddonInfo('profile')).decode('utf-8')
+
+ if not xbmcvfs.exists(profiles):
+ # Create the profiles folder
+ xbmcvfs.mkdir(profiles)
+
+ timestamp = time.strftime("%Y-%m-%d %H-%M-%S")
+ profile = "%s%s_profile_(%s).tab" % (profiles, profileName, timestamp)
+
+ f = open(profile, 'wb')
+ f.write("NumbCalls\tTotalTime\tCumulativeTime\tFunctionName\tFileName\r\n")
+ for (key, value) in ps.stats.items():
+ (filename, count, func_name) = key
+ (ccalls, ncalls, total_time, cumulative_time, callers) = value
+ try:
+ f.write(
+ "%s\t%s\t%s\t%s\t%s\r\n"
+ % (ncalls, "{:10.4f}".format(total_time),
+ "{:10.4f}".format(cumulative_time), func_name, filename))
+ except ValueError:
+ f.write(
+ "%s\t%s\t%s\t%s\t%s\r\n"
+ % (ncalls, "{0}".format(total_time),
+ "{0}".format(cumulative_time), func_name, filename))
+ f.close()
+
+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
+ for i in range(1, 3):
+
+ for source in root.findall('.//path'):
+ if source.text == "smb://embydummy/dummypath%s/" % i:
+ # Already there, skip
+ break
+ else:
+ source = etree.SubElement(video, 'source')
+ etree.SubElement(source, 'name').text = "Emby"
+ etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = (
+
+ "smb://embydummy/dummypath%s/" % i
+ )
+ 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", settings('ipaddress'))
+ 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, viewtype="", delete=False):
+ # Tagname is in unicode - actions: add or delete
+ tagname = tagname.encode('utf-8')
+ cleantagname = normalize_nodes(tagname)
+ path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
+ if viewtype == "mixed":
+ plname = "%s - %s" % (tagname, mediatype)
+ xsppath = "%sEmby %s - %s.xsp" % (path, cleantagname, mediatype)
+ else:
+ plname = tagname
+ xsppath = "%sEmby %s.xsp" % (path, cleantagname)
+
+ # Create the playlist directory
+ if not xbmcvfs.exists(path):
+ 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"
+ }
+ f = open(xsppath, 'w')
+ 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)
\ No newline at end of file
diff --git a/resources/lib/videonodes.py b/resources/lib/videonodes.py
new file mode 100644
index 00000000..1dc9d5a6
--- /dev/null
+++ b/resources/lib/videonodes.py
@@ -0,0 +1,344 @@
+# -*- 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, delete=False):
+
+ kodiversion = self.kodiversion
+
+ if mediatype == "homevideos":
+ # Treat homevideos as movies
+ mediatype = "movies"
+
+ tagname = tagname.encode('utf-8')
+ cleantagname = utils.normalize_nodes(tagname)
+ if viewtype == "mixed":
+ dirname = "%s - %s" % (cleantagname, mediatype)
+ else:
+ dirname = cleantagname
+
+ 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):
+ # 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 utils.window('Emby.nodes.%s.index' % i) == path:
+ return
+
+ utils.window('Emby.nodes.%s.index' % indexnumber, value=path)
+ # Root
+ root = self.commonRoot(order=0, label=dirname, 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': "nextupepisodes",
+ '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},
+ }
+
+ nodes = mediatypes[mediatype]
+ for node in nodes:
+
+ nodetype = nodetypes[node]
+ nodeXML = "%s%s_%s.xml" % (nodepath, cleantagname, 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 nodetype == "nextupepisodes":
+ # 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, cleantagname, nodetype)
+ windowpath = "ActivateWindow(Video, %s, return)" % path
+
+ if nodetype == "all":
+
+ if viewtype == "mixed":
+ templabel = dirname
+ else:
+ templabel = label
+
+ embynode = "Emby.nodes.%s" % indexnumber
+ utils.window('%s.title' % embynode, value=templabel)
+ utils.window('%s.path' % embynode, value=windowpath)
+ utils.window('%s.content' % embynode, value=path)
+ utils.window('%s.type' % embynode, value=mediatype)
+ else:
+ embynode = "Emby.nodes.%s.%s" % (indexnumber, nodetype)
+ utils.window('%s.title' % embynode, value=label)
+ utils.window('%s.path' % embynode, value=windowpath)
+ utils.window('%s.content' % embynode, value=path)
+
+ if xbmcvfs.exists(nodeXML):
+ # Don't recreate xml if already exists
+ continue
+
+
+ # Create the root
+ if nodetype == "nextupepisodes" 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):
+
+ 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
+ utils.window('%s.title' % embynode, value=label)
+ utils.window('%s.path' % embynode, value=windowpath)
+ utils.window('%s.content' % embynode, value=path)
+ utils.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):
+
+ self.logMsg("Clearing nodes properties.", 1)
+ embyprops = utils.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:
+ utils.window('Emby.nodes.%s.%s' % (str(i), prop), clear=True)
\ No newline at end of file