Revert "Replace"

This reverts commit f28c9fe069.
This commit is contained in:
angelblue05 2015-12-24 13:56:11 -06:00
parent f28c9fe069
commit 08f9add813
10 changed files with 3351 additions and 3682 deletions

View file

@ -1,38 +1,32 @@
# -*- coding: utf-8 -*-
##################################################################################################
import json
import requests
import logging
import xbmc import xbmc
import xbmcgui import xbmcgui
import xbmcaddon
import utils import requests
import clientinfo import json
import logging
################################################################################################## import Utils as utils
from ClientInformation import ClientInformation
from requests.packages.urllib3.exceptions import InsecureRequestWarning
# Disable requests logging # Disable requests logging
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning) requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
#logging.getLogger('requests').setLevel(logging.WARNING) #logging.getLogger("requests").setLevel(logging.WARNING)
##################################################################################################
class DownloadUtils(): class DownloadUtils():
# Borg - multiple instances, shared state # Borg - multiple instances, shared state
_shared_state = {} _shared_state = {}
clientInfo = clientinfo.ClientInfo() clientInfo = ClientInformation()
addonName = clientInfo.getAddonName() addonName = clientInfo.getAddonName()
addon = xbmcaddon.Addon()
WINDOW = xbmcgui.Window(10000)
# Requests session # Requests session
s = None s = None
timeout = 30 timeout = 60
def __init__(self): def __init__(self):
@ -40,44 +34,41 @@ class DownloadUtils():
def logMsg(self, msg, lvl=1): def logMsg(self, msg, lvl=1):
className = self.__class__.__name__ self.className = self.__class__.__name__
utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) utils.logMsg("%s %s" % (self.addonName, self.className), msg, int(lvl))
def setUsername(self, username): def setUsername(self, username):
# Reserved for userclient only # Reserved for UserClient only
self.username = username self.username = username
self.logMsg("Set username: %s" % username, 2) self.logMsg("Set username: %s" % username, 2)
def setUserId(self, userId): def setUserId(self, userId):
# Reserved for userclient only # Reserved for UserClient only
self.userId = userId self.userId = userId
self.logMsg("Set userId: %s" % userId, 2) self.logMsg("Set userId: %s" % userId, 2)
def setServer(self, server): def setServer(self, server):
# Reserved for userclient only # Reserved for UserClient only
self.server = server self.server = server
self.logMsg("Set server: %s" % server, 2) self.logMsg("Set server: %s" % server, 2)
def setToken(self, token): def setToken(self, token):
# Reserved for userclient only # Reserved for UserClient only
self.token = token self.token = token
self.logMsg("Set token: %s" % token, 2) self.logMsg("Set token: %s" % token, 2)
def setSSL(self, ssl, sslclient): def setSSL(self, ssl, sslclient):
# Reserved for userclient only # Reserved for UserClient only
self.sslverify = ssl self.sslverify = ssl
self.sslclient = sslclient self.sslclient = sslclient
self.logMsg("Verify SSL host certificate: %s" % ssl, 2) self.logMsg("Verify SSL host certificate: %s" % ssl, 2)
self.logMsg("SSL client side certificate: %s" % sslclient, 2) self.logMsg("SSL client side certificate: %s" % sslclient, 2)
def postCapabilities(self, deviceId): def postCapabilities(self, deviceId):
# Post settings to session # Post settings to session
url = "{server}/emby/Sessions/Capabilities/Full?format=json" url = "{server}/mediabrowser/Sessions/Capabilities/Full"
data = { data = {
'PlayableMediaTypes': "Audio,Video", 'PlayableMediaTypes': "Audio,Video",
'SupportsMediaControl': True, 'SupportsMediaControl': True,
'SupportedCommands': ( 'SupportedCommands': (
@ -95,57 +86,49 @@ class DownloadUtils():
} }
self.logMsg("Capabilities URL: %s" % url, 2) self.logMsg("Capabilities URL: %s" % url, 2)
self.logMsg("Postdata: %s" % data, 2) self.logMsg("PostData: %s" % data, 2)
self.downloadUrl(url, postBody=data, type="POST") try:
self.logMsg("Posted capabilities to %s" % self.server, 2) self.downloadUrl(url, postBody=data, type="POST")
self.logMsg("Posted capabilities to %s" % self.server, 1)
except:
self.logMsg("Posted capabilities failed.")
# Attempt at getting sessionId # Attempt at getting sessionId
url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId url = "{server}/mediabrowser/Sessions?DeviceId=%s&format=json" % deviceId
result = self.downloadUrl(url)
try: try:
sessionId = result[0]['Id'] result = self.downloadUrl(url)
except (KeyError, TypeError):
self.logMsg("Failed to retrieve sessionId.", 1)
else:
self.logMsg("Session: %s" % result, 2) self.logMsg("Session: %s" % result, 2)
self.logMsg("SessionId: %s" % sessionId, 1)
utils.window('emby_sessionId', value=sessionId)
sessionId = result[0][u'Id']
self.logMsg("SessionId: %s" % sessionId)
self.WINDOW.setProperty("sessionId%s" % self.username, sessionId)
except:
self.logMsg("Failed to retrieve sessionId.", 1)
else:
# Post any permanent additional users # Post any permanent additional users
additionalUsers = utils.settings('additionalUsers') additionalUsers = utils.settings('additionalUsers').split(',')
if additionalUsers: self.logMsg("List of permanent users that should be added to the session: %s" % str(additionalUsers), 1)
# Get the user list from server to get the userId
additionalUsers = additionalUsers.split(',') url = "{server}/mediabrowser/Users?format=json"
self.logMsg( result = self.downloadUrl(url)
"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 result:
for user in result:
username = user['Name'].lower()
userId = user['Id']
for additional in additionalUsers:
addUser = additional.decode('utf-8').lower()
if username in addUser: if username in addUser:
userId = user['Id'] url = "{server}/mediabrowser/Sessions/%s/Users/%s" % (sessionId, userId)
url = ( postdata = {}
"{server}/emby/Sessions/%s/Users/%s?format=json" self.downloadUrl(url, postBody=postdata, type="POST")
% (sessionId, userId) #xbmcgui.Dialog().notification("Success!", "%s added to viewing session" % username, time=1000)
)
self.downloadUrl(url, postBody={}, type="POST")
def startSession(self): def startSession(self):
self.deviceId = self.clientInfo.getDeviceId() self.deviceId = self.clientInfo.getMachineId()
# User is identified from this point # User is identified from this point
# Attach authenticated header to the session # Attach authenticated header to the session
@ -169,7 +152,7 @@ class DownloadUtils():
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1)) self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
self.s.mount("https://", 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) self.logMsg("Requests session started on: %s" % self.server)
def stopSession(self): def stopSession(self):
try: try:
@ -182,116 +165,93 @@ class DownloadUtils():
clientInfo = self.clientInfo clientInfo = self.clientInfo
deviceName = clientInfo.getDeviceName() deviceName = clientInfo.getDeviceName()
deviceId = clientInfo.getDeviceId() deviceId = clientInfo.getMachineId()
version = clientInfo.getVersion() version = clientInfo.getVersion()
if not authenticate: if not authenticate:
# If user is not authenticated # If user is not authenticated
auth = ( auth = 'MediaBrowser Client="Kodi", Device="%s", DeviceId="%s", Version="%s"' % (deviceName, deviceId, version)
'MediaBrowser Client="Kodi", Device="%s", DeviceId="%s", Version="%s"' header = {'Content-type': 'application/json', 'Accept-encoding': 'gzip', 'Accept-Charset': 'UTF-8,*', 'Authorization': auth}
% (deviceName, deviceId, version))
header = {
'Content-type': 'application/json',
'Accept-encoding': 'gzip',
'Accept-Charset': 'UTF-8,*',
'Authorization': auth
}
self.logMsg("Header: %s" % header, 2) self.logMsg("Header: %s" % header, 2)
return header
else: else:
userId = self.userId userId = self.userId
token = self.token token = self.token
# Attached to the requests session # Attached to the requests session
auth = ( auth = 'MediaBrowser UserId="%s", Client="Kodi", Device="%s", DeviceId="%s", Version="%s"' % (userId, deviceName, deviceId, version)
'MediaBrowser UserId="%s", Client="Kodi", Device="%s", DeviceId="%s", Version="%s"' header = {'Content-type': 'application/json', 'Accept-encoding': 'gzip', 'Accept-Charset': 'UTF-8,*', 'Authorization': auth, 'X-MediaBrowser-Token': token}
% (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) self.logMsg("Header: %s" % header, 2)
return header
return header
def downloadUrl(self, url, postBody=None, type="GET", parameters=None, authenticate=True): def downloadUrl(self, url, postBody=None, type="GET", authenticate=True):
self.logMsg("=== ENTER downloadUrl ===", 2) self.logMsg("=== ENTER downloadUrl ===", 2)
WINDOW = self.WINDOW
timeout = self.timeout timeout = self.timeout
default_link = "" default_link = ""
try: try:
# If user is authenticated # If user is authenticated
if (authenticate): if (authenticate):
# Get requests session # Get requests session
try: try:
s = self.s s = self.s
# Replace for the real values # Replace for the real values and append api_key
url = url.replace("{server}", self.server) url = url.replace("{server}", self.server, 1)
url = url.replace("{UserId}", self.userId) url = url.replace("{UserId}", self.userId, 1)
self.logMsg("URL: %s" % url, 2)
# Prepare request # Prepare request
if type == "GET": if type == "GET":
r = s.get(url, json=postBody, params=parameters, timeout=timeout) r = s.get(url, json=postBody, timeout=timeout)
elif type == "POST": elif type == "POST":
r = s.post(url, json=postBody, timeout=timeout) r = s.post(url, json=postBody, timeout=timeout)
elif type == "DELETE": elif type == "DELETE":
r = s.delete(url, json=postBody, timeout=timeout) r = s.delete(url, json=postBody, timeout=timeout)
except AttributeError: except AttributeError:
# request session does not exists
# Get user information # Get user information
self.userId = utils.window('emby_currUser') self.username = WINDOW.getProperty('currUser')
self.server = utils.window('emby_server%s' % self.userId) self.userId = WINDOW.getProperty('userId%s' % self.username)
self.token = utils.window('emby_accessToken%s' % self.userId) self.server = WINDOW.getProperty('server%s' % self.username)
self.token = WINDOW.getProperty('accessToken%s' % self.username)
header = self.getHeader() header = self.getHeader()
verifyssl = False verifyssl = False
cert = None cert = None
# IF user enables ssl verification # IF user enables ssl verification
if utils.settings('sslverify') == "true": try:
verifyssl = True if utils.settings('sslverify') == "true":
if utils.settings('sslcert') != "None": verifyssl = True
cert = utils.settings('sslcert') if utils.settings('sslcert') != "None":
cert = utils.settings('sslcert')
except:
self.logMsg("Could not load SSL settings.", 1)
pass
# Replace for the real values # Replace for the real values and append api_key
url = url.replace("{server}", self.server) url = url.replace("{server}", self.server, 1)
url = url.replace("{UserId}", self.userId) url = url.replace("{UserId}", self.userId, 1)
self.logMsg("URL: %s" % url, 2)
# Prepare request # Prepare request
if type == "GET": if type == "GET":
r = requests.get(url, r = requests.get(url, json=postBody, headers=header, timeout=timeout, cert=cert, verify=verifyssl)
json=postBody,
params=parameters,
headers=header,
timeout=timeout,
cert=cert,
verify=verifyssl)
elif type == "POST": elif type == "POST":
r = requests.post(url, r = requests.post(url, json=postBody, headers=header, timeout=timeout, cert=cert, verify=verifyssl)
json=postBody,
headers=header,
timeout=timeout,
cert=cert,
verify=verifyssl)
elif type == "DELETE": elif type == "DELETE":
r = requests.delete(url, r = requests.delete(url, json=postBody, headers=header, timeout=timeout, cert=cert, verify=verifyssl)
json=postBody,
headers=header,
timeout=timeout,
cert=cert,
verify=verifyssl)
# If user is not authenticated # If user is not authenticated
elif not authenticate: elif not authenticate:
self.logMsg("URL: %s" % url, 2)
header = self.getHeader(authenticate=False) header = self.getHeader(authenticate=False)
verifyssl = False verifyssl = False
@ -303,49 +263,41 @@ class DownloadUtils():
# Prepare request # Prepare request
if type == "GET": if type == "GET":
r = requests.get(url, r = requests.get(url, json=postBody, headers=header, timeout=timeout, verify=verifyssl)
json=postBody,
params=parameters,
headers=header,
timeout=timeout,
verify=verifyssl)
elif type == "POST": elif type == "POST":
r = requests.post(url, r = requests.post(url, json=postBody, headers=header, timeout=timeout, verify=verifyssl)
json=postBody,
headers=header,
timeout=timeout,
verify=verifyssl)
##### THE RESPONSE ##### # Process the response
self.logMsg(r.url, 2)
if r.status_code == 204: if r.status_code == 204:
# No body in the response # No body in the response
self.logMsg("====== 204 Success ======", 2) self.logMsg("====== 204 Success ======", 2)
return default_link
elif r.status_code == requests.codes.ok: elif r.status_code == requests.codes.ok:
try: try:
# UTF-8 - JSON object # UTF-8 - JSON object
r = r.json() r = r.json()
self.logMsg("====== 200 Success ======", 2) self.logMsg("====== 200 Success ======", 2)
self.logMsg("Response: %s" % r, 2) self.logMsg("Response: %s" % r, 2)
return r return r
except: except:
if r.headers.get('content-type') != "text/html": if r.headers.get('content-type') == "text/html":
pass
else:
self.logMsg("Unable to convert the response for: %s" % url, 1) self.logMsg("Unable to convert the response for: %s" % url, 1)
else: else:
r.raise_for_status() r.raise_for_status()
##### EXCEPTIONS #####
return default_link
# TO REVIEW EXCEPTIONS
except requests.exceptions.ConnectionError as e: except requests.exceptions.ConnectionError as e:
# Make the addon aware of status # Make the addon aware of status
if utils.window('emby_online') != "false": if WINDOW.getProperty("Server_online") != "false":
self.logMsg("Server unreachable at: %s" % url, 0) self.logMsg("Server unreachable at: %s" % url, 0)
self.logMsg(e, 2) self.logMsg(e, 2)
utils.window('emby_online', value="false") WINDOW.setProperty("Server_online", "false")
pass
except requests.exceptions.ConnectTimeout as e: except requests.exceptions.ConnectTimeout as e:
self.logMsg("Server timeout at: %s" % url, 0) self.logMsg("Server timeout at: %s" % url, 0)
@ -355,35 +307,29 @@ class DownloadUtils():
if r.status_code == 401: if r.status_code == 401:
# Unauthorized # Unauthorized
status = utils.window('emby_serverStatus') status = WINDOW.getProperty("Server_status")
if 'X-Application-Error-Code' in r.headers: if 'x-application-error-code' in r.headers:
# Emby server errors
if r.headers['X-Application-Error-Code'] == "ParentalControl": if r.headers['X-Application-Error-Code'] == "ParentalControl":
# Parental control - access restricted # Parental control - access restricted
utils.window('emby_serverStatus', value="restricted") WINDOW.setProperty("Server_status", "restricted")
xbmcgui.Dialog().notification( xbmcgui.Dialog().notification("Emby server", "Access restricted.", xbmcgui.NOTIFICATION_ERROR, time=5000)
heading="Emby server",
message="Access restricted.",
icon=xbmcgui.NOTIFICATION_ERROR,
time=5000)
return False return False
elif r.headers['X-Application-Error-Code'] == "UnauthorizedAccessException": elif r.headers['X-Application-Error-Code'] == "UnauthorizedAccessException":
# User tried to do something his emby account doesn't allow # User tried to do something his emby account doesn't allow - admin restricted in some way
pass pass
elif status not in ("401", "Auth"): elif (status == "401") or (status == "Auth"):
# Tell userclient token has been revoked. pass
utils.window('emby_serverStatus', value="401")
else:
# Tell UserClient token has been revoked.
WINDOW.setProperty("Server_status", "401")
self.logMsg("HTTP Error: %s" % e, 0) self.logMsg("HTTP Error: %s" % e, 0)
xbmcgui.Dialog().notification( xbmcgui.Dialog().notification("Error connecting", "Unauthorized.", xbmcgui.NOTIFICATION_ERROR)
heading="Error connecting",
message="Unauthorized.",
icon=xbmcgui.NOTIFICATION_ERROR)
return 401 return 401
elif r.status_code in (301, 302): elif (r.status_code == 301) or (r.status_code == 302):
# Redirects # Redirects
pass pass
elif r.status_code == 400: elif r.status_code == 400:
@ -398,4 +344,4 @@ class DownloadUtils():
self.logMsg("Unknown error connecting to: %s" % url, 0) self.logMsg("Unknown error connecting to: %s" % url, 0)
self.logMsg(e, 1) self.logMsg(e, 1)
return default_link return default_link

File diff suppressed because it is too large Load diff

View file

@ -1,195 +1,150 @@
# -*- coding: utf-8 -*-
################################################################################################# #################################################################################################
# Kodi Monitor
import json # Watched events that occur in Kodi, like setting media watched
#################################################################################################
import xbmc import xbmc
import xbmcgui import xbmcgui
import xbmcaddon
import json
import clientinfo import Utils as utils
import downloadutils from WriteKodiVideoDB import WriteKodiVideoDB
import embydb_functions as embydb from ReadKodiDB import ReadKodiDB
import playbackutils as pbutils from PlayUtils import PlayUtils
import utils from DownloadUtils import DownloadUtils
from PlaybackUtils import PlaybackUtils
#################################################################################################
class KodiMonitor(xbmc.Monitor): class Kodi_Monitor( xbmc.Monitor ):
WINDOW = xbmcgui.Window(10000)
def __init__(self, *args, **kwargs):
xbmc.Monitor.__init__(self)
def __init__(self): def logMsg(self, msg, lvl = 1):
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)
className = self.__class__.__name__
utils.logMsg("%s %s" % ("EMBY", className), msg, int(lvl))
def onScanStarted(self, library): def onScanStarted(self, library):
self.logMsg("Kodi library scan %s running." % library, 2) utils.window('kodiScan', value="true")
if library == "video": self.logMsg("Kodi library scan running.", 2)
utils.window('emby_kodiScan', value="true")
def onScanFinished(self, library): def onScanFinished(self, library):
self.logMsg("Kodi library scan %s finished." % library, 2) utils.window('kodiScan', clear=True)
if library == "video": self.logMsg("Kodi library scan finished.", 2)
utils.window('emby_kodiScan', clear=True)
#this library monitor is used to detect a watchedstate change by the user through the library
#as well as detect when a library item has been deleted to pass the delete to the Emby server
def onNotification (self, sender, method, data):
def onNotification(self, sender, method, data): WINDOW = self.WINDOW
downloadUtils = DownloadUtils()
#player started playing an item -
if ("Playlist.OnAdd" in method or "Player.OnPlay" in method):
doUtils = self.doUtils jsondata = json.loads(data)
if method not in ("Playlist.OnAdd"): if jsondata:
self.logMsg("Method: %s Data: %s" % (method, data), 1) if jsondata.has_key("item"):
if jsondata.get("item").has_key("id") and jsondata.get("item").has_key("type"):
id = jsondata.get("item").get("id")
type = jsondata.get("item").get("type")
if (utils.settings('useDirectPaths')=='true' and not type == "song") or (type == "song" and utils.settings('enableMusicSync') == "true"):
if type == "song":
connection = utils.KodiSQL('music')
cursor = connection.cursor()
embyid = ReadKodiDB().getEmbyIdByKodiId(id, type, connection, cursor)
cursor.close()
else:
embyid = ReadKodiDB().getEmbyIdByKodiId(id,type)
if embyid:
url = "{server}/mediabrowser/Users/{UserId}/Items/%s?format=json" % embyid
result = downloadUtils.downloadUrl(url)
self.logMsg("Result: %s" % result, 2)
playurl = None
count = 0
while not playurl and count < 2:
try:
playurl = xbmc.Player().getPlayingFile()
except RuntimeError:
xbmc.sleep(200)
else:
listItem = xbmcgui.ListItem()
PlaybackUtils().setProperties(playurl, result, listItem)
if type == "song" and utils.settings('directstreammusic') == "true":
utils.window('%splaymethod' % playurl, value="DirectStream")
else:
utils.window('%splaymethod' % playurl, value="DirectPlay")
count += 1
if method == "VideoLibrary.OnUpdate":
# Triggers 4 times, the following is only for manually marking as watched/unwatched
jsondata = json.loads(data)
if data:
data = json.loads(data)
if method == "Player.OnPlay":
# Set up report progress for emby playback
item = data.get('item')
try: try:
kodiid = item['id'] playcount = jsondata.get('playcount')
type = item['type'] item = jsondata['item']['id']
except (KeyError, TypeError): type = jsondata['item']['type']
self.logMsg("Properties already set for item.", 1) prop = utils.window('Played%s%s' % (type, item))
except:
self.logMsg("Could not process VideoLibrary.OnUpdate data.", 1)
else: else:
if ((utils.settings('useDirectPaths') == "1" and not type == "song") or self.logMsg("VideoLibrary.OnUpdate: %s" % data, 2)
(type == "song" and utils.settings('disableMusic') == "false")): if prop != "true":
# Set up properties for player # Set property to prevent the multi triggering
embyconn = utils.kodiSQL('emby') utils.window('Played%s%s' % (type, item), "true")
embycursor = embyconn.cursor() WriteKodiVideoDB().updatePlayCountFromKodi(item, type, playcount)
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 self.clearProperty(type, item)
count = 0
while not playurl and count < 2: if method == "System.OnWake":
try: xbmc.sleep(10000) #Allow network to wake up
playurl = xbmc.Player().getPlayingFile() WINDOW.setProperty("OnWakeSync", "true")
except RuntimeError:
count += 1
xbmc.sleep(200)
else:
listItem = xbmcgui.ListItem()
playback = pbutils.PlaybackUtils(result)
if type == "song" and utils.settings('streamMusic') == "true": if method == "VideoLibrary.OnRemove":
utils.window('emby_%s.playmethod' % playurl, xbmc.log('Intercepted remove from sender: ' + sender + ' method: ' + method + ' data: ' + data)
value="DirectStream") jsondata = json.loads(data)
else: id = ReadKodiDB().getEmbyIdByKodiId(jsondata.get("id"), jsondata.get("type"))
utils.window('emby_%s.playmethod' % playurl, if id == None:
value="DirectPlay") return
# Set properties for player.py xbmc.log("Deleting Emby ID: " + id + " from database")
playback.setProperties(playurl, listItem) connection = utils.KodiSQL()
finally: cursor = connection.cursor()
embycursor.close() cursor.execute("DELETE FROM emby WHERE emby_id = ?", (id,))
connection.commit()
cursor.close
if jsondata:
elif method == "VideoLibrary.OnUpdate": if jsondata.get("type") == "episode" or "movie":
# Manually marking as watched/unwatched url='{server}/mediabrowser/Items?Ids=' + id + '&format=json'
playcount = data.get('playcount') #This is a check to see if the item exists on the server, if it doesn't it may have already been deleted by another client
item = data.get('item') result = DownloadUtils().downloadUrl(url)
try: item = result.get("Items")[0]
kodiid = item['id'] if data:
type = item['type'] return_value = xbmcgui.Dialog().yesno("Confirm Delete", "Delete file on Emby Server?")
except (KeyError, TypeError): if return_value:
self.logMsg("Item is invalid for playstate update.", 1) url='{server}/mediabrowser/Items/' + id
else: xbmc.log('Deleting via URL: ' + url)
# Send notification to the server. DownloadUtils().downloadUrl(url, type="DELETE")
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": elif method == "Playlist.OnClear":
utils.window('emby_customPlaylist', clear=True, windowid=10101) self.logMsg("Clear playback properties.", 2)
#xbmcgui.Window(10101).clearProperties() utils.window('propertiesPlayback', clear=True)
self.logMsg("Clear playlist properties.")
def clearProperty(self, type, id):
# The sleep is necessary since VideoLibrary.OnUpdate
# triggers 4 times in a row.
xbmc.sleep(100)
utils.window('Played%s%s' % (type,id), clear=True)
# Clear the widget cache
utils.window('clearwidgetcache', value="clear")

File diff suppressed because it is too large Load diff

View file

@ -6,279 +6,227 @@ import xbmc
import xbmcgui import xbmcgui
import xbmcvfs import xbmcvfs
import clientinfo from ClientInformation import ClientInformation
import utils import Utils as utils
################################################################################################# #################################################################################################
class PlayUtils(): class PlayUtils():
def __init__(self, item):
self.item = item clientInfo = ClientInformation()
addonName = clientInfo.getAddonName()
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): def logMsg(self, msg, lvl=1):
className = self.__class__.__name__
utils.logMsg("%s %s" % (self.addonName, className), msg, int(lvl))
self.className = self.__class__.__name__ def getPlayUrl(self, server, id, result):
utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
def getPlayUrl(self): if self.isDirectPlay(result,True):
# Try direct play
playurl = self.directPlay(result)
if playurl:
self.logMsg("File is direct playing.", 1)
utils.window("%splaymethod" % playurl.encode('utf-8'), value="DirectPlay")
item = self.item elif self.isDirectStream(result):
playurl = None # Try direct stream
playurl = self.directStream(result, server, id)
if playurl:
self.logMsg("File is direct streaming.", 1)
utils.window("%splaymethod" % playurl, value="DirectStream")
if item['MediaSources'][0]['Protocol'] == "Http": elif self.isTranscoding(result):
# Only play as http # Try transcoding
self.logMsg("File protocol is http.", 1) playurl = self.transcoding(result, server, id)
playurl = self.httpPlay() if playurl:
utils.window('emby_%s.playmethod' % playurl, value="DirectStream") self.logMsg("File is transcoding.", 1)
utils.window("%splaymethod" % playurl, value="Transcode")
else: # Error
utils.window("playurlFalse", value="true")
return
elif self.isDirectPlay(): return playurl.encode('utf-8')
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(): def isDirectPlay(self, result, dialog = False):
# Requirements for Direct play:
self.logMsg("File is direct streaming.", 1) # FileSystem, Accessible path
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": if utils.settings('playFromStream') == "true":
# User forcing to play via HTTP # User forcing to play via HTTP instead of SMB
self.logMsg("Can't direct play, play from HTTP enabled.", 1) self.logMsg("Can't direct play: Play from HTTP is enabled.", 1)
return False return False
# Avoid H265 1080p
if (utils.settings('transcodeH265') == "true" and if (utils.settings('transcodeH265') == "true" and
result['MediaSources'][0]['Name'].startswith("1080P/H265")): result['MediaSources'][0]['Name'].startswith("1080P/H265")):
# Avoid H265 1080p
self.logMsg("Option to transcode 1080P/H265 enabled.", 1) self.logMsg("Option to transcode 1080P/H265 enabled.", 1)
return False return False
canDirectPlay = item['MediaSources'][0]['SupportsDirectPlay'] canDirectPlay = result['MediaSources'][0]['SupportsDirectPlay']
# Make sure direct play is supported by the server # Make sure it's supported by server
if not canDirectPlay: if not canDirectPlay:
self.logMsg("Can't direct play, server doesn't allow/support it.", 1) self.logMsg("Can't direct play: Server does not allow or support it.", 1)
return False return False
location = item['LocationType'] location = result['LocationType']
if location == "FileSystem": # File needs to be "FileSystem"
# Verify the path if 'FileSystem' in location:
if not self.fileExists(): # Verify if path is accessible
self.logMsg("Unable to direct play.") if self.fileExists(result):
try: return True
count = int(utils.settings('failCount')) else:
except ValueError: self.logMsg("Unable to direct play. Verify the following path is accessible by the device: %s. You might also need to add SMB credentials in the add-on settings." % result['MediaSources'][0]['Path'], 1)
count = 0 if dialog:
self.logMsg("Direct play failed: %s times." % count, 1)
failCount = int(utils.settings('directSteamFailedCount'))
self.logMsg("Direct Play failCount: %s." % failCount, 1)
if failCount < 2:
# Let user know that direct play failed
utils.settings('directSteamFailedCount', value=str(failCount + 1))
xbmcgui.Dialog().notification("Emby server", "Unable to direct play. Verify your log for more information.", 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")
xbmcgui.Dialog().notification("Emby server", "Enabled play from HTTP in add-on settings.", icon="special://home/addons/plugin.video.emby/icon.png", sound=False)
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 False
return True def directPlay(self, result):
def directPlay(self):
item = self.item
try: try:
playurl = item['MediaSources'][0]['Path'] playurl = result['MediaSources'][0]['Path']
except (IndexError, KeyError): except KeyError:
playurl = item['Path'] playurl = result['Path']
if item.get('VideoType'): if 'VideoType' in result:
# Specific format modification # Specific format modification
type = item['VideoType'] if 'Dvd' in result['VideoType']:
if type == "Dvd":
playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl
elif type == "Bluray": elif 'BluRay' in result['VideoType']:
playurl = "%s/BDMV/index.bdmv" % playurl playurl = "%s/BDMV/index.bdmv" % playurl
# Assign network protocol # Network - SMB protocol
if playurl.startswith('\\\\'): if "\\\\" in playurl:
playurl = playurl.replace("\\\\", "smb://") smbuser = utils.settings('smbusername')
smbpass = utils.settings('smbpassword')
# Network share
if smbuser:
playurl = playurl.replace("\\\\", "smb://%s:%s@" % (smbuser, smbpass))
else:
playurl = playurl.replace("\\\\", "smb://")
playurl = playurl.replace("\\", "/") playurl = playurl.replace("\\", "/")
if "apple.com" in playurl: if "apple.com" in playurl:
USER_AGENT = "QuickTime/7.7.4" USER_AGENT = "QuickTime/7.7.4"
playurl += "?|User-Agent=%s" % USER_AGENT playurl += "?|User-Agent=%s" % USER_AGENT
return playurl return playurl
def fileExists(self):
if 'Path' not in self.item: def isDirectStream(self, result):
# File has no path defined in server # Requirements for Direct stream:
return False # FileSystem or Remote, BitRate, supported encoding
# 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
# Avoid H265 1080p
if (utils.settings('transcodeH265') == "true" and if (utils.settings('transcodeH265') == "true" and
result['MediaSources'][0]['Name'].startswith("1080P/H265")): result['MediaSources'][0]['Name'].startswith("1080P/H265")):
# Avoid H265 1080p
self.logMsg("Option to transcode 1080P/H265 enabled.", 1) self.logMsg("Option to transcode 1080P/H265 enabled.", 1)
return False return False
# Requirement: BitRate, supported encoding canDirectStream = result['MediaSources'][0]['SupportsDirectStream']
canDirectStream = item['MediaSources'][0]['SupportsDirectStream'] # Make sure it's supported by server
# Make sure the server supports it
if not canDirectStream: if not canDirectStream:
return False return False
# Verify the bitrate location = result['LocationType']
if not self.isNetworkSufficient(): # File can be FileSystem or Remote, not Virtual
self.logMsg("The network speed is insufficient to direct stream file.", 1) if 'Virtual' in location:
self.logMsg("File location is virtual. Can't proceed.", 1)
return False
# Verify BitRate
if not self.isNetworkQualitySufficient(result):
self.logMsg("The network speed is insufficient to playback the file.", 1)
return False return False
return True return True
def directStream(self, result, server, id, type = "Video"):
def directStream(self): if result['Path'].endswith('.strm'):
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 # Allow strm loading when direct streaming
playurl = self.directPlay() playurl = self.directPlay(result)
elif type == "Audio": return playurl
playurl = "%s/emby/Audio/%s/stream.mp3" % (server, itemid)
else: if "ThemeVideo" in type:
playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid) playurl = "%s/mediabrowser/Videos/%s/stream?static=true" % (server, id)
elif "Video" in type:
playurl = "%s/mediabrowser/Videos/%s/stream?static=true" % (server, id)
elif "Audio" in type:
playurl = "%s/mediabrowser/Audio/%s/stream.mp3" % (server, id)
return playurl return playurl
def isNetworkSufficient(self):
settings = self.getBitrate()*1000 def isTranscoding(self, result):
# Last resort, no requirements
try: # BitRate
sourceBitrate = int(self.item['MediaSources'][0]['Bitrate']) canTranscode = result['MediaSources'][0]['SupportsTranscoding']
except (KeyError, TypeError): # Make sure it's supported by server
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: if not canTranscode:
return False return False
location = result['LocationType']
# File can be FileSystem or Remote, not Virtual
if 'Virtual' in location:
return False
return True return True
def transcoding(self): def transcoding(self, result, server, id):
item = self.item if result['Path'].endswith('.strm'):
if 'Path' in item and item['Path'].endswith('.strm'):
# Allow strm loading when transcoding # Allow strm loading when transcoding
playurl = self.directPlay() playurl = self.directPlay(result)
else: return playurl
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))
# Play transcoding
deviceId = self.clientInfo.getMachineId()
playurl = "%s/mediabrowser/Videos/%s/master.m3u8?mediaSourceId=%s" % (server, id, id)
playurl = "%s&VideoCodec=h264&AudioCodec=ac3&MaxAudioChannels=6&deviceId=%s&VideoBitrate=%s" % (playurl, deviceId, self.getVideoBitRate()*1000)
playurl = self.audioSubsPref(playurl, result.get('MediaSources'))
self.logMsg("Playurl: %s" % playurl, 1)
return playurl return playurl
def getBitrate(self): def isNetworkQualitySufficient(self, result):
# Works out if the network quality can play directly or if transcoding is needed
settingsVideoBitRate = self.getVideoBitRate()
settingsVideoBitRate = settingsVideoBitRate * 1000
try:
mediaSources = result['MediaSources']
sourceBitRate = int(mediaSources[0]['Bitrate'])
except KeyError:
self.logMsg("Bitrate value is missing.", 1)
else:
self.logMsg("The video quality selected is: %s, the video bitrate required to direct stream is: %s." % (settingsVideoBitRate, sourceBitRate), 1)
if settingsVideoBitRate < sourceBitRate:
return False
return True
def getVideoBitRate(self):
# get the addon video quality # get the addon video quality
videoQuality = utils.settings('videoBitrate') videoQuality = utils.settings('videoBitRate')
bitrate = { bitrate = {
'0': 664, '0': 664,
@ -304,8 +252,35 @@ class PlayUtils():
# max bit rate supported by server (max signed 32bit integer) # max bit rate supported by server (max signed 32bit integer)
return bitrate.get(videoQuality, 2147483) return bitrate.get(videoQuality, 2147483)
def fileExists(self, result):
if 'Path' not in result:
# File has no path in server
return False
def audioSubsPref(self, url): # Convert Emby path to a path we can verify
path = self.directPlay(result)
try:
pathexists = xbmcvfs.exists(path)
except:
pathexists = False
# Verify the device has access to the direct path
if pathexists:
# Local or Network path
self.logMsg("Path exists.", 2)
return True
elif ":" not in path:
# Give benefit of the doubt for nfs.
self.logMsg("Can't verify path (assumed NFS). Still try direct play.", 2)
return True
else:
self.logMsg("Path is detected as follow: %s. Try direct streaming." % path, 2)
return False
def audioSubsPref(self, url, mediaSources):
# For transcoding only # For transcoding only
# Present the list of audio to select from # Present the list of audio to select from
audioStreamsList = {} audioStreamsList = {}
@ -317,21 +292,15 @@ class PlayUtils():
selectSubsIndex = "" selectSubsIndex = ""
playurlprefs = "%s" % url playurlprefs = "%s" % url
item = self.item mediaStream = mediaSources[0].get('MediaStreams')
try: for stream in mediaStream:
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. # Since Emby returns all possible tracks together, have to sort them.
index = stream['Index'] index = stream['Index']
type = stream['Type'] type = stream['Type']
if 'Audio' in type: if 'Audio' in type:
codec = stream['Codec'] codec = stream['Codec']
channelLayout = stream.get('ChannelLayout', "") channelLayout = stream['ChannelLayout']
try: try:
track = "%s - %s - %s %s" % (index, stream['Language'], codec, channelLayout) track = "%s - %s - %s %s" % (index, stream['Language'], codec, channelLayout)
@ -343,8 +312,6 @@ class PlayUtils():
audioStreams.append(track) audioStreams.append(track)
elif 'Subtitle' in type: elif 'Subtitle' in type:
if stream['IsExternal']:
continue
try: try:
track = "%s - %s" % (index, stream['Language']) track = "%s - %s" % (index, stream['Language'])
except: except:
@ -369,7 +336,7 @@ class PlayUtils():
selectAudioIndex = audioStreamsList[selected] selectAudioIndex = audioStreamsList[selected]
playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
else: # User backed out of selection else: # User backed out of selection
playurlprefs += "&AudioStreamIndex=%s" % mediasources['DefaultAudioStreamIndex'] playurlprefs += "&AudioStreamIndex=%s" % mediaSources[0]['DefaultAudioStreamIndex']
else: # There's only one audiotrack. else: # There's only one audiotrack.
selectAudioIndex = audioStreamsList[audioStreams[0]] selectAudioIndex = audioStreamsList[audioStreams[0]]
playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
@ -385,7 +352,7 @@ class PlayUtils():
selectSubsIndex = subtitleStreamsList[selected] selectSubsIndex = subtitleStreamsList[selected]
playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex
else: # User backed out of selection else: # User backed out of selection
playurlprefs += "&SubtitleStreamIndex=%s" % mediasources.get('DefaultSubtitleStreamIndex', "") playurlprefs += "&SubtitleStreamIndex=%s" % mediaSources[0].get('DefaultSubtitleStreamIndex', "")
# Get number of channels for selected audio track # Get number of channels for selected audio track
audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0) audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0)

View file

@ -2,69 +2,71 @@
################################################################################################# #################################################################################################
import json import datetime
import json as json
import sys import sys
import xbmc import xbmc
import xbmcgui import xbmcaddon
import xbmcplugin import xbmcplugin
import xbmcgui
import api from API import API
import artwork from DownloadUtils import DownloadUtils
import clientinfo from PlayUtils import PlayUtils
import downloadutils from ClientInformation import ClientInformation
import playutils as putils import Utils as utils
import playlist
import read_embyserver as embyserver
import utils
################################################################################################# #################################################################################################
class PlaybackUtils(): class PlaybackUtils():
clientInfo = ClientInformation()
def __init__(self, item): doUtils = DownloadUtils()
api = API()
self.item = item addon = xbmcaddon.Addon()
self.API = api.API(self.item) language = addon.getLocalizedString
addonName = clientInfo.getAddonName()
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): def logMsg(self, msg, lvl=1):
className = self.__class__.__name__
utils.logMsg("%s %s" % (self.addonName, className), msg, int(lvl))
self.className = self.__class__.__name__ def PLAY(self, result, setup = "service"):
utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl)
self.logMsg("PLAY Called", 1)
def play(self, itemid, dbid=None): api = self.api
self.logMsg("Play called.", 1)
doUtils = self.doUtils doUtils = self.doUtils
item = self.item username = utils.window('currUser')
API = self.API server = utils.window('server%s' % username)
listitem = xbmcgui.ListItem()
playutils = putils.PlayUtils(item)
playurl = playutils.getPlayUrl() id = result['Id']
if not playurl: userdata = result['UserData']
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) # Get the playurl - direct play, direct stream or transcoding
playurl = PlayUtils().getPlayUrl(server, id, result)
listItem = xbmcgui.ListItem()
if dbid is None: if utils.window('playurlFalse') == "true":
# Item is not in Kodi database # Playurl failed - set in PlayUtils.py
listitem.setPath(playurl) utils.window('playurlFalse', clear=True)
self.setProperties(playurl, listitem) self.logMsg("Failed to retrieve the playback path/url or dialog was cancelled.", 1)
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listItem)
############### -- SETUP MAIN ITEM ################
# Set listitem and properties for main item
self.logMsg("Returned playurl: %s" % playurl, 1)
listItem.setPath(playurl)
self.setProperties(playurl, result, listItem)
mainArt = API().getArtwork(result, "Primary")
listItem.setThumbnailImage(mainArt)
listItem.setIconImage(mainArt)
############### ORGANIZE CURRENT PLAYLIST ################ ############### ORGANIZE CURRENT PLAYLIST ################
@ -72,45 +74,58 @@ class PlaybackUtils():
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
startPos = max(playlist.getposition(), 0) # Can return -1 startPos = max(playlist.getposition(), 0) # Can return -1
sizePlaylist = playlist.size() sizePlaylist = playlist.size()
currentPosition = startPos
propertiesPlayback = utils.window('emby_playbackProps', windowid=10101) == "true" propertiesPlayback = utils.window('propertiesPlayback') == "true"
introsPlaylist = False introsPlaylist = False
dummyPlaylist = False dummyPlaylist = False
currentPosition = startPos
self.logMsg("Playlist start position: %s" % startPos, 2)
self.logMsg("Playlist plugin position: %s" % currentPosition, 2)
self.logMsg("Playlist size: %s" % sizePlaylist, 2)
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 ################ ############### RESUME POINT ################
userdata = API.getUserData() # Resume point for widget only
seektime = API.adjustResume(userdata['Resume']) timeInfo = api.getTimeInfo(result)
jumpBackSec = int(utils.settings('resumeJumpBack'))
seekTime = round(float(timeInfo.get('ResumeTime')), 6)
if seekTime > jumpBackSec:
# To avoid negative bookmark
seekTime = seekTime - jumpBackSec
# Show the additional resume dialog if launched from a widget
if homeScreen and seekTime:
# Dialog presentation
displayTime = str(datetime.timedelta(seconds=(int(seekTime))))
display_list = ["%s %s" % (self.language(30106), displayTime), self.language(30107)]
resume_result = xbmcgui.Dialog().select(self.language(30105), display_list)
if resume_result == 0:
# User selected to resume, append resume point to listitem
listItem.setProperty('StartOffset', str(seekTime))
elif resume_result > 0:
# User selected to start from beginning
seekTime = 0
else: # User cancelled the dialog
self.logMsg("User cancelled resume dialog.", 1)
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listItem)
# We need to ensure we add the intro and additional parts only once. # We need to ensure we add the intro and additional parts only once.
# Otherwise we get a loop. # Otherwise we get a loop.
if not propertiesPlayback: if not propertiesPlayback:
utils.window('emby_playbackProps', value="true", windowid=10101) utils.window('propertiesPlayback', value="true")
self.logMsg("Setting up properties in playlist.", 1) self.logMsg("Setting up properties in playlist.")
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 ################ ############### -- CHECK FOR INTROS ################
if utils.settings('enableCinema') == "true" and not seektime: if utils.settings('disableCinema') == "false" and not seekTime:
# if we have any play them when the movie/show is not being resumed # 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 url = "{server}/mediabrowser/Users/{UserId}/Items/%s/Intros?format=json&ImageTypeLimit=1&Fields=Etag" % id
intros = doUtils.downloadUrl(url) intros = doUtils.downloadUrl(url)
if intros['TotalRecordCount'] != 0: if intros['TotalRecordCount'] != 0:
@ -126,15 +141,17 @@ class PlaybackUtils():
if getTrailers: if getTrailers:
for intro in intros['Items']: for intro in intros['Items']:
# The server randomly returns intros, process them. # The server randomly returns intros, process them.
introId = intro['Id']
introPlayurl = PlayUtils().getPlayUrl(server, introId, intro)
introListItem = xbmcgui.ListItem() introListItem = xbmcgui.ListItem()
introPlayurl = putils.PlayUtils(intro).getPlayUrl()
self.logMsg("Adding Intro: %s" % introPlayurl, 1) self.logMsg("Adding Intro: %s" % introPlayurl, 1)
# Set listitem and properties for intros # Set listitem and properties for intros
pbutils = PlaybackUtils(intro) self.setProperties(introPlayurl, intro, introListItem)
pbutils.setProperties(introPlayurl, introListItem) self.setListItemProps(server, introId, introListItem, intro)
self.pl.insertintoPlaylist(currentPosition, url=introPlayurl) playlist.add(introPlayurl, introListItem, index=currentPosition)
introsPlaylist = True introsPlaylist = True
currentPosition += 1 currentPosition += 1
@ -142,126 +159,109 @@ class PlaybackUtils():
############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ############### ############### -- ADD MAIN ITEM ONLY FOR HOMESCREEN ###############
if homeScreen and not sizePlaylist: if homeScreen and not sizePlaylist:
# Extend our current playlist with the actual item to play # Extend our current playlist with the actual item to play only if there's no playlist first
# only if there's no playlist first
self.logMsg("Adding main item to playlist.", 1) self.logMsg("Adding main item to playlist.", 1)
self.pl.addtoPlaylist(dbid, item['Type'].lower()) self.setListItemProps(server, id, listItem, result)
playlist.add(playurl, listItem, index=currentPosition)
# Ensure that additional parts are played after the main item # Ensure that additional parts are played after the main item
currentPosition += 1 currentPosition += 1
############### -- CHECK FOR ADDITIONAL PARTS ################ ############### -- CHECK FOR ADDITIONAL PARTS ################
if item.get('PartCount'): if result.get('PartCount'):
# Only add to the playlist after intros have played # Only add to the playlist after intros have played
partcount = item['PartCount'] partcount = result['PartCount']
url = "{server}/emby/Videos/%s/AdditionalParts?format=json" % itemid url = "{server}/mediabrowser/Videos/%s/AdditionalParts" % id
parts = doUtils.downloadUrl(url) parts = doUtils.downloadUrl(url)
for part in parts['Items']: for part in parts['Items']:
partId = part['Id']
additionalPlayurl = PlayUtils().getPlayUrl(server, partId, part)
additionalListItem = xbmcgui.ListItem() additionalListItem = xbmcgui.ListItem()
additionalPlayurl = putils.PlayUtils(part).getPlayUrl()
self.logMsg("Adding additional part: %s" % partcount, 1) self.logMsg("Adding additional part: %s" % partcount, 1)
# Set listitem and properties for each additional parts # Set listitem and properties for each additional parts
pbutils = PlaybackUtils(part) self.setProperties(additionalPlayurl, part, additionalListItem)
pbutils.setProperties(additionalPlayurl, additionalListItem) self.setListItemProps(server, partId, additionalListItem, part)
pbutils.setArtwork(additionalListItem)
playlist.add(additionalPlayurl, additionalListItem, index=currentPosition) playlist.add(additionalPlayurl, additionalListItem, index=currentPosition)
self.pl.verifyPlaylist()
currentPosition += 1 currentPosition += 1
if dummyPlaylist:
# Added a dummy file to the playlist, ############### ADD DUMMY TO PLAYLIST #################
# because the first item is going to fail automatically.
self.logMsg("Processed as a playlist. First item is skipped.", 1) if (not homeScreen and introsPlaylist) or (homeScreen and sizePlaylist > 0):
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) # Playlist will fail on the current position. Adding dummy url
dummyPlaylist = True
self.logMsg("Adding dummy url to counter the setResolvedUrl error.", 2)
playlist.add(playurl, index=startPos)
currentPosition += 1
# We just skipped adding properties. Reset flag for next time. # We just skipped adding properties. Reset flag for next time.
elif propertiesPlayback: elif propertiesPlayback:
self.logMsg("Resetting properties playback flag.", 2) self.logMsg("Resetting properties playback flag.", 2)
utils.window('emby_playbackProps', clear=True, windowid=10101) utils.window('propertiesPlayback', clear=True)
#self.pl.verifyPlaylist()
########## SETUP MAIN ITEM ##########
# For transcoding only, ask for audio/subs pref self.verifyPlaylist()
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 ################ ############### PLAYBACK ################
if not homeScreen and not introsPlaylist:
self.logMsg("Processed as a single item.", 1)
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listItem)
if homeScreen and seektime: elif dummyPlaylist:
self.logMsg("Play as a widget item.", 1) # Added a dummy file to the playlist because the first item is going to fail automatically.
self.setListItem(listitem) self.logMsg("Processed as a playlist. First item is skipped.", 1)
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, 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: else:
self.logMsg("Play as a regular item.", 1) self.logMsg("Play as a regular item.", 1)
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) xbmc.Player().play(playlist, startpos=startPos)
def setProperties(self, playurl, listitem):
# Set all properties necessary for plugin path playback def verifyPlaylist(self):
item = self.item
itemid = item['Id'] playlistitems = '{"jsonrpc": "2.0", "method": "Playlist.GetItems", "params": { "playlistid": 1 }, "id": 1}'
itemtype = item['Type'] items = xbmc.executeJSONRPC(playlistitems)
self.logMsg(items, 2)
embyitem = "emby_%s" % playurl def removeFromPlaylist(self, pos):
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": playlistremove = '{"jsonrpc": "2.0", "method": "Playlist.Remove", "params": { "playlistid": 1, "position": %d }, "id": 1}' % pos
utils.window('%s.refreshid' % embyitem, value=item.get('SeriesId')) result = xbmc.executeJSONRPC(playlistremove)
else: self.logMsg(result, 1)
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, id, playurl, mediaSources):
def externalSubs(self, playurl):
username = utils.window('currUser')
server = utils.window('server%s' % username)
externalsubs = [] externalsubs = []
mapping = {} mapping = {}
item = self.item mediaStream = mediaSources[0].get('MediaStreams')
itemid = item['Id']
try:
mediastreams = item['MediaSources'][0]['MediaStreams']
except (TypeError, KeyError, IndexError):
return
kodiindex = 0 kodiindex = 0
for stream in mediastreams: for stream in mediaStream:
index = stream['Index'] index = stream['Index']
# Since Emby returns all possible tracks together, have to pull only external subtitles. # Since Emby returns all possible tracks together, have to pull only external subtitles.
# IsTextSubtitleStream if true, is available to download from emby. # IsTextSubtitleStream if true, is available to download from emby.
if (stream['Type'] == "Subtitle" and if "Subtitle" in stream['Type'] and stream['IsExternal'] and stream['IsTextSubtitleStream']:
stream['IsExternal'] and stream['IsTextSubtitleStream']):
playmethod = utils.window("%splaymethod" % playurl)
# Direct stream if "DirectPlay" in playmethod:
url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.srt" # Direct play, get direct path
% (self.server, itemid, itemid, index)) url = PlayUtils().directPlay(stream)
elif "DirectStream" in playmethod: # Direct stream
url = "%s/Videos/%s/%s/Subtitles/%s/Stream.srt" % (server, id, id, index)
# map external subtitles for mapping # map external subtitles for mapping
mapping[kodiindex] = index mapping[kodiindex] = index
@ -269,79 +269,69 @@ class PlaybackUtils():
kodiindex += 1 kodiindex += 1
mapping = json.dumps(mapping) mapping = json.dumps(mapping)
utils.window('emby_%s.indexMapping' % playurl, value=mapping) utils.window('%sIndexMapping' % playurl, value=mapping)
return externalsubs return externalsubs
def setArtwork(self, listItem):
# Set up item and item info
item = self.item
artwork = self.artwork
allartwork = artwork.getAllArtwork(item, parentInfo=True) def setProperties(self, playurl, result, listItem):
# Set artwork for listitem # Set runtimeticks, type, refresh_id and item_id
arttypes = { id = result.get('Id')
type = result.get('Type', "")
'poster': "Primary", utils.window("%sruntimeticks" % playurl, value=str(result.get('RunTimeTicks')))
'tvshow.poster': "Primary", utils.window("%stype" % playurl, value=type)
'clearart': "Art", utils.window("%sitem_id" % playurl, value=id)
'tvshow.clearart': "Art",
'clearlogo': "Logo",
'tvshow.clearlogo': "Logo",
'discart': "Disc",
'fanart_image': "Backdrop",
'landscape': "Thumb"
}
for arttype in arttypes:
art = arttypes[arttype] if type == "Episode":
if art == "Backdrop": utils.window("%srefresh_id" % playurl, value=result.get('SeriesId'))
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: else:
listItem.setArt({arttype: path}) utils.window("%srefresh_id" % playurl, value=id)
def setListItem(self, listItem): if utils.window("%splaymethod" % playurl) != "Transcode":
# Only for direct play and direct stream
# Append external subtitles to stream
subtitleList = self.externalSubs(id, playurl, result['MediaSources'])
listItem.setSubtitles(subtitleList)
item = self.item def setArt(self, list, name, path):
type = item['Type']
API = self.API if name in ("thumb", "fanart_image", "small_poster", "tiny_poster", "medium_landscape", "medium_poster", "small_fanartimage", "medium_fanartimage", "fanart_noindicators"):
people = API.getPeople() list.setProperty(name, path)
studios = API.getStudios() else:
list.setArt({name:path})
return list
def setListItemProps(self, server, id, listItem, result):
# Set up item and item info
api = self.api
type = result.get('Type')
people = api.getPeople(result)
studios = api.getStudios(result)
metadata = { metadata = {
'title': item.get('Name', "Missing name"), 'title': result.get('Name', "Missing name"),
'year': item.get('ProductionYear'), 'year': result.get('ProductionYear'),
'plot': API.getOverview(), 'plot': api.getOverview(result),
'director': people.get('Director'), 'director': people.get('Director'),
'writer': people.get('Writer'), 'writer': people.get('Writer'),
'mpaa': API.getMpaa(), 'mpaa': api.getMpaa(result),
'genre': " / ".join(item['Genres']), 'genre': api.getGenre(result),
'studio': " / ".join(studios), 'studio': " / ".join(studios),
'aired': API.getPremiereDate(), 'aired': api.getPremiereDate(result),
'rating': item.get('CommunityRating'), 'rating': result.get('CommunityRating'),
'votes': item.get('VoteCount') 'votes': result.get('VoteCount')
} }
if "Episode" in type: if "Episode" in type:
# Only for tv shows # Only for tv shows
thumbId = item.get('SeriesId') thumbId = result.get('SeriesId')
season = item.get('ParentIndexNumber', -1) season = result.get('ParentIndexNumber', -1)
episode = item.get('IndexNumber', -1) episode = result.get('IndexNumber', -1)
show = item.get('SeriesName', "") show = result.get('SeriesName', "")
metadata['TVShowTitle'] = show metadata['TVShowTitle'] = show
metadata['season'] = season metadata['season'] = season
@ -350,4 +340,123 @@ class PlaybackUtils():
listItem.setProperty('IsPlayable', 'true') listItem.setProperty('IsPlayable', 'true')
listItem.setProperty('IsFolder', 'false') listItem.setProperty('IsFolder', 'false')
listItem.setLabel(metadata['title']) listItem.setLabel(metadata['title'])
listItem.setInfo('video', infoLabels=metadata) listItem.setInfo('video', infoLabels=metadata)
# Set artwork for listitem
self.setArt(listItem,'poster', API().getArtwork(result, "Primary"))
self.setArt(listItem,'tvshow.poster', API().getArtwork(result, "SeriesPrimary"))
self.setArt(listItem,'clearart', API().getArtwork(result, "Art"))
self.setArt(listItem,'tvshow.clearart', API().getArtwork(result, "Art"))
self.setArt(listItem,'clearlogo', API().getArtwork(result, "Logo"))
self.setArt(listItem,'tvshow.clearlogo', API().getArtwork(result, "Logo"))
self.setArt(listItem,'discart', API().getArtwork(result, "Disc"))
self.setArt(listItem,'fanart_image', API().getArtwork(result, "Backdrop"))
self.setArt(listItem,'landscape', API().getArtwork(result, "Thumb"))
def seekToPosition(self, seekTo):
# Set a loop to wait for positive confirmation of playback
count = 0
while not xbmc.Player().isPlaying():
count += 1
if count >= 10:
return
else:
xbmc.sleep(500)
# Jump to seek position
count = 0
while xbmc.Player().getTime() < (seekToTime - 5) and count < 11: # only try 10 times
count += 1
xbmc.Player().seekTime(seekTo)
xbmc.sleep(100)
def PLAYAllItems(self, items, startPositionTicks):
self.logMsg("== ENTER: PLAYAllItems ==")
self.logMsg("Items: %s" % items)
doUtils = self.doUtils
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
playlist.clear()
started = False
for itemId in items:
self.logMsg("Adding Item to playlist: %s" % itemId, 1)
url = "{server}/mediabrowser/Users/{UserId}/Items/%s?format=json" % itemId
result = doUtils.downloadUrl(url)
addition = self.addPlaylistItem(playlist, result)
if not started and addition:
started = True
self.logMsg("Starting Playback Pre", 1)
xbmc.Player().play(playlist)
if not started:
self.logMsg("Starting Playback Post", 1)
xbmc.Player().play(playlist)
# Seek to position
if startPositionTicks:
seekTime = startPositionTicks / 10000000.0
self.seekToPosition(seekTime)
def AddToPlaylist(self, itemIds):
self.logMsg("== ENTER: PLAYAllItems ==")
doUtils = self.doUtils
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
for itemId in itemIds:
self.logMsg("Adding Item to Playlist: %s" % itemId)
url = "{server}/mediabrowser/Users/{UserId}/Items/%s?format=json" % itemId
result = doUtils.downloadUrl(url)
self.addPlaylistItem(playlist, result)
return playlist
def addPlaylistItem(self, playlist, item):
id = item['Id']
username = utils.window('currUser')
server = utils.window('server%s' % username)
playurl = PlayUtils().getPlayUrl(server, id, item)
if utils.window('playurlFalse') == "true":
# Playurl failed - set in PlayUtils.py
utils.window('playurlFalse', clear=True)
self.logMsg("Failed to retrieve the playback path/url or dialog was cancelled.", 1)
return
self.logMsg("Playurl: %s" % playurl)
thumb = API().getArtwork(item, "Primary")
listItem = xbmcgui.ListItem(path=playurl, iconImage=thumb, thumbnailImage=thumb)
self.setListItemProps(server, id, listItem, item)
self.setProperties(playurl, item, listItem)
playlist.add(playurl, listItem)
# Not currently being used
'''def PLAYAllEpisodes(self, items):
WINDOW = xbmcgui.Window(10000)
username = WINDOW.getProperty('currUser')
userid = WINDOW.getProperty('userId%s' % username)
server = WINDOW.getProperty('server%s' % username)
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
playlist.clear()
for item in items:
item_url = "{server}/mediabrowser/Users/{UserId}/Items/%s?format=json&ImageTypeLimit=1" % item["Id"]
jsonData = self.downloadUtils.downloadUrl(item_url)
item_data = jsonData
self.addPlaylistItem(playlist, item_data, server, userid)
xbmc.Player().play(playlist)'''

View file

@ -2,47 +2,45 @@
################################################################################################# #################################################################################################
import json import json as json
import xbmc import xbmc
import xbmcgui import xbmcgui
import utils from DownloadUtils import DownloadUtils
import clientinfo from WebSocketClient import WebSocketThread
import downloadutils from ClientInformation import ClientInformation
import kodidb_functions as kodidb from LibrarySync import LibrarySync
import websocket_client as wsc import Utils as utils
################################################################################################# #################################################################################################
class Player( xbmc.Player ):
class Player(xbmc.Player):
# Borg - multiple instances, shared state # Borg - multiple instances, shared state
_shared_state = {} _shared_state = {}
played_info = {} xbmcplayer = xbmc.Player()
doUtils = DownloadUtils()
clientInfo = ClientInformation()
ws = WebSocketThread()
librarySync = LibrarySync()
addonName = clientInfo.getAddonName()
played_information = {}
playStats = {} playStats = {}
currentFile = None currentFile = None
def __init__(self, *args):
def __init__(self):
self.__dict__ = self._shared_state 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) self.logMsg("Starting playback monitor.", 2)
def logMsg(self, msg, lvl=1): def logMsg(self, msg, lvl=1):
self.className = self.__class__.__name__ self.className = self.__class__.__name__
utils.logMsg("%s %s" % (self.addonName, self.className), msg, lvl) utils.logMsg("%s %s" % (self.addonName, self.className), msg, int(lvl))
def GetPlayStats(self): def GetPlayStats(self):
return self.playStats return self.playStats
@ -76,50 +74,39 @@ class Player(xbmc.Player):
self.currentFile = currentFile self.currentFile = currentFile
# We may need to wait for info to be set in kodi monitor # We may need to wait for info to be set in kodi monitor
itemId = utils.window("emby_%s.itemid" % currentFile) itemId = utils.window("%sitem_id" % currentFile)
tryCount = 0 tryCount = 0
while not itemId: while not itemId:
xbmc.sleep(200) xbmc.sleep(200)
itemId = utils.window("emby_%s.itemid" % currentFile) itemId = utils.window("%sitem_id" % currentFile)
if tryCount == 20: # try 20 times or about 10 seconds if tryCount == 20: # try 20 times or about 10 seconds
self.logMsg("Could not find itemId, cancelling playback report...", 1) self.logMsg("Could not find itemId, cancelling playback report...", 1)
break break
else: tryCount += 1 else: tryCount += 1
else: else:
self.logMsg("ONPLAYBACK_STARTED: %s itemid: %s" % (currentFile, itemId), 0) self.logMsg("ONPLAYBACK_STARTED: %s ITEMID: %s" % (currentFile, itemId), 0)
# Only proceed if an itemId was found. # Only proceed if an itemId was found.
embyitem = "emby_%s" % currentFile runtime = utils.window("%sruntimeticks" % currentFile)
runtime = utils.window("%s.runtime" % embyitem) refresh_id = utils.window("%srefresh_id" % currentFile)
refresh_id = utils.window("%s.refreshid" % embyitem) playMethod = utils.window("%splaymethod" % currentFile)
playMethod = utils.window("%s.playmethod" % embyitem) itemType = utils.window("%stype" % currentFile)
itemType = utils.window("%s.type" % embyitem)
utils.window('emby_skipWatched%s' % itemId, value="true")
seekTime = xbmcplayer.getTime() seekTime = xbmcplayer.getTime()
# Get playback volume # Get playback volume
volume_query = { volume_query = '{"jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["volume","muted"]}, "id": 1}'
result = xbmc.executeJSONRPC(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 = json.loads(result)
result = result.get('result') result = result.get('result')
volume = result.get('volume') volume = result.get('volume')
muted = result.get('muted') muted = result.get('muted')
# Postdata structure to send to Emby server # Postdata structure to send to Emby server
url = "{server}/emby/Sessions/Playing" url = "{server}/mediabrowser/Sessions/Playing"
postdata = { postdata = {
'QueueableMediaTypes': "Video", 'QueueableMediaTypes': "Video",
@ -136,22 +123,12 @@ class Player(xbmc.Player):
if playMethod == "Transcode": if playMethod == "Transcode":
# property set in PlayUtils.py # property set in PlayUtils.py
postdata['AudioStreamIndex'] = utils.window("%sAudioStreamIndex" % currentFile) postdata['AudioStreamIndex'] = utils.window("%sAudioStreamIndex" % currentFile)
postdata['SubtitleStreamIndex'] = utils.window("%sSubtitleStreamIndex" postdata['SubtitleStreamIndex'] = utils.window("%sSubtitleStreamIndex" % currentFile)
% currentFile)
else: else:
# Get the current kodi audio and subtitles and convert to Emby equivalent # Get the current kodi audio and subtitles and convert to Emby equivalent
tracks_query = { track_query = '{"jsonrpc": "2.0", "method": "Player.GetProperties", "params": {"playerid": 1,"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]} , "id": 1}'
result = xbmc.executeJSONRPC(track_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 = json.loads(result)
result = result.get('result') result = result.get('result')
@ -178,9 +155,9 @@ class Player(xbmc.Player):
# Number of audiotracks to help get Emby Index # Number of audiotracks to help get Emby Index
audioTracks = len(xbmc.Player().getAvailableAudioStreams()) audioTracks = len(xbmc.Player().getAvailableAudioStreams())
mapping = utils.window("%s.indexMapping" % embyitem) mapping = utils.window("%sIndexMapping" % currentFile)
if mapping: # Set in playbackutils.py if mapping: # Set in PlaybackUtils.py
self.logMsg("Mapping for external subtitles index: %s" % mapping, 2) self.logMsg("Mapping for external subtitles index: %s" % mapping, 2)
externalIndex = json.loads(mapping) externalIndex = json.loads(mapping)
@ -190,8 +167,7 @@ class Player(xbmc.Player):
postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)] postdata['SubtitleStreamIndex'] = externalIndex[str(indexSubs)]
else: else:
# Internal subtitle currently selected # Internal subtitle currently selected
subindex = indexSubs - len(externalIndex) + audioTracks + 1 postdata['SubtitleStreamIndex'] = indexSubs - len(externalIndex) + audioTracks + 1
postdata['SubtitleStreamIndex'] = subindex
else: # Direct paths enabled scenario or no external subtitles set else: # Direct paths enabled scenario or no external subtitles set
postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1 postdata['SubtitleStreamIndex'] = indexSubs + audioTracks + 1
@ -208,7 +184,7 @@ class Player(xbmc.Player):
runtime = int(runtime) runtime = int(runtime)
except ValueError: except ValueError:
runtime = xbmcplayer.getTotalTime() runtime = xbmcplayer.getTotalTime()
self.logMsg("Runtime is missing, Kodi runtime: %s" % runtime, 1) self.logMsg("Runtime is missing, grabbing runtime from Kodi player: %s" % runtime, 1)
# Save data map for updates and position calls # Save data map for updates and position calls
data = { data = {
@ -224,8 +200,8 @@ class Player(xbmc.Player):
'currentPosition': int(seekTime) 'currentPosition': int(seekTime)
} }
self.played_info[currentFile] = data self.played_information[currentFile] = data
self.logMsg("ADDING_FILE: %s" % self.played_info, 1) self.logMsg("ADDING_FILE: %s" % self.played_information, 1)
# log some playback stats # log some playback stats
'''if(itemType != None): '''if(itemType != None):
@ -249,7 +225,7 @@ class Player(xbmc.Player):
# Get current file # Get current file
currentFile = self.currentFile currentFile = self.currentFile
data = self.played_info.get(currentFile) data = self.played_information.get(currentFile)
# only report playback if emby has initiated the playback (item_id has value) # only report playback if emby has initiated the playback (item_id has value)
if data: if data:
@ -263,23 +239,15 @@ class Player(xbmc.Player):
# Get playback volume # Get playback volume
volume_query = { volume_query = '{"jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["volume","muted"]}, "id": 1}'
result = xbmc.executeJSONRPC(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 = json.loads(result)
result = result.get('result') result = result.get('result')
volume = result.get('volume') volume = result.get('volume')
muted = result.get('muted') muted = result.get('muted')
# Postdata for the websocketclient report # Postdata for the websocketclient report
postdata = { postdata = {
@ -301,18 +269,8 @@ class Player(xbmc.Player):
else: else:
# Get current audio and subtitles track # Get current audio and subtitles track
tracks_query = { track_query = '{"jsonrpc": "2.0", "method": "Player.GetProperties", "params": {"playerid":1,"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]} , "id": 1}'
result = xbmc.executeJSONRPC(track_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 = json.loads(result)
result = result.get('result') result = result.get('result')
@ -339,7 +297,7 @@ class Player(xbmc.Player):
# Number of audiotracks to help get Emby Index # Number of audiotracks to help get Emby Index
audioTracks = len(xbmc.Player().getAvailableAudioStreams()) audioTracks = len(xbmc.Player().getAvailableAudioStreams())
mapping = utils.window("emby_%s.indexMapping" % currentFile) mapping = utils.window("%sIndexMapping" % currentFile)
if mapping: # Set in PlaybackUtils.py if mapping: # Set in PlaybackUtils.py
@ -348,16 +306,13 @@ class Player(xbmc.Player):
if externalIndex.get(str(indexSubs)): if externalIndex.get(str(indexSubs)):
# If the current subtitle is in the mapping # If the current subtitle is in the mapping
subindex = [externalIndex[str(indexSubs)]] * 2 data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [externalIndex[str(indexSubs)]] * 2
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
else: else:
# Internal subtitle currently selected # Internal subtitle currently selected
subindex = [indexSubs - len(externalIndex) + audioTracks + 1] * 2 data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [indexSubs - len(externalIndex) + audioTracks + 1] * 2
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
else: # Direct paths enabled scenario or no external subtitles set else: # Direct paths enabled scenario or no external subtitles set
subindex = [indexSubs + audioTracks + 1] * 2 data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [indexSubs + audioTracks + 1] * 2
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
else: else:
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [""] * 2 data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [""] * 2
@ -371,8 +326,8 @@ class Player(xbmc.Player):
currentFile = self.currentFile currentFile = self.currentFile
self.logMsg("PLAYBACK_PAUSED: %s" % currentFile, 2) self.logMsg("PLAYBACK_PAUSED: %s" % currentFile, 2)
if self.played_info.get(currentFile): if self.played_information.get(currentFile):
self.played_info[currentFile]['paused'] = True self.played_information[currentFile]['paused'] = True
self.reportPlayback() self.reportPlayback()
@ -381,8 +336,8 @@ class Player(xbmc.Player):
currentFile = self.currentFile currentFile = self.currentFile
self.logMsg("PLAYBACK_RESUMED: %s" % currentFile, 2) self.logMsg("PLAYBACK_RESUMED: %s" % currentFile, 2)
if self.played_info.get(currentFile): if self.played_information.get(currentFile):
self.played_info[currentFile]['paused'] = False self.played_information[currentFile]['paused'] = False
self.reportPlayback() self.reportPlayback()
@ -391,17 +346,15 @@ class Player(xbmc.Player):
currentFile = self.currentFile currentFile = self.currentFile
self.logMsg("PLAYBACK_SEEK: %s" % currentFile, 2) self.logMsg("PLAYBACK_SEEK: %s" % currentFile, 2)
if self.played_info.get(currentFile): if self.played_information.get(currentFile):
position = self.xbmcplayer.getTime() position = self.xbmcplayer.getTime()
self.played_info[currentFile]['currentPosition'] = position self.played_information[currentFile]['currentPosition'] = position
self.reportPlayback() self.reportPlayback()
def onPlayBackStopped( self ): def onPlayBackStopped( self ):
# Will be called when user stops xbmc playing a file # Will be called when user stops xbmc playing a file
self.logMsg("ONPLAYBACK_STOPPED", 2) self.logMsg("ONPLAYBACK_STOPPED", 2)
xbmcgui.Window(10101).clearProperties()
self.logMsg("Clear playlist properties.")
self.stopAll() self.stopAll()
def onPlayBackEnded( self ): def onPlayBackEnded( self ):
@ -411,16 +364,14 @@ class Player(xbmc.Player):
def stopAll(self): def stopAll(self):
doUtils = self.doUtils if not self.played_information:
if not self.played_info:
return return
self.logMsg("Played_information: %s" % self.played_info, 1) self.logMsg("Played_information: %s" % self.played_information, 1)
# Process each items # Process each items
for item in self.played_info: for item in self.played_information:
data = self.played_info.get(item) data = self.played_information.get(item)
if data: if data:
self.logMsg("Item path: %s" % item, 2) self.logMsg("Item path: %s" % item, 2)
@ -428,61 +379,47 @@ class Player(xbmc.Player):
runtime = data['runtime'] runtime = data['runtime']
currentPosition = data['currentPosition'] currentPosition = data['currentPosition']
itemid = data['item_id'] itemId = data['item_id']
refresh_id = data['refresh_id'] refresh_id = data['refresh_id']
currentFile = data['currentfile'] currentFile = data['currentfile']
type = data['Type'] type = data['Type']
playMethod = data['playmethod'] playMethod = data['playmethod']
if currentPosition and runtime: if currentPosition and runtime:
try: percentComplete = (currentPosition * 10000000) / int(runtime)
percentComplete = (currentPosition * 10000000) / int(runtime)
except ZeroDivisionError:
# Runtime is 0.
percentComplete = 0
markPlayedAt = float(utils.settings('markPlayed')) / 100 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 self.logMsg("Percent complete: %s Mark played at: %s" % (percentComplete, markPlayedAt), 1)
utils.window('emby_skipWatched%s' % itemid, value="true") # Prevent manually mark as watched in Kodi monitor > WriteKodiVideoDB().UpdatePlaycountFromKodi()
utils.window('SkipWatched%s' % itemId, "true")
self.stopPlayback(data) self.stopPlayback(data)
# Stop transcoding offerDelete = utils.settings('offerDelete') == "true"
if playMethod == "Transcode": offerTypeDelete = False
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. if type == "Episode" and utils.settings('offerDeleteTV') == "true":
offerDelete = False offerTypeDelete = True
if type == "Episode" and utils.settings('deleteTV') == "true": elif type == "Movie" and utils.settings('offerDeleteMovies') == "true":
offerDelete = True offerTypeDelete = True
elif type == "Movie" and utils.settings('deleteMovies') == "true":
offerDelete = True
if utils.settings('offerDelete') != "true": if percentComplete >= markPlayedAt and offerDelete and offerTypeDelete:
# Delete could be disabled, even if the subsetting is enabled. # Make the bigger setting be able to disable option easily.
offerDelete = False self.logMsg("Offering deletion for: %s." % itemId, 1)
return_value = xbmcgui.Dialog().yesno("Offer Delete", "Delete %s" % currentFile.split("/")[-1], "on Emby Server?")
if percentComplete >= markPlayedAt and offerDelete: if return_value:
if utils.settings('skipConfirmDelete') != "true": # Delete Kodi entry before Emby
resp = xbmcgui.Dialog().yesno( listItem = [itemId]
heading="Confirm delete", LibrarySync().removefromDB(listItem, True)
line1="Delete file on Emby Server?")
if not resp: # Stop transcoding
self.logMsg("User skipped deletion.", 1) if playMethod == "Transcode":
continue self.logMsg("Transcoding for %s terminated." % itemId, 1)
deviceId = self.clientInfo.getMachineId()
url = "{server}/emby/Items/%s?format=json" % itemid url = "{server}/mediabrowser/Videos/ActiveEncodings?DeviceId=%s" % deviceId
self.logMsg("Deleting request: %s" % itemid) self.doUtils.downloadUrl(url, type="DELETE")
doUtils.downloadUrl(url, type="DELETE")
self.played_info.clear() self.played_information.clear()
def stopPlayback(self, data): def stopPlayback(self, data):
@ -492,11 +429,12 @@ class Player(xbmc.Player):
currentPosition = data['currentPosition'] currentPosition = data['currentPosition']
positionTicks = int(currentPosition * 10000000) positionTicks = int(currentPosition * 10000000)
url = "{server}/emby/Sessions/Playing/Stopped" url = "{server}/mediabrowser/Sessions/Playing/Stopped"
postdata = { postdata = {
'ItemId': itemId, 'ItemId': itemId,
'MediaSourceId': itemId, 'MediaSourceId': itemId,
'PositionTicks': positionTicks 'PositionTicks': positionTicks
} }
self.doUtils.downloadUrl(url, postBody=postdata, type="POST") self.doUtils.downloadUrl(url, postBody=postdata, type="POST")

View file

@ -1,21 +1,22 @@
# -*- coding: utf-8 -*- #################################################################################################
# UserClient thread
################################################################################################## #################################################################################################
import hashlib
import threading
import xbmc import xbmc
import xbmcgui import xbmcgui
import xbmcaddon import xbmcaddon
import xbmcvfs import xbmcvfs
import artwork import threading
import utils import hashlib
import clientinfo import json as json
import downloadutils
################################################################################################## import KodiMonitor
import Utils as utils
from ClientInformation import ClientInformation
from DownloadUtils import DownloadUtils
from Player import Player
from API import API
class UserClient(threading.Thread): class UserClient(threading.Thread):
@ -23,7 +24,16 @@ class UserClient(threading.Thread):
# Borg - multiple instances, shared state # Borg - multiple instances, shared state
_shared_state = {} _shared_state = {}
clientInfo = ClientInformation()
doUtils = DownloadUtils()
KodiMonitor = KodiMonitor.Kodi_Monitor()
addonName = clientInfo.getAddonName()
addon = xbmcaddon.Addon()
WINDOW = xbmcgui.Window(10000)
stopClient = False stopClient = False
logLevel = int(addon.getSetting('logLevel'))
auth = True auth = True
retry = 0 retry = 0
@ -34,25 +44,25 @@ class UserClient(threading.Thread):
HasAccess = True HasAccess = True
AdditionalUser = [] AdditionalUser = []
userSettings = None def __init__(self, *args):
def __init__(self):
self.__dict__ = self._shared_state self.__dict__ = self._shared_state
self.addon = xbmcaddon.Addon() threading.Thread.__init__(self, *args)
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): def logMsg(self, msg, lvl=1):
className = self.__class__.__name__ className = self.__class__.__name__
utils.logMsg("%s %s" % (self.addonName, className), msg, lvl) utils.logMsg("%s %s" % (self.addonName, className), str(msg), int(lvl))
def getUsername(self):
username = utils.settings('username')
if (username == ""):
self.logMsg("No username saved.", 2)
return ""
return username
def getAdditionalUsers(self): def getAdditionalUsers(self):
@ -61,21 +71,11 @@ class UserClient(threading.Thread):
if additionalUsers: if additionalUsers:
self.AdditionalUser = additionalUsers.split(',') 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): def getLogLevel(self):
try: try:
logLevel = int(utils.settings('logLevel')) logLevel = int(utils.settings('logLevel'))
except ValueError: except:
logLevel = 0 logLevel = 0
return logLevel return logLevel
@ -83,84 +83,71 @@ class UserClient(threading.Thread):
def getUserId(self): def getUserId(self):
username = self.getUsername() username = self.getUsername()
w_userId = utils.window('emby_userId%s' % username) w_userId = self.WINDOW.getProperty('userId%s' % username)
s_userId = utils.settings('userId%s' % username) s_userId = utils.settings('userId%s' % username)
# Verify the window property # Verify the window property
if w_userId: if (w_userId != ""):
if not s_userId: self.logMsg("Returning userId from WINDOW for username: %s UserId: %s" % (username, w_userId), 2)
# 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 return w_userId
# Verify the settings # Verify the settings
elif s_userId: elif (s_userId != ""):
self.logMsg( self.logMsg("Returning userId from SETTINGS for username: %s userId: %s" % (username, s_userId), 2)
"Returning userId from SETTINGS for username: %s userId: %s"
% (username, s_userId), 2)
return s_userId return s_userId
# No userId found # No userId found
else: else:
self.logMsg("No userId saved for username: %s." % username, 1) self.logMsg("No userId saved for username: %s." % username)
return
def getServer(self, prefix=True): def getServer(self, prefix=True):
alternate = utils.settings('altip') == "true" alternate = utils.settings('altip') == "true"
# For https support
HTTPS = utils.settings('https')
host = utils.settings('ipaddress')
port = utils.settings('port')
# Alternate host
if alternate: if alternate:
# Alternate host HTTPS = utils.settings('secondhttps')
HTTPS = utils.settings('secondhttps') == "true"
host = utils.settings('secondipaddress') host = utils.settings('secondipaddress')
port = utils.settings('secondport') port = utils.settings('secondport')
else:
# Original host
HTTPS = utils.settings('https') == "true"
host = utils.settings('ipaddress')
port = utils.settings('port')
server = host + ":" + port server = host + ":" + port
if not host: if host == "":
self.logMsg("No server information saved.", 2) self.logMsg("No server information saved.", 2)
return False return ""
# If https is true # If https is true
if prefix and HTTPS: if prefix and (HTTPS == "true"):
server = "https://%s" % server server = "https://%s" % server
return server return server
# If https is false # If https is false
elif prefix and not HTTPS: elif prefix and (HTTPS == "false"):
server = "http://%s" % server server = "http://%s" % server
return server return server
# If only the host:port is required # If only the host:port is required
elif not prefix: elif (prefix == False):
return server return server
def getToken(self): def getToken(self):
username = self.getUsername() username = self.getUsername()
w_token = utils.window('emby_accessToken%s' % username) w_token = self.WINDOW.getProperty('accessToken%s' % username)
s_token = utils.settings('accessToken') s_token = utils.settings('accessToken')
# Verify the window property # Verify the window property
if w_token: if (w_token != ""):
if not s_token: self.logMsg("Returning accessToken from WINDOW for username: %s accessToken: %s" % (username, w_token), 2)
# 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 return w_token
# Verify the settings # Verify the settings
elif s_token: elif (s_token != ""):
self.logMsg( self.logMsg("Returning accessToken from SETTINGS for username: %s accessToken: %s" % (username, s_token), 2)
"Returning accessToken from SETTINGS for username: %s accessToken: %s" self.WINDOW.setProperty('accessToken%s' % username, s_token)
% (username, s_token), 2)
utils.window('emby_accessToken%s' % username, value=s_token)
return s_token return s_token
else: else:
self.logMsg("No token found.", 1) self.logMsg("No token found.")
return "" return ""
def getSSLverify(self): def getSSLverify(self):
@ -187,63 +174,71 @@ class UserClient(threading.Thread):
def setUserPref(self): def setUserPref(self):
doUtils = self.doUtils player = Player()
server = self.getServer()
userId = self.getUserId()
url = "{server}/mediabrowser/Users/{UserId}?format=json"
result = self.doUtils.downloadUrl(url)
url = "{server}/emby/Users/{UserId}?format=json"
result = doUtils.downloadUrl(url)
self.userSettings = result
# Set user image for skin display # Set user image for skin display
if result.get('PrimaryImageTag'): self.WINDOW.setProperty("EmbyUserImage",API().getUserArtwork(result,"Primary"))
utils.window('EmbyUserImage', value=artwork.Artwork().getUserArtwork(result, 'Primary'))
# Set resume point max # Load the resume point from Emby and set as setting
url = "{server}/emby/System/Configuration?format=json" url = "{server}/mediabrowser/System/Configuration?format=json"
result = doUtils.downloadUrl(url) result = self.doUtils.downloadUrl(url)
utils.settings('markPlayed', value=str(result['MaxResumePct'])) utils.settings('markPlayed', value=str(result['MaxResumePct']))
return True
def getPublicUsers(self): def getPublicUsers(self):
server = self.getServer() server = self.getServer()
# Get public Users # Get public Users
url = "%s/emby/Users/Public?format=json" % server url = "%s/mediabrowser/Users/Public?format=json" % server
result = self.doUtils.downloadUrl(url, authenticate=False) result = self.doUtils.downloadUrl(url, authenticate=False)
if result != "": users = []
return result
if (result != ""):
users = result
else: else:
# Server connection failed # Server connection failed
return False return False
return users
def hasAccess(self): def hasAccess(self):
# hasAccess is verified in service.py
url = "{server}/emby/Users?format=json" url = "{server}/mediabrowser/Users"
result = self.doUtils.downloadUrl(url) result = self.doUtils.downloadUrl(url)
if result == False: if result is False:
# Access is restricted, set in downloadutils.py via exception # Access is restricted
self.logMsg("Access is restricted.", 1) self.logMsg("Access is restricted.")
self.HasAccess = False self.HasAccess = False
return
elif utils.window('emby_online') != "true": elif self.WINDOW.getProperty('Server_online') != "true":
# Server connection failed # Server connection failed
pass return
elif utils.window('emby_serverStatus') == "restricted": if self.WINDOW.getProperty("Server_status") == "restricted":
self.logMsg("Access is granted.", 1) self.logMsg("Access is granted.")
self.HasAccess = True self.HasAccess = True
utils.window('emby_serverStatus', clear=True) self.WINDOW.setProperty("Server_status", "")
xbmcgui.Dialog().notification("Emby server", "Access is enabled.") xbmcgui.Dialog().notification("Emby server", "Access is enabled.")
return
def loadCurrUser(self, authenticated=False): def loadCurrUser(self, authenticated=False):
WINDOW = self.WINDOW
doUtils = self.doUtils doUtils = self.doUtils
username = self.getUsername() username = self.getUsername()
userId = self.getUserId()
# Only to be used if token exists # Only to be used if token exists
self.currUserId = userId self.currUserId = self.getUserId()
self.currServer = self.getServer() self.currServer = self.getServer()
self.currToken = self.getToken() self.currToken = self.getToken()
self.ssl = self.getSSLverify() self.ssl = self.getSSLverify()
@ -251,21 +246,21 @@ class UserClient(threading.Thread):
# Test the validity of current token # Test the validity of current token
if authenticated == False: if authenticated == False:
url = "%s/emby/Users/%s?format=json" % (self.currServer, userId) url = "%s/mediabrowser/Users/%s" % (self.currServer, self.currUserId)
utils.window('emby_currUser', value=userId) WINDOW.setProperty("currUser", username)
utils.window('emby_accessToken%s' % userId, value=self.currToken) WINDOW.setProperty("accessToken%s" % username, self.currToken)
result = doUtils.downloadUrl(url) result = doUtils.downloadUrl(url)
if result == 401: if result == 401:
# Token is no longer valid # Token is no longer valid
self.resetClient() self.resetClient()
return False return False
# Set to windows property # Set to windows property
utils.window('emby_currUser', value=userId) WINDOW.setProperty("currUser", username)
utils.window('emby_accessToken%s' % userId, value=self.currToken) WINDOW.setProperty("accessToken%s" % username, self.currToken)
utils.window('emby_server%s' % userId, value=self.currServer) WINDOW.setProperty("server%s" % username, self.currServer)
utils.window('emby_server_%s' % userId, value=self.getServer(prefix=False)) WINDOW.setProperty("server_%s" % username, self.getServer(prefix=False))
WINDOW.setProperty("userId%s" % username, self.currUserId)
# Set DownloadUtils values # Set DownloadUtils values
doUtils.setUsername(username) doUtils.setUsername(username)
@ -278,194 +273,188 @@ class UserClient(threading.Thread):
# Start DownloadUtils session # Start DownloadUtils session
doUtils.startSession() doUtils.startSession()
self.getAdditionalUsers() self.getAdditionalUsers()
# Set user preferences in settings
self.currUser = username self.currUser = username
# Set user preferences in settings
self.setUserPref() self.setUserPref()
def authenticate(self): def authenticate(self):
# Get /profile/addon_data
addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8') WINDOW = self.WINDOW
hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir) addon = self.addon
username = self.getUsername() username = self.getUsername()
server = self.getServer() server = self.getServer()
addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8')
hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir)
# If there's no settings.xml # If there's no settings.xml
if not hasSettings: if (hasSettings == 0):
self.logMsg("No settings.xml found.", 1) self.logMsg("No settings.xml found.")
self.auth = False self.auth = False
return return
# If no user information # If no user information
elif not server or not username: if (server == "") or (username == ""):
self.logMsg("Missing server information.", 1) self.logMsg("Missing server information.")
self.auth = False self.auth = False
return return
# If there's a token, load the user # If there's a token
elif self.getToken(): if (self.getToken() != ""):
result = self.loadCurrUser() result = self.loadCurrUser()
if result == False: if result == False:
pass pass
else: else:
self.logMsg("Current user: %s" % self.currUser, 1) self.logMsg("Current user: %s" % self.currUser, 0)
self.logMsg("Current userId: %s" % self.currUserId, 1) self.logMsg("Current userId: %s" % self.currUserId, 0)
self.logMsg("Current accessToken: %s" % self.currToken, 2) self.logMsg("Current accessToken: %s" % self.currToken, 0)
return return
##### AUTHENTICATE USER #####
users = self.getPublicUsers() users = self.getPublicUsers()
password = "" password = ""
# Find user in list # Find user in list
for user in users: for user in users:
name = user['Name'] name = user[u'Name']
userHasPassword = False
if username.decode('utf-8') in name: if (unicode(username, 'utf-8') in name):
# Verify if user has a password
if (user.get("HasPassword") == True):
userHasPassword = True
# If user has password # If user has password
if user['HasPassword'] == True: if (userHasPassword):
password = xbmcgui.Dialog().input( password = xbmcgui.Dialog().input("Enter password for user: %s" % username, option=xbmcgui.ALPHANUM_HIDE_INPUT)
heading="Enter password for user: %s" % username,
option=xbmcgui.ALPHANUM_HIDE_INPUT)
# If password dialog is cancelled # If password dialog is cancelled
if not password: if (password == ""):
self.logMsg("No password entered.", 0) self.logMsg("No password entered.", 0)
utils.window('emby_serverStatus', value="Stop") self.WINDOW.setProperty("Server_status", "Stop")
self.auth = False self.auth = False
return return
break break
else: else:
# Manual login, user is hidden # Manual login, user is hidden
password = xbmcgui.Dialog().input( password = xbmcgui.Dialog().input("Enter password for user: %s" % username, option=xbmcgui.ALPHANUM_HIDE_INPUT)
heading="Enter password for user: %s" % username,
option=xbmcgui.ALPHANUM_HIDE_INPUT)
sha1 = hashlib.sha1(password) sha1 = hashlib.sha1(password)
sha1 = sha1.hexdigest() sha1 = sha1.hexdigest()
# Authenticate username and password # Authenticate username and password
url = "%s/emby/Users/AuthenticateByName?format=json" % server url = "%s/mediabrowser/Users/AuthenticateByName?format=json" % server
data = {'username': username, 'password': sha1} data = {'username': username, 'password': sha1}
self.logMsg(data, 2) self.logMsg(data, 2)
result = self.doUtils.downloadUrl(url, postBody=data, type="POST", authenticate=False) result = self.doUtils.downloadUrl(url, postBody=data, type="POST", authenticate=False)
accessToken = None
try: try:
self.logMsg("Auth response: %s" % result, 1) self.logMsg("Auth_Reponse: %s" % result, 1)
accessToken = result['AccessToken'] accessToken = result[u'AccessToken']
except:
except (KeyError, TypeError): pass
self.logMsg("Failed to retrieve the api key.", 1)
accessToken = None
if accessToken is not None: if (result != None and accessToken != None):
self.currUser = username self.currUser = username
xbmcgui.Dialog().notification("Emby server", "Welcome %s!" % self.currUser) xbmcgui.Dialog().notification("Emby server", "Welcome %s!" % self.currUser)
userId = result['User']['Id'] userId = result[u'User'][u'Id']
utils.settings('accessToken', value=accessToken) utils.settings("accessToken", accessToken)
utils.settings('userId%s' % username, value=userId) utils.settings("userId%s" % username, userId)
self.logMsg("User Authenticated: %s" % accessToken, 1) self.logMsg("User Authenticated: %s" % accessToken)
self.loadCurrUser(authenticated=True) self.loadCurrUser(authenticated=True)
utils.window('emby_serverStatus', clear=True) self.WINDOW.setProperty("Server_status", "")
self.retry = 0 self.retry = 0
return
else: else:
self.logMsg("User authentication failed.", 1) self.logMsg("User authentication failed.")
utils.settings('accessToken', value="") utils.settings("accessToken", "")
utils.settings('userId%s' % username, value="") utils.settings("userId%s" % username, "")
xbmcgui.Dialog().ok("Error connecting", "Invalid username or password.") xbmcgui.Dialog().ok("Error connecting", "Invalid username or password.")
# Give two attempts at entering 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.retry += 1
if self.retry == 2:
self.logMsg("Too many retries. You can retry by selecting the option in the addon settings.")
self.WINDOW.setProperty("Server_status", "Stop")
xbmcgui.Dialog().ok("Error connecting", "Failed to authenticate too many times. You can retry by selecting the option in the addon settings.")
self.auth = False self.auth = False
return
def resetClient(self): def resetClient(self):
self.logMsg("Reset UserClient authentication.", 1)
username = self.getUsername() username = self.getUsername()
self.logMsg("Reset UserClient authentication.", 1)
if self.currToken is not None: if (self.currToken != None):
# In case of 401, removed saved token # In case of 401, removed saved token
utils.settings('accessToken', value="") utils.settings("accessToken", "")
utils.window('emby_accessToken%s' % username, clear=True) self.WINDOW.setProperty("accessToken%s" % username, "")
self.currToken = None self.currToken = None
self.logMsg("User token has been removed.", 1) self.logMsg("User token has been removed.", 1)
self.auth = True self.auth = True
self.currUser = None self.currUser = None
return
def run(self): def run(self):
monitor = xbmc.Monitor() self.logMsg("|---- Starting UserClient ----|", 0)
self.logMsg("----===## Starting UserClient ##===----", 0)
while not monitor.abortRequested(): while not self.KodiMonitor.abortRequested():
# Verify the log level # Verify the log level
currLogLevel = self.getLogLevel() currLogLevel = self.getLogLevel()
if self.logLevel != currLogLevel: if self.logLevel != currLogLevel:
# Set new log level # Set new log level
self.logLevel = currLogLevel self.logLevel = currLogLevel
utils.window('emby_logLevel', value=str(currLogLevel))
self.logMsg("New Log Level: %s" % currLogLevel, 0) self.logMsg("New Log Level: %s" % currLogLevel, 0)
self.WINDOW.setProperty('getLogLevel', str(currLogLevel))
if (self.WINDOW.getProperty("Server_status") != ""):
status = utils.window('emby_serverStatus') status = self.WINDOW.getProperty("Server_status")
if status:
# Verify the connection status to server
if status == "restricted": if status == "restricted":
# Parental control is restricting access # Parental control is restricting access
self.HasAccess = False self.HasAccess = False
elif status == "401": elif status == "401":
# Unauthorized access, revoke token self.WINDOW.setProperty("Server_status", "Auth")
utils.window('emby_serverStatus', value="Auth") # Revoked token
self.resetClient() self.resetClient()
if self.auth and (self.currUser is None): if self.auth and (self.currUser == None):
# Try to authenticate user status = self.WINDOW.getProperty("Server_status")
status = utils.window('emby_serverStatus')
if not status or status == "Auth": if (status == "") or (status == "Auth"):
# Set auth flag because we no longer need
# to authenticate the user
self.auth = False self.auth = False
self.authenticate() self.authenticate()
if (self.auth == False) and (self.currUser == None):
if not self.auth and (self.currUser is None): # Only if there's information found to login
# If authenticate failed.
server = self.getServer() server = self.getServer()
username = self.getUsername() username = self.getUsername()
status = utils.window('emby_serverStatus') status = self.WINDOW.getProperty("Server_status")
# If user didn't enter a password when prompted
if status == "Stop":
pass
# The status Stop is for when user cancelled password dialog. elif (server != "") and (username != ""):
if server and username and status != "Stop": self.logMsg("Server found: %s" % server)
# Only if there's information found to login self.logMsg("Username found: %s" % username)
self.logMsg("Server found: %s" % server, 2)
self.logMsg("Username found: %s" % username, 2)
self.auth = True self.auth = True
# If stopping the client didn't work
if self.stopClient == True: if self.stopClient == True:
# If stopping the client didn't work
break break
if monitor.waitForAbort(1): if self.KodiMonitor.waitForAbort(1):
# Abort was requested while waiting. We should exit # Abort was requested while waiting. We should exit
break break
self.doUtils.stopSession() self.doUtils.stopSession()
self.logMsg("##===---- UserClient Stopped ----===##", 0) self.logMsg("|---- UserClient Stopped ----|", 0)
def stopClient(self): def stopClient(self):
# When emby for kodi terminates # As last resort
self.stopClient = True self.stopClient = True

View file

@ -1,76 +1,59 @@
# -*- coding: utf-8 -*- #################################################################################################
# utils
################################################################################################# #################################################################################################
import xbmc
import xbmcgui
import xbmcaddon
import xbmcvfs
import json
import os
import cProfile import cProfile
import inspect
import pstats import pstats
import sqlite3
import time import time
import inspect
import sqlite3
import string
import unicodedata import unicodedata
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
import xbmc from API import API
import xbmcaddon from PlayUtils import PlayUtils
import xbmcgui from DownloadUtils import DownloadUtils
import xbmcvfs
################################################################################################# downloadUtils = DownloadUtils()
addon = xbmcaddon.Addon()
language = addon.getLocalizedString
def logMsg(title, msg, level=1): def logMsg(title, msg, level = 1):
WINDOW = xbmcgui.Window(10000)
# Get the logLevel set in UserClient # Get the logLevel set in UserClient
try: logLevel = int(WINDOW.getProperty('getLogLevel'))
logLevel = int(window('emby_logLevel'))
except ValueError:
logLevel = 0
if logLevel >= level: if(logLevel >= level):
if(logLevel == 2): # inspect.stack() is expensive
if logLevel == 2: # inspect.stack() is expensive
try: try:
xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg)) xbmc.log(title + " -> " + inspect.stack()[1][3] + " : " + str(msg))
except UnicodeEncodeError: except UnicodeEncodeError:
xbmc.log("%s -> %s : %s" % (title, inspect.stack()[1][3], msg.encode('utf-8'))) xbmc.log(title + " -> " + inspect.stack()[1][3] + " : " + str(msg.encode('utf-8')))
else: else:
try: try:
xbmc.log("%s -> %s" % (title, msg)) xbmc.log(title + " -> " + str(msg))
except UnicodeEncodeError: except UnicodeEncodeError:
xbmc.log("%s -> %s" % (title, msg.encode('utf-8'))) xbmc.log(title + " -> " + str(msg.encode('utf-8')))
def window(property, value=None, clear=False, windowid=10000): def convertEncoding(data):
# Get or set window property #nasty hack to make sure we have a unicode string
WINDOW = xbmcgui.Window(windowid) try:
return data.decode('utf-8')
except:
return data
def KodiSQL(type="video"):
if clear: if type == "music":
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() dbPath = getKodiMusicDBPath()
elif type == "texture": elif type == "texture":
dbPath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8') dbPath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8')
@ -111,140 +94,219 @@ def getKodiMusicDBPath():
"special://database/MyMusic%s.db" "special://database/MyMusic%s.db"
% dbVersion.get(kodibuild, "")).decode('utf-8') % dbVersion.get(kodibuild, "")).decode('utf-8')
return dbPath 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 def prettifyXml(elem):
settings('SyncInstallRunDone', value="false") rough_string = etree.tostring(elem, "utf-8")
reparsed = minidom.parseString(rough_string)
# Remove emby info return reparsed.toprettyxml(indent="\t")
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(): def startProfiling():
pr = cProfile.Profile() pr = cProfile.Profile()
pr.enable() pr.enable()
return pr return pr
def stopProfiling(pr, profileName):
def stopProfiling(pr, profileName):
pr.disable() pr.disable()
ps = pstats.Stats(pr) ps = pstats.Stats(pr)
profiles = xbmc.translatePath("%sprofiles/" addondir = xbmc.translatePath(xbmcaddon.Addon(id='plugin.video.emby').getAddonInfo('profile'))
% 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') fileTimeStamp = time.strftime("%Y-%m-%d %H-%M-%S")
tabFileNamepath = os.path.join(addondir, "profiles")
tabFileName = os.path.join(addondir, "profiles" , profileName + "_profile_(" + fileTimeStamp + ").tab")
if not xbmcvfs.exists(tabFileNamepath):
xbmcvfs.mkdir(tabFileNamepath)
f = open(tabFileName, 'wb')
f.write("NumbCalls\tTotalTime\tCumulativeTime\tFunctionName\tFileName\r\n") f.write("NumbCalls\tTotalTime\tCumulativeTime\tFunctionName\tFileName\r\n")
for (key, value) in ps.stats.items(): for (key, value) in ps.stats.items():
(filename, count, func_name) = key (filename, count, func_name) = key
(ccalls, ncalls, total_time, cumulative_time, callers) = value (ccalls, ncalls, total_time, cumulative_time, callers) = value
try: try:
f.write( f.write(str(ncalls) + "\t" + "{:10.4f}".format(total_time) + "\t" + "{:10.4f}".format(cumulative_time) + "\t" + func_name + "\t" + filename + "\r\n")
"%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: except ValueError:
f.write( f.write(str(ncalls) + "\t" + "{0}".format(total_time) + "\t" + "{0}".format(cumulative_time) + "\t" + func_name + "\t" + filename + "\r\n")
"%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() f.close()
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 createSources():
# To make Master lock compatible
path = xbmc.translatePath("special://profile/").decode("utf-8")
xmlpath = "%ssources.xml" % path
if xbmcvfs.exists(xmlpath):
# Modify the existing file
try:
xmlparse = etree.parse(xmlpath)
except:
root = etree.Element('sources')
else:
root = xmlparse.getroot()
video = root.find('video')
if video is None:
video = etree.SubElement(root, 'video')
else:
# We need to create the file
root = etree.Element('sources')
video = etree.SubElement(root, 'video')
# Add elements
etree.SubElement(video, 'default', attrib={'pathversion': "1"})
# First dummy source
source_one = etree.SubElement(video, 'source')
etree.SubElement(source_one, 'name').text = "Emby"
etree.SubElement(source_one, 'path', attrib={'pathversion': "1"}).text = (
"smb://embydummy/dummypath1/"
)
etree.SubElement(source_one, 'allowsharing').text = "true"
# Second dummy source
source_two = etree.SubElement(video, 'source')
etree.SubElement(source_two, 'name').text = "Emby"
etree.SubElement(source_two, 'path', attrib={'pathversion': "1"}).text = (
"smb://embydummy/dummypath2/"
)
etree.SubElement(source_two, 'allowsharing').text = "true"
try:
indent(root)
except:pass
etree.ElementTree(root).write(xmlpath)
def pathsubstitution(add=True):
path = xbmc.translatePath('special://userdata').decode('utf-8')
xmlpath = "%sadvancedsettings.xml" % path
xmlpathexists = xbmcvfs.exists(xmlpath)
# original address
originalServer = settings('ipaddress')
originalPort = settings('port')
originalHttp = settings('https') == "true"
if originalHttp:
originalHttp = "https"
else:
originalHttp = "http"
# Process add or deletion
if add:
# second address
secondServer = settings('secondipaddress')
secondPort = settings('secondport')
secondHttp = settings('secondhttps') == "true"
if secondHttp:
secondHttp = "https"
else:
secondHttp = "http"
logMsg("EMBY", "Original address: %s://%s:%s, alternate is: %s://%s:%s" % (originalHttp, originalServer, originalPort, secondHttp, secondServer, secondPort), 1)
if xmlpathexists:
# we need to modify the file.
try:
xmlparse = etree.parse(xmlpath)
except: # Document is blank
root = etree.Element('advancedsettings')
else:
root = xmlparse.getroot()
pathsubs = root.find('pathsubstitution')
if pathsubs is None:
pathsubs = etree.SubElement(root, 'pathsubstitution')
else:
# we need to create the file.
root = etree.Element('advancedsettings')
pathsubs = etree.SubElement(root, 'pathsubstitution')
substitute = etree.SubElement(pathsubs, 'substitute')
# From original address
etree.SubElement(substitute, 'from').text = "%s://%s:%s" % (originalHttp, originalServer, originalPort)
# To secondary address
etree.SubElement(substitute, 'to').text = "%s://%s:%s" % (secondHttp, secondServer, secondPort)
etree.ElementTree(root).write(xmlpath)
settings('pathsub', "true")
else: # delete the path substitution, we don't need it anymore.
logMsg("EMBY", "Alternate address is disabled, removing path substitution for: %s://%s:%s" % (originalHttp, originalServer, originalPort), 1)
xmlparse = etree.parse(xmlpath)
root = xmlparse.getroot()
iterator = root.getiterator("pathsubstitution")
for substitutes in iterator:
for substitute in substitutes:
frominsert = substitute.find(".//from").text == "%s://%s:%s" % (originalHttp, originalServer, originalPort)
if frominsert:
# Found a match, in case there's more than one substitution.
substitutes.remove(substitute)
etree.ElementTree(root).write(xmlpath)
settings('pathsub', "false")
def settings(setting, value = None):
# Get or add addon setting
addon = xbmcaddon.Addon()
if value:
addon.setSetting(setting, value)
else:
return addon.getSetting(setting)
def window(property, value = None, clear = False):
# Get or set window property
WINDOW = xbmcgui.Window(10000)
if clear:
WINDOW.clearProperty(property)
elif value:
WINDOW.setProperty(property, value)
else:
return WINDOW.getProperty(property)
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 normalize_nodes(text): def normalize_nodes(text):
# For video nodes # For video nodes
text = text.replace(":", "") text = text.replace(":", "")
@ -265,223 +327,99 @@ def normalize_nodes(text):
return text return text
def normalize_string(text): def reloadProfile():
# For theme media, do not modify unless # Useful to reload the add-on without restarting Kodi.
# modified in TV Tunes profile = xbmc.getInfoLabel('System.ProfileName')
text = text.replace(":", "") xbmc.executebuiltin("LoadProfile(%s)" % profile)
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 reset():
def indent(elem, level=0): WINDOW = xbmcgui.Window( 10000 )
# Prettify xml trees return_value = xbmcgui.Dialog().yesno("Warning", "Are you sure you want to reset your local Kodi database?")
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(): if return_value == 0:
# To make Master lock compatible return
path = xbmc.translatePath("special://profile/").decode('utf-8')
xmlpath = "%ssources.xml" % path
try: # Because the settings dialog could be open
xmlparse = etree.parse(xmlpath) # it seems to override settings so we need to close it before we reset settings.
except: # Document is blank or missing xbmc.executebuiltin("Dialog.Close(all,true)")
root = etree.Element('sources')
else: #cleanup video nodes
root = xmlparse.getroot() import shutil
path = "special://profile/library/video/"
if xbmcvfs.exists(path):
allDirs, allFiles = xbmcvfs.listdir(path)
for dir in allDirs:
if dir.startswith("Emby "):
shutil.rmtree(xbmc.translatePath("special://profile/library/video/" + dir))
for file in allFiles:
if file.startswith("emby"):
xbmcvfs.delete(path + file)
settings('SyncInstallRunDone', "false")
# Ask if user information should be deleted too.
return_user = xbmcgui.Dialog().yesno("Warning", "Reset all Emby Addon settings?")
if return_user == 1:
WINDOW.setProperty('deletesettings', "true")
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)
# first stop any db sync
WINDOW.setProperty("SyncDatabaseShouldStop", "true")
count = 0
while(WINDOW.getProperty("SyncDatabaseRunning") == "true"):
xbmc.log("Sync Running, will wait : " + str(count))
count += 1
if(count > 10):
dialog = xbmcgui.Dialog()
dialog.ok('Warning', 'Could not stop DB sync, you should try again.')
return
xbmc.sleep(1000)
# delete video db table data
print "Doing Video DB Reset"
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)
cursor.execute("DROP TABLE IF EXISTS emby")
connection.commit()
cursor.close()
if settings('enableMusicSync') == "true":
# delete video db table data
print "Doing Music DB Reset"
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)
cursor.execute("DROP TABLE IF EXISTS emby")
connection.commit()
cursor.close()
video = root.find('video') # reset the install run flag
if video is None: #settings('SyncInstallRunDone', "false")
video = etree.SubElement(root, 'video') #WINDOW.setProperty("SyncInstallRunDone", "false")
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() dialog = xbmcgui.Dialog()
credentials = settings('networkCreds') # Reload would work instead of restart since the add-on is a service.
if credentials: #dialog.ok('Emby Reset', 'Database reset has completed, Kodi will now restart to apply the changes.')
# Present user with options #WINDOW.clearProperty("SyncDatabaseShouldStop")
option = dialog.select("Modify/Remove network credentials", ["Modify", "Remove"]) #reloadProfile()
dialog.ok('Emby Reset', 'Database reset has completed, Kodi will now restart to apply the changes.')
if option < 0: xbmc.executebuiltin("RestartApp")
# 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(
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
'<smartplaylist type="%s">\n\t'
'<name>Emby %s</name>\n\t'
'<match>all</match>\n\t'
'<rule field="tag" operator="is">\n\t\t'
'<value>%s</value>\n\t'
'</rule>'
% (itemtypes.get(mediatype, mediatype), plname, tagname))
f.close()
logMsg("EMBY", "Successfully added playlist: %s" % tagname)

View file

@ -1,344 +1,466 @@
# -*- coding: utf-8 -*- #################################################################################################
# VideoNodes - utils to create video nodes listings in kodi for the emby addon
################################################################################################# #################################################################################################
import shutil
import xml.etree.ElementTree as etree
import xbmc import xbmc
import xbmcgui
import xbmcaddon import xbmcaddon
import xbmcvfs import xbmcvfs
import json
import os
import shutil
#import common elementree because cElementree has issues with kodi
import xml.etree.ElementTree as etree
import clientinfo import Utils as utils
import utils
################################################################################################# from ReadEmbyDB import ReadEmbyDB
WINDOW = xbmcgui.Window(10000)
addonSettings = xbmcaddon.Addon()
language = addonSettings.getLocalizedString
class VideoNodes(object): class VideoNodes():
def __init__(self): def buildVideoNodeForView(self, tagname, type, windowPropId):
#this method will build a video node for a particular Emby view (= tag in kodi)
clientInfo = clientinfo.ClientInfo() #we set some window props here to for easy future reference and to be used in skins (for easy access only)
self.addonName = clientInfo.getAddonName() tagname_normalized = utils.normalize_nodes(tagname.encode('utf-8'))
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') libraryPath = xbmc.translatePath("special://profile/library/video/Emby - %s/" %tagname_normalized)
nodepath = xbmc.translatePath( kodiVersion = 14
"special://profile/library/video/Emby - %s/" % dirname).decode('utf-8') if xbmc.getInfoLabel("System.BuildVersion").startswith("15") or xbmc.getInfoLabel("System.BuildVersion").startswith("16"):
kodiVersion = 15
# Verify the video directory
if not xbmcvfs.exists(path): #create tag node - index
shutil.copytree( xbmcvfs.mkdir(libraryPath)
src=xbmc.translatePath("special://xbmc/system/library/video/").decode('utf-8'), nodefile = os.path.join(libraryPath, "index.xml")
dst=xbmc.translatePath("special://profile/library/video/").decode('utf-8')) root = etree.Element("node", {"order":"0"})
xbmcvfs.exists(path) etree.SubElement(root, "label").text = tagname
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
# Create the node directory path = "library://video/Emby - %s/" %tagname_normalized
if not xbmcvfs.exists(nodepath): WINDOW.setProperty("Emby.nodes.%s.index" %str(windowPropId),path)
# 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: try:
utils.indent(root) etree.ElementTree(root).write(nodefile, xml_declaration=True)
except: pass except:
etree.ElementTree(root).write(nodeXML) etree.ElementTree(root).write(nodefile)
#create tag node - all items
nodetypes = { nodefile = os.path.join(libraryPath, tagname_normalized + "_all.xml")
root = etree.Element("node", {"order":"1", "type":"filter"})
'1': "all", etree.SubElement(root, "label").text = tagname
'2': "recent", etree.SubElement(root, "match").text = "all"
'3': "recentepisodes", etree.SubElement(root, "content").text = type
'4': "inprogress", etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
'5': "inprogressepisodes", etree.SubElement(root, "order", {"direction":"ascending"}).text = "sorttitle"
'6': "unwatched", Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
'7': "nextupepisodes", WINDOW.setProperty("Emby.nodes.%s.title" %str(windowPropId),tagname)
'8': "sets", path = "library://video/Emby - %s/%s_all.xml"%(tagname_normalized,tagname_normalized)
'9': "genres", WINDOW.setProperty("Emby.nodes.%s.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
'10': "random", WINDOW.setProperty("Emby.nodes.%s.content" %str(windowPropId),path)
'11': "recommended" WINDOW.setProperty("Emby.nodes.%s.type" %str(windowPropId),type)
} etree.SubElement(Rule, "value").text = tagname
mediatypes = { try:
# label according to nodetype per mediatype etree.ElementTree(root).write(nodefile, xml_declaration=True)
'movies': { except:
'1': tagname, etree.ElementTree(root).write(nodefile)
'2': 30174,
'4': 30177, #create tag node - recent items
'6': 30189, nodefile = os.path.join(libraryPath, tagname_normalized + "_recent.xml")
'8': 20434, root = etree.Element("node", {"order":"2", "type":"filter"})
'9': 135, if type == "tvshows":
'10': 30229, label = language(30170)
'11': 30230}, else:
label = language(30174)
'tvshows': { etree.SubElement(root, "label").text = label
'1': tagname, etree.SubElement(root, "match").text = "all"
'2': 30170, etree.SubElement(root, "content").text = type
'3': 30175, etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
'4': 30171, Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
'5': 30178, etree.SubElement(Rule, "value").text = tagname
'7': 30179, etree.SubElement(root, "order", {"direction":"descending"}).text = "dateadded"
'9': 135, #set limit to 25 --> currently hardcoded --> TODO: add a setting for this ?
'10': 30229, etree.SubElement(root, "limit").text = "25"
'11': 30230}, #exclude watched items --> currently hardcoded --> TODO: add a setting for this ?
} Rule2 = etree.SubElement(root, "rule", {"field":"playcount","operator":"is"})
etree.SubElement(Rule2, "value").text = "0"
nodes = mediatypes[mediatype] WINDOW.setProperty("Emby.nodes.%s.recent.title" %str(windowPropId),label)
for node in nodes: path = "library://video/Emby - %s/%s_recent.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.recent.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
nodetype = nodetypes[node] WINDOW.setProperty("Emby.nodes.%s.recent.content" %str(windowPropId),path)
nodeXML = "%s%s_%s.xml" % (nodepath, cleantagname, nodetype) try:
# Get label etree.ElementTree(root).write(nodefile, xml_declaration=True)
stringid = nodes[node] except:
if node != '1': etree.ElementTree(root).write(nodefile)
label = utils.language(stringid)
if not label: #create tag node - inprogress items
label = xbmc.getLocalizedString(stringid) nodefile = os.path.join(libraryPath, tagname_normalized + "_progress.xml")
else: root = etree.Element("node", {"order":"3", "type":"filter"})
label = stringid if type == "tvshows":
label = language(30171)
# Set window properties else:
if nodetype == "nextupepisodes": label = language(30177)
# Custom query etree.SubElement(root, "label").text = label
path = "plugin://plugin.video.emby/?id=%s&mode=nextup&limit=25" % tagname etree.SubElement(root, "match").text = "all"
elif kodiversion == 14 and nodetype == "recentepisodes": etree.SubElement(root, "content").text = type
# Custom query etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
path = "plugin://plugin.video.emby/?id=%s&mode=recentepisodes&limit=25" % tagname Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
elif kodiversion == 14 and nodetype == "inprogressepisodes": etree.SubElement(Rule, "value").text = tagname
# Custom query #set limit to 25 --> currently hardcoded --> TODO: add a setting for this ?
path = "plugin://plugin.video.emby/?id=%s&mode=inprogressepisodes&limit=25"% tagname etree.SubElement(root, "limit").text = "25"
else: Rule2 = etree.SubElement(root, "rule", {"field":"inprogress","operator":"true"})
path = "library://video/Emby - %s/%s_%s.xml" % (dirname, cleantagname, nodetype) WINDOW.setProperty("Emby.nodes.%s.inprogress.title" %str(windowPropId),label)
windowpath = "ActivateWindow(Video, %s, return)" % path path = "library://video/Emby - %s/%s_progress.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.inprogress.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.inprogress.content" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#some movies-only nodes
if type == "movies":
if nodetype == "all": #unwatched movies
nodefile = os.path.join(libraryPath, tagname_normalized + "_unwatched.xml")
if viewtype == "mixed": root = etree.Element("node", {"order":"4", "type":"filter"})
templabel = dirname label = language(30189)
else: etree.SubElement(root, "label").text = label
templabel = label etree.SubElement(root, "match").text = "all"
etree.SubElement(root, "content").text = "movies"
embynode = "Emby.nodes.%s" % indexnumber etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
utils.window('%s.title' % embynode, value=templabel) Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
utils.window('%s.path' % embynode, value=windowpath) etree.SubElement(Rule, "value").text = tagname
utils.window('%s.content' % embynode, value=path) Rule = etree.SubElement(root, "rule", {"field":"playcount","operator":"is"})
utils.window('%s.type' % embynode, value=mediatype) etree.SubElement(Rule, "value").text = "0"
else: etree.SubElement(root, "order", {"direction":"ascending"}).text = "sorttitle"
embynode = "Emby.nodes.%s.%s" % (indexnumber, nodetype) Rule2 = etree.SubElement(root, "rule", {"field":"playcount","operator":"is"})
utils.window('%s.title' % embynode, value=label) etree.SubElement(Rule2, "value").text = "0"
utils.window('%s.path' % embynode, value=windowpath) WINDOW.setProperty("Emby.nodes.%s.unwatched.title" %str(windowPropId),label)
utils.window('%s.content' % embynode, value=path) path = "library://video/Emby - %s/%s_unwatched.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.unwatched.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
if xbmcvfs.exists(nodeXML): WINDOW.setProperty("Emby.nodes.%s.unwatched.content" %str(windowPropId),path)
# 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: try:
utils.indent(root) etree.ElementTree(root).write(nodefile, xml_declaration=True)
except: pass except:
etree.ElementTree(root).write(nodeXML) etree.ElementTree(root).write(nodefile)
#sets
nodefile = os.path.join(libraryPath, tagname_normalized + "_sets.xml")
root = etree.Element("node", {"order":"9", "type":"filter"})
label = xbmc.getLocalizedString(20434)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "match").text = "all"
etree.SubElement(root, "group").text = "sets"
etree.SubElement(root, "content").text = type
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
etree.SubElement(root, "order", {"direction":"ascending"}).text = "sorttitle"
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
etree.SubElement(Rule, "value").text = tagname
WINDOW.setProperty("Emby.nodes.%s.sets.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_sets.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.sets.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.sets.content" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
def singleNode(self, indexnumber, tagname, mediatype, itemtype): #create tag node - genres
nodefile = os.path.join(libraryPath, tagname_normalized + "_genres.xml")
tagname = tagname.encode('utf-8') root = etree.Element("node", {"order":"9", "type":"filter"})
cleantagname = utils.normalize_nodes(tagname) label = xbmc.getLocalizedString(135)
nodepath = xbmc.translatePath("special://profile/library/video/").decode('utf-8') etree.SubElement(root, "label").text = label
nodeXML = "%semby_%s.xml" % (nodepath, cleantagname) etree.SubElement(root, "match").text = "all"
path = "library://video/emby_%s.xml" % (cleantagname) etree.SubElement(root, "group").text = "genres"
windowpath = "ActivateWindow(Video, %s, return)" % path etree.SubElement(root, "content").text = type
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
etree.SubElement(root, "order", {"direction":"ascending"}).text = "sorttitle"
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
etree.SubElement(Rule, "value").text = tagname
WINDOW.setProperty("Emby.nodes.%s.genres.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_genres.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.genres.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.genres.content" %str(windowPropId),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: try:
utils.indent(root) etree.ElementTree(root).write(nodefile, xml_declaration=True)
except: pass except:
etree.ElementTree(root).write(nodeXML) etree.ElementTree(root).write(nodefile)
def clearProperties(self):
self.logMsg("Clearing nodes properties.", 1)
embyprops = utils.window('Emby.nodes.total')
propnames = [
"index","path","title","content", #create tag node - random items
"inprogress.content","inprogress.title", nodefile = os.path.join(libraryPath, tagname_normalized + "_random.xml")
"inprogress.content","inprogress.path", root = etree.Element("node", {"order":"10", "type":"filter"})
"nextepisodes.title","nextepisodes.content", label = language(30229)
"nextepisodes.path","unwatched.title", etree.SubElement(root, "label").text = label
"unwatched.content","unwatched.path", etree.SubElement(root, "match").text = "all"
"recent.title","recent.content","recent.path", etree.SubElement(root, "content").text = type
"recentepisodes.title","recentepisodes.content", etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
"recentepisodes.path","inprogressepisodes.title", Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
"inprogressepisodes.content","inprogressepisodes.path" etree.SubElement(Rule, "value").text = tagname
] #set limit to 25 --> currently hardcoded --> TODO: add a setting for this ?
etree.SubElement(root, "limit").text = "25"
etree.SubElement(root, "order", {"direction":"ascending"}).text = "random"
WINDOW.setProperty("Emby.nodes.%s.random.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_random.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.random.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.random.content" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#create tag node - recommended items
nodefile = os.path.join(libraryPath, tagname_normalized + "_recommended.xml")
root = etree.Element("node", {"order":"10", "type":"filter"})
label = language(30230)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "match").text = "all"
etree.SubElement(root, "content").text = type
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
etree.SubElement(Rule, "value").text = tagname
Rule2 = etree.SubElement(root, "rule", {"field":"playcount","operator":"is"})
etree.SubElement(Rule2, "value").text = "0"
Rule3 = etree.SubElement(root, "rule", {"field":"rating","operator":"greaterthan"})
etree.SubElement(Rule3, "value").text = "7"
#set limit to 25 --> currently hardcoded --> TODO: add a setting for this ?
etree.SubElement(root, "limit").text = "25"
etree.SubElement(root, "order", {"direction":"descending"}).text = "rating"
WINDOW.setProperty("Emby.nodes.%s.random.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_recommended.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.recommended.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.recommended.content" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#### TAGS ONLY FOR TV SHOWS COLLECTIONS ####
if type == "tvshows":
#as from kodi isengard you can use tags for episodes to filter
#for below isengard we still use the plugin's entrypoint to build a listing
if kodiVersion == 15:
#create tag node - recent episodes
nodefile = os.path.join(libraryPath, tagname_normalized + "_recent_episodes.xml")
root = etree.Element("node", {"order":"3", "type":"filter"})
label = language(30175)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "match").text = "all"
etree.SubElement(root, "content").text = "episodes"
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
etree.SubElement(root, "order", {"direction":"descending"}).text = "dateadded"
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
etree.SubElement(Rule, "value").text = tagname
#set limit to 25 --> currently hardcoded --> TODO: add a setting for this ?
etree.SubElement(root, "limit").text = "25"
#exclude watched items --> currently hardcoded --> TODO: add a setting for this ?
Rule2 = etree.SubElement(root, "rule", {"field":"playcount","operator":"is"})
etree.SubElement(Rule2, "value").text = "0"
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_recent_episodes.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.content" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#create tag node - inprogress episodes
nodefile = os.path.join(libraryPath, tagname_normalized + "_progress_episodes.xml")
root = etree.Element("node", {"order":"4", "type":"filter"})
label = language(30178)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "match").text = "all"
etree.SubElement(root, "content").text = "episodes"
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
etree.SubElement(Rule, "value").text = tagname
#set limit to 25 --> currently hardcoded --> TODO: add a setting for this ?
etree.SubElement(root, "limit").text = "25"
Rule2 = etree.SubElement(root, "rule", {"field":"inprogress","operator":"true"})
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_progress_episodes.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.content" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
if kodiVersion == 14:
#create tag node - recent episodes
nodefile = os.path.join(libraryPath, tagname_normalized + "_recent_episodes.xml")
root = etree.Element("node", {"order":"4", "type":"folder"})
label = language(30175)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "content").text = "episodes"
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
path = "plugin://plugin.video.emby/?id=%s&mode=recentepisodes&limit=25" %tagname
etree.SubElement(root, "path").text = path
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_recent_episodes.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.recentepisodes.content" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#create tag node - inprogress items
nodefile = os.path.join(libraryPath, tagname_normalized + "_progress_episodes.xml")
root = etree.Element("node", {"order":"5", "type":"folder"})
label = language(30178)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "content").text = "episodes"
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
path = "plugin://plugin.video.emby/?id=%s&mode=inprogressepisodes&limit=25" %tagname
etree.SubElement(root, "path").text = path
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_progress_episodes.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.inprogressepisodes.content" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#create tag node - nextup items
#for nextup we always use the dynamic content approach with the plugin's entrypoint because it involves a custom query
nodefile = os.path.join(libraryPath, tagname_normalized + "_nextup_episodes.xml")
root = etree.Element("node", {"order":"6", "type":"folder"})
label = language(30179)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "content").text = "episodes"
path = "plugin://plugin.video.emby/?id=%s&mode=nextup&limit=25" %tagname
etree.SubElement(root, "path").text = path
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
WINDOW.setProperty("Emby.nodes.%s.nextepisodes.title" %str(windowPropId),label)
path = "library://video/Emby - %s/%s_nextup_episodes.xml"%(tagname_normalized,tagname_normalized)
WINDOW.setProperty("Emby.nodes.%s.nextepisodes.path" %str(windowPropId),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.nextepisodes.content" %str(windowPropId),path)
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
def buildVideoNodesListing(self):
try:
# the library path doesn't exist on all systems
if not xbmcvfs.exists("special://profile/library/"):
xbmcvfs.mkdir("special://profile/library")
if not xbmcvfs.exists("special://profile/library/video/"):
#we need to copy over the default items
shutil.copytree(xbmc.translatePath("special://xbmc/system/library/video"), xbmc.translatePath("special://profile/library/video"))
#always cleanup existing Emby video nodes first because we don't want old stuff to stay in there
path = "special://profile/library/video/"
if xbmcvfs.exists(path):
allDirs, allFiles = xbmcvfs.listdir(path)
for dir in allDirs:
if dir.startswith("Emby "):
shutil.rmtree(xbmc.translatePath("special://profile/library/video/" + dir))
for file in allFiles:
if file.startswith("emby"):
xbmcvfs.delete(path + file)
#we build up a listing and set window props for all nodes we created
#the window props will be used by the main entry point to quickly build up the listing and can be used in skins (like titan) too for quick reference
#comment marcelveldt: please leave the window props as-is because I will be referencing them in titan skin...
totalNodesCount = 0
#build the listing for all views
views_movies = ReadEmbyDB().getCollections("movies")
if views_movies:
for view in views_movies:
title = view.get('title')
content = view.get('content')
if content == "mixed":
title = "%s - Movies" % title
self.buildVideoNodeForView(title, "movies", totalNodesCount)
totalNodesCount +=1
views_shows = ReadEmbyDB().getCollections("tvshows")
if views_shows:
for view in views_shows:
title = view.get('title')
content = view.get('content')
if content == "mixed":
title = "%s - TV Shows" % title
self.buildVideoNodeForView(title, "tvshows", totalNodesCount)
totalNodesCount +=1
if embyprops: #create tag node for emby channels
totalnodes = int(embyprops) nodefile = os.path.join(xbmc.translatePath("special://profile/library/video"), "emby_channels.xml")
for i in range(totalnodes): root = etree.Element("node", {"order":"1", "type":"folder"})
for prop in propnames: label = language(30173)
utils.window('Emby.nodes.%s.%s' % (str(i), prop), clear=True) etree.SubElement(root, "label").text = label
etree.SubElement(root, "content").text = "movies"
etree.SubElement(root, "path").text = "plugin://plugin.video.emby/?id=0&mode=channels"
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
WINDOW.setProperty("Emby.nodes.%s.title" %str(totalNodesCount),label)
WINDOW.setProperty("Emby.nodes.%s.type" %str(totalNodesCount),"channels")
path = "library://video/emby_channels.xml"
WINDOW.setProperty("Emby.nodes.%s.path" %str(totalNodesCount),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.content" %str(totalNodesCount),path)
totalNodesCount +=1
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#create tag node - favorite shows
nodefile = os.path.join(xbmc.translatePath("special://profile/library/video"),"emby_favorite_shows.xml")
root = etree.Element("node", {"order":"1", "type":"filter"})
label = language(30181)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "match").text = "all"
etree.SubElement(root, "content").text = "tvshows"
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
etree.SubElement(root, "order", {"direction":"ascending"}).text = "sorttitle"
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
etree.SubElement(Rule, "value").text = "Favorite tvshows" #do not localize the tagname itself
WINDOW.setProperty("Emby.nodes.%s.title" %str(totalNodesCount),label)
WINDOW.setProperty("Emby.nodes.%s.type" %str(totalNodesCount),"favourites")
path = "library://video/emby_favorite_shows.xml"
WINDOW.setProperty("Emby.nodes.%s.path" %str(totalNodesCount),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.content" %str(totalNodesCount),path)
totalNodesCount +=1
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
#create tag node - favorite movies
nodefile = os.path.join(xbmc.translatePath("special://profile/library/video"),"emby_favorite_movies.xml")
root = etree.Element("node", {"order":"1", "type":"filter"})
label = language(30180)
etree.SubElement(root, "label").text = label
etree.SubElement(root, "match").text = "all"
etree.SubElement(root, "content").text = "movies"
etree.SubElement(root, "icon").text = "special://home/addons/plugin.video.emby/icon.png"
etree.SubElement(root, "order", {"direction":"ascending"}).text = "sorttitle"
Rule = etree.SubElement(root, "rule", {"field":"tag","operator":"is"})
etree.SubElement(Rule, "value").text = "Favorite movies" #do not localize the tagname itself
WINDOW.setProperty("Emby.nodes.%s.title" %str(totalNodesCount),label)
WINDOW.setProperty("Emby.nodes.%s.type" %str(totalNodesCount),"favourites")
path = "library://video/emby_favorite_movies.xml"
WINDOW.setProperty("Emby.nodes.%s.path" %str(totalNodesCount),"ActivateWindow(Video,%s,return)"%path)
WINDOW.setProperty("Emby.nodes.%s.content" %str(totalNodesCount),path)
totalNodesCount +=1
try:
etree.ElementTree(root).write(nodefile, xml_declaration=True)
except:
etree.ElementTree(root).write(nodefile)
WINDOW.setProperty("Emby.nodes.total", str(totalNodesCount))
except Exception as e:
utils.logMsg("Emby addon","Error while creating videonodes listings, restart required ?")
print e